1use std::fmt;
8use std::sync::Arc;
9
10use crate::swarm::{BatchId, PublicKey, Reference};
11
12#[derive(Debug, Clone, Copy)]
16pub struct UploadProgress<'a> {
17 pub path: &'a str,
19 pub size: u64,
21 pub index: usize,
23 pub total: usize,
25}
26
27pub type OnEntryFn = Arc<dyn for<'a> Fn(UploadProgress<'a>) + Send + Sync>;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33#[repr(u8)]
34pub enum RedundancyLevel {
35 Off = 0,
37 Medium = 1,
39 Strong = 2,
41 Insane = 3,
43 Paranoid = 4,
45}
46
47impl RedundancyLevel {
48 pub fn as_u8(self) -> u8 {
50 self as u8
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57#[repr(u8)]
58pub enum RedundancyStrategy {
59 None = 0,
61 Data = 1,
63 Proximity = 2,
65 Race = 3,
67}
68
69impl RedundancyStrategy {
70 pub fn as_u8(self) -> u8 {
72 self as u8
73 }
74}
75
76#[derive(Clone, Debug, Default)]
79pub struct UploadOptions {
80 pub act: Option<bool>,
84 pub act_history_address: Option<Reference>,
87 pub pin: Option<bool>,
89 pub encrypt: Option<bool>,
92 pub tag: u32,
95 pub deferred: Option<bool>,
99}
100
101#[derive(Clone, Debug, Default)]
104pub struct RedundantUploadOptions {
105 pub base: UploadOptions,
107 pub redundancy_level: Option<RedundancyLevel>,
109}
110
111#[derive(Clone, Debug, Default)]
114pub struct FileUploadOptions {
115 pub base: UploadOptions,
117 pub size: Option<u64>,
120 pub content_type: Option<String>,
122 pub redundancy_level: Option<RedundancyLevel>,
124}
125
126#[derive(Clone, Default)]
129pub struct CollectionUploadOptions {
130 pub base: UploadOptions,
132 pub index_document: Option<String>,
134 pub error_document: Option<String>,
136 pub redundancy_level: Option<RedundancyLevel>,
138 pub on_entry: Option<OnEntryFn>,
142}
143
144impl CollectionUploadOptions {
145 pub fn with_on_entry<F>(mut self, f: F) -> Self
149 where
150 F: for<'a> Fn(UploadProgress<'a>) + Send + Sync + 'static,
151 {
152 self.on_entry = Some(Arc::new(f));
153 self
154 }
155}
156
157impl fmt::Debug for CollectionUploadOptions {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.debug_struct("CollectionUploadOptions")
160 .field("base", &self.base)
161 .field("index_document", &self.index_document)
162 .field("error_document", &self.error_document)
163 .field("redundancy_level", &self.redundancy_level)
164 .field("on_entry", &self.on_entry.as_ref().map(|_| "<callback>"))
165 .finish()
166 }
167}
168
169#[derive(Clone, Debug, Default)]
172pub struct DownloadOptions {
173 pub redundancy_strategy: Option<RedundancyStrategy>,
175 pub fallback: Option<bool>,
177 pub timeout_ms: Option<u64>,
179 pub act_publisher: Option<PublicKey>,
181 pub act_history_address: Option<Reference>,
183 pub act_timestamp: Option<i64>,
185}
186
187#[derive(Clone, Debug, Default)]
190pub struct PostageBatchOptions {
191 pub label: Option<String>,
193 pub immutable: Option<bool>,
195 pub gas_price: Option<String>,
197 pub gas_limit: Option<String>,
199}
200
201pub type HeaderPairs = Vec<(&'static str, String)>;
206
207fn bool_str(b: bool) -> &'static str {
208 if b { "true" } else { "false" }
209}
210
211fn push_upload_options(out: &mut HeaderPairs, opts: &UploadOptions) {
212 if let Some(v) = opts.pin {
213 out.push(("Swarm-Pin", bool_str(v).to_string()));
214 }
215 if let Some(v) = opts.encrypt {
216 out.push(("Swarm-Encrypt", bool_str(v).to_string()));
217 }
218 if opts.tag > 0 {
219 out.push(("Swarm-Tag", opts.tag.to_string()));
220 }
221 if let Some(v) = opts.deferred {
222 out.push(("Swarm-Deferred-Upload", bool_str(v).to_string()));
223 }
224 if let Some(v) = opts.act {
225 out.push(("Swarm-Act", bool_str(v).to_string()));
226 }
227 if let Some(ref r) = opts.act_history_address {
228 out.push(("Swarm-Act-History-Address", r.to_hex()));
229 }
230}
231
232pub fn prepare_upload_headers(batch_id: &BatchId, opts: Option<&UploadOptions>) -> HeaderPairs {
234 let mut out = vec![("Swarm-Postage-Batch-Id", batch_id.to_hex())];
235 if let Some(o) = opts {
236 push_upload_options(&mut out, o);
237 }
238 out
239}
240
241pub fn prepare_redundant_upload_headers(
243 batch_id: &BatchId,
244 opts: Option<&RedundantUploadOptions>,
245) -> HeaderPairs {
246 match opts {
247 None => prepare_upload_headers(batch_id, None),
248 Some(o) => {
249 let mut out = prepare_upload_headers(batch_id, Some(&o.base));
250 if let Some(level) = o.redundancy_level {
251 if !matches!(level, RedundancyLevel::Off) {
252 out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
253 }
254 }
255 out
256 }
257 }
258}
259
260pub fn prepare_file_upload_headers(
262 batch_id: &BatchId,
263 opts: Option<&FileUploadOptions>,
264) -> HeaderPairs {
265 match opts {
266 None => prepare_upload_headers(batch_id, None),
267 Some(o) => {
268 let mut out = prepare_upload_headers(batch_id, Some(&o.base));
269 if let Some(size) = o.size {
270 out.push(("Content-Length", size.to_string()));
271 }
272 if let Some(ref ct) = o.content_type {
273 out.push(("Content-Type", ct.clone()));
274 }
275 if let Some(level) = o.redundancy_level {
276 if !matches!(level, RedundancyLevel::Off) {
277 out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
278 }
279 }
280 out
281 }
282 }
283}
284
285pub fn prepare_collection_upload_headers(
287 batch_id: &BatchId,
288 opts: Option<&CollectionUploadOptions>,
289) -> HeaderPairs {
290 match opts {
291 None => prepare_upload_headers(batch_id, None),
292 Some(o) => {
293 let mut out = prepare_upload_headers(batch_id, Some(&o.base));
294 if let Some(ref idx) = o.index_document {
295 out.push(("Swarm-Index-Document", idx.clone()));
296 }
297 if let Some(ref err) = o.error_document {
298 out.push(("Swarm-Error-Document", err.clone()));
299 }
300 if let Some(level) = o.redundancy_level {
301 if !matches!(level, RedundancyLevel::Off) {
302 out.push(("Swarm-Redundancy-Level", level.as_u8().to_string()));
303 }
304 }
305 out
306 }
307 }
308}
309
310pub fn prepare_download_headers(opts: Option<&DownloadOptions>) -> HeaderPairs {
314 let mut out = HeaderPairs::new();
315 let Some(o) = opts else { return out };
316
317 if let Some(s) = o.redundancy_strategy {
318 out.push(("Swarm-Redundancy-Strategy", s.as_u8().to_string()));
319 }
320 if let Some(v) = o.fallback {
321 out.push(("Swarm-Redundancy-Fallback-Mode", bool_str(v).to_string()));
322 }
323 if let Some(ms) = o.timeout_ms {
324 if ms > 0 {
325 out.push(("Swarm-Chunk-Retrieval-Timeout", ms.to_string()));
326 }
327 }
328 let mut act = false;
329 if let Some(ref pk) = o.act_publisher {
330 if let Ok(hex) = pk.compressed_hex() {
331 out.push(("Swarm-Act-Publisher", hex));
332 act = true;
333 }
334 }
335 if let Some(ref r) = o.act_history_address {
336 out.push(("Swarm-Act-History-Address", r.to_hex()));
337 act = true;
338 }
339 if let Some(ts) = o.act_timestamp {
340 if ts > 0 {
341 out.push(("Swarm-Act-Timestamp", ts.to_string()));
342 act = true;
343 }
344 }
345 if act {
346 out.push(("Swarm-Act", "true".to_string()));
347 }
348 out
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 fn batch() -> BatchId {
356 BatchId::new(&[0xab; 32]).unwrap()
357 }
358
359 fn header<'a>(h: &'a [(&'static str, String)], name: &str) -> Option<&'a str> {
360 h.iter().find(|(k, _)| *k == name).map(|(_, v)| v.as_str())
361 }
362
363 #[test]
364 fn upload_headers_omit_unset_fields() {
365 let h = prepare_upload_headers(&batch(), None);
366 assert_eq!(
367 header(&h, "Swarm-Postage-Batch-Id"),
368 Some("ab".repeat(32).as_str())
369 );
370 assert!(header(&h, "Swarm-Pin").is_none());
371 assert!(header(&h, "Swarm-Encrypt").is_none());
372 }
373
374 #[test]
375 fn upload_headers_distinguish_none_from_some_false() {
376 let opts = UploadOptions {
377 pin: Some(false),
378 ..Default::default()
379 };
380 let h = prepare_upload_headers(&batch(), Some(&opts));
381 assert_eq!(header(&h, "Swarm-Pin"), Some("false"));
382 }
383
384 #[test]
385 fn redundancy_level_off_is_omitted() {
386 let opts = RedundantUploadOptions {
387 redundancy_level: Some(RedundancyLevel::Off),
388 ..Default::default()
389 };
390 let h = prepare_redundant_upload_headers(&batch(), Some(&opts));
391 assert!(header(&h, "Swarm-Redundancy-Level").is_none());
392 }
393
394 #[test]
395 fn redundancy_level_medium_emits_header() {
396 let opts = RedundantUploadOptions {
397 redundancy_level: Some(RedundancyLevel::Medium),
398 ..Default::default()
399 };
400 let h = prepare_redundant_upload_headers(&batch(), Some(&opts));
401 assert_eq!(header(&h, "Swarm-Redundancy-Level"), Some("1"));
402 }
403
404 #[test]
405 fn collection_upload_uses_swarm_index_document_header() {
406 let opts = CollectionUploadOptions {
407 index_document: Some("index.html".into()),
408 ..Default::default()
409 };
410 let h = prepare_collection_upload_headers(&batch(), Some(&opts));
411 assert_eq!(header(&h, "Swarm-Index-Document"), Some("index.html"));
412 }
413
414 #[test]
415 fn download_act_implies_swarm_act_true() {
416 let opts = DownloadOptions {
417 act_history_address: Some(Reference::from_hex(&"00".repeat(32)).unwrap()),
418 ..Default::default()
419 };
420 let h = prepare_download_headers(Some(&opts));
421 assert_eq!(header(&h, "Swarm-Act"), Some("true"));
422 }
423
424 #[test]
425 fn download_no_options_no_headers() {
426 assert!(prepare_download_headers(None).is_empty());
427 assert!(prepare_download_headers(Some(&DownloadOptions::default())).is_empty());
428 }
429}