1use std::path::Path;
20use std::time::Duration;
21
22use serde_json::{Map, Value};
23
24use crate::msg::{ConnectionField, FieldKind};
25use crate::options::{OptionField, OptionType};
26use crate::plugin::{ErrorKind, PluginError};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum IfExists {
31 Overwrite,
33 Error,
35 Number,
37}
38
39impl IfExists {
40 pub fn parse(s: &str) -> Option<IfExists> {
41 match s {
42 "overwrite" => Some(IfExists::Overwrite),
43 "error" => Some(IfExists::Error),
44 "number" => Some(IfExists::Number),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Rules {
53 pub if_exists: IfExists,
54 pub atomic: bool,
56 pub temp_dir: Option<String>,
59 pub retries: u32,
61 pub retry_wait: Option<Duration>,
64 pub max_retry_wait: Duration,
66 pub connect_timeout: Duration,
68 pub timeout: Duration,
70}
71
72pub const DEFAULT_RETRIES: u32 = 3;
73pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
74pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
75pub const DEFAULT_MAX_RETRY_WAIT: Duration = Duration::from_secs(600);
76
77impl Default for Rules {
78 fn default() -> Rules {
79 Rules {
80 if_exists: IfExists::Overwrite,
81 atomic: true,
82 temp_dir: None,
83 retries: DEFAULT_RETRIES,
84 retry_wait: None,
85 max_retry_wait: DEFAULT_MAX_RETRY_WAIT,
86 connect_timeout: DEFAULT_CONNECT_TIMEOUT,
87 timeout: DEFAULT_TIMEOUT,
88 }
89 }
90}
91
92impl Rules {
93 pub fn legacy() -> Rules {
96 Rules {
97 atomic: false,
98 retries: 0,
99 ..Rules::default()
100 }
101 }
102
103 pub fn from_settings(
108 base: Rules,
109 connection: &Map<String, Value>,
110 options: &Map<String, Value>,
111 aliases: &[(&str, &str)],
112 ) -> Result<(Rules, Vec<String>), String> {
113 let mut warnings = Vec::new();
114 let get = |key: &str, warnings: &mut Vec<String>| -> Option<Value> {
115 for map in [options, connection] {
116 if let Some(v) = map.get(key).filter(|v| !v.is_null()) {
117 return Some(v.clone());
118 }
119 for (old, new) in aliases {
120 if *new == key
121 && let Some(v) = map.get(*old).filter(|v| !v.is_null())
122 {
123 warnings.push(format!("`{old}` is deprecated; use `{new}`"));
124 return Some(v.clone());
125 }
126 }
127 }
128 None
129 };
130 let mut r = base;
131 if let Some(v) = get("if_exists", &mut warnings) {
132 r.if_exists = v.as_str().and_then(IfExists::parse).ok_or_else(|| {
133 format!("`if_exists` must be one of `overwrite`, `error`, `number`, got {v}")
134 })?;
135 }
136 if let Some(v) = get("atomic", &mut warnings) {
137 r.atomic = v
138 .as_bool()
139 .ok_or_else(|| format!("`atomic` must be true or false, got {v}"))?;
140 }
141 if let Some(v) = get("temp_dir", &mut warnings) {
142 let s = v
143 .as_str()
144 .ok_or_else(|| format!("`temp_dir` must be a path, got {v}"))?;
145 r.temp_dir = Some(s.to_string()).filter(|s| !s.is_empty());
146 }
147 if let Some(v) = get("retries", &mut warnings) {
148 r.retries = v
149 .as_u64()
150 .or_else(|| v.as_str().and_then(|s| s.trim().parse().ok()))
151 .and_then(|n| u32::try_from(n).ok())
152 .ok_or_else(|| format!("`retries` must be a whole number, got {v}"))?;
153 }
154 if let Some(v) = get("retry_wait", &mut warnings) {
155 r.retry_wait = Some(parse_duration(&v).map_err(|e| format!("`retry_wait` {e}"))?);
156 }
157 if let Some(v) = get("max_retry_wait", &mut warnings) {
158 r.max_retry_wait = parse_duration(&v).map_err(|e| format!("`max_retry_wait` {e}"))?;
159 }
160 if let Some(v) = get("connect_timeout", &mut warnings) {
161 r.connect_timeout = parse_duration(&v).map_err(|e| format!("`connect_timeout` {e}"))?;
162 }
163 if let Some(v) = get("timeout", &mut warnings) {
164 r.timeout = parse_duration(&v).map_err(|e| format!("`timeout` {e}"))?;
165 }
166 Ok((r, warnings))
167 }
168}
169
170impl Rules {
171 pub fn wait(&self, attempt: u32, retry_after: Option<Duration>) -> Duration {
174 let w = retry_after.unwrap_or_else(|| match self.retry_wait {
175 Some(first) => first.saturating_mul(2u32.saturating_pow(attempt.saturating_sub(1))),
176 None => backoff(attempt),
177 });
178 w.min(self.max_retry_wait)
179 }
180}
181
182pub fn parse_duration(v: &Value) -> Result<Duration, String> {
184 let bad = || format!("must be a duration such as `30s`, `5m` or a number of seconds, got {v}");
185 if let Some(n) = v.as_f64() {
186 return (n >= 0.0 && n.is_finite())
187 .then(|| Duration::from_secs_f64(n))
188 .ok_or_else(bad);
189 }
190 let s = v.as_str().ok_or_else(bad)?.trim();
191 let (num, unit) = s.split_at(
192 s.find(|c: char| !c.is_ascii_digit() && c != '.')
193 .unwrap_or(s.len()),
194 );
195 let n: f64 = num.parse().map_err(|_| bad())?;
196 let secs = match unit.trim() {
197 "" | "s" => n,
198 "ms" => n / 1000.0,
199 "m" => n * 60.0,
200 "h" => n * 3600.0,
201 _ => return Err(bad()),
202 };
203 Ok(Duration::from_secs_f64(secs))
204}
205
206pub fn timeout_fields() -> Vec<ConnectionField> {
209 connection_fields()
210 .into_iter()
211 .filter(|f| f.name != "retries")
212 .collect()
213}
214
215pub fn connection_fields() -> Vec<ConnectionField> {
217 vec![
218 ConnectionField::new(
219 "connect_timeout",
220 "how long to wait for a connection (`30s`, `2m`, or seconds)",
221 )
222 .default("30s")
223 .kind(FieldKind::Duration)
224 .manual(),
225 ConnectionField::new(
226 "timeout",
227 "how long a read or write may make no progress before it fails",
228 )
229 .default("60s")
230 .kind(FieldKind::Duration)
231 .manual(),
232 ConnectionField::new(
233 "retries",
234 "how many times to try again after a temporary error (0: never)",
235 )
236 .default(DEFAULT_RETRIES)
237 .kind(FieldKind::Integer)
238 .manual(),
239 ConnectionField::new(
240 "retry_wait",
241 "the first wait before trying again (`30s`), doubling each try; a server's Retry-After wins",
242 )
243 .kind(FieldKind::Duration)
244 .manual(),
245 ConnectionField::new("max_retry_wait", "the longest wait between tries (`10m`)")
246 .default("10m")
247 .kind(FieldKind::Duration)
248 .manual(),
249 ]
250}
251
252pub fn retry_option_fields() -> Vec<OptionField> {
256 vec![
257 OptionField::new(
258 "retries",
259 OptionType::Integer,
260 "how many times to try again after a temporary error such as a rate limit (0: never)",
261 )
262 .range(Some(0.0), None),
263 OptionField::new(
264 "retry_wait",
265 OptionType::String,
266 "the first wait before trying again (`30s`), doubling each try; a server's Retry-After wins",
267 ),
268 OptionField::new(
269 "max_retry_wait",
270 OptionType::String,
271 "the longest wait between tries (`10m`)",
272 ),
273 ]
274}
275
276pub fn option_fields() -> Vec<OptionField> {
278 vec![
279 OptionField::new(
280 "if_exists",
281 OptionType::String,
282 "when a file is already at the path: `overwrite` it, fail with `error`, or `number` the new one",
283 )
284 .choices(&["overwrite", "error", "number"])
285 .default("overwrite"),
286 OptionField::new(
287 "atomic",
288 OptionType::Boolean,
289 "upload under a temporary name, then rename, so no half-written file appears",
290 )
291 .default(true),
292 OptionField::new(
293 "temp_dir",
294 OptionType::String,
295 "where the temporary file goes (on the same server), for receivers that pick up any new file",
296 ),
297 ]
298 .into_iter()
299 .chain(retry_option_fields())
300 .collect()
301}
302
303#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
305pub struct Caps {
306 pub create_exclusive: bool,
308 pub rename_no_replace: bool,
310 pub visible_when_complete: bool,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
316pub enum StoreError {
317 Exists,
319 Temporary {
322 message: String,
323 retry_after: Option<Duration>,
324 },
325 Failed(String),
327}
328
329impl StoreError {
330 pub fn temporary(message: impl Into<String>) -> StoreError {
331 StoreError::Temporary {
332 message: message.into(),
333 retry_after: None,
334 }
335 }
336}
337
338impl std::fmt::Display for StoreError {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 match self {
341 StoreError::Exists => f.write_str("a file is already there"),
342 StoreError::Temporary { message, .. } | StoreError::Failed(message) => f.write_str(message),
343 }
344 }
345}
346
347pub trait Store {
349 fn caps(&self) -> Caps;
350 fn write(&mut self, local: &Path, remote: &str, exclusive: bool) -> Result<(), StoreError>;
353 fn rename(&mut self, from: &str, to: &str, replace: bool) -> Result<(), StoreError>;
356 fn exists(&mut self, remote: &str) -> Result<bool, StoreError>;
357 fn delete(&mut self, remote: &str) -> Result<(), StoreError>;
359 fn reset(&mut self) {}
362}
363
364static ATTEMPTS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
367
368fn note_attempts(n: u32) {
369 ATTEMPTS.fetch_max(n, std::sync::atomic::Ordering::Relaxed);
370}
371
372pub fn take_attempts() -> u32 {
374 ATTEMPTS.swap(0, std::sync::atomic::Ordering::Relaxed).max(1)
375}
376
377#[derive(Debug)]
379pub enum Retry<E> {
380 Temporary(E, Option<Duration>),
383 Fail(E),
386}
387
388pub fn retry<T, E: std::fmt::Display>(
392 retries: u32,
393 what: &str,
394 f: impl FnMut() -> Result<T, Retry<E>>,
395) -> Result<T, E> {
396 retry_with(retries, what, &mut std::thread::sleep, f)
397}
398
399pub fn retry_rules<T, E: std::fmt::Display>(
401 rules: &Rules,
402 what: &str,
403 mut f: impl FnMut() -> Result<T, Retry<E>>,
404) -> Result<T, E> {
405 let mut attempt = 1;
406 loop {
407 let r = f();
408 note_attempts(attempt);
409 match r {
410 Ok(v) => return Ok(v),
411 Err(Retry::Temporary(e, retry_after)) if attempt <= rules.retries => {
412 let wait = rules.wait(attempt, retry_after);
413 crate::log::info!(
414 "{what}: {e}; trying again in {}s (attempt {} of {})",
415 wait.as_secs(),
416 attempt + 1,
417 rules.retries + 1
418 );
419 std::thread::sleep(wait);
420 attempt += 1;
421 }
422 Err(Retry::Temporary(e, _) | Retry::Fail(e)) => return Err(e),
423 }
424 }
425}
426
427pub fn retry_with<T, E: std::fmt::Display>(
429 retries: u32,
430 what: &str,
431 sleep: &mut dyn FnMut(Duration),
432 mut f: impl FnMut() -> Result<T, Retry<E>>,
433) -> Result<T, E> {
434 let mut attempt = 1;
435 loop {
436 let r = f();
437 note_attempts(attempt);
438 match r {
439 Ok(v) => return Ok(v),
440 Err(Retry::Temporary(e, retry_after)) if attempt <= retries => {
441 let wait = retry_after.unwrap_or_else(|| backoff(attempt));
442 crate::log::info!(
443 "{what}: {e}; trying again in {}s (attempt {} of {})",
444 wait.as_secs(),
445 attempt + 1,
446 retries + 1
447 );
448 sleep(wait);
449 attempt += 1;
450 }
451 Err(Retry::Temporary(e, _) | Retry::Fail(e)) => return Err(e),
452 }
453 }
454}
455
456pub fn is_connection_error(e: &std::io::Error) -> bool {
459 use std::io::ErrorKind::*;
460 matches!(
461 e.kind(),
462 ConnectionRefused
463 | ConnectionReset
464 | ConnectionAborted
465 | NotConnected
466 | BrokenPipe
467 | TimedOut
468 | UnexpectedEof
469 | HostUnreachable
470 | NetworkUnreachable
471 | NetworkDown
472 )
473}
474
475pub fn retry_after(header: Option<&str>) -> Option<Duration> {
477 header?.trim().parse::<u64>().ok().map(Duration::from_secs)
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct Delivered {
483 pub path: String,
485 pub attempts: u32,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
491pub struct DeliveryError {
492 pub message: String,
493 pub exists: bool,
495 pub attempts: u32,
496}
497
498impl std::fmt::Display for DeliveryError {
499 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500 f.write_str(&self.message)
501 }
502}
503
504impl std::error::Error for DeliveryError {}
505
506impl From<DeliveryError> for PluginError {
507 fn from(e: DeliveryError) -> PluginError {
508 let pe = PluginError::new(ErrorKind::Delivery, e.message);
509 if e.exists { pe.code("file-exists") } else { pe }
510 }
511}
512
513pub fn deliver(
515 store: &mut dyn Store,
516 local: &Path,
517 remote: &str,
518 rules: &Rules,
519) -> Result<Delivered, DeliveryError> {
520 deliver_with(store, local, remote, rules, &mut std::thread::sleep)
521}
522
523pub fn deliver_with(
525 store: &mut dyn Store,
526 local: &Path,
527 remote: &str,
528 rules: &Rules,
529 sleep: &mut dyn FnMut(Duration),
530) -> Result<Delivered, DeliveryError> {
531 let mut attempt = 1;
532 loop {
533 let r = try_once(store, local, remote, rules);
534 note_attempts(attempt);
535 match r {
536 Ok(path) => {
537 return Ok(Delivered {
538 path,
539 attempts: attempt,
540 });
541 }
542 Err(StoreError::Temporary { message, retry_after }) if attempt <= rules.retries => {
543 let wait = rules.wait(attempt, retry_after);
544 crate::log::info!(
545 "{remote}: {message}; trying again in {}s (attempt {} of {})",
546 wait.as_secs(),
547 attempt + 1,
548 rules.retries + 1
549 );
550 store.reset();
551 sleep(wait);
552 attempt += 1;
553 }
554 Err(e) => {
555 let exists = e == StoreError::Exists;
556 let message = if exists {
557 format!("{remote} already exists (`if_exists: error`)")
558 } else if attempt > 1 {
559 format!("{e} (after {attempt} tries)")
560 } else {
561 e.to_string()
562 };
563 return Err(DeliveryError {
564 message,
565 exists,
566 attempts: attempt,
567 });
568 }
569 }
570 }
571}
572
573fn backoff(attempt: u32) -> Duration {
575 let base = 4f64.powi(attempt as i32 - 1);
576 let nanos = std::time::SystemTime::now()
577 .duration_since(std::time::UNIX_EPOCH)
578 .map(|d| d.subsec_nanos())
579 .unwrap_or(0);
580 let jitter = 0.75 + (nanos % 1000) as f64 / 2000.0;
581 Duration::from_secs_f64(base * jitter)
582}
583
584fn try_once(store: &mut dyn Store, local: &Path, remote: &str, rules: &Rules) -> Result<String, StoreError> {
586 let caps = store.caps();
587 let candidates = |i: u32| numbered(remote, i);
588 let limit = if rules.if_exists == IfExists::Number {
589 1000
590 } else {
591 1
592 };
593 if rules.atomic && !caps.visible_when_complete {
594 let temp = temp_path(remote, rules.temp_dir.as_deref());
595 if let Err(e) = store.write(local, &temp, false) {
596 let _ = store.delete(&temp);
597 return Err(e);
598 }
599 let replace = rules.if_exists == IfExists::Overwrite;
600 for i in 1..=limit {
601 let to = candidates(i);
602 let r = if !replace && !caps.rename_no_replace {
603 match store.exists(&to) {
605 Ok(true) => Err(StoreError::Exists),
606 Ok(false) => store.rename(&temp, &to, false),
607 Err(e) => Err(e),
608 }
609 } else {
610 store.rename(&temp, &to, replace)
611 };
612 match r {
613 Ok(()) => return Ok(to),
614 Err(StoreError::Exists) if i < limit => continue,
615 Err(e) => {
616 let _ = store.delete(&temp);
617 return Err(e);
618 }
619 }
620 }
621 unreachable!("the loop returns on its last candidate")
622 }
623 let exclusive = rules.if_exists != IfExists::Overwrite;
624 for i in 1..=limit {
625 let to = candidates(i);
626 let r = if exclusive && !caps.create_exclusive {
627 match store.exists(&to) {
628 Ok(true) => Err(StoreError::Exists),
629 Ok(false) => store.write(local, &to, false),
630 Err(e) => Err(e),
631 }
632 } else {
633 store.write(local, &to, exclusive)
634 };
635 match r {
636 Ok(()) => return Ok(to),
637 Err(StoreError::Exists) if i < limit => continue,
638 Err(e) => return Err(e),
639 }
640 }
641 unreachable!("the loop returns on its last candidate")
642}
643
644fn numbered(remote: &str, i: u32) -> String {
646 if i == 1 {
647 return remote.to_string();
648 }
649 let (dir, file) = split(remote);
650 let (stem, ext) = match file.rfind('.') {
651 Some(p) if p > 0 => (&file[..p], &file[p..]),
652 _ => (file, ""),
653 };
654 format!("{dir}{stem}_{i}{ext}")
655}
656
657fn temp_path(remote: &str, temp_dir: Option<&str>) -> String {
659 let (dir, file) = split(remote);
660 let name = format!(".{file}.dre-part");
661 match temp_dir {
662 None => format!("{dir}{name}"),
663 Some(t) if t.starts_with('/') || Path::new(t).is_absolute() => {
664 format!("{}/{name}", t.trim_end_matches(['/', '\\']))
665 }
666 Some(t) => format!("{dir}{}/{name}", t.trim_end_matches(['/', '\\'])),
667 }
668}
669
670fn split(remote: &str) -> (&str, &str) {
672 match remote.rfind(['/', '\\']) {
673 Some(p) => (&remote[..=p], &remote[p + 1..]),
674 None => ("", remote),
675 }
676}
677
678#[derive(Debug, Default)]
680pub struct LocalStore;
681
682fn io_failed(what: &str, path: &str, e: std::io::Error) -> StoreError {
683 if e.kind() == std::io::ErrorKind::AlreadyExists {
684 StoreError::Exists
685 } else {
686 StoreError::Failed(format!("can't {what} {path}: {e}"))
687 }
688}
689
690fn make_parent(path: &str) -> Result<(), StoreError> {
691 match Path::new(path).parent() {
692 Some(p) if !p.as_os_str().is_empty() => std::fs::create_dir_all(p)
693 .map_err(|e| StoreError::Failed(format!("can't create {}: {e}", p.display()))),
694 _ => Ok(()),
695 }
696}
697
698impl Store for LocalStore {
699 fn caps(&self) -> Caps {
700 Caps {
701 create_exclusive: true,
702 rename_no_replace: true,
703 visible_when_complete: false,
704 }
705 }
706
707 fn write(&mut self, local: &Path, remote: &str, exclusive: bool) -> Result<(), StoreError> {
708 make_parent(remote)?;
709 if !exclusive {
710 return std::fs::copy(local, remote)
711 .map(|_| ())
712 .map_err(|e| io_failed("write", remote, e));
713 }
714 let mut src = std::fs::File::open(local)
715 .map_err(|e| StoreError::Failed(format!("can't read {}: {e}", local.display())))?;
716 let mut dst = std::fs::OpenOptions::new()
717 .write(true)
718 .create_new(true)
719 .open(remote)
720 .map_err(|e| io_failed("write", remote, e))?;
721 std::io::copy(&mut src, &mut dst).map_err(|e| {
722 let _ = std::fs::remove_file(remote);
723 io_failed("write", remote, e)
724 })?;
725 Ok(())
726 }
727
728 fn rename(&mut self, from: &str, to: &str, replace: bool) -> Result<(), StoreError> {
729 make_parent(to)?;
730 if replace {
731 return std::fs::rename(from, to).map_err(|e| io_failed("rename to", to, e));
732 }
733 std::fs::hard_link(from, to).map_err(|e| io_failed("rename to", to, e))?;
735 std::fs::remove_file(from).map_err(|e| io_failed("remove", from, e))
736 }
737
738 fn exists(&mut self, remote: &str) -> Result<bool, StoreError> {
739 Ok(Path::new(remote).exists())
740 }
741
742 fn delete(&mut self, remote: &str) -> Result<(), StoreError> {
743 match std::fs::remove_file(remote) {
744 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(io_failed("remove", remote, e)),
745 _ => Ok(()),
746 }
747 }
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753 use serde_json::json;
754 use std::collections::BTreeMap;
755
756 #[derive(Default)]
758 struct Fake {
759 caps: Caps,
760 files: BTreeMap<String, String>,
761 flaky: u32,
763 break_rename: bool,
765 log: Vec<String>,
766 }
767
768 impl Store for Fake {
769 fn caps(&self) -> Caps {
770 self.caps
771 }
772 fn write(&mut self, local: &Path, remote: &str, exclusive: bool) -> Result<(), StoreError> {
773 self.log
774 .push(format!("write {remote}{}", if exclusive { " excl" } else { "" }));
775 if self.flaky > 0 {
776 self.flaky -= 1;
777 return Err(StoreError::temporary("connection reset"));
778 }
779 if exclusive && self.files.contains_key(remote) {
780 return Err(StoreError::Exists);
781 }
782 self.files.insert(remote.into(), local.display().to_string());
783 Ok(())
784 }
785 fn rename(&mut self, from: &str, to: &str, replace: bool) -> Result<(), StoreError> {
786 self.log.push(format!(
787 "rename {from} {to}{}",
788 if replace { " replace" } else { "" }
789 ));
790 if std::mem::take(&mut self.break_rename) {
791 return Err(StoreError::Failed("connection dropped".into()));
792 }
793 if !replace && self.files.contains_key(to) {
794 return Err(StoreError::Exists);
795 }
796 let v = self
797 .files
798 .remove(from)
799 .ok_or(StoreError::Failed("no such file".into()))?;
800 self.files.insert(to.into(), v);
801 Ok(())
802 }
803 fn exists(&mut self, remote: &str) -> Result<bool, StoreError> {
804 self.log.push(format!("exists {remote}"));
805 Ok(self.files.contains_key(remote))
806 }
807 fn delete(&mut self, remote: &str) -> Result<(), StoreError> {
808 self.files.remove(remote);
809 Ok(())
810 }
811 }
812
813 fn all_caps() -> Caps {
814 Caps {
815 create_exclusive: true,
816 rename_no_replace: true,
817 visible_when_complete: false,
818 }
819 }
820
821 fn run(store: &mut Fake, rules: &Rules) -> Result<Delivered, DeliveryError> {
822 deliver_with(store, Path::new("new"), "out/r.xlsx", rules, &mut |_| {})
823 }
824
825 fn rules(if_exists: IfExists, atomic: bool) -> Rules {
826 Rules {
827 if_exists,
828 atomic,
829 ..Rules::default()
830 }
831 }
832
833 #[test]
834 fn legacy_rules_write_straight_to_the_final_name() {
835 let mut s = Fake::default();
836 let d = run(&mut s, &Rules::legacy()).unwrap();
837 assert_eq!((d.path.as_str(), d.attempts), ("out/r.xlsx", 1));
838 assert_eq!(s.log, ["write out/r.xlsx"]);
839 }
840
841 #[test]
842 fn atomic_uploads_under_a_temporary_name_then_renames() {
843 let mut s = Fake {
844 caps: all_caps(),
845 ..Fake::default()
846 };
847 s.files.insert("out/r.xlsx".into(), "old".into());
848 run(&mut s, &rules(IfExists::Overwrite, true)).unwrap();
849 assert_eq!(
850 s.log,
851 [
852 "write out/.r.xlsx.dre-part",
853 "rename out/.r.xlsx.dre-part out/r.xlsx replace"
854 ]
855 );
856 assert_eq!(s.files.keys().collect::<Vec<_>>(), ["out/r.xlsx"]);
857 assert_eq!(s.files["out/r.xlsx"], "new");
858 }
859
860 #[test]
861 fn a_dropped_rename_leaves_no_file_at_the_final_name_or_temporary_one() {
862 let mut s = Fake {
863 caps: all_caps(),
864 break_rename: true,
865 ..Fake::default()
866 };
867 assert!(run(&mut s, &rules(IfExists::Overwrite, true)).is_err());
868 assert!(s.files.is_empty(), "{:?}", s.files);
869 }
870
871 #[test]
872 fn temp_dir_holds_the_temporary_file() {
873 let mut s = Fake {
874 caps: all_caps(),
875 ..Fake::default()
876 };
877 let mut r = rules(IfExists::Overwrite, true);
878 r.temp_dir = Some("../staging".into());
879 run(&mut s, &r).unwrap();
880 assert_eq!(s.log[0], "write out/../staging/.r.xlsx.dre-part");
881 r.temp_dir = Some("/tmp/".into());
882 s.log.clear();
883 run(&mut s, &r).unwrap();
884 assert_eq!(s.log[0], "write /tmp/.r.xlsx.dre-part");
885 }
886
887 #[test]
888 fn error_fails_when_the_file_exists_and_cleans_up() {
889 for atomic in [false, true] {
890 let mut s = Fake {
891 caps: all_caps(),
892 ..Fake::default()
893 };
894 s.files.insert("out/r.xlsx".into(), "old".into());
895 let e = run(&mut s, &rules(IfExists::Error, atomic)).unwrap_err();
896 assert!(e.exists && e.message.contains("already exists"), "{e:?}");
897 assert_eq!(s.files.len(), 1, "atomic={atomic}: {:?}", s.files);
898 assert_eq!(s.files["out/r.xlsx"], "old");
899 let pe = PluginError::from(e);
900 assert_eq!(
901 (pe.kind, pe.code.as_deref()),
902 (ErrorKind::Delivery, Some("file-exists"))
903 );
904 }
905 }
906
907 #[test]
908 fn number_keeps_both_files() {
909 for (atomic, caps) in [
910 (false, all_caps()),
911 (true, all_caps()),
912 (false, Caps::default()),
913 (true, Caps::default()),
914 ] {
915 let mut s = Fake {
916 caps,
917 ..Fake::default()
918 };
919 s.files.insert("out/r.xlsx".into(), "old".into());
920 s.files.insert("out/r_2.xlsx".into(), "old2".into());
921 let d = run(&mut s, &rules(IfExists::Number, atomic)).unwrap();
922 assert_eq!(d.path, "out/r_3.xlsx", "atomic={atomic} caps={caps:?}");
923 assert_eq!(s.files["out/r_3.xlsx"], "new");
924 assert_eq!(s.files.len(), 3, "{:?}", s.files);
925 }
926 }
927
928 #[test]
929 fn without_single_step_checks_it_looks_first() {
930 let mut s = Fake::default();
931 s.files.insert("out/r.xlsx".into(), "old".into());
932 assert!(run(&mut s, &rules(IfExists::Error, false)).unwrap_err().exists);
933 assert_eq!(s.log, ["exists out/r.xlsx"]);
934 }
935
936 #[test]
937 fn temporary_errors_are_retried_then_give_up() {
938 let mut s = Fake {
939 flaky: 2,
940 ..Fake::default()
941 };
942 let mut waits = Vec::new();
943 let d = deliver_with(&mut s, Path::new("new"), "r.csv", &Rules::default(), &mut |w| {
944 waits.push(w)
945 })
946 .unwrap();
947 assert_eq!(d.attempts, 3);
948 assert_eq!(waits.len(), 2);
949 assert!(waits[0] < waits[1]);
950 let mut s = Fake {
951 flaky: 9,
952 ..Fake::default()
953 };
954 let e = deliver_with(&mut s, Path::new("new"), "r.csv", &Rules::default(), &mut |_| {}).unwrap_err();
955 assert_eq!(e.attempts, 4);
956 assert!(e.message.contains("after 4 tries"), "{e}");
957 let no_retry = Rules {
958 retries: 0,
959 ..Rules::default()
960 };
961 let mut s = Fake {
962 flaky: 1,
963 ..Fake::default()
964 };
965 assert_eq!(run(&mut s, &no_retry).unwrap_err().attempts, 1);
966 }
967
968 #[test]
969 fn object_stores_need_no_temporary_name() {
970 let mut s = Fake {
971 caps: Caps {
972 create_exclusive: true,
973 visible_when_complete: true,
974 ..Caps::default()
975 },
976 ..Fake::default()
977 };
978 run(&mut s, &rules(IfExists::Error, true)).unwrap();
979 assert_eq!(s.log, ["write out/r.xlsx excl"]);
980 }
981
982 #[test]
983 fn settings_parse_with_aliases_and_errors() {
984 let conn = json!({"connect_timeout": "2m", "upload_timeout": 90, "retries": "0"});
985 let opts = json!({"if_exists": "number", "atomic": false, "temp_dir": "../in"});
986 let (r, warnings) = Rules::from_settings(
987 Rules::default(),
988 conn.as_object().unwrap(),
989 opts.as_object().unwrap(),
990 &[("upload_timeout", "timeout")],
991 )
992 .unwrap();
993 assert_eq!(r.if_exists, IfExists::Number);
994 assert!(!r.atomic);
995 assert_eq!(r.temp_dir.as_deref(), Some("../in"));
996 assert_eq!(r.retries, 0);
997 assert_eq!(r.connect_timeout, Duration::from_secs(120));
998 assert_eq!(r.timeout, Duration::from_secs(90));
999 assert_eq!(warnings, ["`upload_timeout` is deprecated; use `timeout`"]);
1000 let bad = json!({"if_exists": "keep"});
1001 assert!(
1002 Rules::from_settings(Rules::default(), &Map::new(), bad.as_object().unwrap(), &[])
1003 .unwrap_err()
1004 .contains("if_exists")
1005 );
1006 }
1007
1008 #[test]
1009 fn durations_take_units_or_seconds() {
1010 for (v, secs) in [
1011 (json!("30s"), 30.0),
1012 (json!("5m"), 300.0),
1013 (json!("1h"), 3600.0),
1014 (json!("1500ms"), 1.5),
1015 (json!(45), 45.0),
1016 (json!("7"), 7.0),
1017 ] {
1018 assert_eq!(parse_duration(&v).unwrap(), Duration::from_secs_f64(secs), "{v}");
1019 }
1020 for v in [json!("soon"), json!("5 days"), json!(-1), json!(true)] {
1021 assert!(parse_duration(&v).is_err(), "{v}");
1022 }
1023 }
1024
1025 #[test]
1026 fn names_number_and_hide_the_temporary_file() {
1027 assert_eq!(numbered("out/r.xlsx", 2), "out/r_2.xlsx");
1028 assert_eq!(numbered("r", 3), "r_3");
1029 assert_eq!(numbered("a.b/.env", 2), "a.b/.env_2");
1030 assert_eq!(numbered("x.tar.gz", 2), "x.tar_2.gz");
1031 assert_eq!(temp_path("r.csv", None), ".r.csv.dre-part");
1032 }
1033
1034 #[test]
1035 fn the_local_store_follows_every_rule() {
1036 let dir = tempfile::tempdir().unwrap();
1037 let src = dir.path().join("src.csv");
1038 std::fs::write(&src, "new").unwrap();
1039 let dst = dir.path().join("out/r.csv");
1040 let dst = dst.to_str().unwrap();
1041 let mut s = LocalStore;
1042 deliver(&mut s, &src, dst, &Rules::legacy()).unwrap();
1043 assert_eq!(std::fs::read_to_string(dst).unwrap(), "new");
1044 std::fs::write(&src, "newer").unwrap();
1045 deliver(&mut s, &src, dst, &rules(IfExists::Overwrite, true)).unwrap();
1046 assert_eq!(std::fs::read_to_string(dst).unwrap(), "newer");
1047 let e = deliver(&mut s, &src, dst, &rules(IfExists::Error, true)).unwrap_err();
1048 assert!(e.exists);
1049 let d = deliver(&mut s, &src, dst, &rules(IfExists::Number, true)).unwrap();
1050 assert!(d.path.ends_with("r_2.csv"), "{d:?}");
1051 let d = deliver(&mut s, &src, dst, &rules(IfExists::Number, false)).unwrap();
1052 assert!(d.path.ends_with("r_3.csv"), "{d:?}");
1053 let mut names: Vec<String> = std::fs::read_dir(dir.path().join("out"))
1054 .unwrap()
1055 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
1056 .collect();
1057 names.sort();
1058 assert_eq!(names, ["r.csv", "r_2.csv", "r_3.csv"]);
1059 }
1060}
1061
1062pub fn content_type(name: &str) -> &'static str {
1065 let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
1066 match ext.as_deref() {
1067 Some("csv") => "text/csv",
1068 Some("txt" | "dat" | "tsv" | "aba") => "text/plain",
1069 Some("md") => "text/markdown",
1070 Some("html" | "htm") => "text/html",
1071 Some("pdf") => "application/pdf",
1072 Some("xlsx") => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1073 Some("parquet") => "application/vnd.apache.parquet",
1074 Some("json") => "application/json",
1075 _ => "application/octet-stream",
1076 }
1077}
1078
1079#[cfg(test)]
1080mod content_type_tests {
1081 #[test]
1082 fn documents_have_their_types() {
1083 assert_eq!(super::content_type("s/101.PDF"), "application/pdf");
1084 assert_eq!(super::content_type("a.html"), "text/html");
1085 assert_eq!(super::content_type("a.bin"), "application/octet-stream");
1086 }
1087}
1088
1089#[cfg(test)]
1090mod retry_wait_tests {
1091 use std::time::Duration;
1092
1093 use serde_json::json;
1094
1095 use super::Rules;
1096
1097 #[test]
1098 fn waits_double_from_retry_wait_capped_and_retry_after_wins() {
1099 let opts = json!({"retry_wait": "30s", "max_retry_wait": "2m"});
1100 let (r, _) = Rules::from_settings(
1101 Rules::default(),
1102 &Default::default(),
1103 opts.as_object().unwrap(),
1104 &[],
1105 )
1106 .unwrap();
1107 assert_eq!(r.wait(1, None), Duration::from_secs(30));
1108 assert_eq!(r.wait(2, None), Duration::from_secs(60));
1109 assert_eq!(r.wait(3, None), Duration::from_secs(120));
1110 assert_eq!(r.wait(4, None), Duration::from_secs(120));
1111 assert_eq!(r.wait(1, Some(Duration::from_secs(5))), Duration::from_secs(5));
1112 assert_eq!(
1113 r.wait(1, Some(Duration::from_secs(900))),
1114 Duration::from_secs(120)
1115 );
1116 }
1117}