/*************************************************************************** * __________ __ ___. * 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 "powermgmt.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 */ const char* toplevel_name; bool is_dir; int objects; /* how many files and subdirectories*/ int processed; unsigned long long total_size; unsigned long long processed_size; size_t append; /* Append position in 'path' for stack push */ size_t extra_len; /* Length added by dst path compared to src */ }; static int prompt_name(char* buf, size_t bufsz) { if (kbd_input(buf, bufsz, NULL) < 0) return FORC_CANCELLED; /* at least prevent escapes out of the base directory from keyboard- entered filenames; the file code should reject other invalidities */ if (*buf != '\0' && !strchr(buf, PATH_SEPCH) && !is_dotdir_name(buf)) return FORC_SUCCESS; return FORC_UNKNOWN_FAILURE; } static bool poll_cancel_action(int operation, struct file_op_params *param) { static unsigned long last_tick; if (operation == FOC_COUNT) { if (param->objects <= 1) last_tick = current_tick; else if (TIME_AFTER(current_tick, last_tick + HZ/2)) { clear_screen_buffer(false); splashf(0, "%s (%d)", str(LANG_SCANNING_DISK), param->objects); last_tick = current_tick; } } else { const char *op_str = (operation == FOC_DELETE) ? str(LANG_DELETING) : (operation == FOC_COPY) ? str(LANG_COPYING) : str(LANG_MOVING); if ((operation == FOC_DELETE || !param->total_size) && param->objects > 0) { splash_progress(param->processed, param->objects, "%s %s", op_str, param->toplevel_name); } else if (param->total_size >= 10 * 1024 * 1024) { int total_shft = (int) (param->total_size >> 15); int current_shft = (int) (param->processed_size >> 15); splash_progress(current_shft, total_shft, "%s %s (%d MiB)\n%d MiB", op_str, param->toplevel_name, total_shft >> 5, current_shft >> 5); } else if (param->total_size >= 1024) { int total_kib = (int) (param->total_size >> 10); int current_kib = (int) (param->processed_size >> 10); splash_progress(current_kib, total_kib, "%s %s (%d KiB)\n%d KiB", op_str, param->toplevel_name, total_kib, current_kib); } } return ACTION_STD_CANCEL == get_action(CONTEXT_STD, TIMEOUT_NOBLOCK); } static void 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)); path_basename(basename, &basename); param->toplevel_name = basename; } else { param->append = path_append(param->path, basename, selected_file, sizeof (param->path)); param->toplevel_name = selected_file; } param->is_dir = dir_exists(param->path); param->extra_len = 0; param->objects = 0; /* how many files and subdirectories*/ param->processed = 0; param->total_size = 0; param->processed_size = 0; } /* 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); /* recursion */ } else { /* remove a file */ if (poll_cancel_action(FOC_DELETE, param)) { rc = FORC_CANCELLED; break; } rc = remove(param->path); } } else /* count objects */ { param->objects++; param->total_size += info.size; if (poll_cancel_action(FOC_COUNT, param)) { rc = FORC_CANCELLED; break; } if (param->append + param->extra_len >= sizeof (param->path)) { rc = FORC_PATH_TOO_LONG; break; /* no space left in buffer */ } if (info.attribute & ATTR_DIRECTORY) { /* enter subdirectory */ rc = directory_fileop(param, FOC_COUNT); /* recursion */ } } 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(FOC_DELETE, param)) { rc = FORC_CANCELLED; } else { rc = rmdir(param->path); } } return rc; } /* Walk a directory tree and count the number of objects (dirs & files) * also check that enough resources exist to do an operation */ static int check_count_fileobjects(struct file_op_params *param) { cpu_boost(true); int rc = directory_fileop(param, FOC_COUNT); cpu_boost(false); DEBUGF("%s res:(%d) objects %d \n", __func__, rc, param->objects); return rc; } /* Attempt to just rename a file or directory */ static int move_by_rename(struct file_op_params *src, const char *dst_path, unsigned int *pflags) { unsigned int flags = *pflags; int rc = FORC_UNKNOWN_FAILURE; reset_poweroff_timer(); if (!(flags & (PASTE_COPY | PASTE_EXDEV))) { if ((flags & PASTE_OVERWRITE) || !file_exists(dst_path)) { /* Just try to move the directory / file */ rc = rename(src->path, dst_path); #ifdef HAVE_MULTIVOLUME if (rc < FORC_SUCCESS && errno == EXDEV) { /* Failed because cross volume rename doesn't work */ *pflags |= PASTE_EXDEV; /* force a move instead */ } #endif /* HAVE_MULTIVOLUME */ /* if (errno == ENOTEMPTY && (flags & PASTE_OVERWRITE)) { * Directory is not empty thus rename() will not do a quick overwrite */ } } return rc; } /* Paste a file */ static int copy_move_file(struct file_op_params *src, const char *dst_path, unsigned int flags) { /* Try renaming first */ int rc = move_by_rename(src, dst_path, &flags); if (rc == FORC_SUCCESS) { src->total_size = 0; /* switch from counting size to number of items */ 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); if (!src->total_size && !src->processed) /* single file copy */ src->total_size = src_sz; 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(!(flags & PASTE_COPY) ? FOC_MOVE : FOC_COPY, src)) { 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; src->processed_size += byteswritten; if (bytesread < (ssize_t)buffersize) { /* EOF with trailing bytes */ break; } } if (rc == FORC_SUCCESS) { if (total_size != src_sz) rc = FORC_UNKNOWN_FAILURE; else { /* 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) { DIR *srcdir = opendir(src->path); if (!srcdir) return FORC_PATH_NOT_EXIST; /* Make a directory to copy things to */ int 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(!(flags & PASTE_COPY) ? FOC_MOVE : FOC_COPY, src)) { rc = FORC_CANCELLED; break; } DEBUGF("Copy %s to %s\n", src->path, dst->path); if (info.attribute & ATTR_DIRECTORY) { src->processed_size += info.size; /* Copy/move a subdirectory */ rc = copy_move_directory(src, dst, flags); /* recursion */; } else { /* Copy/move a file */ rc = copy_move_file(src, 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; } /************************************************************************************/ /* PUBLIC FUNCTIONS */ /************************************************************************************/ /* Copy or move a file or directory see: file_op_flags */ int copy_move_fileobject(const char *src_path, const char *dst_path, unsigned int flags) { if (!src_path[0]) return FORC_NOOP; 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); if (dst.append >= sizeof (dst.path)) return FORC_PATH_TOO_LONG; int rel = relate(src_path, dst.path); if (rel == RELATE_SAME) return FORC_NOOP; if (rel == RELATE_DIFFERENT) { int rc; if (file_exists(dst.path)) { /* If user chooses not to overwrite, cancel */ if (!yesno_pop(ID2P(LANG_REALLY_OVERWRITE))) { splash(HZ, ID2P(LANG_CANCEL)); return FORC_NOOVERWRT; } flags |= PASTE_OVERWRITE; } init_file_op(&src, src_path, NULL); if (src.append >= sizeof (src.path)) return FORC_PATH_TOO_LONG; /* Now figure out what we're doing */ cpu_boost(true); if (src.is_dir) { /* Copy or move a subdirectory */ /* Try renaming first */ rc = move_by_rename(&src, dst.path, &flags); if (rc < FORC_SUCCESS) { int extra_len = dst.append - src.append; if (extra_len > 0) src.extra_len = extra_len; rc = check_count_fileobjects(&src); if (rc == FORC_SUCCESS) { rc = copy_move_directory(&src, &dst, flags); } } } else { /* Copy or move a file */ rc = copy_move_file(&src, dst.path, flags); } cpu_boost(false); DEBUGF("%s res: %d, ct: %d/%d %s\n", __func__, rc, src.objects, src.processed, src.path); return rc; } /* Else Some other relation / failure */ DEBUGF("%s res: %d, rel: %d\n", __func__, FORC_UNKNOWN_FAILURE, rel); return FORC_UNKNOWN_FAILURE; } int create_dir(void) { int rc; 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)) return FORC_PATH_TOO_LONG; rc = prompt_name(basename, sizeof (dirname) - pathlen); if (rc == FORC_SUCCESS) rc = mkdir(dirname) * 10; return rc; } /* share code for file and directory deletion, saves space */ int delete_fileobject(const char *selected_file) { int rc; struct file_op_params param; init_file_op(¶m, selected_file, NULL); if (param.append >= sizeof (param.path)) return FORC_PATH_TOO_LONG; /* Note: delete_fileobject() will happily delete whatever * path is passed (after confirmation) */ if (confirm_delete_yesno(param.path) != YESNO_YES) { return FORC_CANCELLED; } if (param.is_dir) { 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; } clear_screen_buffer(true); if (param.is_dir) { /* if directory */ cpu_boost(true); rc = directory_fileop(¶m, FOC_DELETE); cpu_boost(false); } else { param.objects = param.processed = 1; if (poll_cancel_action(FOC_DELETE, ¶m)) return FORC_CANCELLED; rc = remove(param.path) * 10; } return rc; } int rename_file(const char *selected_file) { int rc; char newname[MAX_PATH]; char *newext = NULL; 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) return FORC_PATH_TOO_LONG; if ((*tree_get_context()->dirfilter > NUM_FILTER_MODES) && (newext = strrchr(newbase, '.'))) /* hide extension when renaming in lists restricted to a single file format, such as in the Playlists menu */ *newext = '\0'; rc = prompt_name(newbase, sizeof (newname) - pathlen); if (rc != FORC_SUCCESS) return rc; if (newext) /* re-add original extension */ strlcat(newbase, strrchr(selection, '.'), sizeof (newname) - pathlen); if (!strcmp(oldbase, newbase)) return FORC_NOOP; /* No change at all */ int rel = relate(selection, newname); if (rel == RELATE_DIFFERENT) { if (file_exists(newname)) { /* don't overwrite */ return FORC_PATH_EXISTS; } return rename(selection, newname) * 10; } if (rel == RELATE_SAME) return rename(selection, newname) * 10; /* Else Some other relation / failure */ return FORC_UNKNOWN_FAILURE; }