diff --git a/README.md b/README.md index 0d841a8..f490b50 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ When the launcher is newly installed or updated, the payload also asks the PS5 to open that local browser URL once. Later loads keep the tile available without forcing the browser open again. +Launcher state is stored in `/data/BS5fm`. The PS5 app tile itself still lives +under `/user/app/BS5F00001`, which is the console app location used by the app +install API. + The payload also shows a PS5 notification with the external browser URL when the web server is ready. Port `5905` is fixed so this payload can run next to older builds that may still be listening on another port. @@ -59,9 +63,10 @@ At a high level the payload has five parts: notifications, installs the launcher tile if needed, and starts the HTTP server on port `5905`. - `src/app_installer.c` writes the embedded launcher metadata/icon under - `/user/app/BS5F00001` and registers the PS5 home-screen tile. The installer - resolves the title-directory install function from `libSceAppInstUtil.sprx` - at runtime and falls back to the broader app install call when needed. + `/user/app/BS5F00001`, stores its update marker under `/data/BS5fm`, and + registers the PS5 home-screen tile. The installer resolves the title-directory + install function from `libSceAppInstUtil.sprx` at runtime and falls back to + the broader app install call when needed. - `src/websrv_lite.c` is a compact HTTP server. It serves the embedded UI, static downloads under `/fs`, and JSON APIs under `/api/fs`. - `src/transfer.c` implements the file-manager API: list, stat, folder size, @@ -114,3 +119,5 @@ port `5905` instead. Recursive folder size, copy, move, and delete can still take time on very large external drives. Progress polling is tolerant of short hiccups, and active jobs continue on the PS5 side even if the browser temporarily loses contact. +Copy, move, and delete jobs report PS5-side throughput in `MB/s` plus ETA from +the server-side job counters. diff --git a/assets/files.html b/assets/files.html index fca6e55..f01f25f 100644 --- a/assets/files.html +++ b/assets/files.html @@ -727,6 +727,12 @@ return s + 's'; } + function speedMBps(bytesPerSecond) { + if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return ''; + const mb = bytesPerSecond / (1024 * 1024); + return mb.toFixed(mb >= 10 ? 1 : 2) + ' MB/s'; + } + function formatDate(seconds) { if (!Number.isFinite(seconds) || seconds <= 0) return 'Unknown'; return new Date(seconds * 1000).toLocaleString(); @@ -739,7 +745,10 @@ const totalFiles = Number(job.totalFiles || 0); const failedFiles = Number(job.failedFiles || 0); const elapsed = Math.max(0, (Date.now() / 1000) - Number(job.startedAt || 0)); - const speed = elapsed > 0 && copied > 0 ? copied / elapsed : 0; + const reportedSpeed = Number(job.speedBytesPerSec || 0); + const speed = reportedSpeed > 0 ? reportedSpeed : + (elapsed > 0 && copied > 0 ? copied / elapsed : 0); + const reportedEta = Number(job.etaSeconds || 0); const pct = total > 0 ? Math.max(0, Math.min(100, (copied / total) * 100)) : 0; const parts = []; @@ -755,8 +764,12 @@ } if (speed > 0) { - parts.push(shortSize(speed) + '/s'); - if (total > copied) parts.push('ETA ' + shortDuration((total - copied) / speed)); + parts.push(speedMBps(speed)); + if (reportedEta > 0) { + parts.push('ETA ' + shortDuration(reportedEta)); + } else if (total > copied) { + parts.push('ETA ' + shortDuration((total - copied) / speed)); + } } if (failedFiles > 0) parts.push(failedFiles + ' failed'); @@ -1027,7 +1040,7 @@ if (!job.busy) return job; } catch (err) { missed++; - if (missed >= 120) throw err; + if (missed >= 600) throw err; } await new Promise((resolve) => setTimeout(resolve, 700)); } @@ -1171,7 +1184,7 @@ } catch (err) { missed++; text.textContent = 'Waiting for status...'; - if (missed >= 120) { + if (missed >= 600) { clearInterval(state.jobTimer); state.jobTimer = 0; box.classList.remove('show'); diff --git a/src/app_installer.c b/src/app_installer.c index c80c812..8b2d0a7 100644 --- a/src/app_installer.c +++ b/src/app_installer.c @@ -18,7 +18,9 @@ #define BS5FM_APP_TITLE_ID "BS5F00001" #define BS5FM_APP_ROOT "/user/app" #define BS5FM_APP_PARENT BS5FM_APP_ROOT "/" -#define BS5FM_INSTALL_MARKER "bs5filemanager-launcher-v2\n" +#define BS5FM_DATA_DIR "/data/BS5fm" +#define BS5FM_MARKER_PATH BS5FM_DATA_DIR "/launcher.ok" +#define BS5FM_INSTALL_MARKER "bs5filemanager-launcher-v3\n" #define INCASSET(name, file) \ __asm__(".section .rodata\n" \ @@ -37,6 +39,7 @@ INCASSET(bs5fm_icon0_png, "assets-app/icon0.png"); int sceAppInstUtilInitialize(void); int sceAppInstUtilAppInstallAll(void *); +int sceAppInstUtilAppUnInstall(const char *); typedef int (*app_install_title_dir_fn)(const char *, const char *, void *); @@ -106,27 +109,33 @@ mkdir_if_needed(const char *path) { } +static int +ensure_data_dir(void) { + if(mkdir_if_needed("/data") != 0) return -1; + return mkdir_if_needed(BS5FM_DATA_DIR); +} + + int bs5fm_install_app_if_needed(void) { char app_dir[256]; char sce_sys_dir[256]; char param_path[256]; char icon_path[256]; - char marker_path[256]; struct stat st; snprintf(app_dir, sizeof(app_dir), BS5FM_APP_ROOT "/%s", BS5FM_APP_TITLE_ID); snprintf(sce_sys_dir, sizeof(sce_sys_dir), "%s/sce_sys", app_dir); snprintf(param_path, sizeof(param_path), "%s/param.json", sce_sys_dir); snprintf(icon_path, sizeof(icon_path), "%s/icon0.png", sce_sys_dir); - snprintf(marker_path, sizeof(marker_path), "%s/bs5fm.ok", app_dir); - int needs_install = stat(app_dir, &st) != 0 || + int app_exists = stat(app_dir, &st) == 0; + int needs_install = !app_exists || file_differs(param_path, bs5fm_param_json, bs5fm_param_json_size) || file_differs(icon_path, bs5fm_icon0_png, bs5fm_icon0_png_size) || - file_differs(marker_path, g_install_marker, + file_differs(BS5FM_MARKER_PATH, g_install_marker, sizeof(g_install_marker) - 1); if(!needs_install) return 0; @@ -143,6 +152,11 @@ bs5fm_install_app_if_needed(void) { return -1; } + if(app_exists) { + int uninstall_err = sceAppInstUtilAppUnInstall(BS5FM_APP_TITLE_ID); + printf(" launcher install: refresh old tile 0x%08x\n", uninstall_err); + } + if(mkdir_if_needed(app_dir) != 0 || mkdir_if_needed(sce_sys_dir) != 0) { printf(" launcher install: mkdir failed errno %d\n", errno); return -1; @@ -164,9 +178,13 @@ bs5fm_install_app_if_needed(void) { return -1; } - if(write_file(marker_path, g_install_marker, + if(ensure_data_dir() != 0) { + printf(" launcher install: warning, failed creating %s errno %d\n", + BS5FM_DATA_DIR, errno); + } else if(write_file(BS5FM_MARKER_PATH, g_install_marker, sizeof(g_install_marker) - 1) != 0) { - printf(" launcher install: warning, failed writing %s\n", marker_path); + printf(" launcher install: warning, failed writing %s\n", + BS5FM_MARKER_PATH); } bs5fm_notify("BS5FileManager app ready", diff --git a/src/lite_main.c b/src/lite_main.c index 6f09b9e..c9c1f7c 100644 --- a/src/lite_main.c +++ b/src/lite_main.c @@ -25,7 +25,7 @@ #define BS5FM_WEB_PORT 5905 int sceNetCtlInit(void); -int sceSystemServiceLaunchWebBrowser(const char *uri); +int sceSystemServiceLaunchWebBrowser(const char *uri, void *); int sceUserServiceInitialize(void *); @@ -76,7 +76,7 @@ static void launch_browser_once(ready_state_t *state) { if(!state->launch_browser || state->browser_launched) return; - int err = sceSystemServiceLaunchWebBrowser("http://127.0.0.1:5905/"); + int err = sceSystemServiceLaunchWebBrowser("http://127.0.0.1:5905/", NULL); if(err == 0) { puts(" ps5 browser: opened http://127.0.0.1:5905/"); } else { diff --git a/src/transfer.c b/src/transfer.c index e6f2c77..78db14f 100644 --- a/src/transfer.c +++ b/src/transfer.c @@ -388,7 +388,7 @@ copy_recursive(const char *src, const char *dst) { static int -delete_recursive(const char *path) { +delete_recursive(const char *path, int count_progress) { if(job_cancelled()) { errno = ECANCELED; return -1; @@ -405,7 +405,7 @@ delete_recursive(const char *path) { if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) continue; char child[1024]; join_path(child, sizeof(child), path, ent->d_name); - if(delete_recursive(child) != 0) { + if(delete_recursive(child, count_progress) != 0) { rc = -1; atomic_fetch_add(&g_job.failed_files, 1); if(job_cancelled()) break; @@ -415,16 +415,24 @@ delete_recursive(const char *path) { if(rc == 0) { job_set_current(path); if(rmdir(path) != 0) return -1; - if(st.st_size > 0) atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size); - atomic_fetch_add(&g_job.done_files, 1); + if(count_progress) { + if(st.st_size > 0) { + atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size); + } + atomic_fetch_add(&g_job.done_files, 1); + } } return rc; } job_set_current(path); if(unlink(path) != 0) return -1; - if(st.st_size > 0) atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size); - atomic_fetch_add(&g_job.done_files, 1); + if(count_progress) { + if(st.st_size > 0) { + atomic_fetch_add(&g_job.copied_bytes, (long)st.st_size); + } + atomic_fetch_add(&g_job.done_files, 1); + } return 0; } @@ -492,7 +500,7 @@ copy_worker(void *arg) { free(a); return NULL; } - if(a->is_move && delete_recursive(a->src) != 0) { + if(a->is_move && delete_recursive(a->src, 0) != 0) { char err[160]; snprintf(err, sizeof(err), "post-move cleanup: %s", strerror(errno)); job_end(-1, err); @@ -520,7 +528,7 @@ delete_worker(void *arg) { atomic_store(&g_job.total_files, (int)(items > 0 ? items : 1)); atomic_store(&g_job.total_bytes, bytes); - if(delete_recursive(a->path) != 0) { + if(delete_recursive(a->path, 1) != 0) { char err[160]; snprintf(err, sizeof(err), "delete: %s", strerror(errno)); job_end(-1, err); @@ -882,6 +890,16 @@ status_handler(const http_request_t *req) { ended_at = g_job.ended_at; pthread_mutex_unlock(&g_job.lock); + time_t now = time(NULL); + time_t ref_at = busy ? now : (ended_at ? ended_at : now); + long elapsed = started_at > 0 && ref_at > started_at + ? (long)(ref_at - started_at) + : 0; + long speed = elapsed > 0 && cb > 0 ? cb / elapsed : 0; + long eta = busy && speed > 0 && tb > cb + ? (tb - cb + speed - 1) / speed + : 0; + json_buf_t b = {0}; if(json_appendf(&b, "{\"ok\":true,\"busy\":%s,\"cancelling\":%s,\"verb\":", @@ -894,8 +912,11 @@ status_handler(const http_request_t *req) { json_appendf(&b, ",\"totalBytes\":%ld,\"copiedBytes\":%ld," "\"totalFiles\":%d,\"doneFiles\":%d,\"failedFiles\":%d," - "\"startedAt\":%ld,\"endedAt\":%ld}", - tb, cb, tf, df, ff, (long)started_at, (long)ended_at) != 0) { + "\"startedAt\":%ld,\"endedAt\":%ld," + "\"elapsedSeconds\":%ld,\"speedBytesPerSec\":%ld," + "\"etaSeconds\":%ld}", + tb, cb, tf, df, ff, (long)started_at, (long)ended_at, + elapsed, speed, eta) != 0) { free(b.data); return -1; }