/*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id$ * * Copyright (C) 2002 Björn Stenberg * Copyright (C) 2024 William Wilgus * * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ****************************************************************************/ #include #include #include #include #include #include "string-extra.h" #include "debug.h" #include "misc.h" #include "plugin.h" #include "dir.h" #include "tree.h" #include "fileop.h" #include "pathfuncs.h" #include "settings.h" #include "lang.h" #include "yesno.h" #include "splash.h" #include "keyboard.h" /* Used for directory move, copy and delete */ struct file_op_params { char path[MAX_PATH]; /* Buffer for full path */ bool is_dir; int objects; /* how many files and subdirectories*/ int processed; size_t append; /* Append position in 'path' for stack push */ }; static bool poll_cancel_action(const char *path, int operation, int current, int total) { const char *op_str = ""; clear_screen_buffer(false); if (operation == FOC_COPY) op_str = str(LANG_COPYING); else if (operation == FOC_MOVE) op_str = str(LANG_MOVING); else if (operation == FOC_COUNT) op_str = str(LANG_SCANNING_DISK); else if (operation == FOC_DELETE) op_str = str(LANG_DELETING); path_basename(path, &path); if (total <= 0) splashf(0, "%s (%d) %s", op_str, current, path); else splash_progress(current, total, "%s %s", op_str, path); return ACTION_STD_CANCEL == get_action(CONTEXT_STD, TIMEOUT_NOBLOCK); } static struct file_op_params* init_file_op(struct file_op_params *param, const char *basename, const char *selected_file) { /* Assumes: basename will never be NULL */ if (selected_file == NULL) { param->append = strlcpy(param->path, basename, sizeof (param->path)); } else param->append = path_append(param->path, basename, selected_file, sizeof (param->path)); param->is_dir = dir_exists(param->path); param->objects = 0; /* how many files and subdirectories*/ param->processed = 0; return param; } /* counts file objects, deletes file objects */ static int directory_fileop(struct file_op_params *param, enum file_op_current fileop) { errno = 0; DIR *dir = opendir(param->path); if (!dir) { if (errno == EMFILE) { return FORC_TOO_MANY_SUBDIRS; } return FORC_PATH_NOT_EXIST; /* open error */ } int rc = FORC_SUCCESS; size_t append = param->append; /* walk through the directory content */ while (rc == FORC_SUCCESS) { errno = 0; /* distinguish failure from eod */ struct dirent *entry = readdir(dir); if (!entry) { if (errno) { rc = FORC_PATH_NOT_EXIST; } break; } struct dirinfo info = dir_get_info(dir, entry); if ((info.attribute & ATTR_DIRECTORY) && is_dotdir_name(entry->d_name)) { continue; /* skip these */ } /* append name to current directory */ param->append = append + path_append(¶m->path[append], PA_SEP_HARD, entry->d_name, sizeof (param->path) - append); if (fileop == FOC_DELETE) { param->processed++; /* at this point we've already counted and never had a path too long in the other branch so we 'should not' encounter one here either */ if (param->processed > param->objects) { rc = FORC_UNKNOWN_FAILURE; break; } if (info.attribute & ATTR_DIRECTORY) { /* remove a subdirectory */ rc = directory_fileop(param, fileop); } else { /* remove a file */ if (poll_cancel_action(param->path, FOC_DELETE, param->processed, param->objects)) { rc = FORC_CANCELLED; break; } rc = remove(param->path); } } else /* count objects */ { param->objects++; if (param->append >= sizeof (param->path)) { rc = FORC_PATH_TOO_LONG; break; /* no space left in buffer */ } if (info.attribute & ATTR_DIRECTORY) { /* remove a subdirectory */ rc = directory_fileop(param, FOC_COUNT); } else { if (poll_cancel_action(param->path, FOC_COUNT, param->objects, 0)) { rc = FORC_CANCELLED; break; } } } param->append = append; /* other functions may use param, reset append */ /* Remove basename we added above */ param->path[append] = '\0'; } closedir(dir); if (fileop == FOC_DELETE && rc == FORC_SUCCESS) { /* remove the now empty directory */ if (poll_cancel_action(param->path, FOC_DELETE, param->processed, param->objects)) { rc = FORC_CANCELLED; } else { rc = rmdir(param->path); } } return rc; } static int check_count_fileobjects(struct file_op_params *param) { int rc = directory_fileop(param, FOC_COUNT); DEBUGF("%s res:(%d) objects %d \n", __func__, rc, param->objects); return rc; } static bool check_new_name(const char *basename) { /* at least prevent escapes out of the base directory from keyboard- entered filenames; the file code should reject other invalidities */ return *basename != '\0' && !strchr(basename, PATH_SEPCH) && !is_dotdir_name(basename); } int create_dir(void) { int rc = FORC_UNKNOWN_FAILURE; char dirname[MAX_PATH]; size_t pathlen = path_append(dirname, getcwd(NULL, 0), PA_SEP_HARD, sizeof (dirname)); char *basename = dirname + pathlen; if (pathlen >= sizeof (dirname)) { /* Too long */ } else if (kbd_input(basename, sizeof (dirname) - pathlen, NULL) < 0) { rc = FORC_CANCELLED; } else if (check_new_name(basename)) { rc = mkdir(dirname); } return rc; } /************************************************************************************/ /* share code for file and directory deletion, saves space */ static int delete_file_dir(struct file_op_params *param) { /* Note: delete_file_dir() will happily delete whatever * path is passed (after confirmation) */ if (confirm_delete_yesno(param->path) != YESNO_YES) { return FORC_CANCELLED; } clear_screen_buffer(true); poll_cancel_action(param->path, FOC_DELETE, param->processed, param->objects); int rc = FORC_UNKNOWN_FAILURE; if (param->is_dir) { /* if directory */ cpu_boost(true); rc = directory_fileop(param, FOC_DELETE); cpu_boost(false); } else { rc = remove(param->path); } return rc; } int delete_fileobject(const char *selected_file) { struct file_op_params param; if (init_file_op(¶m, selected_file, NULL)->is_dir == true) { int rc = check_count_fileobjects(¶m); DEBUGF("%s res: %d, ct: %d, %s", __func__, rc, param.objects, param.path); if (rc != FORC_SUCCESS) return rc; } return delete_file_dir(¶m); } int rename_file(const char *selected_file) { int rc = FORC_UNKNOWN_FAILURE; char newname[MAX_PATH]; const char *oldbase, *selection = selected_file; path_basename(selection, &oldbase); size_t pathlen = oldbase - selection; char *newbase = newname + pathlen; if (strmemccpy(newname, selection, sizeof (newname)) == NULL) { /* Too long */ } else if (kbd_input(newbase, sizeof (newname) - pathlen, NULL) < 0) { rc = FORC_CANCELLED; } else if (!strcmp(oldbase, newbase)) { rc = FORC_NOOP; /* No change at all */ } else if (check_new_name(newbase)) { switch (relate(selection, newname)) { case RELATE_DIFFERENT: if (file_exists(newname)) { break; /* don't overwrite */ } /* Fall-through */ case RELATE_SAME: rc = rename(selection, newname); break; case RELATE_PREFIX: default: break; } } return rc; } static int move_by_rename(const char *src_path, const char *dst_path, unsigned int *pflags) { unsigned int flags = *pflags; int rc = FORC_UNKNOWN_FAILURE; while (!(flags & (PASTE_COPY | PASTE_EXDEV))) { if ((flags & PASTE_OVERWRITE) || !file_exists(dst_path)) { /* Just try to move the directory / file */ if (poll_cancel_action(src_path, FOC_MOVE, 0 , 0)) { rc = FORC_CANCELLED; } else { rc = rename(src_path, dst_path); } if (rc < 0) { int errnum = errno; if (errnum == ENOTEMPTY && (flags & PASTE_OVERWRITE)) { /* Directory is not empty thus rename() will not do a quick overwrite */ break; } #ifdef HAVE_MULTIVOLUME else if (errnum == EXDEV) { /* Failed because cross volume rename doesn't work; force a move instead */ *pflags |= PASTE_EXDEV; break; } #endif /* HAVE_MULTIVOLUME */ } } break; } return rc; } /* Paste a file */ static int copy_move_file(const char *src_path, const char *dst_path, unsigned int flags) { /* Try renaming first */ int rc = move_by_rename(src_path, dst_path, &flags); if (rc == FORC_SUCCESS) return rc; /* See if we can get the plugin buffer for the file copy buffer */ size_t buffersize; char *buffer = (char *) plugin_get_buffer(&buffersize); if (buffer == NULL || buffersize < 512) { /* Not large enough, try for a disk sector worth of stack instead */ buffersize = 512; buffer = (char *)alloca(buffersize); } if (buffer == NULL) { return FORC_NO_BUFFER_AVAIL; } buffersize &= ~0x1ff; /* Round buffer size to multiple of sector size */ int src_fd = open(src_path, O_RDONLY); if (src_fd >= 0) { off_t src_sz = lseek(src_fd, 0, SEEK_END); lseek(src_fd, 0, SEEK_SET); int oflag = O_WRONLY|O_CREAT; if (!(flags & PASTE_OVERWRITE)) { oflag |= O_EXCL; } int dst_fd = open(dst_path, oflag, 0666); if (dst_fd >= 0) { off_t total_size = 0; off_t next_cancel_test = 0; /* No excessive button polling */ rc = FORC_SUCCESS; while (rc == FORC_SUCCESS) { if (total_size >= next_cancel_test) { next_cancel_test = total_size + 0x10000; if (poll_cancel_action(src_path, !(flags & PASTE_COPY) ? FOC_MOVE : FOC_COPY, total_size, src_sz)) { rc = FORC_CANCELLED; break; } } ssize_t bytesread = read(src_fd, buffer, buffersize); if (bytesread <= 0) { if (bytesread < 0) { rc = FORC_READ_FAILURE; } /* else eof on buffer boundary; nothing to write */ break; } ssize_t byteswritten = write(dst_fd, buffer, bytesread); if (byteswritten < bytesread) { /* Some I/O error */ rc = FORC_WRITE_FAILURE; break; } total_size += byteswritten; if (bytesread < (ssize_t)buffersize) { /* EOF with trailing bytes */ break; } } if (rc == FORC_SUCCESS) { /* If overwriting, set the correct length if original was longer */ rc = ftruncate(dst_fd, total_size) * 10; } close(dst_fd); if (rc != FORC_SUCCESS) { /* Copy failed. Cleanup. */ remove(dst_path); } } close(src_fd); } if (rc == FORC_SUCCESS && !(flags & PASTE_COPY)) { /* Remove the source file */ rc = remove(src_path) * 10; } return rc; } /* Paste a directory */ static int copy_move_directory(struct file_op_params *src, struct file_op_params *dst, unsigned int flags) { int rc = FORC_UNKNOWN_FAILURE; DIR *srcdir = opendir(src->path); if (srcdir) { /* Make a directory to copy things to */ rc = mkdir(dst->path) * 10; if (rc < 0 && errno == EEXIST && (flags & PASTE_OVERWRITE)) { /* Exists and overwrite was approved */ rc = FORC_SUCCESS; } } size_t srcap = src->append, dstap = dst->append; /* Walk through the directory content; this loop will exit as soon as there's a problem */ while (rc == FORC_SUCCESS) { errno = 0; /* Distinguish failure from eod */ struct dirent *entry = readdir(srcdir); if (!entry) { if (errno) { rc = FORC_PATH_NOT_EXIST; } break; } struct dirinfo info = dir_get_info(srcdir, entry); if ((info.attribute & ATTR_DIRECTORY) && is_dotdir_name(entry->d_name)) { continue; /* Skip these */ } /* Append names to current directories */ src->append = srcap + path_append(&src->path[srcap], PA_SEP_HARD, entry->d_name, sizeof(src->path) - srcap); dst->append = dstap + path_append(&dst->path[dstap], PA_SEP_HARD, entry->d_name, sizeof (dst->path) - dstap); /* src length was already checked by check_count_fileobjects() */ if (dst->append >= sizeof (dst->path)) { rc = FORC_PATH_TOO_LONG; /* No space left in buffer */ break; } src->processed++; if (src->processed > src->objects) { rc = FORC_UNKNOWN_FAILURE; break; } if (poll_cancel_action(src->path, !(flags & PASTE_COPY) ? FOC_MOVE : FOC_COPY, src->processed, src->objects)) { rc = FORC_CANCELLED; break; } DEBUGF("Copy %s to %s\n", src->path, dst->path); if (info.attribute & ATTR_DIRECTORY) { /* Copy/move a subdirectory */ rc = copy_move_directory(src, dst, flags); /* recursion */; } else { /* Copy/move a file */ rc = copy_move_file(src->path, dst->path, flags); } /* Remove basenames we added above */ src->path[srcap] = '\0'; dst->path[dstap] = '\0'; } if (rc == FORC_SUCCESS && !(flags & PASTE_COPY)) { /* Remove the now empty directory */ rc = rmdir(src->path) * 10; } closedir(srcdir); return rc; } int copy_move_fileobject(const char *src_path, const char *dst_path, unsigned int flags) { if (!src_path[0]) return FORC_NOOP; int rc = FORC_UNKNOWN_FAILURE; struct file_op_params src, dst; /* Figure out the name of the selection */ const char *nameptr; path_basename(src_path, &nameptr); /* Final target is current directory plus name of selection */ init_file_op(&dst, dst_path, nameptr); switch (dst.append < sizeof (dst.path) ? relate(src_path, dst.path) : FORC_PATH_TOO_LONG) { case RELATE_SAME: rc = FORC_NOOP; break; case RELATE_DIFFERENT: if (file_exists(dst.path)) { /* If user chooses not to overwrite, cancel */ if (confirm_overwrite_yesno() == YESNO_NO) { rc = FORC_NOOVERWRT; break; } flags |= PASTE_OVERWRITE; } /* Now figure out what we're doing */ cpu_boost(true); init_file_op(&src, src_path, NULL); if (src.is_dir) { /* Copy or move a subdirectory */ if (src.append < sizeof (src.path)) { /* Try renaming first */ rc = move_by_rename(src.path, dst.path, &flags); if (rc != FORC_SUCCESS && rc != FORC_CANCELLED) { if (check_count_fileobjects(&src) == FORC_SUCCESS) { rc = copy_move_directory(&src, &dst, flags); } } } } else { /* Copy or move a file */ rc = copy_move_file(src_path, dst.path, flags); } cpu_boost(false); break; case RELATE_PREFIX: default: /* Some other relation / failure */ break; } return rc; }