use std::fmt;
use std::sync::Arc;
use crate::swarm::{BatchId, PublicKey, Reference};
#[derive(Debug, Clone, Copy)]
pub struct UploadProgress<'a> {
pub path: &'a str,
pub size: u64,
pub index: usize,
pub total: usize,
}
pub type OnEntryFn = Arc<dyn for<'a> Fn(UploadProgress<'a>) + Send + Sync>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum RedundancyLevel {
Off = 0,
Medium = 1,
Strong = 2,
Insane = 3,
Paranoid = 4,
}
impl RedundancyLevel {
pub fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum RedundancyStrategy {
None = 0,
Data = 1,
Proximity = 2,
Race = 3,
}
impl RedundancyStrategy {
pub fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Clone, Debug, Default)]
pub struct UploadOptions {
pub act: Option<bool>,
pub act_history_address: Option<Reference>,
pub pin: Option<bool>,
pub encrypt: Option<bool>,
pub tag: u32,
pub deferred: Option<bool>,
}
#[derive(Clone, Debug, Default)]
pub struct RedundantUploadOptions {
pub base: UploadOptions,
pub redundancy_level: Option<RedundancyLevel>,
}
#[derive(Clone, Debug, Default)]
pub struct FileUploadOptions {
pub base: UploadOptions,
pub size: Option<u64>,
pub content_type: Option<String>,
pub redundancy_level: Option<RedundancyLevel>,
}
#[derive(Clone, Default)]
pub struct CollectionUploadOptions {
pub base: UploadOptions,
pub index_document: Option<String>,
pub error_document: Option<String>,
pub redundancy_level: Option<RedundancyLevel>,
pub on_entry: Option<OnEntryFn>,
}
impl CollectionUploadOptions {
pub fn with_on_entry<F>(mut self, f: F) -> Self
where
F: for<'a> Fn(UploadProgress<'a>) + Send + Sync + 'static,
{
self.on_entry = Some(Arc::new(f));
self
}
}
impl fmt::Debug for CollectionUploadOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CollectionUploadOptions")
.field("base", &self.base)
.field("index_document", &self.index_document)
.field("error_document", &self.error_document)
.field("redundancy_level", &self.redundancy_level)
.field("on_entry", &self.on_entry.as_ref().map(|_| "<callback>"))
.finish()
}
}
#[derive(Clone, Debug, Default)]
pub struct DownloadOptions {
pub redundancy_strategy: Option<RedundancyStrategy>,
pub fallback: Option<bool>,
pub timeout_ms: Option<u64>,
pub act_publisher: Option<PublicKey>,
pub act_history_address: Option<Reference>,
pub act_timestamp: Option<i64>,
}
#[derive(Clone, Debug, Default)]
pub struct PostageBatchOptions {
pub label: Option<String>,
pub immutable: Option<bool>,
pub gas_price: Option<String>,
pub gas_limit: Option<String>,
}
pub type HeaderPairs = Vec<(&'static str, String)>;
fn bool_str(b: bool) -> &'static str {
if b { "true" } else { "false" }
}
fn push_upload_options(out: &mut HeaderPairs, opts: &UploadOptions) {
if let Some(v) = opts.pin {
out.push(("Swarm-Pin", bool_str(v).to_string()));
}
if let Some(v) = opts.encrypt {
out.push(("Swarm-Encrypt", bool_str(v).to_string()));
}
if opts.tag > 0 {
out.push(("Swarm-Tag", opts.tag.to_string()));
}
if let Some(v) = opts.deferred {
out.push(("Swarm-Deferred-Upload", bool_str(v).to_string()));
}
if let Some(v) = opts.act {
out.push(("Swarm-Act", bool_str(v).to_string()));
}
if let Some(ref r) = opts.act_history_address {
out.push(("Swarm-Act-History-Address", r.to_hex()));
}
}
pub fn prepare_upload_headers(batch_id: &BatchId, opts: Option<&UploadOptions>) -> HeaderPairs {
let mut out = vec![("Swarm-Postage-Batch-Id", batch_id.to_hex())];
if let Some(o) = opts {
push_upload_options(&mut out, o);
}
out
}
pub fn prepare_redundant_upload_headers(
batch_id: &BatchId,
opts: Option<&RedundantUploadOptions>,
) -> HeaderPairs {
match opts {
None => prepare_upload_headers(batch_id, None),
Some(o) => {
let mut out = prepare_upload_headers(batch_id, Some(&o.base));
if let Some(level) = o.redundancy_level {
if !matches!(level, RedundancyLevel::Off) {
out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
}
}
out
}
}
}
pub fn prepare_file_upload_headers(
batch_id: &BatchId,
opts: Option<&FileUploadOptions>,
) -> HeaderPairs {
match opts {
None => prepare_upload_headers(batch_id, None),
Some(o) => {
let mut out = prepare_upload_headers(batch_id, Some(&o.base));
if let Some(size) = o.size {
out.push(("Content-Length", size.to_string()));
}
if let Some(ref ct) = o.content_type {
out.push(("Content-Type", ct.clone()));
}
if let Some(level) = o.redundancy_level {
if !matches!(level, RedundancyLevel::Off) {
out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
}
}
out
}
}
}
pub fn prepare_collection_upload_headers(
batch_id: &BatchId,
opts: Option<&CollectionUploadOptions>,
) -> HeaderPairs {
match opts {
None => prepare_upload_headers(batch_id, None),
Some(o) => {
let mut out = prepare_upload_headers(batch_id, Some(&o.base));
if let Some(ref idx) = o.index_document {
out.push(("Swarm-Index-Document", idx.clone()));
}
if let Some(ref err) = o.error_document {
out.push(("Swarm-Error-Document", err.clone()));
}
if let Some(level) = o.redundancy_level {
if !matches!(level, RedundancyLevel::Off) {
out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
}
}
out
}
}
}
pub fn prepare_download_headers(opts: Option<&DownloadOptions>) -> HeaderPairs {
let mut out = HeaderPairs::new();
let Some(o) = opts else { return out };
if let Some(s) = o.redundancy_strategy {
out.push(("Swarm-Redundancy-Strategy", s.as_u8().to_string()));
}
if let Some(v) = o.fallback {
out.push(("Swarm-Redundancy-Fallback-Mode", bool_str(v).to_string()));
}
if let Some(ms) = o.timeout_ms {
if ms > 0 {
out.push(("Swarm-Chunk-Retrieval-Timeout", ms.to_string()));
}
}
let mut act = false;
if let Some(ref pk) = o.act_publisher {
if let Ok(hex) = pk.compressed_hex() {
out.push(("Swarm-Act-Publisher", hex));
act = true;
}
}
if let Some(ref r) = o.act_history_address {
out.push(("Swarm-Act-History-Address", r.to_hex()));
act = true;
}
if let Some(ts) = o.act_timestamp {
if ts > 0 {
out.push(("Swarm-Act-Timestamp", ts.to_string()));
act = true;
}
}
if act {
out.push(("Swarm-Act", "true".to_string()));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn batch() -> BatchId {
BatchId::new(&[0xab; 32]).unwrap()
}
fn header<'a>(h: &'a [(&'static str, String)], name: &str) -> Option<&'a str> {
h.iter().find(|(k, _)| *k == name).map(|(_, v)| v.as_str())
}
#[test]
fn upload_headers_omit_unset_fields() {
let h = prepare_upload_headers(&batch(), None);
assert_eq!(
header(&h, "Swarm-Postage-Batch-Id"),
Some("ab".repeat(32).as_str())
);
assert!(header(&h, "Swarm-Pin").is_none());
assert!(header(&h, "Swarm-Encrypt").is_none());
}
#[test]
fn upload_headers_distinguish_none_from_some_false() {
let opts = UploadOptions {
pin: Some(false),
..Default::default()
};
let h = prepare_upload_headers(&batch(), Some(&opts));
assert_eq!(header(&h, "Swarm-Pin"), Some("false"));
}
#[test]
fn redundancy_level_off_is_omitted() {
let opts = RedundantUploadOptions {
redundancy_level: Some(RedundancyLevel::Off),
..Default::default()
};
let h = prepare_redundant_upload_headers(&batch(), Some(&opts));
assert!(header(&h, "Swarm-Redundancy-Level").is_none());
}
#[test]
fn redundancy_level_medium_emits_header() {
let opts = RedundantUploadOptions {
redundancy_level: Some(RedundancyLevel::Medium),
..Default::default()
};
let h = prepare_redundant_upload_headers(&batch(), Some(&opts));
assert_eq!(header(&h, "Swarm-Redundancy-Level"), Some("1"));
}
#[test]
fn collection_upload_uses_swarm_index_document_header() {
let opts = CollectionUploadOptions {
index_document: Some("index.html".into()),
..Default::default()
};
let h = prepare_collection_upload_headers(&batch(), Some(&opts));
assert_eq!(header(&h, "Swarm-Index-Document"), Some("index.html"));
}
#[test]
fn download_act_implies_swarm_act_true() {
let opts = DownloadOptions {
act_history_address: Some(Reference::from_hex(&"00".repeat(32)).unwrap()),
..Default::default()
};
let h = prepare_download_headers(Some(&opts));
assert_eq!(header(&h, "Swarm-Act"), Some("true"));
}
#[test]
fn download_no_options_no_headers() {
assert!(prepare_download_headers(None).is_empty());
assert!(prepare_download_headers(Some(&DownloadOptions::default())).is_empty());
}
}