rockbox/apps/fileop.c
William Wilgus b0dfcde2f5 [Cleanup] onplay.c fileop.c
clean-up a bit more
add/correct some comments

fix some error passing
guard delete path on PATH_TOO_LONG

add some cpu_boost

Change-Id: Icf179dd727271bdc61ab78400e10847222b9f858
2024-07-04 12:44:04 -04:00

598 lines
18 KiB
C

/***************************************************************************
* __________ __ ___.
* 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 <stdbool.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 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(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 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));
}
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;
}
/* 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(&param->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(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) {
/* enter subdirectory */
rc = directory_fileop(param, FOC_COUNT); /* recursion */
} 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;
}
/* 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(const char *src_path,
const char *dst_path,
unsigned int *pflags)
{
unsigned int flags = *pflags;
int rc = FORC_UNKNOWN_FAILURE;
if (!(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);
#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(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 (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(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;
}
/************************************************************************************/
/* 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 (confirm_overwrite_yesno() == YESNO_NO) {
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.path, dst.path, &flags);
if (rc < FORC_SUCCESS) {
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_path, 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__, rc, 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(&param, selected_file, NULL);
if (param.append >= sizeof (param.path))
return FORC_PATH_TOO_LONG;
if (param.is_dir) {
int rc = check_count_fileobjects(&param);
DEBUGF("%s res: %d, ct: %d, %s", __func__, rc, param.objects, param.path);
if (rc != FORC_SUCCESS)
return rc;
}
/* Note: delete_fileobject() will happily delete whatever
* path is passed (after confirmation) */
if (confirm_delete_yesno(param.path) != YESNO_YES) {
return FORC_CANCELLED;
}
clear_screen_buffer(true);
if (poll_cancel_action(param.path, FOC_DELETE, param.processed, param.objects))
return FORC_CANCELLED;
if (param.is_dir) { /* if directory */
cpu_boost(true);
rc = directory_fileop(&param, FOC_DELETE);
cpu_boost(false);
} else {
rc = remove(param.path) * 10;
}
return rc;
}
int rename_file(const char *selected_file)
{
int rc;
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)
return FORC_PATH_TOO_LONG;
rc = prompt_name(newbase, sizeof (newname) - pathlen);
if (rc != FORC_SUCCESS)
return rc;
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;
}