opendal/services/onedrive/
graph_model.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use serde::Deserialize;
19use serde::Serialize;
20
21#[derive(Debug, Deserialize)]
22pub struct GraphOAuthRefreshTokenResponseBody {
23    pub access_token: String,
24    pub refresh_token: String,
25    pub expires_in: i64, // in seconds
26}
27
28/// We `$select` some fields when sending GET requests.
29/// Please keep [`OneDriveItem`] fields and this variable in sync.
30/// Read more at https://learn.microsoft.com/en-us/graph/query-parameters?tabs=http#select-parameter
31pub const GENERAL_SELECT_PARAM: &str =
32    "$select=id,name,lastModifiedDateTime,eTag,size,parentReference,folder,file";
33
34/// We `$select` some fields when listing versions.
35/// Please keep [`OneDriveItemVersion`] fields and this variable in sync.
36/// Read more at https://learn.microsoft.com/en-us/graph/query-parameters?tabs=http#select-parameter
37pub const VERSION_SELECT_PARAM: &str = "$select=id,size,lastModifiedDateTime";
38
39#[derive(Debug, Serialize, Deserialize)]
40pub struct GraphApiOneDriveListResponse {
41    #[serde(rename = "@odata.nextLink")]
42    pub next_link: Option<String>,
43    pub value: Vec<OneDriveItem>,
44}
45
46/// A `DriveItem`
47/// read more at https://learn.microsoft.com/en-us/onedrive/developer/rest-api/resources/driveitem
48#[derive(Debug, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct OneDriveItem {
51    pub id: String,
52    pub name: String,
53    pub last_modified_date_time: String,
54    pub e_tag: String,
55    pub size: i64,
56    pub parent_reference: ParentReference,
57    #[serde(flatten)]
58    pub item_type: ItemType,
59    pub versions: Option<Vec<OneDriveItemVersion>>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct ParentReference {
65    pub path: String,
66    pub drive_id: String,
67    pub id: String,
68}
69
70/// Additional properties when represents a facet of a "DriveItem":
71/// - "file", read more at https://learn.microsoft.com/en-us/onedrive/developer/rest-api/resources/file
72/// - "folder", read more at https://learn.microsoft.com/en-us/onedrive/developer/rest-api/resources/folder
73#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
74#[serde(untagged)]
75pub enum ItemType {
76    Folder { folder: Folder },
77    File { file: File },
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81#[serde(rename_all = "camelCase")]
82pub struct File {
83    mime_type: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87#[serde(rename_all = "camelCase")]
88pub struct Folder {
89    child_count: i32,
90}
91
92#[derive(Debug, Serialize, Deserialize)]
93pub struct GraphApiOneDriveVersionsResponse {
94    pub value: Vec<OneDriveItemVersion>,
95}
96
97/// A `driveItemVersion`
98///
99/// Read more at https://learn.microsoft.com/en-us/graph/api/resources/driveitemversion
100#[derive(Debug, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct OneDriveItemVersion {
103    pub id: String,
104    pub last_modified_date_time: String,
105    pub size: i64,
106}
107
108// Microsoft's documentation wants developers to set this as URL parameters. Some APIs use
109// this as an data field in the payload.
110pub const REPLACE_EXISTING_ITEM_WHEN_CONFLICT: &str = "replace";
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct CreateDirPayload {
114    #[serde(rename = "@microsoft.graph.conflictBehavior")]
115    conflict_behavior: String,
116    name: String,
117    folder: EmptyStruct,
118}
119
120impl CreateDirPayload {
121    pub fn new(name: String) -> Self {
122        Self {
123            conflict_behavior: REPLACE_EXISTING_ITEM_WHEN_CONFLICT.to_string(),
124            name,
125            folder: EmptyStruct {},
126        }
127    }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131struct EmptyStruct {}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134struct FileUploadItem {
135    #[serde(rename = "@microsoft.graph.conflictBehavior")]
136    conflict_behavior: String,
137    name: String,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct OneDriveUploadSessionCreationResponseBody {
143    pub upload_url: String,
144    pub expiration_date_time: String,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct OneDriveUploadSessionCreationRequestBody {
149    item: FileUploadItem,
150}
151
152impl OneDriveUploadSessionCreationRequestBody {
153    pub fn new(path: String) -> Self {
154        OneDriveUploadSessionCreationRequestBody {
155            item: FileUploadItem {
156                conflict_behavior: REPLACE_EXISTING_ITEM_WHEN_CONFLICT.to_string(),
157                name: path,
158            },
159        }
160    }
161}
162
163/// represents copy and rename (update) operations
164#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct OneDrivePatchRequestBody {
167    pub parent_reference: ParentReference,
168    pub name: String,
169}
170
171#[derive(Debug, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct OneDriveMonitorStatus {
174    pub percentage_complete: f64, // useful for debugging
175    pub status: String,
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_parse_one_drive_list_response_json() {
184        let data = r#"{
185            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('opendal@outlook.com')/drive/root/children(id,name,lastModifiedDateTime,eTag,size,parentReference,folder,file)",
186            "value": [
187                {
188                    "@odata.etag": "\"{3B131E1C-7D81-20AF-80D0-450D00000000},10\"",
189                    "eTag": "\"{3B131E1C-7D81-20AF-80D0-450D00000000},10\"",
190                    "id": "A0AA0A000A000A0A!3397",
191                    "lastModifiedDateTime": "2025-02-23T11:45:26Z",
192                    "name": "empty_folder",
193                    "size": 0,
194                    "parentReference": {
195                        "driveType": "personal",
196                        "driveId": "A0AA0A000A000A0A",
197                        "id": "A0AA0A000A000A0A!sea8cc6beffdb43d7976fbc7da445c639",
198                        "name": "Documents",
199                        "path": "/drive/root:",
200                        "siteId": "5f1f11f3-a6b4-4414-aee0-215c774f80db"
201                    },
202                    "folder": {
203                        "childCount": 0,
204                        "view": {
205                            "sortBy": "name",
206                            "sortOrder": "ascending",
207                            "viewType": "thumbnails"
208                        }
209                    }
210                },
211                {
212                    "@odata.etag": "\"{3B131E1C-7D81-20AF-80D0-710000000000},6\"",
213                    "eTag": "\"{3B131E1C-7D81-20AF-80D0-710000000000},6\"",
214                    "id": "A0AA0A000A000A0A!113",
215                    "lastModifiedDateTime": "2025-02-16T19:48:39Z",
216                    "name": "folder_a",
217                    "size": 10560537,
218                    "parentReference": {
219                        "driveType": "personal",
220                        "driveId": "A0AA0A000A000A0A",
221                        "id": "A0AA0A000A000A0A!sea8cc6beffdb43d7976fbc7da445c639",
222                        "name": "Documents",
223                        "path": "/drive/root:",
224                        "siteId": "5f1f11f3-a6b4-4414-aee0-215c774f80db"
225                    },
226                    "folder": {
227                        "childCount": 5,
228                        "view": {
229                            "sortBy": "name",
230                            "sortOrder": "ascending",
231                            "viewType": "thumbnails"
232                        }
233                    }
234                }
235            ]
236        }"#;
237
238        let response: GraphApiOneDriveListResponse = serde_json::from_str(data).unwrap();
239        assert_eq!(response.value.len(), 2);
240        let item = &response.value[0];
241        assert_eq!(item.name, "empty_folder");
242        assert_eq!(item.last_modified_date_time, "2025-02-23T11:45:26Z");
243        assert_eq!(item.e_tag, "\"{3B131E1C-7D81-20AF-80D0-450D00000000},10\"");
244        assert_eq!(item.size, 0);
245        assert_eq!(item.parent_reference.path, "/drive/root:");
246        if let ItemType::Folder { folder, .. } = &item.item_type {
247            assert_eq!(folder.child_count, 0);
248        } else {
249            panic!("item_type is not a folder");
250        }
251    }
252
253    #[test]
254    fn test_parse_one_drive_list_response_with_next_link_json() {
255        let response_json = r#"{
256            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('opendal@outlook.com')/drive/root/children(id,name,lastModifiedDateTime,eTag,size,parentReference,folder,file)",
257            "@odata.nextLink": "https://graph.microsoft.com/v1.0/me/drive/root/children?$select=id%2cname%2clastModifiedDateTime%2ceTag%2csize%2cparentReference%2cfolder%2cfile&$top=2&$skiptoken=UGFnZWQ9VFJVRSZwX1NvcnRCZWhhdmlvcj0xJnBfRmlsZUxlYWZSZWY9Zm9sZGVyX2EmcF9JRD03MDAz",
258            "value": [
259                {
260                    "@odata.etag": "\"{3B131E1C-7D81-20AF-80D0-450D00000000},10\"",
261                    "eTag": "\"{3B131E1C-7D81-20AF-80D0-450D00000000},10\"",
262                    "id": "A0AA0A000A000A0A!3397",
263                    "lastModifiedDateTime": "2025-02-23T11:45:26Z",
264                    "name": "empty_folder",
265                    "size": 0,
266                    "parentReference": {
267                        "driveType": "personal",
268                        "driveId": "A0AA0A000A000A0A",
269                        "id": "A0AA0A000A000A0A!sea8cc6beffdb43d7976fbc7da445c639",
270                        "name": "Documents",
271                        "path": "/drive/root:",
272                        "siteId": "5f1f11f3-a6b4-4414-aee0-215c774f80db"
273                    },
274                    "folder": {
275                        "childCount": 0,
276                        "view": {
277                            "sortBy": "name",
278                            "sortOrder": "ascending",
279                            "viewType": "thumbnails"
280                        }
281                    }
282                }
283            ]
284        }"#;
285
286        let response: GraphApiOneDriveListResponse = serde_json::from_str(response_json).unwrap();
287        assert_eq!(response.value.len(), 1);
288        let item = &response.value[0];
289        if let ItemType::Folder { folder, .. } = &item.item_type {
290            assert_eq!(folder.child_count, 0);
291        } else {
292            panic!("item_type is not a folder");
293        }
294    }
295
296    #[test]
297    fn test_parse_one_drive_file_json() {
298        let data = r#"{
299            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('opendal%40outlook.com')/drive/root(id,name,lastModifiedDateTime,eTag,size,parentReference,folder,file)/$entity",
300            "@odata.etag": "\"{3B131E1C-7D81-20AF-80D0-720000000000},2\"",
301            "eTag": "\"{3B131E1C-7D81-20AF-80D0-720000000000},2\"",
302            "id": "A0AA0A000A000A0A!114",
303            "lastModifiedDateTime": "2025-02-16T19:49:05Z",
304            "name": "filename.txt",
305            "size": 3,
306            "parentReference": {
307                "driveType": "personal",
308                "driveId": "A0AA0A000A000A0A",
309                "id": "A0AA0A000A000A0A!113",
310                "name": "folder_a",
311                "path": "/drive/root:/folder_a",
312                "siteId": "5f1f11f3-a6b4-4414-aee0-215c774f80db"
313            },
314            "file": {
315                "mimeType": "text/plain",
316                "hashes": {
317                    "quickXorHash": "79jFLwAAAAAAAAAAAwAAAAAAAAA=",
318                    "sha1Hash": "57218C316B6921E2CD61027A2387EDC31A2D9471",
319                    "sha256Hash": "F1945CD6C19E56B3C1C78943EF5EC18116907A4CA1EFC40A57D48AB1DB7ADFC5"
320                }
321            }
322        }"#;
323
324        let item: OneDriveItem = serde_json::from_str(data).unwrap();
325        assert_eq!(item.name, "filename.txt");
326        assert_eq!(item.last_modified_date_time, "2025-02-16T19:49:05Z");
327        assert_eq!(item.e_tag, "\"{3B131E1C-7D81-20AF-80D0-720000000000},2\"");
328        assert_eq!(item.size, 3);
329        assert_eq!(item.parent_reference.id, "A0AA0A000A000A0A!113");
330        assert!(item.versions.is_none());
331        if let ItemType::File { file, .. } = &item.item_type {
332            assert_eq!(file.mime_type, "text/plain");
333        } else {
334            panic!("item_type is not a file");
335        }
336    }
337
338    #[test]
339    fn test_parse_one_drive_file_with_version_json() {
340        let data = r#"{
341            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('opendal%40outlook.com')/drive/root(id,name,lastModifiedDateTime,eTag,size,parentReference,folder,file)/$entity",
342            "@odata.etag": "\"{3B131E1C-7D81-20AF-80D0-720000000000},2\"",
343            "eTag": "\"{3B131E1C-7D81-20AF-80D0-720000000000},2\"",
344            "id": "A0AA0A000A000A0A!114",
345            "lastModifiedDateTime": "2025-02-16T19:49:05Z",
346            "name": "filename.txt",
347            "size": 3,
348            "parentReference": {
349                "driveType": "personal",
350                "driveId": "A0AA0A000A000A0A",
351                "id": "A0AA0A000A000A0A!113",
352                "name": "folder_a",
353                "path": "/drive/root:/folder_a",
354                "siteId": "5f1f11f3-a6b4-4414-aee0-215c774f80db"
355            },
356            "file": {
357                "mimeType": "text/plain",
358                "hashes": {
359                    "quickXorHash": "79jFLwAAAAAAAAAAAwAAAAAAAAA=",
360                    "sha1Hash": "57218C316B6921E2CD61027A2387EDC31A2D9471",
361                    "sha256Hash": "F1945CD6C19E56B3C1C78943EF5EC18116907A4CA1EFC40A57D48AB1DB7ADFC5"
362                }
363            },
364            "versions@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('opendal%40outlook.com')/drive/root/versions",
365            "versions": [
366                {
367                    "@microsoft.graph.downloadUrl": "https://my.microsoftpersonalcontent.com/personal/A0AA0A000A000A0A/_layouts/15/download.aspx?UniqueId=3b131e1c-7d81-20af-80d0-720000000000&Translate=false&tempauth=v1e.a&ApiVersion=2.0",
368                    "id": "1.0",
369                    "lastModifiedDateTime": "2025-02-16T19:49:05Z",
370                    "size": 3,
371                    "lastModifiedBy": {
372                        "user": {
373                            "email": "erickgdev@outlook.com",
374                            "displayName": "erickgdev@outlook.com"
375                        }
376                    }
377                }
378            ]
379        }"#;
380
381        let item: OneDriveItem = serde_json::from_str(data).unwrap();
382        let versions = item.versions.expect("Versions present");
383        assert_eq!("1.0", versions[0].id);
384        assert_eq!("2025-02-16T19:49:05Z", versions[0].last_modified_date_time);
385    }
386
387    #[test]
388    fn test_parse_one_drive_monitor_status_json() {
389        let data = r#"{
390            "@odata.context": "https://my.microsoftpersonalcontent.com/personal/A0AA0A000A000A0A/_api/v2.0/$metadata#oneDrive.asynchronousOperationStatus",
391            "percentageComplete": 100.0,
392            "resourceId": "01JP3NYHGSBJ7R42UN65HZ333HZFWQTGL4",
393            "status": "completed"
394        }"#;
395
396        let response: OneDriveMonitorStatus = serde_json::from_str(data).unwrap();
397        assert_eq!(response.status, "completed");
398    }
399
400    #[test]
401    fn test_parse_one_drive_item_versions_json() {
402        let data = r#"{
403            "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('erickgdev%40outlook.com')/drive/root/versions(id,size,lastModifiedDateTime)",
404            "value": [
405                {
406                    "id": "2.0",
407                    "lastModifiedDateTime": "2025-03-16T17:02:49Z",
408                    "size": 74758
409                },
410                {
411                    "id": "1.0",
412                    "lastModifiedDateTime": "2025-03-12T21:59:54Z",
413                    "size": 74756
414                }
415            ]
416        }"#;
417
418        let response: GraphApiOneDriveVersionsResponse = serde_json::from_str(data).unwrap();
419        assert_eq!(response.value.len(), 2);
420        let version = &response.value[0];
421        assert_eq!(version.id, "2.0");
422        assert_eq!(version.last_modified_date_time, "2025-03-16T17:02:49Z");
423        assert_eq!(version.size, 74758);
424    }
425}