Skip to content

Commit a08736f

Browse files
committed
Implement rename fallback when moving across devices
1 parent 121585c commit a08736f

File tree

2 files changed

+161
-4
lines changed

2 files changed

+161
-4
lines changed

crates/backend/src/backend_handler.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,7 +1417,7 @@ impl BackendState {
14171417

14181418
#[cfg(windows)]
14191419
if let Ok(target) = junction::get_target(&instance.root_path) {
1420-
if let Err(err) = std::fs::rename(&target, &path) {
1420+
if let Err(err) = crate::rename_with_fallback_across_devices(&target, &path) {
14211421
log::error!("Unable to move instance files from {target:?} to {path:?}: {err:?}");
14221422
self.send.send_error(format!("Unable to move instance files: {err}"));
14231423
return;
@@ -1435,7 +1435,7 @@ impl BackendState {
14351435
};
14361436

14371437
if let Ok(target) = std::fs::read_link(&instance.root_path) {
1438-
if let Err(err) = std::fs::rename(&target, &path) {
1438+
if let Err(err) = crate::rename_with_fallback_across_devices(&target, &path) {
14391439
log::error!("Unable to move instance files from {target:?} to {path:?}: {err:?}");
14401440
self.send.send_error(format!("Unable to move instance files: {err}"));
14411441
return;
@@ -1463,7 +1463,7 @@ impl BackendState {
14631463
return;
14641464
}
14651465

1466-
if let Err(err) = std::fs::rename(&instance.root_path, &path) {
1466+
if let Err(err) = crate::rename_with_fallback_across_devices(&instance.root_path, &path) {
14671467
log::error!("Unable to move instance files: {err:?}");
14681468
self.send.send_error(format!("Unable to move instance files: {err}"));
14691469
return;

crates/backend/src/lib.rs

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![deny(unused_must_use)]
22

33
mod backend;
4-
use std::{ffi::{OsStr, OsString}, io::Write, path::{Path, PathBuf}, sync::Arc};
4+
use std::{ffi::{OsStr, OsString}, io::{ErrorKind, Write}, path::{Path, PathBuf}, sync::Arc};
55

66
pub use backend::*;
77
use bridge::instance::InstanceContentSummary;
@@ -198,6 +198,163 @@ impl FolderChanges {
198198
}
199199
}
200200

201+
pub fn copy_content_recursive(from: &Path, to: &Path) -> std::io::Result<()> {
202+
let from = from.canonicalize()?;
203+
if !from.is_dir() {
204+
return Err(ErrorKind::NotADirectory.into());
205+
}
206+
if !to.is_dir() {
207+
return Err(ErrorKind::AlreadyExists.into());
208+
}
209+
210+
let mut directories = Vec::new();
211+
let mut files = Vec::new();
212+
let mut internal_symlinks = Vec::new();
213+
let mut external_symlinks = Vec::new();
214+
#[cfg(windows)]
215+
let mut internal_junctions = Vec::new();
216+
#[cfg(windows)]
217+
let mut external_junctions = Vec::new();
218+
219+
let mut total_bytes = 0;
220+
221+
let mut directories_to_visit = Vec::new();
222+
directories_to_visit.push((from.to_path_buf(), 0));
223+
224+
while let Some((directory, depth)) = directories_to_visit.pop() {
225+
let read_dir = std::fs::read_dir(directory)?;
226+
for entry in read_dir {
227+
let entry = entry?;
228+
let path = entry.path();
229+
let file_type = entry.file_type()?;
230+
let Ok(relative) = path.strip_prefix(&from) else {
231+
return Err(ErrorKind::Other.into());
232+
};
233+
if file_type.is_symlink() {
234+
let target = std::fs::read_link(&path)?;
235+
if let Ok(internal) = target.strip_prefix(&from) {
236+
internal_symlinks.push((relative.to_path_buf(), internal.to_path_buf()));
237+
} else {
238+
external_symlinks.push((relative.to_path_buf(), target));
239+
}
240+
} else if file_type.is_file() {
241+
let metadata = entry.metadata()?;
242+
files.push((relative.to_path_buf(), path));
243+
total_bytes += metadata.len();
244+
245+
} else if file_type.is_dir() {
246+
#[cfg(windows)]
247+
if let Some(target) = junction::get_target(&path) {
248+
if let Ok(internal) = target.strip_prefix(&from) {
249+
internal_junctions.push((relative.to_path_buf(), internal.to_path_buf()));
250+
} else {
251+
external_junctions.push((relative.to_path_buf(), target));
252+
}
253+
continue;
254+
}
255+
256+
if depth >= 256 {
257+
return Err(ErrorKind::QuotaExceeded.into());
258+
}
259+
260+
directories.push(relative.to_path_buf());
261+
directories_to_visit.push((path, depth+1));
262+
}
263+
}
264+
}
265+
266+
for directory in directories {
267+
_ = std::fs::create_dir(to.join(directory));
268+
}
269+
let mut copied_bytes = 0;
270+
for (relative, copy_from) in files {
271+
let dest = to.join(relative);
272+
copied_bytes += std::fs::copy(copy_from, dest)?;
273+
}
274+
if copied_bytes != total_bytes {
275+
return Err(ErrorKind::Interrupted.into());
276+
}
277+
for (relative, internal) in internal_symlinks {
278+
let dest = to.join(relative);
279+
let target = to.join(internal);
280+
symlink_dir_or_file(&target, &dest)?;
281+
}
282+
for (relative, target) in external_symlinks {
283+
let dest = to.join(relative);
284+
symlink_dir_or_file(&target, &dest)?;
285+
}
286+
#[cfg(windows)]
287+
for (relative, internal) in internal_junctions {
288+
let dest = to.join(relative);
289+
let target = to.join(internal);
290+
junction::create(&target, &dest)?;
291+
}
292+
#[cfg(windows)]
293+
for (relative, target) in external_junctions {
294+
let dest = to.join(relative);
295+
junction::create(&target, &dest)?;
296+
}
297+
Ok(())
298+
}
299+
300+
pub fn symlink_dir_or_file(original: &Path, link: &Path) -> std::io::Result<()> {
301+
#[cfg(unix)]
302+
{
303+
if !original.exists() {
304+
return Err(ErrorKind::NotFound.into());
305+
}
306+
std::os::unix::fs::symlink(original, link)
307+
}
308+
#[cfg(windows)]
309+
{
310+
let metadata = original.metadata()?;
311+
if metadata.is_dir() {
312+
std::os::windows::fs::symlink_dir(original, link)
313+
} else if metadata.is_file() {
314+
std::os::windows::fs::symlink_file(original, link)
315+
} else {
316+
return Err(ErrorKind::NotFound.into());
317+
}
318+
}
319+
#[cfg(not(any(windows, unix)))]
320+
compile_error!("Unsupported platform: can't symlink");
321+
}
322+
323+
pub fn rename_with_fallback_across_devices(from: &Path, to: &Path) -> std::io::Result<()> {
324+
// Remove empty 'to' directory to ensure consistent behaviour across unix and windows
325+
if let Err(err) = std::fs::remove_dir(to) && err.kind() != ErrorKind::NotADirectory {
326+
return Err(err);
327+
}
328+
if let Err(err) = std::fs::rename(from, to) {
329+
if err.kind() == ErrorKind::CrossesDevices {
330+
// Obviously this is racy, but this is the best we can do
331+
if from.is_symlink() {
332+
let target = std::fs::read_link(from)?;
333+
symlink_dir_or_file(&target, to)?;
334+
_ = std::fs::remove_file(from);
335+
} else if from.is_dir() {
336+
std::fs::create_dir(to)?;
337+
if let Err(err) = copy_content_recursive(from, to) {
338+
_ = std::fs::remove_dir_all(to);
339+
return Err(err);
340+
} else {
341+
_ = std::fs::remove_dir_all(from);
342+
return Ok(());
343+
}
344+
} else if from.is_file() {
345+
std::fs::copy(from, to)?;
346+
_ = std::fs::remove_file(from);
347+
} else {
348+
return Err(ErrorKind::InvalidInput.into());
349+
}
350+
return Ok(());
351+
}
352+
Err(err)
353+
} else {
354+
Ok(())
355+
}
356+
}
357+
201358
pub fn join_windows_shell(args: &[&str]) -> String {
202359
let mut string = String::new();
203360

0 commit comments

Comments
 (0)