/* 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 */ #ifdef _WIN32 #include <windows.h> #endif #include "iop.h" #ifndef _WIN32 #include <sys/ioctl.h> /* ioctl() */ #endif #include <sys/stat.h> /* stat */ #include <sys/types.h> /* mode_t */ #include <unistd.h> /* symlink() unlink() */ #include <assert.h> /* assert() */ #include <errno.h> /* EEXIST ENOENT EISDIR errno */ #include <stddef.h> /* NULL size_t */ #include <stdio.h> /* FILE fpos_t fclose() fgetpos() fflush() fread() fseek() fsetpos() fwrite() snprintf() */ #include <stdlib.h> /* free() */ #include <string.h> /* strchr() */ #include "../compat/fs_limits.h" #include "../compat/os.h" #include "../utils/fs.h" #include "../utils/log.h" #include "../utils/macros.h" #include "../utils/path.h" #include "../utils/str.h" #include "../utils/utf8.h" #include "../utils/utils.h" #include "private/ioc.h" #include "private/ioe.h" #include "private/ioeta.h" #include "ioc.h" /* Amount of data to transfer at once. */ #define BLOCK_SIZE 32*1024 /* Amount of data after which data flush should be performed. */ #define FLUSH_SIZE 256*1024*1024 /* Type of io function used by retry_wrapper(). */ typedef IoRes (*iop_func)(io_args_t *args); static IoRes iop_mkfile_internal(io_args_t *args); static IoRes iop_mkdir_internal(io_args_t *args); static IoRes iop_rmfile_internal(io_args_t *args); static IoRes iop_rmdir_internal(io_args_t *args); static IoRes iop_cp_internal(io_args_t *args); static int clone_file(int dst_fd, int src_fd); #ifdef _WIN32 static DWORD CALLBACK win_progress_cb(LARGE_INTEGER total, LARGE_INTEGER transferred, LARGE_INTEGER stream_size, LARGE_INTEGER stream_transfered, DWORD stream_num, DWORD reason, HANDLE src_file, HANDLE dst_file, LPVOID param); #endif static IoRes iop_ln_internal(io_args_t *args); static IoRes retry_wrapper(iop_func func, io_args_t *args); static IoRes io_res_from_code(int code); IoRes iop_mkfile(io_args_t *args) { return retry_wrapper(&iop_mkfile_internal, args); } /* Implementation of iop_mkfile(). */ static IoRes iop_mkfile_internal(io_args_t *args) { FILE *f; const char *const path = args->arg1.path; ioeta_update(args->estim, path, path, /*finished=*/0, /*size=*/0); if(path_exists(path, NODEREF)) { (void)ioe_errlst_append(&args->result.errors, path, EEXIST, "Such file already exists"); return IO_RES_FAILED; } f = os_fopen(path, "wb"); if(f == NULL) { (void)ioe_errlst_append(&args->result.errors, path, errno, "Failed to open file for writing"); return IO_RES_FAILED; } if(fclose(f) != 0) { (void)ioe_errlst_append(&args->result.errors, path, errno, "Error while closing the file"); return IO_RES_FAILED; } ioeta_update(args->estim, path, path, /*finished=*/1, /*size=*/0); return IO_RES_SUCCEEDED; } IoRes iop_mkdir(io_args_t *args) { return retry_wrapper(&iop_mkdir_internal, args); } /* Implementation of iop_mkdir(). */ static IoRes iop_mkdir_internal(io_args_t *args) { const char *const path = args->arg1.path; const int create_parent = args->arg2.process_parents; const mode_t mode = args->arg3.mode; #ifndef _WIN32 enum { PATH_PREFIX_LEN = 0 }; #else enum { PATH_PREFIX_LEN = 2 }; #endif ioeta_update(args->estim, path, path, /*finished=*/0, /*size=*/0); if(create_parent) { char *const partial_path = strdup(path); char *part = partial_path + PATH_PREFIX_LEN, *state = NULL; while((part = split_and_get(part, '/', &state)) != NULL) { if(is_dir(partial_path)) { continue; } /* Create intermediate directories with 0700 permissions. */ if(os_mkdir(partial_path, S_IRWXU) != 0) { (void)ioe_errlst_append(&args->result.errors, partial_path, errno, "Failed to create one of intermediate directories"); free(partial_path); return IO_RES_FAILED; } } #ifndef _WIN32 if(os_chmod(path, mode) != 0) { (void)ioe_errlst_append(&args->result.errors, partial_path, errno, "Failed to setup directory permissions"); free(partial_path); return IO_RES_FAILED; } #endif free(partial_path); return IO_RES_SUCCEEDED; } if(os_mkdir(path, mode) != 0) { (void)ioe_errlst_append(&args->result.errors, path, errno, "Failed to create directory"); return IO_RES_FAILED; } ioeta_update(args->estim, path, path, /*finished=*/1, /*size=*/0); return IO_RES_SUCCEEDED; } IoRes iop_rmfile(io_args_t *args) { return retry_wrapper(&iop_rmfile_internal, args); } /* Implementation of iop_rmfile(). */ static IoRes iop_rmfile_internal(io_args_t *args) { const char *const path = args->arg1.path; uint64_t size; IoRes result; ioeta_update(args->estim, path, path, 0, 0); size = get_file_size(path); #ifndef _WIN32 result = io_res_from_code(unlink(path)); if(result == IO_RES_FAILED) { (void)ioe_errlst_append(&args->result.errors, path, errno, "Failed to unlink file"); } #else { wchar_t *const utf16_path = utf8_to_utf16(path); const DWORD attributes = GetFileAttributesW(utf16_path); if(attributes & FILE_ATTRIBUTE_READONLY) { SetFileAttributesW(utf16_path, attributes & ~FILE_ATTRIBUTE_READONLY); } result = io_res_from_code(!DeleteFileW(utf16_path)); if(result != 0) { (void)ioe_errlst_append(&args->result.errors, path, IO_ERR_UNKNOWN, "File removal failed"); } free(utf16_path); } #endif ioeta_update(args->estim, NULL, NULL, 1, size); return result; } IoRes iop_rmdir(io_args_t *args) { return retry_wrapper(&iop_rmdir_internal, args); } /* Implementation of iop_rmdir(). */ static IoRes iop_rmdir_internal(io_args_t *args) { const char *const path = args->arg1.path; IoRes result; ioeta_update(args->estim, path, path, 0, 0); #ifndef _WIN32 result = io_res_from_code(os_rmdir(path)); if(result != 0) { (void)ioe_errlst_append(&args->result.errors, path, errno, "Failed to remove directory"); } #else { wchar_t *const utf16_path = utf8_to_utf16(path); result = io_res_from_code(!RemoveDirectoryW(utf16_path)); if(result != 0) { /* FIXME: use real system error message here. */ (void)ioe_errlst_append(&args->result.errors, path, IO_ERR_UNKNOWN, "Failed to remove directory"); } free(utf16_path); } #endif ioeta_update(args->estim, NULL, NULL, 1, 0); return result; } IoRes iop_cp(io_args_t *args) { return retry_wrapper(&iop_cp_internal, args); } /* Implementation of iop_cp(). */ static IoRes iop_cp_internal(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; struct stat st; FILE *in, *out; int error; int cloned; struct stat src_st; const char *open_mode = "wb"; uint64_t orig_out_size = 0U; int correct_out_size = 0; ioeta_update(args->estim, src, dst, 0, 0); #ifdef _WIN32 if(is_symlink(src) || crs != IO_CRS_APPEND_TO_FILES) { DWORD flags; wchar_t *utf16_src, *utf16_dst; flags = COPY_FILE_COPY_SYMLINK; if(crs == IO_CRS_FAIL) { flags |= COPY_FILE_FAIL_IF_EXISTS; } else if(path_exists(dst, NODEREF)) { /* Ask user whether to overwrite destination file. */ if(confirm != NULL && !confirm(args, src, dst)) { return IO_RES_SUCCEEDED; } } utf16_src = utf8_to_utf16(src); utf16_dst = utf8_to_utf16(dst); int success = CopyFileExW(utf16_src, utf16_dst, &win_progress_cb, args, NULL, flags); if(!success) { /* FIXME: use real system error message here. */ (void)ioe_errlst_append(&args->result.errors, dst, IO_ERR_UNKNOWN, "Copy file failed"); } free(utf16_src); free(utf16_dst); if(success) { clone_attribs(dst, src, NULL); } ioeta_update(args->estim, NULL, NULL, 1, 0); return io_res_from_code(!success); } #endif /* Create symbolic link rather than copying file it points to. This check * should go before directory check as is_dir() resolves symbolic links. */ if(is_symlink(src)) { char link_target[PATH_MAX + 1]; io_args_t ln_args = { .arg1.path = link_target, .arg2.target = dst, .arg3.crs = crs, .cancellation = args->cancellation, .result = args->result, }; if(get_link_target(src, link_target, sizeof(link_target)) != 0) { (void)ioe_errlst_append(&args->result.errors, src, IO_ERR_UNKNOWN, "Failed to get symbolic link target"); return IO_RES_FAILED; } IoRes result = iop_ln(&ln_args); args->result = ln_args.result; if(result == IO_RES_FAILED) { (void)ioe_errlst_append(&args->result.errors, src, IO_ERR_UNKNOWN, "Failed to make symbolic link"); } return IO_RES_SUCCEEDED; } if(is_dir(src)) { (void)ioe_errlst_append(&args->result.errors, src, EISDIR, "Target path specifies existing directory"); return IO_RES_FAILED; } if(os_stat(src, &st) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Failed to stat() source file"); return IO_RES_FAILED; } #ifndef _WIN32 /* Fifo/socket/device files don't need to be opened, their content is not * accessed. */ if(S_ISFIFO(st.st_mode) || S_ISSOCK(st.st_mode) || S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode)) { in = NULL; } else #endif { in = os_fopen(src, "rb"); if(in == NULL) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Failed to open source file"); return IO_RES_FAILED; } } if(crs == IO_CRS_APPEND_TO_FILES) { open_mode = "ab"; } else if(crs != IO_CRS_FAIL) { if(path_exists(dst, NODEREF)) { /* Ask user whether to overwrite destination file. */ if(confirm != NULL && !confirm(args, src, dst)) { if(in != NULL && fclose(in) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Error while closing source file"); } return IO_RES_SUCCEEDED; } } int ec = unlink(dst); if(ec != 0 && errno != ENOENT) { (void)ioe_errlst_append(&args->result.errors, dst, errno, "Failed to unlink file"); if(in != NULL && fclose(in) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Error while closing source file"); } return io_res_from_code(ec); } /* XXX: possible improvement would be to generate temporary file name in the * destination directory, write to it and then overwrite destination file, * but this approach has disadvantage of requiring more free space on * destination file system. */ } else if(path_exists(dst, NODEREF)) { (void)ioe_errlst_append(&args->result.errors, src, EEXIST, "Destination path exists"); if(in != NULL && fclose(in) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Error while closing source file"); } return IO_RES_FAILED; } #ifndef _WIN32 /* Replicate fifo without even opening it. */ if(S_ISFIFO(st.st_mode)) { if(mkfifo(dst, st.st_mode & 07777) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Failed to create FIFO"); return IO_RES_FAILED; } return IO_RES_SUCCEEDED; } /* Replicate socket or device file without even opening it. */ if(S_ISSOCK(st.st_mode) || S_ISBLK(st.st_mode) || S_ISCHR(st.st_mode)) { if(mknod(dst, st.st_mode & (S_IFMT | 07777), st.st_rdev) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Failed to create node"); return IO_RES_FAILED; } return IO_RES_SUCCEEDED; } #endif out = os_fopen(dst, open_mode); if(out == NULL) { (void)ioe_errlst_append(&args->result.errors, dst, errno, "Failed to open destination file"); if(fclose(in) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Error while closing source file"); } return IO_RES_FAILED; } error = 0; cloned = 0; if(crs == IO_CRS_APPEND_TO_FILES) { fpos_t pos; /* The following line is required for stupid Windows sometimes. Why? * Probably because it's stupid... Won't harm other systems. */ error |= (fseek(out, 0, SEEK_END) != 0); error |= (fgetpos(out, &pos) != 0 || fsetpos(in, &pos) != 0); orig_out_size = get_file_size(dst); correct_out_size = (orig_out_size != 0U || ftell(out) == 0); error |= !correct_out_size; if(!error) { ioeta_update(args->estim, NULL, NULL, 0, orig_out_size); } } else if(args->arg4.fast_file_cloning) { if(clone_file(fileno(out), fileno(in)) == 0) { cloned = 1; } } /* TODO: use sendfile() if platform supports it. */ if(!error && !cloned) { char block[BLOCK_SIZE]; /* Suppress possible false-positive compiler warning. */ size_t nread = (size_t)-1; #ifndef _WIN32 size_t ncopied = 0U; const int data_sync = args->arg4.data_sync; #endif while((nread = fread(&block, 1, sizeof(block), in)) != 0U) { if(io_cancelled(args)) { error = 1; break; } if(fwrite(&block, 1, nread, out) != nread) { (void)ioe_errlst_append(&args->result.errors, dst, errno, "Write to destination file failed"); error = 1; break; } ioeta_update(args->estim, NULL, NULL, 0, nread); #ifndef _WIN32 /* Force flushing data to disk to not pollute RAM with this data too * much. */ ncopied += nread; if(data_sync && ncopied >= FLUSH_SIZE) { (void)os_fdatasync(fileno(out)); ncopied -= FLUSH_SIZE; } #endif } if(nread == 0U && !feof(in) && ferror(in)) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Read from source file failed"); } /* fwrite() does caching, so we need to force flush to catch output errors * before fclose() (which also does fflush() internally). */ if(fflush(out) != 0) { (void)ioe_errlst_append(&args->result.errors, dst, errno, "Write to destination file failed"); error = 1; } } /* Note that we truncate output file even if operation was cancelled by the * user. */ if(crs == IO_CRS_APPEND_TO_FILES && error != 0 && correct_out_size) { int error; #ifndef _WIN32 error = ftruncate(fileno(out), (off_t)orig_out_size); #else error = _chsize(fileno(out), orig_out_size); #endif if(error != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Error while truncating output file back to original size"); } } if(fclose(in) != 0) { (void)ioe_errlst_append(&args->result.errors, src, errno, "Error while closing source file"); } if(fclose(out) != 0) { (void)ioe_errlst_append(&args->result.errors, dst, errno, "Error while closing destination file"); error = 1; } if(error == 0 && os_lstat(src, &src_st) == 0) { error = os_chmod(dst, src_st.st_mode & 07777); if(error != 0) { (void)ioe_errlst_append(&args->result.errors, dst, errno, "Failed to setup file permissions"); } } if(error == 0) { clone_attribs(dst, src, &st); } ioeta_update(args->estim, NULL, NULL, 1, 0); return io_res_from_code(error); } /* Try to clone file fast on btrfs. Returns 0 on success, otherwise non-zero is * returned. */ static int clone_file(int dst_fd, int src_fd) { #ifdef __linux__ #undef BTRFS_IOCTL_MAGIC #define BTRFS_IOCTL_MAGIC 0x94 #undef BTRFS_IOC_CLONE #define BTRFS_IOC_CLONE _IOW(BTRFS_IOCTL_MAGIC, 9, int) return ioctl(dst_fd, BTRFS_IOC_CLONE, src_fd); #else (void)dst_fd; (void)src_fd; return -1; #endif } #ifdef _WIN32 static DWORD CALLBACK win_progress_cb(LARGE_INTEGER total, LARGE_INTEGER transferred, LARGE_INTEGER stream_size, LARGE_INTEGER stream_transfered, DWORD stream_num, DWORD reason, HANDLE src_file, HANDLE dst_file, LPVOID param) { static LONGLONG last_size; io_args_t *const args = param; const char *const src = args->arg1.src; const char *const dst = args->arg2.dst; ioeta_estim_t *const estim = args->estim; if(transferred.QuadPart < last_size) { last_size = 0; } ioeta_update(estim, src, dst, 0, transferred.QuadPart - last_size); last_size = transferred.QuadPart; return io_cancelled(args) ? PROGRESS_CANCEL : PROGRESS_CONTINUE; } #endif /* TODO: implement iop_chown(). */ IoRes iop_chown(io_args_t *args); /* TODO: implement iop_chgrp(). */ IoRes iop_chgrp(io_args_t *args); /* TODO: implement iop_chmod(). */ IoRes iop_chmod(io_args_t *args); IoRes iop_ln(io_args_t *args) { return retry_wrapper(&iop_ln_internal, args); } /* Implementation of iop_ln(). */ static IoRes iop_ln_internal(io_args_t *args) { const char *const path = args->arg1.path; const char *const target = args->arg2.target; const int overwrite = args->arg3.crs != IO_CRS_FAIL; IoRes result; #ifndef _WIN32 result = io_res_from_code(symlink(path, target)); if(result == IO_RES_FAILED && errno == EEXIST && overwrite && is_symlink(target)) { result = io_res_from_code(remove(target)); if(result == IO_RES_SUCCEEDED) { result = io_res_from_code(symlink(path, target)); if(result == IO_RES_FAILED) { (void)ioe_errlst_append(&args->result.errors, path, errno, "Error while creating symbolic link"); } } else { (void)ioe_errlst_append(&args->result.errors, target, errno, "Error while removing existing destination"); } } else if(result == IO_RES_FAILED && errno != 0) { (void)ioe_errlst_append(&args->result.errors, target, errno, "Error while creating symbolic link"); } #else char cmd[6 + PATH_MAX*2 + 1]; char *escaped_path, *escaped_target; char base_dir[PATH_MAX + 2]; if(path_exists(target, NODEREF)) { if(!overwrite) { (void)ioe_errlst_append(&args->result.errors, target, EEXIST, "Destination path already exists"); return IO_RES_FAILED; } if(!is_symlink(target)) { (void)ioe_errlst_append(&args->result.errors, target, IO_ERR_UNKNOWN, "Target is not a symbolic link"); return IO_RES_FAILED; } } /* We're using os_system() below, hence ST_CMD is hard-coded. */ escaped_path = shell_arg_escape(path, ST_CMD); escaped_target = shell_arg_escape(target, ST_CMD); if(escaped_path == NULL || escaped_target == NULL) { (void)ioe_errlst_append(&args->result.errors, target, IO_ERR_UNKNOWN, "Not enough memory"); free(escaped_target); free(escaped_path); return IO_RES_FAILED; } if(GetModuleFileNameA(NULL, base_dir, ARRAY_LEN(base_dir)) == 0) { (void)ioe_errlst_append(&args->result.errors, target, IO_ERR_UNKNOWN, "Failed to find win_helper"); free(escaped_target); free(escaped_path); return IO_RES_FAILED; } break_atr(base_dir, '\\'); snprintf(cmd, sizeof(cmd), "%s\\win_helper -s %s %s", base_dir, escaped_path, escaped_target); result = io_res_from_code(os_system(cmd)); if(result == IO_RES_FAILED) { (void)ioe_errlst_append(&args->result.errors, target, IO_ERR_UNKNOWN, "Running win_helper has failed"); } free(escaped_target); free(escaped_path); #endif return result; } /* Implements detecting errors and querying user to decide whether to abort, * retry or ignore. Returns zero on success and non-zero on error. */ static IoRes retry_wrapper(iop_func func, io_args_t *args) { IoRes result; ioeta_estim_t estim = {}; /* Replace error list with an empty one to control which errors are going to * be remembered. */ ioe_errlst_t orig_errlist = args->result.errors; ioe_errlst_t empty_errlist = { .active = args->result.errors.active }; args->result.errors = empty_errlist; /* Make restoration point for the estimation if necessary. */ if(args->estim != NULL) { estim = ioeta_save(args->estim); } while(1) { result = func(args); if(result == IO_RES_SUCCEEDED || args->result.errors_cb == NULL || args->result.errors.error_count == 0U) { ioe_errlst_splice(&orig_errlist, &args->result.errors); break; } switch(args->result.errors_cb(args, &args->result.errors.errors[0])) { case IO_ECR_RETRY: ioe_errlst_free(&args->result.errors); args->result.errors = empty_errlist; if(args->estim != NULL) { /* Restore previous state of progress before retrying. */ ioeta_restore(args->estim, &estim); } continue; case IO_ECR_IGNORE: result = IO_RES_SKIPPED; if(args->estim != NULL) { /* When we ignore a file, in order to make progress look nice pretend * that this file was processed in full. */ ioeta_update(args->estim, args->estim->item, args->estim->target, 1, args->estim->total_file_bytes - args->estim->current_file_byte); } ioe_errlst_splice(&orig_errlist, &args->result.errors); break; case IO_ECR_BREAK: result = IO_RES_ABORTED; ioe_errlst_splice(&orig_errlist, &args->result.errors); break; default: assert(0 && "Unknown error handling result."); break; } break; } ioeta_release(&estim); ioe_errlst_free(&args->result.errors); args->result.errors = orig_errlist; return result; } /* Turns exit code into IoRes. Returns IoRes. */ static IoRes io_res_from_code(int code) { return (code == 0 ? IO_RES_SUCCEEDED : IO_RES_FAILED); } /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */ /* vim: set cinoptions+=t0 filetype=c : */