Move pack files instead of copying; one file-op core for all modes

Install: staging and sd:/ are on the same card, so pack folders are now
moved with f_rename (a missing target folder moves in one step, existing
ones are merged file by file). Copying is only a per-entry fallback.
The staging cleanup afterwards only removes empty folders.

Deletion: one f_stat per list entry, no full recursive count before
deleting; large folders are detected with a probe that stops after 51
entries. Progress lines count processed entries instead of needing an
extra pass for a total.

Six copy/delete walkers (fs.c + install.c) are replaced by fs_delete_tree,
fs_move_tree and fs_move_file sharing one path buffer and one copy buffer
per run; the 'alternative copy method' retry is gone. extras/hekate-ams-pro
is moved too. Backups keep copy semantics.
This commit is contained in:
niklascfw committed 2026-11-12 22:57:26 +01:00
1 parent c033b988ca
commit 664216fa11
6 files changed
+453 -784

No files matched your search

+293 -190
View File
@@ -1,6 +1,10 @@
/*
* OmniNX Installer - Filesystem operations with file logging
* Based on HATS Installer
*
* All tree walkers share one path buffer per call (no malloc per entry) and
* visit every entry exactly once. Moving within the SD card uses f_rename and
* only falls back to copying when a rename fails.
*/
#include "fs.h"
@@ -10,8 +14,8 @@
#include <utils/sprintf.h>
#include <stdarg.h>
#define FS_BUFFER_SIZE 0x100000 // 1MB copy buffer
#define LOG_BUFFER_SIZE 1024 // fits two 255-char paths plus text
#define FS_BUFFER_SIZE 0x100000 // 1MB copy buffer, allocated once
#define LOG_BUFFER_SIZE 1024 // fits two 255-char paths plus text
#define LOG_SYNC_EVERY 16
// Log file handle
@@ -20,6 +24,9 @@ static bool log_enabled = false;
static u32 log_lines;
static char log_buf[LOG_BUFFER_SIZE];
// Shared copy buffer (lazy, released with fs_release_buffers)
static u8 *copy_buf;
// Initialize log file
void log_init(const char *path) {
int res = f_open(&log_file, path, FA_WRITE | FA_CREATE_ALWAYS);
@@ -61,6 +68,13 @@ bool log_is_enabled(void) {
return log_enabled;
}
void fs_release_buffers(void) {
if (copy_buf) {
free(copy_buf);
copy_buf = NULL;
}
}
// Convert FatFS error code to string
const char *fs_error_str(int err) {
switch (err) {
@@ -88,270 +102,359 @@ const char *fs_error_str(int err) {
}
}
// Combine two paths
static char *combine_paths(const char *base, const char *add) {
size_t base_len = strlen(base);
size_t add_len = strlen(add);
char *result = malloc(base_len + add_len + 2);
/* ---- path helpers ---------------------------------------------------- */
if (!result) return NULL;
if (base_len > 0 && base[base_len - 1] == '/') {
s_printf(result, "%s%s", base, add);
} else {
s_printf(result, "%s/%s", base, add);
}
return result;
// Append "/name" to path (len = current length). Returns new length or 0 if too long.
static size_t path_push(char *path, size_t len, const char *name) {
size_t nlen = strlen(name);
size_t slash = (len > 0 && path[len - 1] != '/') ? 1 : 0;
if (len + slash + nlen + 1 > FS_PATH_MAX)
return 0;
if (slash)
path[len++] = '/';
memcpy(path + len, name, nlen + 1);
return len + nlen;
}
static size_t path_set(char *path, const char *src) {
size_t len = strlen(src);
if (len + 1 > FS_PATH_MAX)
return 0;
memcpy(path, src, len + 1);
// Normalise "sd:/foo/" -> "sd:/foo" (keep "sd:/")
while (len > 4 && path[len - 1] == '/')
path[--len] = '\0';
return len;
}
static const char *path_basename(const char *path) {
const char *slash = strrchr(path, '/');
return slash ? slash + 1 : path;
}
static bool is_dot_entry(const char *name) {
return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
}
static void clear_readonly(const char *path, BYTE attr) {
if (attr & AM_RDO)
f_chmod(path, attr & ~AM_RDO, AM_RDO);
}
/* ---- single file ----------------------------------------------------- */
// Copy a single file with logging
int file_copy(const char *src, const char *dst) {
FIL fin, fout;
FILINFO fno;
int res;
log_write("COPY: %s -> %s\n", src, dst);
res = f_open(&fin, src, FA_READ | FA_OPEN_EXISTING);
if (res != FR_OK) {
log_write(" ERROR open src: %s\n", fs_error_str(res));
log_write("COPY %s: open src %s\n", src, fs_error_str(res));
return res;
}
f_stat(src, &fno);
u64 file_size = f_size(&fin);
log_write(" Size: %d bytes\n", (u32)file_size);
if (f_stat(src, &fno) != FR_OK)
fno.fattrib = 0;
u64 remaining = f_size(&fin);
res = f_open(&fout, dst, FA_WRITE | FA_CREATE_ALWAYS);
if (res != FR_OK) {
f_close(&fin);
log_write(" ERROR open dst: %s\n", fs_error_str(res));
log_write("COPY %s: open dst %s\n", dst, fs_error_str(res));
return res;
}
u8 *buf = malloc(FS_BUFFER_SIZE);
if (!buf) {
if (!copy_buf)
copy_buf = malloc(FS_BUFFER_SIZE);
if (!copy_buf) {
f_close(&fin);
f_close(&fout);
log_write(" ERROR: Out of memory for buffer\n");
log_write("COPY %s: out of memory\n", src);
return FR_NOT_ENOUGH_CORE;
}
u64 remaining = file_size;
UINT br, bw;
while (remaining > 0) {
UINT to_copy = (remaining > FS_BUFFER_SIZE) ? FS_BUFFER_SIZE : (UINT)remaining;
res = f_read(&fin, buf, to_copy, &br);
if (res != FR_OK) {
log_write(" ERROR read: %s\n", fs_error_str(res));
break;
}
if (br != to_copy) {
log_write(" ERROR: Read %d bytes, expected %d\n", br, to_copy);
res = f_read(&fin, copy_buf, to_copy, &br);
if (res == FR_OK && br != to_copy)
res = FR_DISK_ERR;
if (res != FR_OK)
break;
}
res = f_write(&fout, buf, to_copy, &bw);
if (res != FR_OK) {
log_write(" ERROR write: %s\n", fs_error_str(res));
break;
}
if (bw != to_copy) {
log_write(" ERROR: Wrote %d bytes, expected %d\n", bw, to_copy);
res = f_write(&fout, copy_buf, to_copy, &bw);
if (res == FR_OK && bw != to_copy)
res = FR_DISK_ERR;
if (res != FR_OK)
break;
}
remaining -= to_copy;
}
free(buf);
f_close(&fin);
f_close(&fout);
if (res == FR_OK) {
if (res == FR_OK)
f_chmod(dst, fno.fattrib, 0x3A);
log_write(" OK\n");
}
log_write("COPY %s -> %s: %s\n", src, dst, fs_error_str(res));
return res;
}
// Recursively delete a folder with logging
int folder_delete(const char *path) {
DIR dir;
// Move a single file; replaces dst. Falls back to copy + delete if rename fails.
int fs_move_file(const char *src, const char *dst) {
FILINFO fno;
int res;
log_write("DELETE: %s\n", path);
res = f_opendir(&dir, path);
if (res != FR_OK) {
// Maybe it's a file, try to delete it
log_write(" Not a dir, trying as file...\n");
res = f_unlink(path);
if (res != FR_OK) {
log_write(" ERROR unlink: %s\n", fs_error_str(res));
} else {
log_write(" OK (file deleted)\n");
if (f_stat(dst, &fno) == FR_OK) {
if (fno.fattrib & AM_DIR) {
log_write("MOVE %s: destination is a folder\n", dst);
return FR_DENIED;
}
return res;
clear_readonly(dst, fno.fattrib);
f_unlink(dst);
}
int file_count = 0;
int dir_count = 0;
int res = f_rename(src, dst);
if (res == FR_OK) {
log_write("MOVE %s -> %s\n", src, dst);
return FR_OK;
}
while (1) {
res = f_readdir(&dir, &fno);
if (res != FR_OK) {
log_write(" ERROR readdir: %s\n", fs_error_str(res));
break;
}
if (fno.fname[0] == 0) break; // End of directory
res = file_copy(src, dst);
if (res == FR_OK)
f_unlink(src);
return res;
}
char *full_path = combine_paths(path, fno.fname);
if (!full_path) {
res = FR_NOT_ENOUGH_CORE;
/* ---- trees ----------------------------------------------------------- */
static int delete_tree_rec(char *path, size_t len, fs_progress_cb cb, void *ctx) {
DIR dir;
FILINFO fno;
int res = f_opendir(&dir, path);
if (res != FR_OK)
return res;
while ((res = f_readdir(&dir, &fno)) == FR_OK && fno.fname[0]) {
if (is_dot_entry(fno.fname))
continue;
size_t sub = path_push(path, len, fno.fname);
if (!sub) {
res = FR_INVALID_NAME;
break;
}
if (fno.fattrib & AM_DIR) {
dir_count++;
res = folder_delete(full_path);
res = delete_tree_rec(path, sub, cb, ctx);
if (res == FR_OK) {
clear_readonly(path, fno.fattrib);
res = f_unlink(path);
}
} else {
file_count++;
log_write(" DEL: %s\n", fno.fname);
// Clear read-only attribute if set (common with some CFW packs)
if (fno.fattrib & AM_RDO) {
f_chmod(full_path, fno.fattrib & ~AM_RDO, AM_RDO);
}
res = f_unlink(full_path);
if (res != FR_OK) {
log_write(" ERROR: %s\n", fs_error_str(res));
}
clear_readonly(path, fno.fattrib);
res = f_unlink(path);
}
log_write(" DEL %s: %s\n", path, fs_error_str(res));
path[len] = '\0';
free(full_path);
if (res != FR_OK) break;
if (res != FR_OK)
break;
if (cb)
cb(ctx);
}
f_closedir(&dir);
if (res == FR_OK || res == FR_NO_FILE) {
log_write(" Removing dir: %s (%d files, %d subdirs)\n", path, file_count, dir_count);
// Check and clear read-only attribute on directory if set
FILINFO dir_info;
if (f_stat(path, &dir_info) == FR_OK && (dir_info.fattrib & AM_RDO)) {
f_chmod(path, dir_info.fattrib & ~AM_RDO, AM_RDO);
}
res = f_unlink(path);
if (res != FR_OK) {
log_write(" ERROR rmdir: %s\n", fs_error_str(res));
} else {
log_write(" OK\n");
}
}
return res;
}
// Recursively copy a folder with logging
int folder_copy(const char *src, const char *dst) {
// Delete a file or a whole folder. A missing path counts as success.
int fs_delete_tree(const char *target, fs_progress_cb cb, void *ctx) {
char path[FS_PATH_MAX];
FILINFO fno;
size_t len = path_set(path, target);
if (!len)
return FR_INVALID_NAME;
int res = f_stat(path, &fno);
if (res == FR_NO_FILE || res == FR_NO_PATH)
return FR_OK;
if (res != FR_OK)
return res;
if (fno.fattrib & AM_DIR) {
res = delete_tree_rec(path, len, cb, ctx);
if (res != FR_OK) {
log_write("DELETE %s: %s\n", target, fs_error_str(res));
return res;
}
}
clear_readonly(path, fno.fattrib);
res = f_unlink(path);
log_write("DELETE %s: %s\n", target, fs_error_str(res));
return res;
}
int folder_delete(const char *path) {
return fs_delete_tree(path, NULL, NULL);
}
static int copy_tree_rec(char *src, size_t slen, char *dst, size_t dlen) {
DIR dir;
FILINFO fno;
int res;
log_write("FOLDER COPY: %s -> %s\n", src, dst);
int res = f_mkdir(dst);
if (res != FR_OK && res != FR_EXIST)
return res;
res = f_opendir(&dir, src);
if (res != FR_OK) {
log_write(" ERROR opendir src: %s\n", fs_error_str(res));
if (res != FR_OK)
return res;
}
// Get folder name from src path
const char *folder_name = strrchr(src, '/');
if (folder_name) {
folder_name++;
} else {
folder_name = src;
}
// Create destination folder
char *dst_path = combine_paths(dst, folder_name);
if (!dst_path) {
f_closedir(&dir);
return FR_NOT_ENOUGH_CORE;
}
log_write(" Creating: %s\n", dst_path);
res = f_mkdir(dst_path);
if (res == FR_EXIST) {
log_write(" (already exists)\n");
res = FR_OK;
} else if (res != FR_OK) {
log_write(" ERROR mkdir: %s\n", fs_error_str(res));
f_closedir(&dir);
free(dst_path);
return res;
}
int file_count = 0;
int dir_count = 0;
// Copy contents
while (1) {
res = f_readdir(&dir, &fno);
if (res != FR_OK) {
log_write(" ERROR readdir: %s\n", fs_error_str(res));
break;
}
if (fno.fname[0] == 0) break; // End of directory
char *src_full = combine_paths(src, fno.fname);
char *dst_full = combine_paths(dst_path, fno.fname);
if (!src_full || !dst_full) {
if (src_full) free(src_full);
if (dst_full) free(dst_full);
res = FR_NOT_ENOUGH_CORE;
while ((res = f_readdir(&dir, &fno)) == FR_OK && fno.fname[0]) {
if (is_dot_entry(fno.fname))
continue;
size_t s = path_push(src, slen, fno.fname);
size_t d = path_push(dst, dlen, fno.fname);
if (!s || !d) {
res = FR_INVALID_NAME;
break;
}
if (fno.fattrib & AM_DIR) {
dir_count++;
res = folder_copy(src_full, dst_path);
} else {
file_count++;
res = file_copy(src_full, dst_full);
}
free(src_full);
free(dst_full);
if (res != FR_OK) break;
if (fno.fattrib & AM_DIR)
res = copy_tree_rec(src, s, dst, d);
else
res = file_copy(src, dst);
src[slen] = '\0';
dst[dlen] = '\0';
if (res != FR_OK)
break;
}
f_closedir(&dir);
// Copy folder attributes
if (res == FR_OK) {
FILINFO src_info;
if (f_stat(src, &src_info) == FR_OK) {
f_chmod(dst_path, src_info.fattrib, 0x3A);
}
log_write(" Done: %d files, %d subdirs\n", file_count, dir_count);
}
free(dst_path);
return res;
}
// Recursively copy folder src into dst_parent (creates dst_parent/<name of src>)
int folder_copy(const char *src_dir, const char *dst_parent) {
char src[FS_PATH_MAX];
char dst[FS_PATH_MAX];
size_t slen = path_set(src, src_dir);
size_t dlen = path_set(dst, dst_parent);
if (!slen || !dlen || !(dlen = path_push(dst, dlen, path_basename(src))))
return FR_INVALID_NAME;
int res = copy_tree_rec(src, slen, dst, dlen);
log_write("COPY TREE %s -> %s: %s\n", src_dir, dst, fs_error_str(res));
return res;
}
static int move_tree_rec(char *src, size_t slen, char *dst, size_t dlen, fs_progress_cb cb, void *ctx) {
DIR dir;
FILINFO fno;
FILINFO dst_info;
int res = f_opendir(&dir, src);
if (res != FR_OK)
return res;
while ((res = f_readdir(&dir, &fno)) == FR_OK && fno.fname[0]) {
if (is_dot_entry(fno.fname))
continue;
size_t s = path_push(src, slen, fno.fname);
size_t d = path_push(dst, dlen, fno.fname);
if (!s || !d) {
res = FR_INVALID_NAME;
break;
}
bool dst_exists = f_stat(dst, &dst_info) == FR_OK;
if (fno.fattrib & AM_DIR) {
if (!dst_exists) {
// Whole subtree moves in one step
res = f_rename(src, dst);
if (res != FR_OK)
res = copy_tree_rec(src, s, dst, d);
log_write("MOVE DIR %s -> %s: %s\n", src, dst, fs_error_str(res));
} else if (dst_info.fattrib & AM_DIR) {
res = move_tree_rec(src, s, dst, d, cb, ctx);
} else {
log_write("MOVE %s: destination is a file\n", dst);
res = FR_DENIED;
}
} else {
res = fs_move_file(src, dst);
}
src[slen] = '\0';
dst[dlen] = '\0';
if (res != FR_OK)
break;
if (cb)
cb(ctx);
}
f_closedir(&dir);
return res;
}
// Move folder src_dir into dst_parent (dst_parent/<name>), merging with an existing folder.
int fs_move_tree(const char *src_dir, const char *dst_parent, fs_progress_cb cb, void *ctx) {
char src[FS_PATH_MAX];
char dst[FS_PATH_MAX];
FILINFO fno;
size_t slen = path_set(src, src_dir);
size_t dlen = path_set(dst, dst_parent);
if (!slen || !dlen || !(dlen = path_push(dst, dlen, path_basename(src))))
return FR_INVALID_NAME;
int res = f_stat(dst, &fno);
if (res == FR_NO_FILE) {
// Target missing: one rename moves everything
res = f_rename(src, dst);
log_write("MOVE DIR %s -> %s: %s\n", src, dst, fs_error_str(res));
if (res == FR_OK)
return FR_OK;
res = copy_tree_rec(src, slen, dst, dlen);
log_write("COPY TREE %s -> %s: %s\n", src, dst, fs_error_str(res));
return res;
}
if (res != FR_OK)
return res;
if (!(fno.fattrib & AM_DIR))
return FR_DENIED;
return move_tree_rec(src, slen, dst, dlen, cb, ctx);
}
// Count entries below path, stopping once limit is reached (limit 0 = no limit)
static u32 count_rec(char *path, size_t len, u32 count, u32 limit) {
DIR dir;
FILINFO fno;
if (f_opendir(&dir, path) != FR_OK)
return count;
while (f_readdir(&dir, &fno) == FR_OK && fno.fname[0]) {
if (is_dot_entry(fno.fname))
continue;
count++;
if (limit && count >= limit)
break;
if (fno.fattrib & AM_DIR) {
size_t sub = path_push(path, len, fno.fname);
if (sub) {
count = count_rec(path, sub, count, limit);
path[len] = '\0';
}
if (limit && count >= limit)
break;
}
}
f_closedir(&dir);
return count;
}
u32 fs_count_items(const char *target, u32 limit) {
char path[FS_PATH_MAX];
size_t len = path_set(path, target);
return len ? count_rec(path, len, 0, limit) : 0;
}