Skip to main content

figshare_rs/
upload.rs

1//! Upload input types and file replacement policies.
2
3use std::fmt;
4use std::io::Read;
5use std::path::PathBuf;
6
7/// Policy for reconciling existing article files with new uploads.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum FileReplacePolicy {
10    /// Replace visible article files after the new uploads succeed.
11    ReplaceAll,
12    /// Replace files that share the same filename.
13    UpsertByFilename,
14    /// Keep existing files and add new uploads alongside them.
15    KeepExistingAndAdd,
16}
17
18/// Source data for a single upload.
19pub enum UploadSource {
20    /// Upload from a local file path.
21    Path(
22        /// Local source path.
23        PathBuf,
24    ),
25    /// Upload from a blocking reader with an explicit content length.
26    Reader {
27        /// Reader that produces the upload bytes.
28        reader: Box<dyn Read + Send>,
29        /// Exact number of bytes that the reader will produce.
30        content_length: u64,
31    },
32}
33
34impl fmt::Debug for UploadSource {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::Path(path) => f.debug_tuple("Path").field(path).finish(),
38            Self::Reader { content_length, .. } => f
39                .debug_struct("Reader")
40                .field("content_length", content_length)
41                .finish_non_exhaustive(),
42        }
43    }
44}
45
46/// Specification for one file upload.
47#[derive(Debug)]
48pub struct UploadSpec {
49    /// Filename to expose in Figshare.
50    pub filename: String,
51    /// Upload source.
52    pub source: UploadSource,
53}
54
55impl UploadSpec {
56    /// Builds an upload spec from a local path.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the path does not contain a final filename segment.
61    pub fn from_path(path: impl Into<PathBuf>) -> std::io::Result<Self> {
62        let path = path.into();
63        let filename = path
64            .file_name()
65            .map(|name| name.to_string_lossy().into_owned())
66            .ok_or_else(path_without_filename_error)?;
67
68        Ok(Self {
69            filename,
70            source: UploadSource::Path(path),
71        })
72    }
73
74    /// Builds an upload spec from a reader and explicit metadata.
75    #[must_use]
76    pub fn from_reader(
77        filename: impl Into<String>,
78        reader: impl Read + Send + 'static,
79        content_length: u64,
80    ) -> Self {
81        Self {
82            filename: filename.into(),
83            source: UploadSource::Reader {
84                reader: Box::new(reader),
85                content_length,
86            },
87        }
88    }
89}
90
91fn path_without_filename_error() -> std::io::Error {
92    std::io::Error::new(
93        std::io::ErrorKind::InvalidInput,
94        "path has no final file name segment",
95    )
96}
97
98#[cfg(test)]
99mod tests {
100    use std::path::PathBuf;
101
102    use super::{path_without_filename_error, UploadSource, UploadSpec};
103
104    #[test]
105    fn path_upload_extracts_filename() {
106        let spec = UploadSpec::from_path(PathBuf::from("/tmp/archive.tar.gz")).unwrap();
107        assert_eq!(spec.filename, "archive.tar.gz");
108    }
109
110    #[test]
111    fn path_upload_rejects_missing_filename() {
112        let error = UploadSpec::from_path(PathBuf::from("/")).unwrap_err();
113        assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
114    }
115
116    #[test]
117    fn missing_filename_error_has_stable_message() {
118        let error = path_without_filename_error();
119        assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
120        assert_eq!(error.to_string(), "path has no final file name segment");
121    }
122
123    #[test]
124    fn reader_upload_debug_hides_reader() {
125        let spec = UploadSpec::from_reader("artifact.bin", std::io::Cursor::new(vec![1, 2, 3]), 3);
126
127        match spec.source {
128            UploadSource::Reader { content_length, .. } => assert_eq!(content_length, 3),
129            UploadSource::Path(_) => panic!("expected reader source"),
130        }
131        assert!(format!("{spec:?}").contains("artifact.bin"));
132    }
133
134    #[test]
135    fn path_upload_debug_shows_path_variant() {
136        let spec = UploadSpec::from_path(PathBuf::from("/tmp/archive.tar.gz")).unwrap();
137
138        match &spec.source {
139            UploadSource::Path(path) => assert_eq!(path, &PathBuf::from("/tmp/archive.tar.gz")),
140            UploadSource::Reader { .. } => panic!("expected path source"),
141        }
142        assert!(format!("{:?}", spec.source).contains("archive.tar.gz"));
143    }
144}