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 as_str(&self) -> &str {
19 &self.0
20 }
21}
22
23impl Default for UploadId {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl std::fmt::Display for UploadId {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.write_str(&self.0)
32 }
33}
34
35impl From<String> for UploadId {
36 fn from(s: String) -> Self {
37 Self(s)
38 }
39}
40
41impl From<&str> for UploadId {
42 fn from(s: &str) -> Self {
43 Self(s.to_string())
44 }
45}
46
47#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct Metadata(pub HashMap<String, Option<String>>);
53
54impl Metadata {
55 pub fn parse(header: &str) -> Result<Self, TusError> {
57 let mut map = HashMap::new();
58 for pair in header.split(',') {
59 let pair = pair.trim();
60 if pair.is_empty() {
61 continue;
62 }
63 let mut parts = pair.splitn(2, ' ');
64 let key = parts.next().unwrap_or("").trim().to_string();
65 if key.is_empty() {
66 return Err(TusError::InvalidMetadata("empty key in metadata".into()));
67 }
68 let value = match parts.next() {
69 None | Some("") => None,
70 Some(b64) => {
71 let decoded = STANDARD
72 .decode(b64.trim())
73 .map_err(|e| TusError::InvalidMetadata(e.to_string()))?;
74 Some(
75 String::from_utf8(decoded).map_err(|e| {
76 TusError::InvalidMetadata(format!("non-UTF8 value for key {key}: {e}"))
77 })?,
78 )
79 }
80 };
81 map.insert(key, value);
82 }
83 Ok(Self(map))
84 }
85
86 pub fn encode(&self) -> String {
88 self.0
89 .iter()
90 .map(|(k, v)| match v {
91 None => k.clone(),
92 Some(val) => format!("{k} {}", STANDARD.encode(val)),
93 })
94 .collect::<Vec<_>>()
95 .join(",")
96 }
97
98 pub fn get(&self, key: &str) -> Option<&Option<String>> {
99 self.0.get(key)
100 }
101
102 pub fn is_empty(&self) -> bool {
103 self.0.is_empty()
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct UploadInfo {
110 pub id: UploadId,
111 pub size: Option<u64>,
113 pub offset: u64,
115 pub metadata: Metadata,
116 pub size_is_deferred: bool,
118 pub expires_at: Option<DateTime<Utc>>,
120 pub is_partial: bool,
122 pub is_final: bool,
124 pub partial_uploads: Vec<UploadId>,
126 pub storage: HashMap<String, String>,
128}
129
130impl UploadInfo {
131 pub fn new(id: UploadId, size: Option<u64>) -> Self {
132 Self {
133 id,
134 size,
135 offset: 0,
136 metadata: Metadata::default(),
137 size_is_deferred: size.is_none(),
138 expires_at: None,
139 is_partial: false,
140 is_final: false,
141 partial_uploads: Vec::new(),
142 storage: HashMap::new(),
143 }
144 }
145
146 pub fn is_complete(&self) -> bool {
148 match self.size {
149 Some(s) => self.offset == s,
150 None => false,
151 }
152 }
153}
154
155#[derive(Debug, Default)]
157pub struct UploadInfoChanges {
158 pub id: Option<UploadId>,
159 pub metadata: Option<Metadata>,
160 pub storage: Option<HashMap<String, String>>,
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn metadata_parse_key_value() {
169 let m = Metadata::parse("filename dGVzdC50eHQ=,type dGV4dC9wbGFpbg==").unwrap();
170 assert_eq!(m.0["filename"], Some("test.txt".into()));
171 assert_eq!(m.0["type"], Some("text/plain".into()));
172 }
173
174 #[test]
175 fn metadata_parse_key_only() {
176 let m = Metadata::parse("is_private").unwrap();
177 assert_eq!(m.0["is_private"], None);
178 }
179
180 #[test]
181 fn metadata_parse_mixed() {
182 let m = Metadata::parse("filename dGVzdC50eHQ=,is_private,size MTAyNA==").unwrap();
183 assert_eq!(m.0["filename"], Some("test.txt".into()));
184 assert_eq!(m.0["is_private"], None);
185 assert_eq!(m.0["size"], Some("1024".into()));
186 }
187
188 #[test]
189 fn metadata_parse_empty_string() {
190 let m = Metadata::parse("").unwrap();
191 assert!(m.0.is_empty());
192 }
193
194 #[test]
195 fn metadata_parse_invalid_base64() {
196 assert!(Metadata::parse("key not!!valid_b64").is_err());
197 }
198
199 #[test]
200 fn metadata_roundtrip() {
201 let original = "filename dGVzdC50eHQ=";
202 let m = Metadata::parse(original).unwrap();
203 let encoded = m.encode();
204 let m2 = Metadata::parse(&encoded).unwrap();
205 assert_eq!(m.0["filename"], m2.0["filename"]);
206 }
207
208 #[test]
209 fn upload_info_is_complete() {
210 let mut info = UploadInfo::new(UploadId::new(), Some(100));
211 assert!(!info.is_complete());
212 info.offset = 100;
213 assert!(info.is_complete());
214 }
215
216 #[test]
217 fn upload_info_deferred_never_complete_until_size_set() {
218 let info = UploadInfo::new(UploadId::new(), None);
219 assert!(!info.is_complete());
220 }
221}