1use anyhow::anyhow;
5use cid::Cid;
6use co_api::{
7 co, tags, AbsolutePath, AbsolutePathOwned, BlockStorageExt, CoMap, CoSet, CoreBlockStorage, Date, Did, Link,
8 OptionLink, PathExt, PathOwned, Reducer, ReducerAction, Tags,
9};
10use futures::{FutureExt, TryStreamExt};
11use std::collections::{BTreeMap, BTreeSet, VecDeque};
12
13#[co(state)]
14pub struct File {
15 pub nodes: CoMap<AbsolutePathOwned, CoSet<Node>>,
16}
17
18#[co]
19pub enum Node {
20 Folder(FolderNode),
21 File(FileNode),
22 Link(LinkNode),
23}
24
25#[co]
26pub struct FileNode {
27 pub name: String,
28 pub create_time: Date,
29 pub modify_time: Date,
30 pub size: u64,
31 pub mode: u32,
32 pub tags: Tags,
33 pub contents: Cid,
35 pub owner: Did,
36}
37
38#[co]
39pub struct FolderNode {
40 pub name: String,
41 pub create_time: Date,
42 pub modify_time: Date,
43 pub tags: Tags,
44 pub owner: Did,
45 pub mode: u32,
46}
47
48#[co]
49pub struct LinkNode {
50 pub name: String,
51 pub tags: Tags,
52 pub contents: PathOwned,
53}
54
55#[co]
56pub enum FileAction {
57 Create {
60 path: AbsolutePathOwned,
62 node: Node,
64 recursive: bool,
66 },
67
68 Remove { path: AbsolutePathOwned, recursive: bool },
71
72 Modify { path: AbsolutePathOwned, modifications: Vec<FileModification> },
74}
75
76#[co]
77pub enum FileModification {
78 Rename(String),
80
81 Move(AbsolutePathOwned),
83
84 SetCreateTime(Date),
86
87 SetModifyTime(Date),
89
90 SetMode(u32),
92
93 SetOwner(Did),
95
96 TagsInsert(Tags),
98
99 TagsRemove(Tags),
101
102 SetContents(Cid, u64),
105
106 SetLink(PathOwned),
109}
110
111impl Reducer<FileAction> for File {
112 async fn reduce(
113 state: OptionLink<Self>,
114 event: Link<ReducerAction<FileAction>>,
115 storage: &CoreBlockStorage,
116 ) -> Result<Link<Self>, anyhow::Error> {
117 let action = storage.get_value(&event).await?;
118 let mut result = storage.get_value_or_default(&state).await?;
119 match &action.payload {
120 FileAction::Create { path, node, recursive } => {
121 reduce_create(storage, &mut result, path, node, &action.from, action.time, *recursive)
122 .boxed()
123 .await?;
124 },
125 FileAction::Remove { path, recursive } => {
126 reduce_remove(storage, &mut result, path, *recursive).boxed().await?;
127 },
128 FileAction::Modify { path, modifications } => {
129 reduce_modify(storage, &mut result, path, modifications).boxed().await?;
130 },
131 }
132 Ok(storage.set_value(&result).await?)
133 }
134}
135
136impl Node {
137 pub fn name(&self) -> &str {
138 match self {
139 Node::Folder(node) => &node.name,
140 Node::File(node) => &node.name,
141 Node::Link(node) => &node.name,
142 }
143 }
144
145 pub fn is_dir(&self) -> bool {
146 matches!(self, Node::Folder(_))
147 }
148
149 pub fn is_file(&self) -> bool {
150 matches!(self, Node::File(_))
151 }
152
153 pub fn is_link(&self) -> bool {
154 matches!(self, Node::Link(_))
155 }
156
157 pub fn modify(
158 &mut self,
159 context: &mut FileModificationContext,
160 modification: &FileModification,
161 ) -> anyhow::Result<()> {
162 match self {
163 Node::Folder(folder_node) => folder_node.modify(context, modification),
164 Node::File(file_node) => file_node.modify(context, modification),
165 Node::Link(link_node) => link_node.modify(context, modification),
166 }
167 }
168}
169
170impl FileNode {
171 pub fn modify(
172 &mut self,
173 _context: &mut FileModificationContext,
174 modification: &FileModification,
175 ) -> anyhow::Result<()> {
176 match modification {
177 FileModification::Rename(name) => {
178 self.name = name.to_owned();
179 },
180 FileModification::Move(_) => {
181 },
183 FileModification::SetCreateTime(time) => {
184 self.create_time = *time;
185 },
186 FileModification::SetModifyTime(time) => {
187 self.modify_time = *time;
188 },
189 FileModification::SetMode(mode) => {
190 self.mode = *mode;
191 },
192 FileModification::SetOwner(owner) => {
193 self.owner = owner.to_owned();
194 },
195 FileModification::TagsInsert(tags) => {
196 self.tags.append(&mut tags.clone());
197 },
198 FileModification::TagsRemove(tags) => {
199 self.tags.clear(Some(tags));
200 },
201 FileModification::SetContents(cid, size) => {
202 self.contents = *cid;
203 self.size = *size;
204 },
205 modification => return Err(anyhow!("Unsupported modification: {:?}", modification)),
206 }
207 Ok(())
208 }
209}
210
211impl FolderNode {
212 pub fn modify(
213 &mut self,
214 context: &mut FileModificationContext,
215 modification: &FileModification,
216 ) -> anyhow::Result<()> {
217 match modification {
218 FileModification::Rename(name) => {
219 if &self.name != name {
220 context.reparent(
221 context.path(),
222 context
223 .path()
224 .parent()
225 .ok_or(anyhow!("No parent: {}", context.path()))?
226 .join_path(name)?,
227 )?;
228 }
229 self.name = name.to_owned();
230 },
231 FileModification::Move(_to) => {
232 },
234 FileModification::SetCreateTime(time) => {
235 self.create_time = *time;
236 },
237 FileModification::SetModifyTime(time) => {
238 self.modify_time = *time;
239 },
240 FileModification::SetMode(mode) => {
241 self.mode = *mode;
242 },
243 FileModification::SetOwner(owner) => {
244 self.owner = owner.to_owned();
245 },
246 FileModification::TagsInsert(tags) => {
247 self.tags.append(&mut tags.clone());
248 },
249 FileModification::TagsRemove(tags) => {
250 self.tags.clear(Some(tags));
251 },
252 modification => return Err(anyhow!("Unsupported modification: {:?}", modification)),
253 }
254 Ok(())
255 }
256}
257
258impl LinkNode {
259 pub fn modify(
260 &mut self,
261 _context: &mut FileModificationContext,
262 modification: &FileModification,
263 ) -> anyhow::Result<()> {
264 match modification {
265 FileModification::Rename(name) => {
266 self.name = name.to_owned();
267 },
268 FileModification::Move(_to) => {
269 },
272 FileModification::TagsInsert(tags) => {
291 self.tags.append(&mut tags.clone());
292 },
293 FileModification::TagsRemove(tags) => {
294 self.tags.clear(Some(tags));
295 },
296 FileModification::SetLink(path) => {
297 self.contents = path.to_owned();
298 },
299 modification => return Err(anyhow!("Unsupported modification: {:?}", modification)),
300 }
301 Ok(())
302 }
303}
304
305async fn reduce_create(
306 storage: &CoreBlockStorage,
307 state: &mut File,
308 path: &AbsolutePath,
309 node: &Node,
310 from: &Did,
311 time: Date,
312 recursive: bool,
313) -> Result<(), anyhow::Error> {
314 let path = path.normalize()?;
315
316 let node_path = path.join_path(node.name())?;
318 if get_node(storage, &state.nodes, &node_path, true).await?.is_some() {
319 return Ok(());
320 }
321
322 let root: AbsolutePathOwned = AbsolutePath::new_unchecked("/").to_owned();
324 if !state.nodes.contains(storage, &root).await? {
325 state.nodes.insert(storage, root, Default::default()).await?;
326 }
327
328 if recursive {
330 for parent in path.paths() {
331 let parent_owned = parent.to_owned();
332 if !state.nodes.contains(storage, &parent_owned).await? {
333 create_folder(storage, &mut state.nodes, parent, from, time).await?;
334 }
335 }
336 }
337
338 create_node(storage, &mut state.nodes, &path, node.clone()).await
340}
341
342async fn reduce_remove(
343 storage: &CoreBlockStorage,
344 state: &mut File,
345 path: &AbsolutePath,
346 recursive: bool,
347) -> Result<(), anyhow::Error> {
348 let path = path.normalize()?;
349 let (parent_path, name) = path.parent_and_file_name_result()?;
350
351 let mut stack = VecDeque::new();
353 stack.push_back(path.clone());
354 while let Some(current) = stack.pop_front() {
355 let children = state.nodes.get(storage, ¤t).await?;
356 if let Some(children) = children {
357 if !recursive {
359 return Ok(());
360 }
361
362 let child_nodes: Vec<Node> = children.stream(storage).try_collect().await?;
364 for child in &child_nodes {
365 stack.push_back(current.join_path(child.name())?);
366 }
367
368 state.nodes.remove(storage, current).await?;
370 }
371 }
372
373 remove_node_by_name(storage, &mut state.nodes, parent_path, name).await?;
375
376 Ok(())
377}
378
379async fn reduce_modify(
380 storage: &CoreBlockStorage,
381 state: &mut File,
382 path: &AbsolutePath,
383 modifications: &[FileModification],
384) -> Result<(), anyhow::Error> {
385 let path = path.normalize()?;
386 let (parent_path, name) = path.parent_and_file_name_result()?;
387 let parent_path = parent_path.to_owned();
388 let mut file_modification_context = FileModificationContext::new(path.clone());
389
390 for to_parent in modifications.iter().filter_map(|item| match item {
392 FileModification::Move(path) => Some(path),
393 _ => None,
394 }) {
395 let validated_to_parent = if to_parent == "/" {
397 to_parent.to_owned()
398 } else if let Some((to_parent, node)) = get_node(storage, &state.nodes, to_parent, true).await? {
399 if !node.is_dir() {
400 return Err(anyhow!("Can only move into folders: {}", to_parent));
401 }
402 to_parent
403 } else {
404 return Err(anyhow!("Not found: {}", to_parent));
405 };
406
407 let to_path = validated_to_parent.join_path(name)?;
409 if get_node(storage, &state.nodes, &to_path, true).await?.is_some() {
410 return Err(anyhow!("Node exists: {}", to_path));
411 }
412
413 let removed = remove_node_by_name(storage, &mut state.nodes, &parent_path, name).await?;
415
416 for node in removed {
418 create_node(storage, &mut state.nodes, &validated_to_parent, node).await?;
419 }
420
421 file_modification_context.reparent(path.clone(), to_path)?;
423 }
424
425 let modifications: Vec<&FileModification> = modifications
427 .iter()
428 .filter_map(|item| match item {
429 FileModification::Move(_) => None,
430 modification => Some(modification),
431 })
432 .collect();
433 if !modifications.is_empty() {
434 if let Some(mut node_set) = state.nodes.get(storage, &parent_path).await? {
436 for modification in modifications.iter() {
438 if let FileModification::Rename(new_name) = modification {
439 let has_conflict = node_set
440 .stream(storage)
441 .try_any(|node| std::future::ready(node.name() == new_name))
442 .await?;
443 if has_conflict {
444 return Err(anyhow!("File exists: {}", parent_path.join_path(new_name)?));
445 }
446 }
447 }
448
449 let nodes: Vec<Node> = node_set.stream(storage).try_collect().await?;
451 let mut updated_nodes = Vec::with_capacity(nodes.len());
452 for mut node in nodes {
453 if node.name() == name {
454 for modification in modifications.iter() {
455 node.modify(&mut file_modification_context, modification)?;
456 }
457 }
458 updated_nodes.push(node);
459 }
460 node_set = CoSet::from_iter(storage, updated_nodes).await?;
461 state.nodes.insert(storage, parent_path.clone(), node_set).await?;
462 }
463 }
464
465 for (from, to) in file_modification_context.reparent.iter() {
467 reparent(storage, &mut state.nodes, from, to).await?;
468 }
469
470 Ok(())
471}
472
473async fn reparent(
474 storage: &CoreBlockStorage,
475 nodes: &mut CoMap<AbsolutePathOwned, CoSet<Node>>,
476 from: &AbsolutePath,
477 to: &AbsolutePath,
478) -> Result<(), anyhow::Error> {
479 let from_owned = from.to_owned();
480 if let Some(items) = nodes.remove(storage, from_owned).await? {
481 let child_nodes: Vec<Node> = items.stream(storage).try_collect().await?;
483 for child in &child_nodes {
484 if child.is_dir() {
485 Box::pin(reparent(storage, nodes, &from.join_path(child.name())?, &to.join_path(child.name())?))
486 .await?;
487 }
488 }
489
490 let to_owned = to.to_owned();
492 if nodes.contains(storage, &to_owned).await? {
493 return Err(anyhow!("Path exists: {}", to));
494 }
495 nodes.insert(storage, to_owned, items).await?;
496 }
497 Ok(())
498}
499
500#[derive(Debug)]
501pub struct FileModificationContext {
502 path: AbsolutePathOwned,
504
505 reparent: BTreeMap<AbsolutePathOwned, AbsolutePathOwned>,
507}
508impl FileModificationContext {
509 pub fn new(path: AbsolutePathOwned) -> Self {
510 Self { path, reparent: Default::default() }
511 }
512
513 pub fn path(&self) -> AbsolutePathOwned {
514 self.path.clone()
515 }
516
517 pub fn reparent(&mut self, from: AbsolutePathOwned, to: AbsolutePathOwned) -> Result<(), anyhow::Error> {
518 let from = from.normalize()?;
519 let to = to.normalize()?;
520 if from != to {
521 self.reparent.insert(from, to);
522 }
523 Ok(())
524 }
525}
526
527async fn get_node(
529 storage: &CoreBlockStorage,
530 nodes: &CoMap<AbsolutePathOwned, CoSet<Node>>,
531 path: &AbsolutePath,
532 resolve_link: bool,
533) -> Result<Option<(AbsolutePathOwned, Node)>, anyhow::Error> {
534 let (parent_path, name) = path.parent_and_file_name_result()?;
535 let parent_owned = parent_path.to_owned();
536 let Some(node_set) = nodes.get(storage, &parent_owned).await? else {
537 return Ok(None);
538 };
539
540 let all_nodes: Vec<Node> = node_set.stream(storage).try_collect().await?;
541 let node = all_nodes.into_iter().find(|node| node.name() == name);
542
543 if let Some(node) = &node {
545 if resolve_link {
546 if let Node::Link(link) = node {
547 let target = parent_path.join(&link.contents)?;
548 return Box::pin(get_node(storage, nodes, &target, resolve_link)).await;
549 }
550 }
551 }
552
553 Ok(node.map(|node| (path.to_owned(), node)))
554}
555
556async fn create_node(
557 storage: &CoreBlockStorage,
558 nodes: &mut CoMap<AbsolutePathOwned, CoSet<Node>>,
559 parent_path: &AbsolutePath,
560 node: Node,
561) -> Result<(), anyhow::Error> {
562 let validated_parent_path = match parent_path.as_str() {
564 "/" => parent_path.to_owned(),
566 _ => {
568 get_node(storage, nodes, parent_path, true)
569 .await?
570 .ok_or(anyhow!("No such directory: {}", parent_path))?
571 .0
572 },
573 };
574
575 let mut node_set = nodes.get(storage, &validated_parent_path).await?.unwrap_or_default();
577
578 let all_nodes: Vec<Node> = node_set.stream(storage).try_collect().await?;
580 let name_exists = all_nodes.iter().any(|existing| existing.name() == node.name());
581 if !name_exists {
582 node_set.insert(storage, node).await?;
583 nodes.insert(storage, validated_parent_path, node_set).await?;
584 }
585
586 Ok(())
587}
588
589async fn remove_node_by_name(
591 storage: &CoreBlockStorage,
592 nodes: &mut CoMap<AbsolutePathOwned, CoSet<Node>>,
593 parent_path: &AbsolutePath,
594 name: &str,
595) -> Result<BTreeSet<Node>, anyhow::Error> {
596 let parent_owned = parent_path.to_owned();
597 let node_set = nodes.get(storage, &parent_owned).await?.unwrap_or_default();
598
599 let all_nodes: Vec<Node> = node_set.stream(storage).try_collect().await?;
600 let mut kept = Vec::new();
601 let mut removed = BTreeSet::new();
602 for node in all_nodes {
603 if node.name() == name {
604 removed.insert(node);
605 } else {
606 kept.push(node);
607 }
608 }
609
610 if kept.is_empty() && parent_path != "/" {
612 nodes.remove(storage, parent_owned).await?;
613 } else {
614 let new_set = CoSet::from_iter(storage, kept).await?;
615 nodes.insert(storage, parent_owned, new_set).await?;
616 }
617
618 Ok(removed)
619}
620
621async fn create_folder(
622 storage: &CoreBlockStorage,
623 nodes: &mut CoMap<AbsolutePathOwned, CoSet<Node>>,
624 path: &AbsolutePath,
625 from: &Did,
626 time: Date,
627) -> Result<(), anyhow::Error> {
628 let (parent_path, name) = path.parent_and_file_name_result()?;
629 let node = Node::Folder(FolderNode {
630 name: name.to_owned(),
631 create_time: time,
632 modify_time: time,
633 tags: tags!(),
634 owner: from.to_owned(),
635 mode: 0o665,
636 });
637 create_node(storage, nodes, parent_path, node).await
638}
639
640#[cfg(test)]
641mod tests {
642 use crate::{File, FileAction, FileModification, FileNode, Node};
643 use co_api::{
644 AbsolutePath, AbsolutePathOwned, BlockSerializer, BlockStorage, BlockStorageExt, CoreBlockStorage, Link,
645 OptionLink, Reducer, ReducerAction,
646 };
647 use co_storage::MemoryBlockStorage;
648 use futures::TryStreamExt;
649
650 fn new_storage() -> MemoryBlockStorage {
651 MemoryBlockStorage::default()
652 }
653
654 fn core_storage(storage: &MemoryBlockStorage) -> CoreBlockStorage {
655 CoreBlockStorage::new(storage.clone(), false)
656 }
657
658 async fn create_test_file_state() -> (MemoryBlockStorage, Link<File>) {
659 let storage = new_storage();
660
661 let block = BlockSerializer::default().serialize(&"hello world").unwrap();
663 let contents = *block.cid();
664 storage.set(block).await.unwrap();
665 let node = Node::File(FileNode {
666 contents,
667 create_time: 123,
668 modify_time: 123,
669 mode: 0o655,
670 name: "test.txt".to_owned(),
671 owner: "did:local:test".to_owned(),
672 size: 11,
673 tags: Default::default(),
674 });
675 let action = ReducerAction {
676 from: "did:local:test".to_owned(),
677 core: "file".to_owned(),
678 time: 123,
679 payload: FileAction::Create { path: "/hello/world".try_into().unwrap(), node, recursive: true },
680 };
681 let action_link: Link<ReducerAction<FileAction>> = storage.set_value(&action).await.unwrap();
682 let state_link: OptionLink<File> = OptionLink::none();
683 let cs = core_storage(&storage);
684 let state_link = File::reduce(state_link, action_link, &cs).await.unwrap();
685 let state: File = storage.get_value(&state_link).await.unwrap();
686
687 let paths = collect_paths(&storage, &state).await;
689 assert_eq!(paths.len(), 3); assert_eq!(nodes_at(&storage, &state, "/").await.len(), 1); assert_eq!(nodes_at(&storage, &state, "/hello").await.len(), 1); assert_eq!(nodes_at(&storage, &state, "/hello/world").await.len(), 1); (storage, state_link)
695 }
696
697 async fn collect_paths(storage: &MemoryBlockStorage, state: &File) -> Vec<AbsolutePathOwned> {
698 state
699 .nodes
700 .stream(storage)
701 .map_ok(|(key, _): (AbsolutePathOwned, _)| key)
702 .try_collect::<Vec<AbsolutePathOwned>>()
703 .await
704 .unwrap()
705 }
706
707 async fn nodes_at(storage: &MemoryBlockStorage, state: &File, path: &str) -> Vec<Node> {
708 let path_owned = AbsolutePath::new_unchecked(path).to_owned();
709 match state.nodes.get(storage, &path_owned).await.unwrap() {
710 Some(set) => set.stream(storage).try_collect().await.unwrap(),
711 None => vec![],
712 }
713 }
714
715 async fn names(storage: &MemoryBlockStorage, state: &File, path: &str) -> Vec<String> {
716 nodes_at(storage, state, path)
717 .await
718 .iter()
719 .map(|node| node.name().to_owned())
720 .collect()
721 }
722
723 async fn reduce_action(
724 storage: &MemoryBlockStorage,
725 state_link: Link<File>,
726 action: ReducerAction<FileAction>,
727 ) -> (File, Link<File>) {
728 let action_link: Link<ReducerAction<FileAction>> = storage.set_value(&action).await.unwrap();
729 let cs = core_storage(storage);
730 let next_link = File::reduce(state_link.into(), action_link, &cs).await.unwrap();
731 let state: File = storage.get_value(&next_link).await.unwrap();
732 (state, next_link)
733 }
734
735 #[tokio::test]
736 async fn test_create() {
737 let (_storage, _state_link) = create_test_file_state().await;
738 }
739
740 #[tokio::test]
741 async fn test_delete_recursive() {
742 let (storage, state_link) = create_test_file_state().await;
743
744 let action = ReducerAction {
745 from: "did:local:test".to_owned(),
746 core: "file".to_owned(),
747 time: 456,
748 payload: FileAction::Remove { path: "/hello".try_into().unwrap(), recursive: true },
749 };
750 let (state, _) = reduce_action(&storage, state_link, action).await;
751 let paths = collect_paths(&storage, &state).await;
752 assert_eq!(paths.len(), 1); assert_eq!(nodes_at(&storage, &state, "/").await.len(), 0);
754 }
755
756 #[tokio::test]
757 async fn test_modify_rename() {
758 let (storage, state_link) = create_test_file_state().await;
759
760 let action = ReducerAction {
761 from: "did:local:test".to_owned(),
762 core: "file".to_owned(),
763 time: 456,
764 payload: FileAction::Modify {
765 path: "/hello/world/test.txt".try_into().unwrap(),
766 modifications: vec![FileModification::Rename("welcome.txt".to_owned())],
767 },
768 };
769 let (state, _) = reduce_action(&storage, state_link, action).await;
770 let files = nodes_at(&storage, &state, "/hello/world").await;
771 assert_eq!(files.len(), 1);
772 assert_eq!(files.first().unwrap().name(), "welcome.txt");
773 }
774
775 #[tokio::test]
776 async fn test_modify_rename_with_children() {
777 let (storage, state_link) = create_test_file_state().await;
778
779 let action = ReducerAction {
780 from: "did:local:test".to_owned(),
781 core: "file".to_owned(),
782 time: 456,
783 payload: FileAction::Modify {
784 path: "/hello".try_into().unwrap(),
785 modifications: vec![FileModification::Rename("test".to_owned())],
786 },
787 };
788 let (state, _) = reduce_action(&storage, state_link, action).await;
789 let mut paths: Vec<String> = collect_paths(&storage, &state).await.iter().map(|p| p.to_string()).collect();
790 paths.sort();
791 assert_eq!(paths, vec!["/", "/test", "/test/world"]);
792 assert_eq!(names(&storage, &state, "/").await, vec!["test"]);
793 assert_eq!(names(&storage, &state, "/test").await, vec!["world"]);
794 assert_eq!(names(&storage, &state, "/test/world").await, vec!["test.txt"]);
795 }
796
797 #[tokio::test]
798 async fn test_modify_move() {
799 let (storage, state_link) = create_test_file_state().await;
800
801 let action = ReducerAction {
802 from: "did:local:test".to_owned(),
803 core: "file".to_owned(),
804 time: 456,
805 payload: FileAction::Modify {
806 path: "/hello/world".try_into().unwrap(),
807 modifications: vec![FileModification::Move("/".try_into().unwrap())],
808 },
809 };
810 let (state, _) = reduce_action(&storage, state_link, action).await;
811 let mut paths: Vec<String> = collect_paths(&storage, &state).await.iter().map(|p| p.to_string()).collect();
812 paths.sort();
813 assert_eq!(paths, vec!["/", "/world"]); let mut root_names = names(&storage, &state, "/").await;
815 root_names.sort();
816 assert_eq!(root_names, vec!["hello", "world"]);
817 assert!(names(&storage, &state, "/hello").await.is_empty());
818 assert_eq!(names(&storage, &state, "/world").await, vec!["test.txt"]);
819 }
820
821 #[tokio::test]
822 async fn test_modify_move_file() {
823 let (storage, state_link) = create_test_file_state().await;
824
825 let action = ReducerAction {
826 from: "did:local:test".to_owned(),
827 core: "file".to_owned(),
828 time: 456,
829 payload: FileAction::Modify {
830 path: "/hello/world/test.txt".try_into().unwrap(),
831 modifications: vec![FileModification::Move("/hello".try_into().unwrap())],
832 },
833 };
834 let (state, _) = reduce_action(&storage, state_link, action).await;
835 let mut paths: Vec<String> = collect_paths(&storage, &state).await.iter().map(|p| p.to_string()).collect();
836 paths.sort();
837 assert_eq!(paths, vec!["/", "/hello"]); assert_eq!(names(&storage, &state, "/").await, vec!["hello"]);
839 let mut hello_names = names(&storage, &state, "/hello").await;
840 hello_names.sort();
841 assert_eq!(hello_names, vec!["test.txt", "world"]);
842 assert!(names(&storage, &state, "/hello/world").await.is_empty());
843 }
844}