mirror of
https://github.com/Rockbox/rockbox.git
synced 2025-10-13 10:07:38 -04:00
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
598 lines
18 KiB
C
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(¶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(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(¶m, selected_file, NULL);
|
|
if (param.append >= sizeof (param.path))
|
|
return FORC_PATH_TOO_LONG;
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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(¶m, 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;
|
|
}
|