gravityfile_ops/
conflict.rs1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone)]
9pub struct Conflict {
10 pub source: PathBuf,
12 pub destination: PathBuf,
14 pub kind: ConflictKind,
16}
17
18impl Conflict {
19 pub fn new(source: PathBuf, destination: PathBuf, kind: ConflictKind) -> Self {
21 Self {
22 source,
23 destination,
24 kind,
25 }
26 }
27
28 pub fn file_exists(source: PathBuf, destination: PathBuf) -> Self {
30 Self::new(source, destination, ConflictKind::FileExists)
31 }
32
33 pub fn directory_exists(source: PathBuf, destination: PathBuf) -> Self {
35 Self::new(source, destination, ConflictKind::DirectoryExists)
36 }
37
38 pub fn source_is_ancestor(source: PathBuf, destination: PathBuf) -> Self {
40 Self::new(source, destination, ConflictKind::SourceIsAncestor)
41 }
42
43 pub fn permission_denied(source: PathBuf, destination: PathBuf) -> Self {
45 Self::new(source, destination, ConflictKind::PermissionDenied)
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum ConflictKind {
52 FileExists,
54 DirectoryExists,
56 SourceIsAncestor,
58 PermissionDenied,
60 SameFile,
62}
63
64impl std::fmt::Display for ConflictKind {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 Self::FileExists => write!(f, "File already exists"),
68 Self::DirectoryExists => write!(f, "Directory already exists"),
69 Self::SourceIsAncestor => write!(f, "Cannot copy/move a directory into itself"),
70 Self::PermissionDenied => write!(f, "Permission denied"),
71 Self::SameFile => write!(f, "Source and destination are the same file"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
78pub enum ConflictResolution {
79 #[default]
81 Skip,
82 Overwrite,
84 AutoRename,
86 SkipAll,
88 OverwriteAll,
90 Abort,
92}
93
94impl ConflictResolution {
95 pub fn is_global(&self) -> bool {
97 matches!(self, Self::SkipAll | Self::OverwriteAll | Self::Abort)
98 }
99
100 pub fn to_single(&self) -> Self {
102 match self {
103 Self::SkipAll => Self::Skip,
104 Self::OverwriteAll => Self::Overwrite,
105 _ => *self,
106 }
107 }
108}
109
110pub fn auto_rename_path(path: &PathBuf) -> PathBuf {
114 let parent = path.parent().unwrap_or(std::path::Path::new(""));
115 let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
116 let extension = path.extension().and_then(|e| e.to_str());
117
118 for i in 1..1000 {
119 let new_name = if let Some(ext) = extension {
120 format!("{} ({}).{}", stem, i, ext)
121 } else {
122 format!("{} ({})", stem, i)
123 };
124
125 let new_path = parent.join(&new_name);
126 if !new_path.exists() {
127 return new_path;
128 }
129 }
130
131 let timestamp = std::time::SystemTime::now()
133 .duration_since(std::time::UNIX_EPOCH)
134 .map(|d| d.as_secs())
135 .unwrap_or(0);
136
137 let new_name = if let Some(ext) = extension {
138 format!("{}_{}.{}", stem, timestamp, ext)
139 } else {
140 format!("{}_{}", stem, timestamp)
141 };
142
143 parent.join(&new_name)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_auto_rename_path() {
152 let path = PathBuf::from("/tmp/test.txt");
153 let renamed = auto_rename_path(&path);
154 assert!(renamed.to_string_lossy().contains("test (1).txt"));
155 }
156
157 #[test]
158 fn test_auto_rename_no_extension() {
159 let path = PathBuf::from("/tmp/testfile");
160 let renamed = auto_rename_path(&path);
161 assert!(renamed.to_string_lossy().contains("testfile (1)"));
162 }
163}