1use std::{collections::BTreeMap, path::Path};
8
9use serde::Deserialize;
10use serde_json::{Value, json};
11
12use crate::{Client, Error, FILE_LINKS, FILE_VERSIONS, FILENODE, PRINCIPALS, Result, ids_of};
13
14const USING: [&str; 3] = [FILENODE, FILE_VERSIONS, PRINCIPALS];
15const PROPERTIES: [&str; 12] = [
16 "parentId",
17 "name",
18 "role",
19 "blobId",
20 "size",
21 "mediaType",
22 "modified",
23 "myRights",
24 "shareWith",
25 "kernoid:vault",
26 "kernoid:vaultKey",
27 "kernoid:encryptedName",
28];
29
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
31#[serde(rename_all = "camelCase", default)]
32pub struct Rights {
33 pub may_read: bool,
34 pub may_add_children: bool,
35 pub may_rename: bool,
36 pub may_delete: bool,
37 pub may_modify_content: bool,
38 pub may_share: bool,
39}
40
41#[derive(Debug, Clone, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct FileNode {
44 #[serde(rename = "@type", default)]
45 pub kind: String,
46 pub id: String,
47 #[serde(default)]
48 pub parent_id: Option<String>,
49 pub name: String,
50 #[serde(default)]
51 pub role: Option<String>,
52 #[serde(default)]
53 pub blob_id: Option<String>,
54 #[serde(default)]
55 pub size: Option<u64>,
56 #[serde(default)]
57 pub media_type: Option<String>,
58 #[serde(default)]
59 pub modified: Option<String>,
60 #[serde(default)]
61 pub my_rights: Option<Rights>,
62 #[serde(default)]
63 pub share_with: Option<BTreeMap<String, Rights>>,
64 #[serde(rename = "kernoid:vault", default)]
66 pub vault: Option<String>,
67 #[serde(rename = "kernoid:vaultKey", default)]
69 pub vault_key: Option<String>,
70 #[serde(rename = "kernoid:encryptedName", default)]
72 pub encrypted_name: Option<String>,
73 #[serde(skip)]
75 pub locked: bool,
76}
77
78impl FileNode {
79 pub fn in_vault(&self) -> bool {
81 self.vault.is_some()
82 }
83
84 pub fn is_directory(&self) -> bool {
85 self.kind == "Directory" || (self.kind.is_empty() && self.blob_id.is_none())
86 }
87
88 pub fn rights(&self) -> Rights {
89 self.my_rights.unwrap_or_default()
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
95pub enum StoreKind {
96 Own,
97 Drive,
98 Shared,
99}
100
101#[derive(Debug, Clone)]
103pub struct Store {
104 pub id: String,
105 pub name: String,
106 pub kind: StoreKind,
107}
108
109pub fn stores(client: &Client) -> Vec<Store> {
111 let own = client.session.primary(FILENODE);
112 let mut out: Vec<Store> = client
113 .session
114 .accounts_with(FILENODE)
115 .into_iter()
116 .map(|(id, account)| Store {
117 id: id.to_owned(),
118 name: account.name.clone(),
119 kind: if Some(id) == own {
120 StoreKind::Own
121 } else if account.drive {
122 StoreKind::Drive
123 } else {
124 StoreKind::Shared
125 },
126 })
127 .collect();
128 out.sort_by(|a, b| (a.kind, &a.name).cmp(&(b.kind, &b.name)));
129 out
130}
131
132#[derive(Deserialize)]
133struct List<T> {
134 list: Vec<T>,
135}
136
137fn list<T: serde::de::DeserializeOwned>(value: Option<&Value>) -> Result<Vec<T>> {
138 let value = value.cloned().unwrap_or(json!({ "list": [] }));
139 serde_json::from_value::<List<T>>(value).map(|list| list.list).map_err(|error| Error::Protocol(error.to_string()))
140}
141
142pub fn sort(nodes: &mut [FileNode]) {
144 nodes.sort_by(|a, b| {
145 b.is_directory().cmp(&a.is_directory()).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
146 });
147}
148
149pub async fn children(client: &Client, account: &str, parent: Option<&str>) -> Result<Vec<FileNode>> {
151 let filter = match parent {
152 Some(parent) => json!({ "parentId": parent }),
153 None => json!({ "isTopLevel": true }),
154 };
155 let responses = client
156 .request(
157 &USING,
158 vec![
159 ("FileNode/query", json!({ "accountId": account, "filter": filter }), "q"),
160 (
161 "FileNode/get",
162 json!({ "accountId": account, "properties": PROPERTIES, "#ids": ids_of("q", "FileNode/query") }),
163 "g",
164 ),
165 ],
166 )
167 .await?;
168 let mut nodes = list(responses.get("g"))?;
169 sort(&mut nodes);
170 Ok(nodes)
171}
172
173#[derive(Debug, Clone)]
176pub struct Snapshot {
177 pub state: String,
178 pub nodes: Vec<FileNode>,
179}
180
181pub async fn snapshot(client: &Client, account: &str) -> Result<Snapshot> {
182 let responses = client
183 .request(
184 &USING,
185 vec![
186 ("FileNode/query", json!({ "accountId": account }), "q"),
187 (
188 "FileNode/get",
189 json!({ "accountId": account, "properties": PROPERTIES, "#ids": ids_of("q", "FileNode/query") }),
190 "g",
191 ),
192 ],
193 )
194 .await?;
195 let state = responses.get("g").and_then(|g| g.get("state")).and_then(Value::as_str).unwrap_or_default().to_owned();
196 Ok(Snapshot { state, nodes: list(responses.get("g"))? })
197}
198
199#[derive(Debug, Clone)]
201pub struct Changes {
202 pub old_state: String,
203 pub new_state: String,
204 pub has_more_changes: bool,
205 pub changed: Vec<FileNode>,
207 pub destroyed: Vec<String>,
209}
210
211pub async fn changes(client: &Client, account: &str, since: &str, max: u64) -> Result<Changes> {
214 let responses = client
215 .request(
216 &USING,
217 vec![
218 ("FileNode/changes", json!({ "accountId": account, "sinceState": since, "maxChanges": max }), "c"),
219 (
220 "FileNode/get",
221 json!({ "accountId": account, "properties": PROPERTIES,
222 "#ids": { "resultOf": "c", "name": "FileNode/changes", "path": "/created" } }),
223 "n",
224 ),
225 (
226 "FileNode/get",
227 json!({ "accountId": account, "properties": PROPERTIES,
228 "#ids": { "resultOf": "c", "name": "FileNode/changes", "path": "/updated" } }),
229 "u",
230 ),
231 ],
232 )
233 .await?;
234 let found = responses.get("c").ok_or_else(|| Error::Protocol("no FileNode/changes".into()))?;
235 let text = |key: &str| found.get(key).and_then(Value::as_str).unwrap_or_default().to_owned();
236 let mut changed = list::<FileNode>(responses.get("n"))?;
237 changed.extend(list::<FileNode>(responses.get("u"))?);
238 let destroyed = found
239 .get("destroyed")
240 .and_then(Value::as_array)
241 .map(|ids| ids.iter().filter_map(Value::as_str).map(str::to_owned).collect())
242 .unwrap_or_default();
243 Ok(Changes {
244 old_state: text("oldState"),
245 new_state: text("newState"),
246 has_more_changes: found.get("hasMoreChanges").and_then(Value::as_bool).unwrap_or(false),
247 changed,
248 destroyed,
249 })
250}
251
252pub async fn trash(client: &Client, account: &str) -> Result<Option<FileNode>> {
254 let responses = client
255 .request(
256 &USING,
257 vec![
258 ("FileNode/query", json!({ "accountId": account, "filter": { "role": "trash" } }), "q"),
259 (
260 "FileNode/get",
261 json!({ "accountId": account, "properties": PROPERTIES, "#ids": ids_of("q", "FileNode/query") }),
262 "g",
263 ),
264 ],
265 )
266 .await?;
267 Ok(list(responses.get("g"))?.into_iter().next())
268}
269
270pub async fn create_directory(client: &Client, account: &str, parent: Option<&str>, name: &str) -> Result<()> {
271 let create = json!({ "create": { "d": { "@type": "Directory", "parentId": parent, "name": name } } });
272 client.set(&USING, "FileNode", account, create, json!({})).await?;
273 Ok(())
274}
275
276pub async fn rename(client: &Client, account: &str, id: &str, name: &str) -> Result<()> {
277 client.set(&USING, "FileNode", account, json!({ "update": { id: { "name": name } } }), json!({})).await?;
278 Ok(())
279}
280
281pub async fn move_to(client: &Client, account: &str, id: &str, parent: Option<&str>) -> Result<()> {
283 let update = json!({ "update": { id: { "parentId": parent } } });
284 client.set(&USING, "FileNode", account, update, json!({ "onExists": "rename" })).await?;
285 Ok(())
286}
287
288pub async fn copy_to(
291 client: &Client,
292 from: &str,
293 to: &str,
294 id: &str,
295 parent: Option<&str>,
296 move_it: bool,
297) -> Result<()> {
298 let result = client
299 .call(
300 &USING,
301 "FileNode/copy",
302 json!({
303 "fromAccountId": from,
304 "accountId": to,
305 "create": { "c": { "id": id, "parentId": parent } },
306 "onSuccessDestroyOriginal": move_it,
307 "onExists": "rename",
308 }),
309 )
310 .await?;
311 crate::set_error(&result)
312}
313
314#[derive(Debug, Clone)]
316pub struct Found {
317 pub node: FileNode,
318 pub path: Vec<FileNode>,
319}
320
321pub async fn search(client: &Client, account: &str, text: &str) -> Result<Vec<Found>> {
323 let responses = client
324 .request(
325 &USING,
326 vec![
327 ("FileNode/query", json!({ "accountId": account, "filter": { "text": text }, "limit": 200 }), "q"),
328 (
329 "FileNode/get",
330 json!({
331 "accountId": account, "properties": PROPERTIES, "fetchParents": true,
332 "#ids": ids_of("q", "FileNode/query"),
333 }),
334 "g",
335 ),
336 ],
337 )
338 .await?;
339 let hits: Vec<String> = responses
340 .get("q")
341 .and_then(|query| query.get("ids"))
342 .and_then(Value::as_array)
343 .map(|ids| ids.iter().filter_map(Value::as_str).map(str::to_owned).collect())
344 .unwrap_or_default();
345 let nodes: Vec<FileNode> = list(responses.get("g"))?;
346 let by_id: BTreeMap<&str, &FileNode> = nodes.iter().map(|node| (node.id.as_str(), node)).collect();
347 Ok(hits
348 .iter()
349 .filter_map(|id| by_id.get(id.as_str()))
350 .map(|node| {
351 let mut path = Vec::new();
352 let mut next = node.parent_id.as_deref();
353 while let Some(parent) = next.and_then(|id| by_id.get(id)) {
354 if path.len() > 64 {
355 break;
356 }
357 path.insert(0, (*parent).clone());
358 next = parent.parent_id.as_deref();
359 }
360 Found { node: (*node).clone(), path }
361 })
362 .collect())
363}
364
365pub async fn destroy(client: &Client, account: &str, id: &str) -> Result<()> {
367 client
368 .set(&USING, "FileNode", account, json!({ "destroy": [id] }), json!({ "onDestroyRemoveChildren": true }))
369 .await?;
370 Ok(())
371}
372
373pub fn media_type(name: &str) -> &'static str {
375 let extension = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase()).unwrap_or_default();
376 match extension.as_str() {
377 "txt" | "text" | "log" => "text/plain",
378 "md" => "text/markdown",
379 "csv" => "text/csv",
380 "html" | "htm" => "text/html",
381 "css" => "text/css",
382 "json" => "application/json",
383 "xml" => "application/xml",
384 "pdf" => "application/pdf",
385 "zip" => "application/zip",
386 "gz" => "application/gzip",
387 "png" => "image/png",
388 "jpg" | "jpeg" => "image/jpeg",
389 "gif" => "image/gif",
390 "webp" => "image/webp",
391 "svg" => "image/svg+xml",
392 "mp3" => "audio/mpeg",
393 "mp4" => "video/mp4",
394 "ics" => "text/calendar",
395 "vcf" => "text/vcard",
396 "eml" => "message/rfc822",
397 "odt" => "application/vnd.oasis.opendocument.text",
398 "ods" => "application/vnd.oasis.opendocument.spreadsheet",
399 "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
400 "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
401 "pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
402 _ => "application/octet-stream",
403 }
404}
405
406pub async fn upload_file(
409 client: &Client,
410 account: &str,
411 parent: Option<&str>,
412 path: &Path,
413 existing: Option<&FileNode>,
414 progress: impl FnMut(u64, u64) + Send + 'static,
415) -> Result<()> {
416 let file = tokio::fs::File::open(path).await?;
417 let metadata = file.metadata().await?;
418 let name = path
419 .file_name()
420 .and_then(|name| name.to_str())
421 .ok_or_else(|| Error::Protocol("the file name is not UTF-8".into()))?
422 .to_owned();
423 let kind = media_type(&name);
424 let upload = client.upload(account, kind, file, metadata.len(), progress).await?;
425 let modified = metadata
426 .modified()
427 .ok()
428 .map(|time| chrono::DateTime::<chrono::Utc>::from(time).to_rfc3339_opts(chrono::SecondsFormat::Secs, true));
429 match existing.filter(|node| !node.is_directory()) {
430 Some(node) => {
431 let update = json!({ "update": { node.id.as_str(): {
432 "blobId": upload.blob_id, "mediaType": kind, "modified": modified,
433 } } });
434 client.set(&USING, "FileNode", account, update, json!({})).await?;
435 }
436 None => {
437 let create = json!({ "create": { "f": {
438 "@type": "File", "parentId": parent, "name": name, "blobId": upload.blob_id,
439 "mediaType": kind, "modified": modified,
440 } } });
441 client.set(&USING, "FileNode", account, create, json!({ "onExists": "rename" })).await?;
442 }
443 }
444 Ok(())
445}
446
447#[derive(Debug, Clone, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct Version {
451 pub id: String,
452 pub blob_id: String,
453 #[serde(default)]
454 pub size: u64,
455 #[serde(default)]
456 pub media_type: Option<String>,
457 #[serde(default)]
458 pub modified: Option<String>,
459 #[serde(default)]
460 pub created: Option<String>,
461 #[serde(default)]
462 pub is_current: bool,
463}
464
465pub async fn versions(client: &Client, account: &str, node: &str) -> Result<Vec<Version>> {
467 let result = client.call(&USING, "FileVersion/get", json!({ "accountId": account, "nodeId": node })).await?;
468 list(Some(&result))
469}
470
471pub async fn restore(client: &Client, account: &str, node: &str, version: &Version) -> Result<()> {
473 let update = json!({ "update": { node: { "blobId": version.blob_id } } });
474 client.set(&USING, "FileNode", account, update, json!({})).await?;
475 Ok(())
476}
477
478#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum Level {
481 Read,
482 Edit,
483 Full,
484}
485
486impl Level {
487 pub fn rights(self) -> Rights {
488 let edit = self != Level::Read;
489 Rights {
490 may_read: true,
491 may_add_children: edit,
492 may_rename: edit,
493 may_delete: edit,
494 may_modify_content: edit,
495 may_share: self == Level::Full,
496 }
497 }
498
499 pub fn of(rights: &Rights) -> Self {
500 if rights.may_share {
501 Level::Full
502 } else if rights.may_modify_content || rights.may_add_children {
503 Level::Edit
504 } else {
505 Level::Read
506 }
507 }
508
509 pub fn name(self) -> &'static str {
510 match self {
511 Level::Read => "read",
512 Level::Edit => "edit",
513 Level::Full => "edit and share",
514 }
515 }
516
517 pub fn next(self) -> Self {
518 match self {
519 Level::Read => Level::Edit,
520 Level::Edit => Level::Full,
521 Level::Full => Level::Read,
522 }
523 }
524}
525
526impl Rights {
527 fn json(&self) -> Value {
528 json!({
529 "mayRead": self.may_read, "mayAddChildren": self.may_add_children, "mayRename": self.may_rename,
530 "mayDelete": self.may_delete, "mayModifyContent": self.may_modify_content, "mayShare": self.may_share,
531 })
532 }
533}
534
535pub async fn share(client: &Client, account: &str, node: &str, with: &[(String, Level)]) -> Result<()> {
537 let share_with: serde_json::Map<String, Value> =
538 with.iter().map(|(principal, level)| (principal.clone(), level.rights().json())).collect();
539 let update = json!({ "update": { node: { "shareWith": share_with } } });
540 client.set(&USING, "FileNode", account, update, json!({})).await?;
541 Ok(())
542}
543
544pub async fn find_principals(client: &Client, text: &str) -> Result<Vec<Principal>> {
546 let account = client.primary(PRINCIPALS)?;
547 let filter = if text.trim().is_empty() { json!({}) } else { json!({ "text": text.trim() }) };
548 let responses = client
549 .request(
550 &USING,
551 vec![
552 ("Principal/query", json!({ "accountId": account, "filter": filter, "limit": 20 }), "q"),
553 ("Principal/get", json!({ "accountId": account, "#ids": ids_of("q", "Principal/query") }), "g"),
554 ],
555 )
556 .await?;
557 list(responses.get("g"))
558}
559
560#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
562pub struct Principal {
563 pub id: String,
564 #[serde(rename = "type", default)]
565 pub kind: String,
566 #[serde(default)]
567 pub name: String,
568 #[serde(default)]
569 pub email: Option<String>,
570}
571
572pub async fn principals(client: &Client, ids: &[&str]) -> Result<Vec<Principal>> {
574 if ids.is_empty() {
575 return Ok(Vec::new());
576 }
577 let account = client.primary(PRINCIPALS)?;
578 let result = client.call(&USING, "Principal/get", json!({ "accountId": account, "ids": ids })).await?;
579 list(Some(&result))
580}
581
582#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
584#[serde(rename_all = "camelCase")]
585pub struct FileLink {
586 pub id: String,
587 pub node_id: String,
588 #[serde(default)]
589 pub expires: Option<String>,
590 #[serde(default)]
591 pub has_password: bool,
592 #[serde(default = "yes")]
593 pub may_read: bool,
594 #[serde(default)]
595 pub may_add: bool,
596 #[serde(default)]
597 pub created: Option<String>,
598}
599
600fn yes() -> bool {
601 true
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq)]
606pub enum LinkMode {
607 View,
608 Add,
609 Drop,
610}
611
612impl LinkMode {
613 pub fn of(link: &FileLink) -> Self {
614 match (link.may_read, link.may_add) {
615 (_, false) => LinkMode::View,
616 (true, true) => LinkMode::Add,
617 (false, true) => LinkMode::Drop,
618 }
619 }
620
621 pub fn name(self) -> &'static str {
622 match self {
623 LinkMode::View => "view and download",
624 LinkMode::Add => "view, download and add files",
625 LinkMode::Drop => "only add files (drop box)",
626 }
627 }
628
629 pub fn next(self, folder: bool) -> Self {
631 match self {
632 _ if !folder => LinkMode::View,
633 LinkMode::View => LinkMode::Add,
634 LinkMode::Add => LinkMode::Drop,
635 LinkMode::Drop => LinkMode::View,
636 }
637 }
638}
639
640pub fn links_available(client: &Client) -> bool {
642 client.session.capabilities.contains_key(FILE_LINKS)
643}
644
645pub async fn links(client: &Client, account: &str, node: &str) -> Result<Vec<FileLink>> {
647 let result =
648 client.call(&[FILENODE, FILE_LINKS], "FileLink/get", json!({ "accountId": account, "nodeId": node })).await?;
649 list(Some(&result))
650}
651
652pub async fn create_link(
655 client: &Client,
656 account: &str,
657 node: &str,
658 mode: LinkMode,
659 expires: Option<&str>,
660 password: Option<&str>,
661) -> Result<String> {
662 let link = json!({
663 "nodeId": node,
664 "mayRead": mode != LinkMode::Drop,
665 "mayAdd": mode != LinkMode::View,
666 "expires": expires,
667 "password": password,
668 });
669 let result =
670 client.set(&[FILENODE, FILE_LINKS], "FileLink", account, json!({ "create": { "l": link } }), json!({})).await?;
671 let token = result
672 .pointer("/created/l/token")
673 .and_then(Value::as_str)
674 .ok_or_else(|| Error::Protocol("the link was made without a token".into()))?;
675 let template = client
676 .session
677 .capabilities
678 .get(FILE_LINKS)
679 .and_then(|capability| capability.get("urlTemplate"))
680 .and_then(Value::as_str)
681 .ok_or_else(|| Error::Protocol("the server names no address for links".into()))?;
682 Ok(template.replace("{token}", token))
684}
685
686pub async fn remove_link(client: &Client, account: &str, id: &str) -> Result<()> {
687 client.set(&[FILENODE, FILE_LINKS], "FileLink", account, json!({ "destroy": [id] }), json!({})).await?;
688 Ok(())
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
696 fn media_types() {
697 assert_eq!(media_type("Report.PDF"), "application/pdf");
698 assert_eq!(media_type("noextension"), "application/octet-stream");
699 }
700
701 #[test]
702 fn sorting() {
703 let node = |kind: &str, name: &str| FileNode {
704 kind: kind.into(),
705 id: name.into(),
706 parent_id: None,
707 name: name.into(),
708 role: None,
709 blob_id: None,
710 size: None,
711 media_type: None,
712 modified: None,
713 my_rights: None,
714 share_with: None,
715 vault: None,
716 vault_key: None,
717 encrypted_name: None,
718 locked: false,
719 };
720 let mut nodes = vec![node("File", "b"), node("Directory", "z"), node("File", "A")];
721 sort(&mut nodes);
722 let names: Vec<_> = nodes.iter().map(|n| n.name.as_str()).collect();
723 assert_eq!(names, ["z", "A", "b"]);
724 }
725}