464 lines
13 KiB
C
464 lines
13 KiB
C
/*
|
|
* 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"
|
|
#include <libs/fatfs/ff.h>
|
|
#include <mem/heap.h>
|
|
#include <string.h>
|
|
#include <utils/sprintf.h>
|
|
#include <stdarg.h>
|
|
|
|
#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
|
|
static FIL log_file;
|
|
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);
|
|
if (res == FR_OK) {
|
|
log_enabled = true;
|
|
log_lines = 0;
|
|
log_write("=== OmniNX Installer Log ===\n\n");
|
|
}
|
|
}
|
|
|
|
// Close log file
|
|
void log_close(void) {
|
|
if (log_enabled) {
|
|
f_sync(&log_file);
|
|
f_close(&log_file);
|
|
log_enabled = false;
|
|
}
|
|
}
|
|
|
|
// Write to log file (variadic version)
|
|
void log_write(const char *fmt, ...) {
|
|
if (!log_enabled) return;
|
|
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
|
|
// BDK formatter (no newlib printf). Callers keep paths <= 255 chars.
|
|
u32 len = s_vprintf(log_buf, fmt, args);
|
|
|
|
va_end(args);
|
|
|
|
UINT bw;
|
|
f_write(&log_file, log_buf, len, &bw);
|
|
if (++log_lines % LOG_SYNC_EVERY == 0)
|
|
f_sync(&log_file);
|
|
}
|
|
|
|
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) {
|
|
case FR_OK: return "OK";
|
|
case FR_DISK_ERR: return "DISK_ERR: Low level disk error";
|
|
case FR_INT_ERR: return "INT_ERR: Internal error";
|
|
case FR_NOT_READY: return "NOT_READY: Drive not ready";
|
|
case FR_NO_FILE: return "NO_FILE: File not found";
|
|
case FR_NO_PATH: return "NO_PATH: Path not found";
|
|
case FR_INVALID_NAME: return "INVALID_NAME: Invalid path name";
|
|
case FR_DENIED: return "DENIED: Access denied";
|
|
case FR_EXIST: return "EXIST: Already exists";
|
|
case FR_INVALID_OBJECT: return "INVALID_OBJECT: Invalid object";
|
|
case FR_WRITE_PROTECTED: return "WRITE_PROTECTED: Write protected";
|
|
case FR_INVALID_DRIVE: return "INVALID_DRIVE: Invalid drive";
|
|
case FR_NOT_ENABLED: return "NOT_ENABLED: Volume not mounted";
|
|
case FR_NO_FILESYSTEM: return "NO_FILESYSTEM: No valid FAT";
|
|
case FR_MKFS_ABORTED: return "MKFS_ABORTED: mkfs aborted";
|
|
case FR_TIMEOUT: return "TIMEOUT: Timeout";
|
|
case FR_LOCKED: return "LOCKED: File locked";
|
|
case FR_NOT_ENOUGH_CORE: return "NOT_ENOUGH_CORE: Out of memory";
|
|
case FR_TOO_MANY_OPEN_FILES: return "TOO_MANY_OPEN_FILES";
|
|
case FR_INVALID_PARAMETER: return "INVALID_PARAMETER";
|
|
default: return "UNKNOWN_ERROR";
|
|
}
|
|
}
|
|
|
|
/* ---- path helpers ---------------------------------------------------- */
|
|
|
|
// 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;
|
|
|
|
res = f_open(&fin, src, FA_READ | FA_OPEN_EXISTING);
|
|
if (res != FR_OK) {
|
|
log_write("COPY %s: open src %s\n", src, fs_error_str(res));
|
|
return res;
|
|
}
|
|
|
|
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("COPY %s: open dst %s\n", dst, fs_error_str(res));
|
|
return res;
|
|
}
|
|
|
|
if (!copy_buf)
|
|
copy_buf = malloc(FS_BUFFER_SIZE);
|
|
if (!copy_buf) {
|
|
f_close(&fin);
|
|
f_close(&fout);
|
|
log_write("COPY %s: out of memory\n", src);
|
|
return FR_NOT_ENOUGH_CORE;
|
|
}
|
|
|
|
UINT br, bw;
|
|
while (remaining > 0) {
|
|
UINT to_copy = (remaining > FS_BUFFER_SIZE) ? FS_BUFFER_SIZE : (UINT)remaining;
|
|
|
|
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, copy_buf, to_copy, &bw);
|
|
if (res == FR_OK && bw != to_copy)
|
|
res = FR_DISK_ERR;
|
|
if (res != FR_OK)
|
|
break;
|
|
|
|
remaining -= to_copy;
|
|
}
|
|
|
|
f_close(&fin);
|
|
f_close(&fout);
|
|
|
|
if (res == FR_OK)
|
|
f_chmod(dst, fno.fattrib, 0x3A);
|
|
log_write("COPY %s -> %s: %s\n", src, dst, fs_error_str(res));
|
|
return res;
|
|
}
|
|
|
|
// 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;
|
|
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;
|
|
}
|
|
clear_readonly(dst, fno.fattrib);
|
|
f_unlink(dst);
|
|
}
|
|
|
|
int res = f_rename(src, dst);
|
|
if (res == FR_OK) {
|
|
log_write("MOVE %s -> %s\n", src, dst);
|
|
return FR_OK;
|
|
}
|
|
|
|
res = file_copy(src, dst);
|
|
if (res == FR_OK)
|
|
f_unlink(src);
|
|
return res;
|
|
}
|
|
|
|
/* ---- 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) {
|
|
res = delete_tree_rec(path, sub, cb, ctx);
|
|
if (res == FR_OK) {
|
|
clear_readonly(path, fno.fattrib);
|
|
res = f_unlink(path);
|
|
}
|
|
} else {
|
|
clear_readonly(path, fno.fattrib);
|
|
res = f_unlink(path);
|
|
}
|
|
log_write(" DEL %s: %s\n", path, fs_error_str(res));
|
|
path[len] = '\0';
|
|
|
|
if (res != FR_OK)
|
|
break;
|
|
if (cb)
|
|
cb(ctx);
|
|
}
|
|
|
|
f_closedir(&dir);
|
|
return res;
|
|
}
|
|
|
|
// 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 = f_mkdir(dst);
|
|
if (res != FR_OK && res != FR_EXIST)
|
|
return res;
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
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) {
|
|
if (cb)
|
|
cb(ctx);
|
|
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;
|
|
}
|