1use std::fmt;
4use std::io::Read;
5use std::path::PathBuf;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum FileReplacePolicy {
10 ReplaceAll,
12 UpsertByFilename,
14 KeepExistingAndAdd,
16}
17
18pub enum UploadSource {
20 Path(
22 PathBuf,
24 ),
25 Reader {
27 reader: Box<dyn Read + Send>,
29 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#[derive(Debug)]
48pub struct UploadSpec {
49 pub filename: String,
51 pub source: UploadSource,
53}
54
55impl UploadSpec {
56 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 #[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}