/* vifm * Copyright (C) 2013 xaizek. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include "ior.h" #include <sys/stat.h> /* stat */ #include <unistd.h> /* unlink() */ #include <errno.h> /* EEXIST EISDIR ENOTEMPTY EXDEV errno */ #include <stddef.h> /* NULL */ #include <stdio.h> /* remove() snprintf() */ #include <stdlib.h> /* free() */ #include <string.h> /* strlen() */ #include "../compat/fs_limits.h" #include "../compat/os.h" #include "../utils/fs.h" #include "../utils/log.h" #include "../utils/path.h" #include "../utils/str.h" #include "../utils/utils.h" #include "../background.h" #include "private/ioc.h" #include "private/ioe.h" #include "private/ioeta.h" #include "private/traverser.h" #include "ioc.h" #include "iop.h" static VisitResult rm_visitor(const char full_path[], VisitAction action, void *param); static VisitResult cp_visitor(const char full_path[], VisitAction action, void *param); static IoRes mv_by_copy(io_args_t *args, int confirmed); static IoRes mv_replacing_all(io_args_t *args); static IoRes mv_replacing_files(io_args_t *args); static int is_file(const char path[]); static VisitResult mv_visitor(const char full_path[], VisitAction action, void *param); static VisitResult cp_mv_visitor(const char full_path[], VisitAction action, void *param, int cp); static VisitResult vr_from_io_res(IoRes result); IoRes ior_rm(io_args_t *args) { const char *const path = args->arg1.path; return traverse(path, &rm_visitor, args); } /* Implementation of traverse() visitor for subtree removal. Returns 0 on * success, otherwise non-zero is returned. */ static VisitResult rm_visitor(const char full_path[], VisitAction action, void *param) { io_args_t *const rm_args = param; VisitResult result = VR_OK; if(io_cancelled(rm_args)) { return VR_CANCELLED; } switch(action) { case VA_DIR_ENTER: /* Do nothing, directories are removed on leaving them. */ result = VR_OK; break; case VA_FILE: { io_args_t args = { .arg1.path = full_path, .cancellation = rm_args->cancellation, .estim = rm_args->estim, .result = rm_args->result, }; result = vr_from_io_res(iop_rmfile(&args)); rm_args->result = args.result; break; } case VA_DIR_LEAVE: { io_args_t args = { .arg1.path = full_path, .cancellation = rm_args->cancellation, .estim = rm_args->estim, .result = rm_args->result, }; result = vr_from_io_res(iop_rmdir(&args)); rm_args->result = args.result; break; } } return result; } IoRes ior_cp(io_args_t *args) { const char *const src = args->arg1.src; const char *const dst = args->arg2.dst; if(is_in_subtree(dst, src, 0)) { (void)ioe_errlst_append(&args->result.errors, src, IO_ERR_UNKNOWN, "Can't copy parent path into subpath"); return IO_RES_FAILED; } if(args->arg3.crs == IO_CRS_REPLACE_ALL) { io_args_t rm_args = { .arg1.path = dst, .cancellation = args->cancellation, .estim = args->estim, .result = args->result, }; const IoRes result = ior_rm(&rm_args); args->result = rm_args.result; if(result != IO_RES_SUCCEEDED) { if(result == IO_RES_FAILED && !io_cancelled(args)) { (void)ioe_errlst_append(&args->result.errors, dst, IO_ERR_UNKNOWN, "Failed to remove"); } return result; } } return traverse(src, &cp_visitor, args); } /* Implementation of traverse() visitor for subtree copying. Returns 0 on * success, otherwise non-zero is returned. */ static VisitResult cp_visitor(const char full_path[], VisitAction action, void *param) { return cp_mv_visitor(full_path, action, param, 1); } IoRes ior_mv(io_args_t *args) { const char *const src = args->arg1.src; const char *const dst = args->arg2.dst; const IoCrs crs = args->arg3.crs; const io_confirm confirm = args->confirm; int confirmed = 0; if(crs == IO_CRS_FAIL && path_exists(dst, NODEREF) && !is_case_change(src, dst)) { (void)ioe_errlst_append(&args->result.errors, dst, EEXIST, "Destination path already exists"); return IO_RES_FAILED; } if(crs == IO_CRS_APPEND_TO_FILES) { if(!is_file(src)) { (void)ioe_errlst_append(&args->result.errors, src, EISDIR, "Can't append when source is not a file"); return IO_RES_FAILED; } if(!is_file(dst)) { (void)ioe_errlst_append(&args->result.errors, dst, EISDIR, "Can't append when destination is not a file"); return IO_RES_FAILED; } } else if(crs == IO_CRS_REPLACE_FILES && path_exists(dst, DEREF)) { /* Ask user whether to overwrite destination file. */ if(confirm != NULL && !confirm(args, src, dst)) { return IO_RES_SKIPPED; } confirmed = 1; } if(os_rename(src, dst) == 0) { ioeta_update(args->estim, src, dst, 1, 0); return IO_RES_SUCCEEDED; } int error = errno; int cross_fs = (error == EXDEV); #ifndef _WIN32 /* At least SSHFS fails to propagate EXDEV and reports EPERM. So we try to do * the copy across mounts anyway, which might actually work despite the error * code. */ cross_fs |= (error == EPERM || error == EACCES); #endif if(cross_fs) { return mv_by_copy(args, confirmed); } int overwrite = (error == EISDIR || error == ENOTEMPTY || error == EEXIST); #ifdef _WIN32 /* For MXE builds running in Wine. */ overwrite |= (error == EPERM || error == EACCES); #endif if(overwrite) { if(crs == IO_CRS_REPLACE_ALL) { return mv_replacing_all(args); } if(crs == IO_CRS_REPLACE_FILES || (!has_atomic_file_replace() && crs == IO_CRS_APPEND_TO_FILES)) { return mv_replacing_files(args); } } (void)ioe_errlst_append(&args->result.errors, src, error, "Rename operation failed"); return (error == 0 ? IO_RES_SUCCEEDED : IO_RES_FAILED); } /* Performs a manual move: copy followed by deletion. Returns status. */ static IoRes mv_by_copy(io_args_t *args, int confirmed) { /* Do not ask for confirmation second time. */ const io_confirm confirm = args->confirm; args->confirm = (confirmed ? NULL : confirm); IoRes result = ior_cp(args); args->confirm = confirm; /* When result is success, there still might be errors if they were * ignored by the user. Do not delete source in this case, some files * might be missing at the destination. */ if(result == IO_RES_SUCCEEDED && args->result.errors.error_count == 0) { io_args_t rm_args = { .arg1.path = args->arg1.src, .cancellation = args->cancellation, .estim = args->estim, .result = args->result, }; /* Disable progress reporting for this "secondary" operation. */ const int silent = ioeta_silent_on(rm_args.estim); result = ior_rm(&rm_args); args->result = rm_args.result; ioeta_silent_set(rm_args.estim, silent); } return result; } /* Performs a move after deleting target first. Returns status. */ static IoRes mv_replacing_all(io_args_t *args) { const char *const src = args->arg1.src; const char *const dst = args->arg2.dst; io_args_t rm_args = { .arg1.path = dst, .cancellation = args->cancellation, .estim = args->estim, .result = args->result, }; /* Ask user whether to overwrite destination file. */ if(args->confirm != NULL && !args->confirm(args, src, dst)) { return IO_RES_SUCCEEDED; } IoRes result = ior_rm(&rm_args); args->result = rm_args.result; if(result != IO_RES_SUCCEEDED) { if(result == IO_RES_FAILED && !io_cancelled(args)) { (void)ioe_errlst_append(&args->result.errors, dst, IO_ERR_UNKNOWN, "Failed to remove"); } return result; } if(os_rename(src, dst) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Rename operation failed"); return IO_RES_FAILED; } return IO_RES_SUCCEEDED; } /* Performs a merging move (for directories). Returns status. */ static IoRes mv_replacing_files(io_args_t *args) { const char *const src = args->arg1.src; const char *const dst = args->arg2.dst; if(!has_atomic_file_replace() && is_file(dst)) { io_args_t rm_args = { .arg1.path = dst, .cancellation = args->cancellation, .estim = args->estim, .result = args->result, }; const IoRes result = iop_rmfile(&rm_args); args->result = rm_args.result; if(result != IO_RES_SUCCEEDED) { if(result == IO_RES_FAILED && !io_cancelled(args)) { (void)ioe_errlst_append(&args->result.errors, dst, IO_ERR_UNKNOWN, "Failed to remove"); } return result; } } return traverse(src, &mv_visitor, args); } /* Checks that path points to a file or symbolic link. Returns non-zero if so, * otherwise zero is returned. */ static int is_file(const char path[]) { return !is_dir(path) || (is_symlink(path) && get_symlink_type(path) != SLT_UNKNOWN); } /* Implementation of traverse() visitor for subtree moving. Returns 0 on * success, otherwise non-zero is returned. */ static VisitResult mv_visitor(const char full_path[], VisitAction action, void *param) { return cp_mv_visitor(full_path, action, param, 0); } /* Generic implementation of traverse() visitor for subtree copying/moving. * Returns 0 on success, otherwise non-zero is returned. */ static VisitResult cp_mv_visitor(const char full_path[], VisitAction action, void *param, int cp) { io_args_t *const cp_args = param; const char *dst_full_path; char *free_me = NULL; VisitResult result = VR_OK; const char *rel_part; if(io_cancelled(cp_args)) { return VR_CANCELLED; } /* TODO: come up with something better than this. */ rel_part = full_path + strlen(cp_args->arg1.src); dst_full_path = (rel_part[0] == '\0') ? cp_args->arg2.dst : (free_me = join_paths(cp_args->arg2.dst, rel_part)); switch(action) { case VA_DIR_ENTER: if(cp_args->arg3.crs != IO_CRS_REPLACE_FILES || !is_dir(dst_full_path)) { io_args_t args = { .arg1.path = dst_full_path, /* Temporary fake rights so we can add files to the directory. */ .arg3.mode = 0700, .cancellation = cp_args->cancellation, .estim = cp_args->estim, .result = cp_args->result, }; result = vr_from_io_res(iop_mkdir(&args)); cp_args->result = args.result; } break; case VA_FILE: { io_args_t args = { .arg1.src = full_path, .arg2.dst = dst_full_path, .arg3.crs = cp_args->arg3.crs, /* It's safe to always use fast file cloning on moving files. */ .arg4.fast_file_cloning = cp ? cp_args->arg4.fast_file_cloning : 1, .arg4.data_sync = cp_args->arg4.data_sync, .cancellation = cp_args->cancellation, .confirm = cp_args->confirm, .estim = cp_args->estim, .result = cp_args->result, }; result = vr_from_io_res(cp ? iop_cp(&args) : ior_mv(&args)); cp_args->result = args.result; break; } case VA_DIR_LEAVE: { struct stat st; if(cp_args->arg3.crs == IO_CRS_REPLACE_FILES && !cp) { io_args_t rm_args = { .arg1.path = full_path, .cancellation = cp_args->cancellation, .estim = cp_args->estim, .result = cp_args->result, }; result = vr_from_io_res(iop_rmdir(&rm_args)); } else if(os_stat(full_path, &st) == 0) { result = (os_chmod(dst_full_path, st.st_mode & 07777) == 0) ? VR_OK : VR_ERROR; if(result == VR_ERROR) { (void)ioe_errlst_append(&cp_args->result.errors, dst_full_path, errno, "Failed to setup directory permissions"); } clone_attribs(dst_full_path, full_path, &st); } else { (void)ioe_errlst_append(&cp_args->result.errors, full_path, errno, "Failed to stat() source directory"); result = VR_ERROR; } break; } } free(free_me); return result; } /* Turns IoRes into VisitResult. Returns VisitResult. */ static VisitResult vr_from_io_res(IoRes result) { switch(result) { case IO_RES_SUCCEEDED: case IO_RES_SKIPPED: return VR_OK; case IO_RES_FAILED: return VR_ERROR; case IO_RES_ABORTED: return VR_CANCELLED; } return VR_ERROR; } /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set cinoptions+=t0 filetype=c : */