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(String::from_utf8(decoded).map_err(|e| {
75 TusError::InvalidMetadata(format!("non-UTF8 value for key {key}: {e}"))
76 })?)
77 }
78 };
79 map.insert(key, value);
80 }
81 Ok(Self(map))
82 }
83
84 pub fn encode(&self) -> String {
86 self.0
87 .iter()
88 .map(|(k, v)| match v {
89 None => k.clone(),
90 Some(val) => format!("{k} {}", STANDARD.encode(val)),
91 })
92 .collect::<Vec<_>>()
93 .join(",")
94 }
95
96 pub fn get(&self, key: &str) -> Option<&Option<String>> {
97 self.0.get(key)
98 }
99
100 pub fn is_empty(&self) -> bool {
101 self.0.is_empty()
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct UploadInfo {
108 pub id: UploadId,
109 pub size: Option<u64>,
111 pub offset: u64,
113 pub metadata: Metadata,
114 pub size_is_deferred: bool,
116 pub expires_at: Option<DateTime<Utc>>,
118 pub is_partial: bool,
120 pub is_final: bool,
122 pub partial_uploads: Vec<UploadId>,
124 pub storage: HashMap<String, String>,
126}
127
128impl UploadInfo {
129 pub fn new(id: UploadId, size: Option<u64>) -> Self {
130 Self {
131 id,
132 size,
133 offset: 0,
134 metadata: Metadata::default(),
135 size_is_deferred: size.is_none(),
136 expires_at: None,
137 is_partial: false,
138 is_final: false,
139 partial_uploads: Vec::new(),
140 storage: HashMap::new(),
141 }
142 }
143
144 pub fn is_complete(&self) -> bool {
146 match self.size {
147 Some(s) => self.offset == s,
148 None => false,
149 }
150 }
151}
152
153#[derive(Debug, Default)]
155pub struct UploadInfoChanges {
156 pub id: Option<UploadId>,
157 pub metadata: Option<Metadata>,
158 pub storage: Option<HashMap<String, String>>,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn metadata_parse_key_value() {
167 let m = Metadata::parse("filename dGVzdC50eHQ=,type dGV4dC9wbGFpbg==").unwrap();
168 assert_eq!(m.0["filename"], Some("test.txt".into()));
169 assert_eq!(m.0["type"], Some("text/plain".into()));
170 }
171
172 #[test]
173 fn metadata_parse_key_only() {
174 let m = Metadata::parse("is_private").unwrap();
175 assert_eq!(m.0["is_private"], None);
176 }
177
178 #[test]
179 fn metadata_parse_mixed() {
180 let m = Metadata::parse("filename dGVzdC50eHQ=,is_private,size MTAyNA==").unwrap();
181 assert_eq!(m.0["filename"], Some("test.txt".into()));
182 assert_eq!(m.0["is_private"], None);
183 assert_eq!(m.0["size"], Some("1024".into()));
184 }
185
186 #[test]
187 fn metadata_parse_empty_string() {
188 let m = Metadata::parse("").unwrap();
189 assert!(m.0.is_empty());
190 }
191
192 #[test]
193 fn metadata_parse_invalid_base64() {
194 assert!(Metadata::parse("key not!!valid_b64").is_err());
195 }
196
197 #[test]
198 fn metadata_roundtrip() {
199 let original = "filename dGVzdC50eHQ=";
200 let m = Metadata::parse(original).unwrap();
201 let encoded = m.encode();
202 let m2 = Metadata::parse(&encoded).unwrap();
203 assert_eq!(m.0["filename"], m2.0["filename"]);
204 }
205
206 #[test]
207 fn upload_info_is_complete() {
208 let mut info = UploadInfo::new(UploadId::new(), Some(100));
209 assert!(!info.is_complete());
210 info.offset = 100;
211 assert!(info.is_complete());
212 }
213
214 #[test]
215 fn upload_info_deferred_never_complete_until_size_set() {
216 let info = UploadInfo::new(UploadId::new(), None);
217 assert!(!info.is_complete());
218 }
219}