1use std::collections::HashMap;
2
3use base64::{engine::general_purpose::STANDARD, Engine};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use crate::error::TusError;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct UploadId(pub String);
12
13impl UploadId {
14 pub fn new() -> Self {
15 Self(uuid::Uuid::new_v4().to_string())
16 }
17
18 pub fn parse(value: &str) -> Result<Self, TusError> {
19 Self::validate_str(value)?;
20 Ok(Self(value.to_string()))
21 }
22
23 pub fn as_str(&self) -> &str {
24 &self.0
25 }
26
27 pub fn validate(&self) -> Result<(), TusError> {
28 Self::validate_str(&self.0)
29 }
30
31 fn validate_str(value: &str) -> Result<(), TusError> {
32 if value.is_empty() || value.len() > 128 {
33 return Err(TusError::InvalidUploadId);
34 }
35 if value == "." || value == ".." {
36 return Err(TusError::InvalidUploadId);
37 }
38 if !value
39 .bytes()
40 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
41 {
42 return Err(TusError::InvalidUploadId);
43 }
44 Ok(())
45 }
46}
47
48impl Default for UploadId {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl std::fmt::Display for UploadId {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.write_str(&self.0)
57 }
58}
59
60impl From<String> for UploadId {
61 fn from(s: String) -> Self {
62 Self(s)
63 }
64}
65
66impl From<&str> for UploadId {
67 fn from(s: &str) -> Self {
68 Self(s.to_string())
69 }
70}
71
72#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct Metadata(pub HashMap<String, Option<String>>);
78
79impl Metadata {
80 pub fn parse(header: &str) -> Result<Self, TusError> {
82 let mut map = HashMap::new();
83 for pair in header.split(',') {
84 let pair = pair.trim();
85 if pair.is_empty() {
86 continue;
87 }
88 let mut parts = pair.splitn(2, ' ');
89 let key = parts.next().unwrap_or("").trim().to_string();
90 if key.is_empty() {
91 return Err(TusError::InvalidMetadata("empty key in metadata".into()));
92 }
93 let value = match parts.next() {
94 None | Some("") => None,
95 Some(b64) => {
96 let decoded = STANDARD
97 .decode(b64.trim())
98 .map_err(|e| TusError::InvalidMetadata(e.to_string()))?;
99 Some(String::from_utf8(decoded).map_err(|e| {
100 TusError::InvalidMetadata(format!("non-UTF8 value for key {key}: {e}"))
101 })?)
102 }
103 };
104 map.insert(key, value);
105 }
106 Ok(Self(map))
107 }
108
109 pub fn encode(&self) -> String {
111 self.0
112 .iter()
113 .map(|(k, v)| match v {
114 None => k.clone(),
115 Some(val) => format!("{k} {}", STANDARD.encode(val)),
116 })
117 .collect::<Vec<_>>()
118 .join(",")
119 }
120
121 pub fn get(&self, key: &str) -> Option<&Option<String>> {
122 self.0.get(key)
123 }
124
125 pub fn is_empty(&self) -> bool {
126 self.0.is_empty()
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct UploadInfo {
133 pub id: UploadId,
134 pub size: Option<u64>,
136 pub offset: u64,
138 pub metadata: Metadata,
139 pub size_is_deferred: bool,
141 pub expires_at: Option<DateTime<Utc>>,
143 pub is_partial: bool,
145 pub is_final: bool,
147 pub partial_uploads: Vec<UploadId>,
149 pub storage: HashMap<String, String>,
151}
152
153impl UploadInfo {
154 pub fn new(id: UploadId, size: Option<u64>) -> Self {
155 Self {
156 id,
157 size,
158 offset: 0,
159 metadata: Metadata::default(),
160 size_is_deferred: size.is_none(),
161 expires_at: None,
162 is_partial: false,
163 is_final: false,
164 partial_uploads: Vec::new(),
165 storage: HashMap::new(),
166 }
167 }
168
169 pub fn is_complete(&self) -> bool {
171 match self.size {
172 Some(s) => self.offset == s,
173 None => false,
174 }
175 }
176}
177
178#[derive(Debug, Default)]
180pub struct UploadInfoChanges {
181 pub id: Option<UploadId>,
182 pub metadata: Option<Metadata>,
183 pub storage: Option<HashMap<String, String>>,
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn metadata_parse_key_value() {
192 let m = Metadata::parse("filename dGVzdC50eHQ=,type dGV4dC9wbGFpbg==").unwrap();
193 assert_eq!(m.0["filename"], Some("test.txt".into()));
194 assert_eq!(m.0["type"], Some("text/plain".into()));
195 }
196
197 #[test]
198 fn metadata_parse_key_only() {
199 let m = Metadata::parse("is_private").unwrap();
200 assert_eq!(m.0["is_private"], None);
201 }
202
203 #[test]
204 fn metadata_parse_mixed() {
205 let m = Metadata::parse("filename dGVzdC50eHQ=,is_private,size MTAyNA==").unwrap();
206 assert_eq!(m.0["filename"], Some("test.txt".into()));
207 assert_eq!(m.0["is_private"], None);
208 assert_eq!(m.0["size"], Some("1024".into()));
209 }
210
211 #[test]
212 fn metadata_parse_empty_string() {
213 let m = Metadata::parse("").unwrap();
214 assert!(m.0.is_empty());
215 }
216
217 #[test]
218 fn metadata_parse_invalid_base64() {
219 assert!(Metadata::parse("key not!!valid_b64").is_err());
220 }
221
222 #[test]
223 fn metadata_roundtrip() {
224 let original = "filename dGVzdC50eHQ=";
225 let m = Metadata::parse(original).unwrap();
226 let encoded = m.encode();
227 let m2 = Metadata::parse(&encoded).unwrap();
228 assert_eq!(m.0["filename"], m2.0["filename"]);
229 }
230
231 #[test]
232 fn upload_info_is_complete() {
233 let mut info = UploadInfo::new(UploadId::new(), Some(100));
234 assert!(!info.is_complete());
235 info.offset = 100;
236 assert!(info.is_complete());
237 }
238
239 #[test]
240 fn upload_info_deferred_never_complete_until_size_set() {
241 let info = UploadInfo::new(UploadId::new(), None);
242 assert!(!info.is_complete());
243 }
244}