diff --git a/assets/main.js b/assets/main.js index ef2a104..5aa0164 100644 --- a/assets/main.js +++ b/assets/main.js @@ -30,6 +30,7 @@ let permissionOriginal = ""; let permissionBusy = false; let downloadFrame = null; let uploadXhr = null; +let uploadTerminalAbort = false; let L = {}; const APP_VERSION = "v1.6"; @@ -458,6 +459,10 @@ function clearTrackedTask() { downloadFrame.parentNode.removeChild(downloadFrame); downloadFrame = null; } + if (trackedTask && trackedTask.op === "upload" && uploadXhr) { + uploadTerminalAbort = true; + uploadXhr.abort(); + } trackedTask = null; } @@ -1798,8 +1803,11 @@ function renderTasks(tasks) { if (confirm(t("cancelTaskConfirm", { label: opLabel(task.op) }))) { requestTaskCancel(task.id); api("/api/cancel", { id: task.id }).then(() => { - if (task.op === "upload" && uploadXhr) uploadXhr.abort(); - return pollTasks(); + if (task.op !== "upload") return pollTasks(); + if (uploadXhr) uploadXhr.abort(); + return api("/api/upload/finish", { task_id: task.id }) + .catch(() => {}) + .then(pollTasks); }).catch(err => { if (trackedTask && trackedTask.id === task.id) trackedTask.cancelRequested = false; setStatus(t("cancelFailed", { error: err.message })); @@ -1941,7 +1949,7 @@ function uploadConflicts(rels) { return conflicts; } -function uploadFileRequest(taskId, file, rel, overwrite) { +function uploadFileRequest(taskId, file, rel, overwrite, index) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); uploadXhr = xhr; @@ -1967,12 +1975,25 @@ function uploadFileRequest(taskId, file, rel, overwrite) { }; xhr.onerror = () => { uploadXhr = null; - reject(new Error(xhr.statusText || t("backendError"))); + const error = new Error(xhr.statusText || t("backendError")); + api("/api/tasks").then(data => { + const task = (data.tasks || []).find(item => item.id === taskId); + if (task && Number(task.completed_count || 0) >= index) { + resolve(); + } else if (task && task.state === "failed") { + reject(new Error(backendErrorText(task.error_code, task.error_arg, + task.error || task.current))); + } else { + reject(error); + } + }).catch(() => reject(error)); }; xhr.onabort = () => { uploadXhr = null; + const terminal = uploadTerminalAbort; + uploadTerminalAbort = false; const err = new Error("aborted"); - err.name = "AbortError"; + err.name = terminal ? "TerminalTaskAbort" : "AbortError"; reject(err); }; xhr.send(file); @@ -2030,21 +2051,32 @@ async function uploadFiles(files) { const file = list[i]; const rel = rels[i]; setStatus(t("uploadingStatus", { index: i + 1, count: list.length, name: rel })); - await uploadFileRequest(taskId, file, rel, overwrite); + await uploadFileRequest(taskId, file, rel, overwrite, i + 1); } await api("/api/upload/finish", { task_id: taskId }); await pollTasks(); setStatus(t("uploadDone", { count: list.length })); } catch (err) { + const terminalAbort = err && err.name === "TerminalTaskAbort"; const canceled = err && err.name === "AbortError"; - if (canceled) { - setStatus(t("taskCanceled", { label: opLabel("upload") })); - } else { - const message = t("uploadFailed", { error: err.message }); - setStatus(message); - alert(message); + if (!terminalAbort) { + if (taskId) { + try { + await api("/api/cancel", { id: taskId }); + await api("/api/upload/finish", { task_id: taskId }); + } catch (cancelErr) { + /* The server may already have moved a failed upload to a terminal state. */ + } + } + await pollTasks(); + if (canceled) { + setStatus(t("taskCanceled", { label: opLabel("upload") })); + } else { + const message = t("uploadFailed", { error: err.message }); + setStatus(message); + alert(message); + } } - await pollTasks(); } finally { uploadXhr = null; uploadFilesEl.value = ""; diff --git a/src/filemgr.c b/src/filemgr.c index 74364c4..b761de0 100644 --- a/src/filemgr.c +++ b/src/filemgr.c @@ -1853,8 +1853,9 @@ api_tasks(struct MHD_Connection *conn) { json_escape(&b, task->error_code); strbuf_append(&b, ",\"error_arg\":"); json_escape(&b, task->error_arg); - strbuf_printf(&b, ",\"src_count\":%zu,\"total\":%llu,\"done\":%llu,\"speed\":%llu,\"eta\":%llu,\"cancel_requested\":%s,\"created_at\":%lld,\"elapsed\":%lld,\"total_elapsed\":%lld,\"updated_at\":%lld}", - task->src_count, task->total, task->done, task->speed, task->eta, + strbuf_printf(&b, ",\"src_count\":%zu,\"completed_count\":%zu,\"total\":%llu,\"done\":%llu,\"speed\":%llu,\"eta\":%llu,\"cancel_requested\":%s,\"created_at\":%lld,\"elapsed\":%lld,\"total_elapsed\":%lld,\"updated_at\":%lld}", + task->src_count, task->upload_completed, + task->total, task->done, task->speed, task->eta, task->cancel_requested ? "true" : "false", (long long)task->created_at, task->transfer_started_at ? (long long)(now - task->transfer_started_at) : 0LL, diff --git a/src/filemgr_internal.h b/src/filemgr_internal.h index ffc1a3f..0465a43 100644 --- a/src/filemgr_internal.h +++ b/src/filemgr_internal.h @@ -45,6 +45,7 @@ typedef struct file_task { size_t src_count; size_t file_count; size_t dir_count; + size_t upload_completed; unsigned int chmod_mode; int recursive; unsigned long long total; @@ -57,6 +58,7 @@ typedef struct file_task { unsigned int eta_sample_next; unsigned int eta_sample_count; int cancel_requested; + unsigned int active_streams; time_t created_at; time_t transfer_started_at; time_t updated_at; diff --git a/src/pkg_installer.c b/src/pkg_installer.c index 1455cf3..212d477 100644 --- a/src/pkg_installer.c +++ b/src/pkg_installer.c @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -14,8 +15,13 @@ typedef struct pkg_metadata { const char *content_id; const char *content_name; const char *icon_url; + uint32_t slot; + uint32_t is_playgo_enabled; } pkg_metadata_t; +_Static_assert(sizeof(pkg_metadata_t) == 0x38, + "sceAppInstUtil metadata ABI mismatch"); + typedef struct pkg_info { char content_id[48]; int type; @@ -23,12 +29,15 @@ typedef struct pkg_info { } pkg_info_t; typedef struct playgo_info { - char lang[8][30]; - char scenario_ids[3][64]; - char content_ids[64]; + char languages[30][8]; + char scenario_ids[64][3]; + char content_ids[64][48]; long unknown[810]; } playgo_info_t; +_Static_assert(sizeof(playgo_info_t) == 0x2700, + "sceAppInstUtil PlayGoInfo ABI mismatch"); + int sceAppInstUtilInitialize(void); int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *, playgo_info_t *); @@ -68,7 +77,9 @@ pkg_installer_install(const char *path) { .playgo_scenario_id = "", .content_id = "", .content_name = "", - .icon_url = "" + .icon_url = "", + .slot = 0, + .is_playgo_enabled = 0 }; pkg_info_t pkg_info = {0}; playgo_info_t playgo_info = {0}; diff --git a/src/task.c b/src/task.c index e61c548..84b639c 100644 --- a/src/task.c +++ b/src/task.c @@ -82,7 +82,7 @@ remove_finished_tasks_locked(void) { while(*link) { file_task_t *task = *link; - if(task_is_active(task)) { + if(task_is_active(task) || task->active_streams) { link = &task->next; continue; } diff --git a/src/upload.c b/src/upload.c index 89828c7..2c5743c 100644 --- a/src/upload.c +++ b/src/upload.c @@ -28,8 +28,22 @@ typedef struct upload_context { int failed; int error; int task_done; + const char *stage; + char error_message[256]; } upload_context_t; +static const char * +upload_error_message(upload_context_t *ctx) { + if(!ctx->error_message[0]) { + snprintf(ctx->error_message, sizeof(ctx->error_message), "%s failed%s%s: %s", + ctx->stage ? ctx->stage : "upload", + ctx->target[0] ? " for " : "", + ctx->target[0] ? ctx->target : "", + strerror(ctx->error ? ctx->error : EIO)); + } + return ctx->error_message; +} + static int upload_flush(upload_context_t *ctx) { size_t written = 0; @@ -81,11 +95,29 @@ finish_upload_task(file_task_t *task, task_state_t state, const char *current, if(error) { snprintf(task->error, sizeof(task->error), "%s", error); } + if(state == TASK_FAILED) { + snprintf(task->error_code, sizeof(task->error_code), "upload_failed"); + snprintf(task->error_arg, sizeof(task->error_arg), "%s", + current ? current : task->src); + } task->updated_at = completed_at; } pthread_mutex_unlock(&g_tasks_lock); } +static void +finish_upload_context_error(upload_context_t *ctx) { + if(!ctx->task || ctx->task_done) { + return; + } + upload_error_message(ctx); + finish_upload_task(ctx->task, + ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED, + ctx->target[0] ? ctx->target : ctx->task->current, + ctx->error == ECANCELED ? "canceled" : ctx->error_message); + ctx->task_done = 1; +} + static file_task_t * upload_task_from_conn(struct MHD_Connection *conn) { char *idstr = request_value(conn, "X-WFM-Task-ID", "task_id"); @@ -300,12 +332,18 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) { return -1; } ctx->fd = -1; + ctx->stage = "preparing upload"; *upload_ctx = ctx; if(size_text) { ctx->expected = strtoull(size_text, NULL, 10); } ctx->task = task; + if(task) { + pthread_mutex_lock(&g_tasks_lock); + task->active_streams++; + pthread_mutex_unlock(&g_tasks_lock); + } if(task && task_cancel_requested(task)) { ctx->failed = 1; ctx->error = ECANCELED; @@ -316,17 +354,20 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) { ctx->error = EBUSY; goto fail; } + ctx->stage = "validating target path"; if(!base || !rel || path_join_relative(ctx->target, sizeof(ctx->target), base, rel)) { ctx->failed = 1; ctx->error = errno ? errno : EINVAL; goto fail; } + ctx->stage = "creating target folders"; if(ensure_parent_dirs(base, rel)) { ctx->failed = 1; ctx->error = errno ? errno : EACCES; goto fail; } + ctx->stage = "checking target path"; if(!lstat(ctx->target, &st)) { if(S_ISDIR(st.st_mode) || !overwrite || strcmp(overwrite, "1")) { ctx->failed = 1; @@ -338,19 +379,26 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) { ctx->error = errno; goto fail; } + ctx->stage = "checking target permissions"; if(check_target_writable(ctx->target, &checked_dirs, &checked_dir_count, error, sizeof(error), code, sizeof(code), arg, sizeof(arg))) { + snprintf(ctx->error_message, sizeof(ctx->error_message), "%s", + error[0] ? error : "target is not writable"); ctx->failed = 1; ctx->error = errno ? errno : EACCES; goto fail; } + ctx->stage = "checking target space"; if(check_target_space(ctx->target, ctx->expected, error, sizeof(error), code, sizeof(code), arg, sizeof(arg))) { + snprintf(ctx->error_message, sizeof(ctx->error_message), "%s", + error[0] ? error : "not enough target space"); ctx->failed = 1; ctx->error = errno ? errno : ENOSPC; goto fail; } + ctx->stage = "creating temporary path"; n = snprintf(ctx->temp, sizeof(ctx->temp), "%s.wfm-upload-%ld-%lld.tmp", ctx->target, (long)getpid(), (long long)time(NULL)); if(n < 0 || (size_t)n >= sizeof(ctx->temp)) { @@ -358,12 +406,14 @@ filemgr_upload_begin(struct MHD_Connection *conn, void **upload_ctx) { ctx->error = ENAMETOOLONG; goto fail; } + ctx->stage = "opening temporary file"; ctx->fd = open(ctx->temp, O_WRONLY | O_CREAT | O_TRUNC, 0600); if(ctx->fd < 0) { ctx->failed = 1; ctx->error = errno; goto fail; } + ctx->stage = "allocating upload buffer"; if(!(ctx->buffer = malloc(UPLOAD_BUFFER_SIZE))) { ctx->failed = 1; ctx->error = ENOMEM; @@ -374,13 +424,7 @@ fail: free_paths(checked_dirs, checked_dir_count); free(base); free(rel); free(overwrite); free(size_text); if(ctx->failed) { - if(ctx->task) { - finish_upload_task(ctx->task, - ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED, - ctx->target[0] ? ctx->target : ctx->task->current, - ctx->error == ECANCELED ? "canceled" : strerror(ctx->error ? ctx->error : EIO)); - ctx->task_done = 1; - } + finish_upload_context_error(ctx); if(ctx->fd >= 0) { close(ctx->fd); ctx->fd = -1; @@ -391,6 +435,7 @@ fail: errno = ctx->error ? ctx->error : EIO; return -1; } + ctx->stage = "writing file"; return 0; } @@ -404,6 +449,7 @@ filemgr_upload_data(void *upload_ctx, const char *data, size_t size) { if(ctx->task && task_cancel_requested(ctx->task)) { ctx->failed = 1; ctx->error = ECANCELED; + finish_upload_context_error(ctx); return -1; } while(size) { @@ -415,6 +461,7 @@ filemgr_upload_data(void *upload_ctx, const char *data, size_t size) { data += take; size -= take; if(ctx->buffered == UPLOAD_BUFFER_SIZE && upload_flush(ctx)) { + finish_upload_context_error(ctx); return -1; } } @@ -430,46 +477,55 @@ filemgr_upload_finish(struct MHD_Connection *conn, void *upload_ctx) { return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path"); } if(ctx->failed) { - if(ctx->task && !ctx->task_done) { - finish_upload_task(ctx->task, - ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED, - ctx->target[0] ? ctx->target : ctx->task->current, - ctx->error == ECANCELED ? "canceled" : strerror(ctx->error ? ctx->error : EIO)); - ctx->task_done = 1; - } + finish_upload_context_error(ctx); errno = ctx->error ? ctx->error : EIO; - return send_json_error(conn, errno == EEXIST ? MHD_HTTP_CONFLICT : - errno == EBUSY ? MHD_HTTP_CONFLICT : - errno == ECANCELED ? MHD_HTTP_CONFLICT : - errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE : - MHD_HTTP_INTERNAL_SERVER_ERROR, NULL); + return send_json_error_detail(conn, + errno == EEXIST ? MHD_HTTP_CONFLICT : + errno == EBUSY ? MHD_HTTP_CONFLICT : + errno == ECANCELED ? MHD_HTTP_CONFLICT : + errno == ENOSPC ? MHD_HTTP_INSUFFICIENT_STORAGE : + MHD_HTTP_INTERNAL_SERVER_ERROR, + ctx->error == ECANCELED ? "canceled" : ctx->error_message, + ctx->error == ECANCELED ? NULL : "upload_failed", + ctx->target[0] ? ctx->target : NULL); } + ctx->stage = "writing file"; if(ctx->buffered && upload_flush(ctx)) { ctx->error = ctx->error ? ctx->error : EIO; goto done; } + ctx->stage = "verifying uploaded size"; if(ctx->expected && ctx->written != ctx->expected) { ctx->error = EIO; goto done; } + ctx->stage = "setting file permissions"; if(fchmod_0777(ctx->fd)) { ctx->error = errno; goto done; } + ctx->stage = "syncing file data"; if(fsync(ctx->fd)) { ctx->error = errno; goto done; } + ctx->stage = "closing temporary file"; if(close(ctx->fd)) { ctx->fd = -1; ctx->error = errno; goto done; } ctx->fd = -1; + ctx->stage = "moving temporary file into place"; if(rename(ctx->temp, ctx->target)) { ctx->error = errno; goto done; } + if(ctx->task) { + pthread_mutex_lock(&g_tasks_lock); + ctx->task->upload_completed++; + pthread_mutex_unlock(&g_tasks_lock); + } ret = 0; done: @@ -478,19 +534,18 @@ done: ctx->fd = -1; } if(ret) { + upload_error_message(ctx); if(ctx->temp[0]) { unlink(ctx->temp); } - if(ctx->task && !ctx->task_done) { - finish_upload_task(ctx->task, - ctx->error == ECANCELED ? TASK_CANCELED : TASK_FAILED, - ctx->target[0] ? ctx->target : ctx->task->current, - ctx->error == ECANCELED ? "canceled" : strerror(ctx->error ? ctx->error : EIO)); - ctx->task_done = 1; - } + finish_upload_context_error(ctx); errno = ctx->error ? ctx->error : EIO; - return send_json_error(conn, errno == ECANCELED ? MHD_HTTP_CONFLICT : - MHD_HTTP_INTERNAL_SERVER_ERROR, NULL); + return send_json_error_detail(conn, + errno == ECANCELED ? MHD_HTTP_CONFLICT : + MHD_HTTP_INTERNAL_SERVER_ERROR, + ctx->error == ECANCELED ? "canceled" : ctx->error_message, + ctx->error == ECANCELED ? NULL : "upload_failed", + ctx->target[0] ? ctx->target : NULL); } return send_json_ok(conn); } @@ -516,5 +571,12 @@ filemgr_upload_free(void *upload_ctx) { } } free(ctx->buffer); + if(ctx->task) { + pthread_mutex_lock(&g_tasks_lock); + if(ctx->task->active_streams) { + ctx->task->active_streams--; + } + pthread_mutex_unlock(&g_tasks_lock); + } free(ctx); }