1use base64::{engine::general_purpose, Engine};
2use candid::CandidType;
3use serde::{Deserialize, Serialize};
4use serde_bytes::{ByteArray, ByteBuf};
5use std::path::Path;
6use url::Url;
7
8use crate::{format_error, MapValue};
9
10pub const CHUNK_SIZE: u32 = 256 * 1024;
11pub const MAX_FILE_SIZE: u64 = 384 * 1024 * 1024 * 1024; pub const MAX_FILE_SIZE_PER_CALL: u64 = 1024 * 2048; pub static CUSTOM_KEY_BY_HASH: &str = "by_hash";
15
16#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
17pub struct FileInfo {
18 pub id: u32,
19 pub parent: u32, pub name: String,
21 pub content_type: String,
22 pub size: u64,
23 pub filled: u64,
24 pub created_at: u64, pub updated_at: u64, pub chunks: u32,
27 pub status: i8, pub hash: Option<ByteArray<32>>,
29 pub dek: Option<ByteBuf>, pub custom: Option<MapValue>, pub ex: Option<MapValue>, }
33
34#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
35pub struct CreateFileInput {
36 pub parent: u32,
37 pub name: String,
38 pub content_type: String,
39 pub size: Option<u64>, pub content: Option<ByteBuf>, pub status: Option<i8>, pub hash: Option<ByteArray<32>>, pub dek: Option<ByteBuf>,
44 pub custom: Option<MapValue>,
45}
46
47pub fn valid_file_name(name: &str) -> bool {
48 if name.is_empty() || name.trim() != name || name.len() > 96 {
49 return false;
50 }
51
52 let p = Path::new(name);
53 p.file_name() == Some(p.as_os_str())
54}
55
56pub fn valid_file_parent(parent: &str) -> bool {
57 if parent.is_empty() || parent == "/" {
58 return true;
59 }
60
61 if !parent.starts_with('/') {
62 return false;
63 }
64
65 for name in parent[1..].split('/') {
66 if !valid_file_name(name) {
67 return false;
68 }
69 }
70 true
71}
72
73impl CreateFileInput {
74 pub fn validate(&self) -> Result<(), String> {
75 if !valid_file_name(&self.name) {
76 return Err("invalid file name".to_string());
77 }
78
79 if self.content_type.is_empty() {
80 return Err("content_type cannot be empty".to_string());
81 }
82
83 if let Some(content) = &self.content {
84 if content.is_empty() {
85 return Err("content cannot be empty".to_string());
86 }
87 }
88
89 if let Some(status) = self.status {
90 if !(0i8..=1i8).contains(&status) {
91 return Err("status should be 0 or 1".to_string());
92 }
93 }
94 Ok(())
95 }
96}
97
98#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
99pub struct CreateFileOutput {
100 pub id: u32,
101 pub created_at: u64,
102}
103
104#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
105pub struct UpdateFileInput {
106 pub id: u32,
107 pub name: Option<String>,
108 pub content_type: Option<String>,
109 pub status: Option<i8>, pub size: Option<u64>, pub hash: Option<ByteArray<32>>,
112 pub custom: Option<MapValue>,
113}
114
115impl UpdateFileInput {
116 pub fn validate(&self) -> Result<(), String> {
117 if let Some(name) = &self.name {
118 if !valid_file_name(name) {
119 return Err("invalid file name".to_string());
120 }
121 }
122 if let Some(content_type) = &self.content_type {
123 if content_type.is_empty() {
124 return Err("content_type cannot be empty".to_string());
125 }
126 }
127 if let Some(status) = self.status {
128 if !(-1i8..=1i8).contains(&status) {
129 return Err("status should be -1, 0 or 1".to_string());
130 }
131 }
132 Ok(())
133 }
134}
135
136#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
137pub struct UpdateFileOutput {
138 pub updated_at: u64,
139}
140
141#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
142pub struct UpdateFileChunkInput {
143 pub id: u32,
144 pub chunk_index: u32,
145 pub content: ByteBuf, }
147
148#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
149pub struct UpdateFileChunkOutput {
150 pub filled: u64,
151 pub updated_at: u64,
152}
153
154#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
155pub struct FileChunk(pub u32, pub ByteBuf);
156
157#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
158pub struct MoveInput {
159 pub id: u32,
160 pub from: u32,
161 pub to: u32,
162}
163
164#[derive(Debug)]
165pub struct UrlFileParam {
166 pub file: u32,
167 pub hash: Option<ByteArray<32>>,
168 pub token: Option<ByteBuf>,
169 pub name: Option<String>,
170 pub inline: bool,
171}
172
173impl UrlFileParam {
174 pub fn from_url(req_url: &str) -> Result<Self, String> {
175 let url = if req_url.starts_with('/') {
176 Url::parse(format!("http://localhost{}", req_url).as_str())
177 } else {
178 Url::parse(req_url)
179 };
180 let url = url.map_err(|_| format!("invalid url: {}", req_url))?;
181 let mut path_segments = url
182 .path_segments()
183 .ok_or_else(|| format!("invalid url path: {}", req_url))?;
184
185 let mut param = match path_segments.next() {
186 Some("f") => Self {
187 file: path_segments
188 .next()
189 .unwrap_or_default()
190 .parse()
191 .map_err(|_| "invalid file id")?,
192 hash: None,
193 token: None,
194 name: None,
195 inline: false,
196 },
197 Some("h") => {
198 let val = path_segments.next().unwrap_or_default();
199 let data = hex::decode(val).map_err(format_error)?;
200 let hash: [u8; 32] = data.try_into().map_err(format_error)?;
201 let hash = ByteArray::from(hash);
202 Self {
203 file: 0,
204 hash: Some(hash),
205 token: None,
206 name: None,
207 inline: false,
208 }
209 }
210 _ => return Err(format!("invalid url path: {}", req_url)),
211 };
212
213 for (key, value) in url.query_pairs() {
214 match key.as_ref() {
215 "token" => {
216 let data = general_purpose::URL_SAFE_NO_PAD
217 .decode(value.as_bytes())
218 .map_err(|_| format!("failed to decode base64 token from {}", value))?;
219 param.token = Some(ByteBuf::from(data));
220 break;
221 }
222 "filename" => {
223 param.name = Some(value.to_string());
224 }
225 "inline" => {
226 param.inline = true;
227 }
228 _ => {}
229 }
230 }
231
232 if let Some(filename) = path_segments.next() {
234 param.name = Some(filename.to_string());
235 }
236
237 Ok(param)
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn valid_file_name_works() {
247 assert!(valid_file_name("file"));
248 assert!(valid_file_name("file.txt"));
249 assert!(valid_file_name(".file.txt"));
250 assert!(valid_file_name("file.txt."));
251 assert!(valid_file_name("..."));
252
253 assert!(!valid_file_name(""));
254 assert!(!valid_file_name("."));
255 assert!(!valid_file_name(".."));
256 assert!(!valid_file_name(" file.txt"));
257 assert!(!valid_file_name("/file.txt"));
258 assert!(!valid_file_name("./file.txt"));
259 assert!(!valid_file_name("test/file.txt"));
260 assert!(!valid_file_name("file.txt/"));
261 }
262
263 #[test]
264 fn valid_file_parent_works() {
265 assert!(valid_file_parent(""));
266 assert!(valid_file_parent("/"));
267 assert!(valid_file_parent("/file"));
268 assert!(valid_file_parent("/file.txt"));
269 assert!(valid_file_parent("/file/.txt"));
270
271 assert!(!valid_file_parent("file.txt"));
272 assert!(!valid_file_parent("//file.txt"));
273 assert!(!valid_file_parent("/./file.txt"));
274 assert!(!valid_file_parent("/../file.txt"));
275 assert!(!valid_file_parent("test/file.txt"));
276 assert!(!valid_file_parent("/file/"));
277 }
278}