Skip to main content

files_sdk/
types.rs

1//! Common types for Files.com API
2//!
3//! This module contains shared types used across multiple endpoints,
4//! including file entities, folder entities, pagination information,
5//! and upload-related types.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Type of a filesystem entry returned by Files.com.
11///
12/// The API reports each entry as either a `"file"` or a `"directory"`.
13/// Using an enum here instead of a raw string gives compile-time safety
14/// and exhaustive matching.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum EntryType {
18    /// A regular file.
19    File,
20    /// A directory (folder).
21    Directory,
22}
23
24impl EntryType {
25    /// Returns `true` if this entry is a file.
26    pub fn is_file(&self) -> bool {
27        matches!(self, EntryType::File)
28    }
29
30    /// Returns `true` if this entry is a directory.
31    pub fn is_directory(&self) -> bool {
32        matches!(self, EntryType::Directory)
33    }
34}
35
36/// Represents a file or directory in Files.com
37///
38/// This is the primary entity returned by most file operations.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct FileEntity {
41    /// File/folder path
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub path: Option<String>,
44
45    /// Display name of file/folder
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub display_name: Option<String>,
48
49    /// Type: file or directory
50    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
51    pub file_type: Option<EntryType>,
52
53    /// Size in bytes
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub size: Option<i64>,
56
57    /// Creation timestamp
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub created_at: Option<String>,
60
61    /// Modification time
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub mtime: Option<String>,
64
65    /// Provided modification time (custom)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub provided_mtime: Option<String>,
68
69    /// CRC32 checksum
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub crc32: Option<String>,
72
73    /// MD5 hash
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub md5: Option<String>,
76
77    /// SHA1 hash
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub sha1: Option<String>,
80
81    /// SHA256 hash
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub sha256: Option<String>,
84
85    /// MIME type
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub mime_type: Option<String>,
88
89    /// Storage region
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub region: Option<String>,
92
93    /// Permissions string
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub permissions: Option<String>,
96
97    /// Whether subfolders are locked
98    #[serde(rename = "subfolders_locked?", skip_serializing_if = "Option::is_none")]
99    pub subfolders_locked: Option<bool>,
100
101    /// Whether file is locked
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub is_locked: Option<bool>,
104
105    /// Download URI (temporary URL for downloading)
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub download_uri: Option<String>,
108
109    /// Priority color
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub priority_color: Option<String>,
112
113    /// Preview ID
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub preview_id: Option<i64>,
116
117    /// Preview information
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub preview: Option<String>,
120
121    /// Custom metadata (max 32 keys)
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub custom_metadata: Option<HashMap<String, String>>,
124
125    /// ID of user who created this
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub created_by_id: Option<i64>,
128
129    /// ID of API key that created this
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub created_by_api_key_id: Option<i64>,
132
133    /// ID of automation that created this
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub created_by_automation_id: Option<i64>,
136
137    /// ID of bundle registration that created this
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub created_by_bundle_registration_id: Option<i64>,
140
141    /// ID of inbox that created this
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub created_by_inbox_id: Option<i64>,
144
145    /// ID of remote server that created this
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub created_by_remote_server_id: Option<i64>,
148
149    /// ID of remote server sync that created this
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub created_by_remote_server_sync_id: Option<i64>,
152
153    /// ID of AS2 incoming message that created this
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub created_by_as2_incoming_message_id: Option<i64>,
156
157    /// ID of user who last modified this
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub last_modified_by_id: Option<i64>,
160
161    /// ID of API key that last modified this
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub last_modified_by_api_key_id: Option<i64>,
164
165    /// ID of automation that last modified this
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub last_modified_by_automation_id: Option<i64>,
168
169    /// ID of bundle registration that last modified this
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub last_modified_by_bundle_registration_id: Option<i64>,
172
173    /// ID of remote server that last modified this
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub last_modified_by_remote_server_id: Option<i64>,
176
177    /// ID of remote server sync that last modified this
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub last_modified_by_remote_server_sync_id: Option<i64>,
180}
181
182/// Represents upload information for a file part
183///
184/// Returned by the begin_upload operation to provide URLs and parameters
185/// for uploading file data.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct FileUploadPartEntity {
188    /// URI to upload this part to
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub upload_uri: Option<String>,
191
192    /// HTTP method to use (usually "PUT")
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub http_method: Option<String>,
195
196    /// Additional headers to include in upload request
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub headers: Option<HashMap<String, String>>,
199
200    /// Additional HTTP parameters to send
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub parameters: Option<HashMap<String, String>>,
203
204    /// Part number for multi-part uploads
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub part_number: Option<i32>,
207
208    /// Size in bytes for this part
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub partsize: Option<i64>,
211
212    /// Size in bytes for the next part
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub next_partsize: Option<i64>,
215
216    /// Reference identifier for this upload
217    #[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
218    pub ref_: Option<String>,
219
220    /// Type of upload action
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub action: Option<String>,
223
224    /// Whether multiple parts can be uploaded in parallel
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub parallel_parts: Option<bool>,
227
228    /// Whether parts can be retried
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub retry_parts: Option<bool>,
231
232    /// Number of parts in the upload
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub available_parts: Option<i32>,
235
236    /// When this upload URL expires
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub expires: Option<String>,
239
240    /// Content-Type and file to send
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub send: Option<HashMap<String, String>>,
243
244    /// File path being uploaded to
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub path: Option<String>,
247
248    /// Whether to ask about overwrites
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub ask_about_overwrites: Option<bool>,
251}
252
253/// Represents a folder (directory) in Files.com
254///
255/// Alias for FileEntity since folders are represented as files with type="directory"
256pub type FolderEntity = FileEntity;
257
258/// Pagination information from response headers
259///
260/// Files.com uses cursor-based pagination with cursors provided in response headers.
261#[derive(Debug, Clone, Default)]
262pub struct PaginationInfo {
263    /// Cursor for the next page of results
264    pub cursor_next: Option<String>,
265
266    /// Cursor for the previous page of results
267    pub cursor_prev: Option<String>,
268}
269
270impl PaginationInfo {
271    /// Creates pagination info from response headers
272    pub fn from_headers(headers: &reqwest::header::HeaderMap) -> Self {
273        let cursor_next = headers
274            .get("X-Files-Cursor-Next")
275            .and_then(|v| v.to_str().ok())
276            .map(String::from);
277
278        let cursor_prev = headers
279            .get("X-Files-Cursor-Prev")
280            .and_then(|v| v.to_str().ok())
281            .map(String::from);
282
283        Self {
284            cursor_next,
285            cursor_prev,
286        }
287    }
288
289    /// Whether there is a next page
290    pub fn has_next(&self) -> bool {
291        self.cursor_next.is_some()
292    }
293
294    /// Whether there is a previous page
295    pub fn has_prev(&self) -> bool {
296        self.cursor_prev.is_some()
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_file_entity_deserialize() {
306        let json = r#"{
307            "path": "/test/file.txt",
308            "display_name": "file.txt",
309            "type": "file",
310            "size": 1024
311        }"#;
312
313        let entity: FileEntity = serde_json::from_str(json).unwrap();
314        assert_eq!(entity.path, Some("/test/file.txt".to_string()));
315        assert_eq!(entity.display_name, Some("file.txt".to_string()));
316        assert_eq!(entity.file_type, Some(EntryType::File));
317        assert_eq!(entity.size, Some(1024));
318    }
319
320    #[test]
321    fn test_pagination_info_empty() {
322        let headers = reqwest::header::HeaderMap::new();
323        let info = PaginationInfo::from_headers(&headers);
324        assert!(!info.has_next());
325        assert!(!info.has_prev());
326    }
327
328    #[test]
329    fn test_pagination_info_with_next() {
330        let mut headers = reqwest::header::HeaderMap::new();
331        headers.insert("X-Files-Cursor-Next", "next-cursor".parse().unwrap());
332
333        let info = PaginationInfo::from_headers(&headers);
334        assert!(info.has_next());
335        assert!(!info.has_prev());
336        assert_eq!(info.cursor_next, Some("next-cursor".to_string()));
337    }
338}