1use std::collections::BTreeSet;
2use std::path::{Path, PathBuf};
3
4use eyre::Result;
5use floppy_disk::prelude::*;
6use tokio::io::AsyncWriteExt;
7use tracing::{error, trace, warn};
8
9pub struct DiskDrive<
10    'a,
11    'b,
12    F1: FloppyDisk<'a> + FloppyDiskUnixExt + Send + Sync + 'a,
13    F2: FloppyDisk<'b> + FloppyDiskUnixExt + Send + Sync + 'b,
14> where
15    <F1 as FloppyDisk<'a>>::Permissions: FloppyUnixPermissions,
16    <F1 as FloppyDisk<'a>>::Metadata: FloppyUnixMetadata,
17
18    <F1 as FloppyDisk<'a>>::DirBuilder: Send,
19    <F1 as FloppyDisk<'a>>::DirEntry: Send,
20    <F1 as FloppyDisk<'a>>::File: Send,
21    <F1 as FloppyDisk<'a>>::FileType: Send,
22    <F1 as FloppyDisk<'a>>::Metadata: Send,
23    <F1 as FloppyDisk<'a>>::OpenOptions: Send,
24    <F1 as FloppyDisk<'a>>::Permissions: Send,
25    <F1 as FloppyDisk<'a>>::ReadDir: Send,
26
27    <F2 as FloppyDisk<'b>>::Permissions: FloppyUnixPermissions,
28    <F2 as FloppyDisk<'b>>::Metadata: FloppyUnixMetadata,
29
30    <F2 as FloppyDisk<'b>>::DirBuilder: Send,
31    <F2 as FloppyDisk<'b>>::DirEntry: Send,
32    <F2 as FloppyDisk<'b>>::File: Send,
33    <F2 as FloppyDisk<'b>>::FileType: Send,
34    <F2 as FloppyDisk<'b>>::Metadata: Send,
35    <F2 as FloppyDisk<'b>>::OpenOptions: Send,
36    <F2 as FloppyDisk<'b>>::Permissions: Send,
37    <F2 as FloppyDisk<'b>>::ReadDir: Send,
38{
39    _f1: std::marker::PhantomData<&'a F1>,
40    _f2: std::marker::PhantomData<&'b F2>,
41}
42
43impl<
44        'a,
45        'b,
46        F1: FloppyDisk<'a> + FloppyDiskUnixExt + Send + Sync + 'a,
47        F2: FloppyDisk<'b> + FloppyDiskUnixExt + Send + Sync + 'b,
48    > DiskDrive<'a, 'b, F1, F2>
49where
50    <F1 as FloppyDisk<'a>>::Permissions: FloppyUnixPermissions,
51    <F1 as FloppyDisk<'a>>::Metadata: FloppyUnixMetadata,
52    <F2 as FloppyDisk<'b>>::Permissions: FloppyUnixPermissions,
53    <F2 as FloppyDisk<'b>>::Metadata: FloppyUnixMetadata,
54{
55    pub async fn copy_between(src: &'a F1, dest: &'b F2) -> Result<()> {
56        Self::do_copy(src, dest, None, None).await
57    }
58
59    pub async fn copy_from_src<P: Into<PathBuf>>(
60        src: &'a F1,
61        dest: &'b F2,
62        src_scope: P,
63    ) -> Result<()> {
64        let src_scope = src_scope.into();
65        let src_scope = if !src_scope.starts_with("/") {
66            PathBuf::from("/").join(src_scope)
67        } else {
68            src_scope
69        };
70        Self::do_copy(src, dest, Some(src_scope), None).await
71    }
72
73    pub async fn copy_to_dest<P: Into<PathBuf>>(
74        src: &'a F1,
75        dest: &'b F2,
76        dest_scope: P,
77    ) -> Result<()> {
78        let dest_scope = dest_scope.into();
79        let dest_scope = if !dest_scope.starts_with("/") {
80            PathBuf::from("/").join(dest_scope)
81        } else {
82            dest_scope
83        };
84        Self::do_copy(src, dest, None, Some(dest_scope)).await
85    }
86
87    pub async fn copy_from_src_to_dest<P: Into<PathBuf>, Q: Into<PathBuf>>(
88        src: &'a F1,
89        dest: &'b F2,
90        src_scope: P,
91        dest_scope: Q,
92    ) -> Result<()> {
93        let src_scope = src_scope.into();
94        let dest_scope = dest_scope.into();
95        let src_scope = if !src_scope.starts_with("/") {
96            PathBuf::from("/").join(src_scope)
97        } else {
98            src_scope
99        };
100        let dest_scope = if !dest_scope.starts_with("/") {
101            PathBuf::from("/").join(dest_scope)
102        } else {
103            dest_scope
104        };
105        Self::do_copy(src, dest, Some(src_scope), Some(dest_scope)).await
106    }
107
108    async fn do_copy(
109        src: &'a F1,
110        dest: &'b F2,
111        src_path: Option<PathBuf>,
112        dest_path: Option<PathBuf>,
113    ) -> Result<()> {
114        let src_path = src_path.unwrap_or_else(|| PathBuf::from("/"));
115        let dest_path = dest_path.unwrap_or_else(|| PathBuf::from("/"));
116        let paths = if src.metadata(&src_path).await?.is_file() {
117            trace!("copying src file {}", src_path.display());
118            let mut out = BTreeSet::new();
119            out.insert(src_path);
120            out
121        } else {
122            trace!("copying src dir {}", src_path.display());
123            nyoom::walk_ordered(src, src_path).await?
124        };
125        for src_path in paths {
126            trace!("processing src_path: {}", src_path.display());
127            match <F1 as FloppyDisk<'a>>::read_link(src, &src_path).await {
128                Ok(_) => {
129                    trace!(
130                        "copy symlink {} -> {}",
131                        src_path.display(),
132                        dest_path.display()
133                    );
134                    Self::add_symlink_to_memfs(src, dest, &src_path, &dest_path).await?;
135                }
136                Err(_) => {
137                    let metadata = <F1 as FloppyDisk<'a>>::metadata(src, &src_path).await?;
138                    let file_type = metadata.file_type();
139                    if file_type.is_dir() {
140                        trace!("copy dir {} -> {}", src_path.display(), dest_path.display());
141                        Self::copy_dir_to_memfs(src, dest, &src_path, &dest_path).await?;
142                    } else if file_type.is_file() {
143                        trace!(
144                            "copy file {} -> {}",
145                            src_path.display(),
146                            dest_path.display()
147                        );
148                        Self::copy_file_to_memfs(src, dest, &src_path, &dest_path).await?;
149                    } else {
150                        error!("unknown file type for source path {src_path:?}");
151                    }
152                }
153            };
154        }
155
156        Ok(())
157    }
158
159    async fn copy_file_to_memfs(
160        src: &'a F1,
161        dest: &'b F2,
162        src_path: &Path,
163        dest_path: &Path,
164    ) -> Result<()> {
165        dest.create_dir_all("/").await?;
166        let dest_path = if !dest_path.starts_with("/") {
167            PathBuf::from("/").join(dest_path)
168        } else {
169            dest_path.to_path_buf()
170        };
171        let dest_metadata = <F2 as FloppyDisk>::metadata(dest, &dest_path).await;
173        let dest_path = if dest_metadata.is_ok() {
174            let dest_metadata = dest_metadata?;
175            if dest_metadata.is_dir() {
176                trace!("dest path is a dir, appending src name!");
177                let src_path = if src_path.starts_with("/") {
179                    src_path.strip_prefix("/").unwrap()
180                } else {
181                    src_path
182                };
183                dest_path.join(src_path)
184            } else {
185                dest_path.to_path_buf()
186            }
187        } else {
188            dest_path.to_path_buf()
189        };
190        let dest_path = dest_path.as_path();
191
192        trace!("creating file {dest_path:?}");
193        if let Some(memfs_parent) = dest_path.parent() {
194            trace!("creating parents: {}", memfs_parent.display());
195            dest.create_dir_all(memfs_parent).await?;
196        }
197
198        let mut src_handle: <F1 as FloppyDisk>::File = <F1::OpenOptions>::new()
199            .read(true)
200            .open(src, src_path)
201            .await?;
202        {
203            if let Some(parent) = dest_path.parent() {
204                trace!("creating dest file parents");
205                dest.create_dir_all(parent).await?;
206            }
207
208            let dest_metadata = <F2 as FloppyDisk>::metadata(dest, dest_path).await;
209            if dest_metadata.is_err() {
211                trace!("dest file {dest_path:?} doesn't exist, copying directly!");
212                let mut dest_handle: <F2 as FloppyDisk>::File = <F2::OpenOptions>::new()
213                    .create(true)
214                    .read(true)
215                    .write(true)
216                    .create_new(true)
217                    .open(dest, dest_path)
218                    .await?;
219                tokio::io::copy(&mut src_handle, &mut dest_handle).await?;
220
221                let src_metadata = src_handle.metadata().await?;
223                let src_permissions = src_metadata.permissions();
224                let mode = <<F1 as FloppyDisk<'_>>::Permissions as FloppyUnixPermissions>::mode(
225                    &src_permissions,
226                );
227
228                let permissions = <<F2 as FloppyDisk>::Permissions>::from_mode(mode);
229                let uid = src_metadata.uid()?;
230                let gid = src_metadata.gid()?;
231
232                <F2 as FloppyDiskUnixExt>::chown(dest, dest_path, uid, gid).await?;
233                <F2 as FloppyDisk>::set_permissions(dest, dest_path, permissions).await?;
234
235                return Ok(());
236            }
237
238            let mut dest_handle: <F2 as FloppyDisk>::File = <F2::OpenOptions>::new()
239                .read(true)
240                .write(true)
241                .open(dest, dest_path)
242                .await?;
243
244            let dest_metadata = dest_metadata?;
246            if dest_metadata.is_dir() {
247                trace!("copying into dir {dest_path:?}");
248                let dest_path = dest_path.join(Path::new(src_path.file_name().unwrap()));
249                trace!("target path = {dest_path:?}");
250                let written = tokio::io::copy(&mut src_handle, &mut dest_handle).await?;
251                trace!("wrote {written} bytes");
252                dest_handle.flush().await?;
253
254                let src_metadata = src_handle.metadata().await?;
256                let src_permissions = src_metadata.permissions();
257                let mode = <<F1 as FloppyDisk<'_>>::Permissions as FloppyUnixPermissions>::mode(
258                    &src_permissions,
259                );
260
261                let permissions = <<F2 as FloppyDisk>::Permissions>::from_mode(mode);
262                let uid = src_metadata.uid()?;
263                let gid = src_metadata.gid()?;
264
265                <F2 as FloppyDiskUnixExt>::chown(dest, &dest_path, uid, gid).await?;
266                <F2 as FloppyDisk>::set_permissions(dest, &dest_path, permissions).await?;
267
268                return Ok(());
269            }
270
271            if dest_metadata.is_file() {
273                trace!("overwriting dest file {dest_path:?}");
274                tokio::io::copy(&mut src_handle, &mut dest_handle).await?;
275
276                let src_metadata = src_handle.metadata().await?;
278                let src_permissions = src_metadata.permissions();
279                let mode = <<F1 as FloppyDisk<'_>>::Permissions as FloppyUnixPermissions>::mode(
280                    &src_permissions,
281                );
282
283                let permissions = <<F2 as FloppyDisk>::Permissions>::from_mode(mode);
284                let uid = src_metadata.uid()?;
285                let gid = src_metadata.gid()?;
286
287                <F2 as FloppyDiskUnixExt>::chown(dest, dest_path, uid, gid).await?;
288                <F2 as FloppyDisk>::set_permissions(dest, dest_path, permissions).await?;
289
290                return Ok(());
291            }
292
293            if dest_metadata.is_symlink() {
295                warn!("dest file path {dest_path:?} is a symlink, skipping copy!");
296                return Ok(());
297            }
298        }
299
300        let src_metadata = src_handle.metadata().await?;
301        let src_permissions = src_metadata.permissions();
302        let mode =
303            <<F1 as FloppyDisk<'_>>::Permissions as FloppyUnixPermissions>::mode(&src_permissions);
304        let permissions = <<F2 as FloppyDisk>::Permissions>::from_mode(mode);
305        let uid = src_metadata.uid()?;
306        let gid = src_metadata.gid()?;
307        <F2 as FloppyDiskUnixExt>::chown(dest, dest_path, uid, gid).await?;
308        <F2 as FloppyDisk>::set_permissions(dest, dest_path, permissions).await?;
309
310        Ok(())
311    }
312
313    async fn copy_dir_to_memfs(
314        src: &'a F1,
315        dest: &'b F2,
316        src_path: &Path,
317        dest_path: &Path,
318    ) -> Result<()> {
319        let dest_path = dest_path.join(src_path);
320        let dest_path = dest_path.as_path();
321        trace!("creating dir {dest_path:?}");
322        dest.create_dir_all(dest_path).await?;
323
324        let src_metadata = src.metadata(src_path).await?;
325        let mode = src_metadata.permissions().mode();
326        let permissions = <F2 as FloppyDisk>::Permissions::from_mode(mode);
327        dest.set_permissions(dest_path, permissions).await?;
328        dest.chown(dest_path, src_metadata.uid()?, src_metadata.gid()?)
329            .await?;
330
331        Ok(())
332    }
333
334    async fn add_symlink_to_memfs(
335        src: &F1,
336        dest: &F2,
337        src_path: &Path,
338        dest_path: &Path,
339    ) -> Result<()> {
340        let dest_path = dest_path.join(src_path);
341        let dest_path = dest_path.as_path();
342        let link = src.read_link(src_path).await?;
343        trace!("linking {dest_path:?} to {link:?}");
344        dest.symlink(link, dest_path.into()).await?;
345
346        Ok(())
347    }
348}