From 1a24160b8be7d506bf4e99f5756cbf519f054621 Mon Sep 17 00:00:00 2001 From: owendswang Date: Sun, 19 Jul 2026 00:20:15 +0800 Subject: [PATCH] Release v0.9 --- Makefile | 2 +- assets/index.html | 21 ++++- assets/lang-en.js | 4 +- assets/lang-zh.js | 4 +- assets/main.css | 7 ++ assets/main.js | 229 ++++++++++++++++++++++++++++++++++++---------- src/filemgr.c | 152 ++++++++++++++++++++++++++---- src/websrv.c | 4 +- 8 files changed, 349 insertions(+), 74 deletions(-) diff --git a/Makefile b/Makefile index 2e43291..c37c6e4 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),) endif endif -VERSION_TAG := v0.8b +VERSION_TAG := v0.9 TITLE_ID := FMGR88888 PYTHON ?= python3 STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip diff --git a/assets/index.html b/assets/index.html index 887670f..c321f0c 100644 --- a/assets/index.html +++ b/assets/index.html @@ -4,6 +4,23 @@ PS5 Web File Manager + @@ -26,7 +43,7 @@ @@ -93,7 +110,7 @@ - + diff --git a/assets/lang-en.js b/assets/lang-en.js index c67aa52..52ea39f 100644 --- a/assets/lang-en.js +++ b/assets/lang-en.js @@ -49,6 +49,7 @@ window.WFM_LANG = { countItems: "{count} items", totalItems: "{count} items", readDir: "Reading folder...", + processing: "Processing...", actionBusy: "{label}...", taskCreated: "{label} task created", actionDone: "{label} done", @@ -62,7 +63,7 @@ window.WFM_LANG = { overwriteFiles: "Files with the same name will be overwritten: {names}", mergeDirs: "Folders with the same name will be merged. Internal files with the same name will be overwritten: {names}", continueConfirm: "Continue?", - preparingPaste: "{label} preparing: calculating size and checking target space...", + preparingPaste: "Preparing: calculating size and checking target space...", preparingStatus: "{label} preparing...", cancelPrepare: "Preparation canceled", cancelTaskConfirm: "Cancel current {label} task?", @@ -114,6 +115,7 @@ window.WFM_LANG = { err_text_invalid_utf8: "The file is not valid UTF-8 text", err_text_file_changed: "The file changed after it was opened. Close and reopen it", err_text_file_not_writable: "The text file is not writable", + err_request_body_too_large: "Too many selected items. Select fewer items and try again", err_unknown_api: "Unknown API", err_out_of_memory: "Out of memory", err_no_source_paths: "No source paths", diff --git a/assets/lang-zh.js b/assets/lang-zh.js index 51a1f35..9dfefd6 100644 --- a/assets/lang-zh.js +++ b/assets/lang-zh.js @@ -49,6 +49,7 @@ window.WFM_LANG = { countItems: "{count} 项", totalItems: "共 {count} 项", readDir: "读取目录中...", + processing: "处理中...", actionBusy: "{label}...", taskCreated: "{label}任务已创建", actionDone: "{label}完成", @@ -62,7 +63,7 @@ window.WFM_LANG = { overwriteFiles: "同名文件将被覆盖: {names}", mergeDirs: "同名目录将被合并,内部同名文件会被覆盖: {names}", continueConfirm: "继续?", - preparingPaste: "{label}准备中:正在计算大小并检查目标空间...", + preparingPaste: "准备中:正在计算大小并检查目标空间...", preparingStatus: "{label}准备中...", cancelPrepare: "已取消准备操作", cancelTaskConfirm: "取消当前{label}任务?", @@ -114,6 +115,7 @@ window.WFM_LANG = { err_text_invalid_utf8: "文件不是有效的 UTF-8 文本", err_text_file_changed: "文件在打开后已发生变化,请关闭后重新打开", err_text_file_not_writable: "文本文件不可写", + err_request_body_too_large: "选择的项目过多,请减少选择后重试", err_unknown_api: "未知接口", err_out_of_memory: "内存不足", err_no_source_paths: "没有源路径", diff --git a/assets/main.css b/assets/main.css index b607809..671442a 100644 --- a/assets/main.css +++ b/assets/main.css @@ -275,17 +275,24 @@ button.primary:hover { } .paste-action > * { + flex: 0 0 auto; margin-right: 8px; } .paste-name { display: inline-block; + flex: 1 1 auto; + min-width: 0; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.paste-count { + white-space: nowrap; +} + .content { position: relative; flex: 1 1 auto; diff --git a/assets/main.js b/assets/main.js index 2d906c4..b652a98 100644 --- a/assets/main.js +++ b/assets/main.js @@ -6,6 +6,7 @@ let trackedTask = null; let busy = false; let clipboard = null; let pendingOverlayText = ""; +let pendingOverlayLabel = ""; let pendingAbortController = null; let taskRefreshPath = "/"; let hoverPaused = false; @@ -13,6 +14,7 @@ let hoverResumeTimer = 0; let hoveredRow = null; let loadingPath = null; let directoryLoadingTimer = 0; +let directoryLoadingStartedAt = 0; let taskOverlayTimer = 0; let lastCompletionId = null; let taskPollFailedAlertShown = false; @@ -22,13 +24,15 @@ let textEditorOriginal = ""; let textEditorBusy = false; let L = {}; -const APP_VERSION = "v0.8b"; +const APP_VERSION = "v0.9"; const LAST_PATH_KEY = "ps5-web-file-mgr:last-path"; const LOADING_DISPLAY_DELAY = 250; +const SELECT_ALL_LOADING_THRESHOLD = 1000; const filesEl = document.getElementById("files"); const contentEl = document.getElementById("content"); const contentLoadingEl = document.getElementById("contentLoading"); +const contentLoadingTextEl = contentLoadingEl.querySelector(".content-loading-text"); const emptyEl = document.getElementById("empty"); const pathEl = document.getElementById("path"); const spaceInfoEl = document.getElementById("spaceInfo"); @@ -40,6 +44,7 @@ const selectAllEl = document.getElementById("selectAll"); const pasteBtn = document.getElementById("pasteBtn"); const pasteVerbEl = document.getElementById("pasteVerb"); const pasteNameEl = document.getElementById("pasteName"); +const pasteCountEl = document.getElementById("pasteCount"); const pasteTargetTextEl = document.getElementById("pasteTargetText"); const clearClipboardBtn = document.getElementById("clearClipboardBtn"); const initLoadingEl = document.getElementById("initLoading"); @@ -112,6 +117,55 @@ function applyStaticText() { if (initLoadingEl) initLoadingEl.hidden = true; } +function nextPaint() { + return new Promise(resolve => { + if (window.requestAnimationFrame) { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + } + else setTimeout(resolve, 0); + }); +} + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function showContentLoading(text) { + contentLoadingTextEl.textContent = text || t("readDir"); + contentLoadingEl.style.top = contentEl.scrollTop + "px"; + contentLoadingEl.hidden = false; + contentEl.classList.add("loading"); + updateButtons(); +} + +function startContentLoadingTimer() { + if (!contentLoadingEl.hidden || directoryLoadingTimer) return; + directoryLoadingStartedAt = Date.now(); + directoryLoadingTimer = setTimeout(() => { + directoryLoadingTimer = 0; + showContentLoading(); + }, LOADING_DISPLAY_DELAY); +} + +async function showContentLoadingAfterDelay() { + if (!contentLoadingEl.hidden) return; + const elapsed = directoryLoadingStartedAt ? Date.now() - directoryLoadingStartedAt : 0; + const remaining = LOADING_DISPLAY_DELAY - elapsed; + if (remaining > 0) await delay(remaining); + clearTimeout(directoryLoadingTimer); + directoryLoadingTimer = 0; + showContentLoading(); +} + +function hideContentLoading() { + clearTimeout(directoryLoadingTimer); + directoryLoadingTimer = 0; + directoryLoadingStartedAt = 0; + contentEl.classList.remove("loading"); + contentLoadingEl.hidden = true; + updateButtons(); +} + async function request(path, params, options) { const qs = new URLSearchParams(params || {}); const fetchOptions = Object.assign({ method: "POST" }, options || {}); @@ -129,6 +183,17 @@ async function api(path, params, options) { return data; } +async function apiForm(path, params, options) { + const fetchOptions = Object.assign({ + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams(params || {}).toString() + }, options || {}); + const data = await (await request(path, null, fetchOptions)).json(); + if (!data.ok) throw new Error(backendErrorText(data.error_code, data.error_arg, data.error)); + return data; +} + function decodeFsText(text) { text = String(text || ""); let needsDecode = false; @@ -284,10 +349,12 @@ function isTerminalTask(task) { return ["done", "failed", "canceled"].includes(task.state); } -function trackTask(id, fromClipboard) { +function trackTask(id, op, fromClipboard, clearClipboardOnDone) { trackedTask = { id, + op, fromClipboard: Boolean(fromClipboard), + clearClipboardOnDone: Boolean(clearClipboardOnDone), cancelRequested: false, state: "queued" }; @@ -327,7 +394,8 @@ function handleTerminalTask(task) { return; } if (task.state === "done") { - if (trackedTask && trackedTask.id === task.id && trackedTask.fromClipboard) clearClipboardAfterPaste(); + if (trackedTask && trackedTask.id === task.id && + (trackedTask.fromClipboard || trackedTask.clearClipboardOnDone)) clearClipboardAfterPaste(); clearTrackedTask(); setStatus(t("actionDone", { label: opLabel(task.op) })); } @@ -342,9 +410,13 @@ function setBusy(value) { updateButtons(); } -function showTaskOverlay() { +function showTaskOverlay(immediate) { setBusy(true); if (!overlayEl.hidden || taskOverlayTimer) return; + if (immediate) { + overlayEl.hidden = false; + return; + } taskOverlayTimer = setTimeout(() => { taskOverlayTimer = 0; overlayEl.hidden = false; @@ -543,7 +615,6 @@ async function actionNewText() { setBusy(true); setStatus(t("creatingText")); const data = await api("/api/text/create", { path: dir, name }); - await load(dir, false, true); setBusy(false); showTextEditor(item, "", data.version); } catch (err) { @@ -576,6 +647,12 @@ function itemTitle(items) { return items.length === 1 ? displayName(items[0]) : t("selectedItems", { name: displayName(items[0]), count: items.length }); } +function itemCountSuffix(items) { + if (items.length <= 1) return ""; + const text = t("selectedItems", { name: "", count: items.length }); + return text.replace(/^\s+/, ""); +} + function pathName(path) { const clean = String(path || "").replace(/\/+$/, ""); const pos = clean.lastIndexOf("/"); @@ -607,6 +684,7 @@ function renderClipboard() { if (!hasClipboard) { pasteVerbEl.textContent = t("paste"); pasteNameEl.textContent = ""; + pasteCountEl.textContent = ""; pasteBtn.title = ""; pasteBtn.disabled = true; clearClipboardBtn.disabled = true; @@ -615,7 +693,8 @@ function renderClipboard() { const title = clipboardTitle(); pasteVerbEl.textContent = clipboard.op === "move" ? t("move") : t("paste"); - pasteNameEl.textContent = title; + pasteNameEl.textContent = clipboard.items.length === 1 ? title : displayName(clipboard.items[0]); + pasteCountEl.textContent = itemCountSuffix(clipboard.items); pasteBtn.title = t("pasteTitle", { label: clipboard.op === "move" ? t("move") : t("paste"), name: title, path: cwd }); pasteBtn.disabled = locked; clearClipboardBtn.disabled = locked; @@ -704,7 +783,30 @@ function findRowFromTarget(target) { function togglePath(path, checked) { if (checked) selected.add(path); else selected.delete(path); - render(); + const row = filesEl.querySelector("tr[data-path=\"" + cssEscape(path) + "\"]"); + if (row) { + if (checked) row.classList.add("selected"); + else row.classList.remove("selected"); + const checkbox = row.querySelector("input[type=\"checkbox\"]"); + if (checkbox) checkbox.checked = checked; + } + updateButtons(); +} + +function clearSelection() { + if (selected.size === 0) { + updateButtons(); + return; + } + selected.forEach(path => { + const row = filesEl.querySelector("tr[data-path=\"" + cssEscape(path) + "\"]"); + if (!row) return; + row.classList.remove("selected"); + const checkbox = row.querySelector("input[type=\"checkbox\"]"); + if (checkbox) checkbox.checked = false; + }); + selected.clear(); + updateButtons(); } function render() { @@ -801,15 +903,14 @@ async function load(path, scrollTop, force) { const shouldScrollTop = scrollTop !== false; if ((busy && !force) || loadingPath) return; loadingPath = path; - directoryLoadingTimer = setTimeout(() => { - contentLoadingEl.style.top = contentEl.scrollTop + "px"; - contentLoadingEl.hidden = false; - contentEl.classList.add("loading"); - updateButtons(); - }, LOADING_DISPLAY_DELAY); + startContentLoadingTimer(); try { setStatus(t("readDir")); const data = await api("/api/list", { path }); + if (contentLoadingEl.hidden && data.entries && data.entries.length > 80) { + await showContentLoadingAfterDelay(); + } + if (!contentLoadingEl.hidden) await nextPaint(); cwd = data.path; pathEl.textContent = displayPath(cwd); savePath(cwd); @@ -827,24 +928,20 @@ async function load(path, scrollTop, force) { } catch (err) { setStatus(err.message); } finally { - clearTimeout(directoryLoadingTimer); - contentEl.classList.remove("loading"); - contentLoadingEl.hidden = true; + hideContentLoading(); loadingPath = null; - updateButtons(); } } -async function runAction(label, fn) { +async function runAction(label, fn, options) { try { taskRefreshPath = cwd; setBusy(true); setStatus(t("actionBusy", { label })); const data = await fn(); setStatus(t("taskCreated", { label })); - trackTask(data.task_id, false); - selected.clear(); - render(); + trackTask(data.task_id, "delete", false, options && options.clearClipboardOnDone); + clearSelection(); await pollTasks(); } catch (err) { setBusy(false); @@ -858,8 +955,7 @@ async function runImmediateAction(label, fn) { setBusy(true); setStatus(t("actionBusy", { label })); await fn(); - selected.clear(); - render(); + clearSelection(); await load(refreshPath, false, true); setStatus(t("actionDone", { label })); } catch (err) { @@ -874,8 +970,7 @@ function setClipboard(op) { const items = selectedEntries().map(item => ({ path: item.path, name: item.name, type: item.type })); if (items.length === 0) return; clipboard = { op, items }; - selected.clear(); - render(); + renderClipboard(); setStatus(t("selectedForPaste", { name: itemTitle(items), verb: op === "move" ? t("move") : t("paste") })); } @@ -886,8 +981,19 @@ function clearClipboard() { setStatus(t("clipboardCleared")); } +function pathsTouchClipboard(paths) { + if (!clipboard || !clipboard.items.length) return false; + for (const path of paths) { + for (const item of clipboard.items) { + if (pathIsSameOrChild(path, item.path)) return true; + } + } + return false; +} + function validatePasteTarget() { const byName = new Map(entries.map(item => [item.name, item])); + const sameTargets = []; const sameFiles = []; const sameDirs = []; @@ -895,10 +1001,8 @@ function validatePasteTarget() { const targetPath = pathJoin(cwd, item.name); const existing = byName.get(item.name); if (item.path === targetPath) { - return { - ok: false, - message: t("sameSourceTarget", { label: clipboard.op === "move" ? t("move") : t("copy"), name: item.name }) - }; + sameTargets.push(item.name); + continue; } if (item.type === "d" && pathIsSameOrChild(item.path, targetPath)) { return { @@ -922,6 +1026,15 @@ function validatePasteTarget() { if (item.type === "d") sameDirs.push(item.name); else sameFiles.push(item.name); } + if (sameTargets.length) { + return { + ok: false, + message: t("sameSourceTarget", { + label: clipboard.op === "move" ? t("move") : t("copy"), + name: conflictText(sameTargets) + }) + }; + } return { ok: true, sameFiles, sameDirs }; } @@ -932,7 +1045,7 @@ function conflictText(names) { return names.length > 4 ? t("selectedItems", { name: shown, count: names.length }) : shown; } -function renderPendingOverlay(text) { +function renderPendingOverlay(text, label) { tasksEl.innerHTML = ""; const div = document.createElement("div"); div.className = "task running"; @@ -941,7 +1054,7 @@ function renderPendingOverlay(text) { head.className = "task-head has-amount"; const name = document.createElement("div"); name.className = "task-name"; - name.textContent = t("preparingTask"); + name.textContent = label || t("preparingTask"); const amount = document.createElement("div"); amount.className = "task-amount"; amount.textContent = t("pleaseWait"); @@ -965,6 +1078,7 @@ function renderPendingOverlay(text) { if (pendingAbortController) pendingAbortController.abort(); pendingAbortController = null; pendingOverlayText = ""; + pendingOverlayLabel = ""; hideTaskOverlay(); setStatus(t("cancelPrepare")); setBusy(false); @@ -974,10 +1088,11 @@ function renderPendingOverlay(text) { tasksEl.appendChild(div); } -function showPendingOverlay(text) { +function showPendingOverlay(text, label) { pendingOverlayText = text; - renderPendingOverlay(text); - showTaskOverlay(); + pendingOverlayLabel = label || ""; + renderPendingOverlay(text, pendingOverlayLabel); + showTaskOverlay(true); } function renderTaskPath(element, path) { @@ -1024,28 +1139,31 @@ async function actionPaste() { if (!confirm(lines.join("\n") + "\n\n" + t("continueConfirm"))) return; } - const payload = { - paths: clipboard.items.map(item => item.path).join("\n"), - dst: cwd, - overwrite: hasFileConflicts || hasDirConflicts ? "1" : "0" - }; try { pendingAbortController = new AbortController(); taskRefreshPath = cwd; - showPendingOverlay(t("preparingPaste", { label })); + showPendingOverlay(t("preparingPaste", { label }), label); + setBusy(true); setStatus(t("preparingStatus", { label })); - const data = await api("/api/" + op, payload, { signal: pendingAbortController.signal }); + await nextPaint(); + const payload = { + paths: clipboard.items.map(item => item.path).join("\n"), + dst: cwd, + overwrite: hasFileConflicts || hasDirConflicts ? "1" : "0" + }; + const data = await apiForm("/api/" + op, payload, { signal: pendingAbortController.signal }); pendingAbortController = null; pendingOverlayText = ""; + pendingOverlayLabel = ""; setStatus(t("taskCreated", { label })); - trackTask(data.task_id, true); - selected.clear(); - render(); + trackTask(data.task_id, op, true); + clearSelection(); await pollTasks(); } catch (err) { const aborted = err && err.name === "AbortError"; pendingAbortController = null; pendingOverlayText = ""; + pendingOverlayLabel = ""; hideTaskOverlay(); if (aborted) setStatus(t("cancelPrepare")); else showActionFailed(label, err.message); @@ -1057,7 +1175,7 @@ async function actionPaste() { function renderTasks(tasks) { const active = tasks.find(task => task.state === "queued" || task.state === "running"); if (!active && pendingOverlayText) { - renderPendingOverlay(pendingOverlayText); + renderPendingOverlay(pendingOverlayText, pendingOverlayLabel); showTaskOverlay(); return; } @@ -1164,6 +1282,7 @@ async function pollTasks() { } const wasBusy = busy; renderTasks(tasks); + if (wasBusy && !busy) startContentLoadingTimer(); let shouldRefresh = false; let sawTrackedTask = false; for (const task of tasks) { @@ -1179,7 +1298,8 @@ async function pollTasks() { } } if (trackedTask && wasBusy && !busy && !sawTrackedTask) { - if (trackedTask.fromClipboard && !trackedTask.cancelRequested) clearClipboardAfterPaste(); + if ((trackedTask.fromClipboard || trackedTask.clearClipboardOnDone) && + !trackedTask.cancelRequested) clearClipboardAfterPaste(); clearTrackedTask(); shouldRefresh = true; } @@ -1218,7 +1338,9 @@ function actionDelete() { const confirmKey = items.some(item => item.type === "d") ? "deleteConfirmRecursive" : "deleteConfirm"; if (!confirm(t(confirmKey, { name: target }))) return; - runAction(t("delete"), () => api("/api/delete", { paths: paths.join("\n") })); + runAction(t("delete"), () => apiForm("/api/delete", { paths: paths.join("\n") }), { + clearClipboardOnDone: pathsTouchClipboard(paths) + }); } function actionExit() { @@ -1281,15 +1403,22 @@ filesEl.addEventListener("click", event => { displayName: decodeFsText(row.dataset.name) }; focusPath(item.path); - if (item.type === "parent" || item.type === "d") load(item.path); + if (item.type === "parent" || item.type === "d") setTimeout(() => load(item.path), 0); else if (isPreviewableImage(item)) openImagePreview(item); else if (isEditableText(item)) openTextEditor(item); else togglePath(item.path, !selected.has(item.path)); }); -selectAllEl.addEventListener("change", () => { +selectAllEl.addEventListener("change", async () => { + const checked = selectAllEl.checked; + const useLoading = entries.length >= SELECT_ALL_LOADING_THRESHOLD; + if (useLoading) { + showContentLoading(t("processing")); + await nextPaint(); + } selected.clear(); - if (selectAllEl.checked) entries.forEach(item => selected.add(item.path)); + if (checked) entries.forEach(item => selected.add(item.path)); render(); + if (useLoading) hideContentLoading(); }); contentEl.addEventListener("mousemove", event => { hoverRow(findRowFromTarget(event.target)); diff --git a/src/filemgr.c b/src/filemgr.c index c1d381c..08d9a32 100644 --- a/src/filemgr.c +++ b/src/filemgr.c @@ -227,6 +227,71 @@ query_value(struct MHD_Connection *conn, const char *key) { return value; } +static int +hex_value(char c) { + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static char * +form_decode(const char *src, size_t len) { + char *out = malloc(len + 1); + size_t i; + size_t j = 0; + + if(!out) { + return NULL; + } + for(i = 0; i < len; i++) { + if(src[i] == '+') { + out[j++] = ' '; + } else if(src[i] == '%' && i + 2 < len) { + int hi = hex_value(src[i + 1]); + int lo = hex_value(src[i + 2]); + if(hi >= 0 && lo >= 0) { + out[j++] = (char)((hi << 4) | lo); + i += 2; + } else { + out[j++] = src[i]; + } + } else { + out[j++] = src[i]; + } + } + out[j] = 0; + return out; +} + +static char * +body_form_value(const char *body, size_t body_size, const char *key) { + size_t key_len = strlen(key); + size_t pos = 0; + + while(body && pos < body_size) { + size_t start = pos; + size_t end; + size_t eq; + + while(pos < body_size && body[pos] != '&') { + pos++; + } + end = pos; + if(pos < body_size && body[pos] == '&') { + pos++; + } + eq = start; + while(eq < end && body[eq] != '=') { + eq++; + } + if(eq - start == key_len && !strncmp(body + start, key, key_len)) { + return form_decode(body + eq + (eq < end), end - eq - (eq < end)); + } + } + return NULL; +} + static void free_paths(char **paths, size_t count) { size_t i; @@ -2465,14 +2530,60 @@ probe_file_writable(const char *path) { } static int -check_target_writable(const char *target, char *error, size_t error_size, +path_seen(char **paths, size_t count, const char *path) { + size_t i; + + for(i = 0; i < count; i++) { + if(!strcmp(paths[i], path)) { + return 1; + } + } + return 0; +} + +static int +remember_path(char ***paths, size_t *count, const char *path) { + char **tmp; + + if(path_seen(*paths, *count, path)) { + return 0; + } + tmp = realloc(*paths, sizeof(char *) * (*count + 1)); + if(!tmp) { + errno = ENOMEM; + return -1; + } + *paths = tmp; + (*paths)[*count] = strdup(path); + if(!(*paths)[*count]) { + errno = ENOMEM; + return -1; + } + (*count)++; + return 0; +} + +static int +probe_dir_once(const char *path, char ***checked, size_t *checked_count) { + if(path_seen(*checked, *checked_count, path)) { + return 0; + } + if(probe_dir_writable(path)) { + return -1; + } + return remember_path(checked, checked_count, path); +} + +static int +check_target_writable(const char *target, char ***checked_dirs, + size_t *checked_dir_count, char *error, size_t error_size, char *code, size_t code_size, char *arg, size_t arg_size) { struct stat st; char parent[PATH_MAX]; if(!stat(target, &st)) { if(S_ISDIR(st.st_mode)) { - if(probe_dir_writable(target)) { + if(probe_dir_once(target, checked_dirs, checked_dir_count)) { set_permission_error_detail(error, error_size, code, code_size, arg, arg_size, "target_dir_not_writable", "target directory is not writable", target); @@ -2502,7 +2613,7 @@ check_parent: "target_check_failed", "cannot check target path", target); return -1; } - if(probe_dir_writable(parent)) { + if(probe_dir_once(parent, checked_dirs, checked_dir_count)) { set_permission_error_detail(error, error_size, code, code_size, arg, arg_size, "target_parent_not_writable", "current directory is not writable", parent); @@ -2514,23 +2625,30 @@ check_parent: static int check_task_targets_writable(file_task_t *task, char *error, size_t error_size, char *code, size_t code_size, char *arg, size_t arg_size) { + char **checked_dirs = NULL; + size_t checked_dir_count = 0; size_t i; for(i = 0; i < task->src_count; i++) { char target[PATH_MAX]; if(task_cancel_requested(task)) { + free_paths(checked_dirs, checked_dir_count); return -1; } if(task_target_path(task, task->srcs[i], target, sizeof(target))) { snprintf(error, error_size, "target path is too long"); snprintf(code, code_size, "target_path_too_long"); + free_paths(checked_dirs, checked_dir_count); return -1; } - if(check_target_writable(target, error, error_size, code, code_size, arg, arg_size)) { + if(check_target_writable(target, &checked_dirs, &checked_dir_count, + error, error_size, code, code_size, arg, arg_size)) { + free_paths(checked_dirs, checked_dir_count); return -1; } } + free_paths(checked_dirs, checked_dir_count); return 0; } @@ -3084,10 +3202,10 @@ api_exit(struct MHD_Connection *conn) { } static enum MHD_Result -api_copy(struct MHD_Connection *conn) { - char *paths_raw = query_value(conn, "paths"); - char *dst = fs_path_value(query_value(conn, "dst")); - char *overwrite = query_value(conn, "overwrite"); +api_copy(struct MHD_Connection *conn, const char *body, size_t body_size) { + char *paths_raw = body_form_value(body, body_size, "paths"); + char *dst = fs_path_value(body_form_value(body, body_size, "dst")); + char *overwrite = body_form_value(body, body_size, "overwrite"); char **paths = NULL; size_t count = 0; enum MHD_Result ret; @@ -3111,10 +3229,10 @@ api_copy(struct MHD_Connection *conn) { } static enum MHD_Result -api_move(struct MHD_Connection *conn) { - char *paths_raw = query_value(conn, "paths"); - char *dst = fs_path_value(query_value(conn, "dst")); - char *overwrite = query_value(conn, "overwrite"); +api_move(struct MHD_Connection *conn, const char *body, size_t body_size) { + char *paths_raw = body_form_value(body, body_size, "paths"); + char *dst = fs_path_value(body_form_value(body, body_size, "dst")); + char *overwrite = body_form_value(body, body_size, "overwrite"); char **paths = NULL; size_t count = 0; enum MHD_Result ret; @@ -3130,8 +3248,8 @@ api_move(struct MHD_Connection *conn) { } static enum MHD_Result -api_delete(struct MHD_Connection *conn) { - char *paths_raw = query_value(conn, "paths"); +api_delete(struct MHD_Connection *conn, const char *body, size_t body_size) { + char *paths_raw = body_form_value(body, body_size, "paths"); char **paths = NULL; size_t count = 0; enum MHD_Result ret; @@ -3416,9 +3534,9 @@ filemgr_api_request(struct MHD_Connection *conn, const char *url, if(!strcmp(url, "/api/space")) return api_space(conn); if(!strcmp(url, "/api/cancel")) return api_cancel(conn); if(!strcmp(url, "/api/exit")) return api_exit(conn); - if(!strcmp(url, "/api/copy")) return api_copy(conn); - if(!strcmp(url, "/api/move")) return api_move(conn); - if(!strcmp(url, "/api/delete")) return api_delete(conn); + if(!strcmp(url, "/api/copy")) return api_copy(conn, body, body_size); + if(!strcmp(url, "/api/move")) return api_move(conn, body, body_size); + if(!strcmp(url, "/api/delete")) return api_delete(conn, body, body_size); if(!strcmp(url, "/api/rename")) return api_rename(conn); if(!strcmp(url, "/api/mkdir")) return api_mkdir(conn); if(!strcmp(url, "/api/text")) { diff --git a/src/websrv.c b/src/websrv.c index d296797..68c5ded 100644 --- a/src/websrv.c +++ b/src/websrv.c @@ -13,7 +13,7 @@ #include "filemgr.h" #include "websrv.h" -#define REQUEST_BODY_MAX (1024 * 1024) +#define REQUEST_BODY_MAX (4 * 1024 * 1024) typedef struct request_context { char *body; @@ -25,7 +25,7 @@ static enum MHD_Result websrv_body_too_large(struct MHD_Connection *conn) { static const char json[] = "{\"ok\":false,\"error\":\"request body is too large\"," - "\"error_code\":\"text_file_too_large\",\"error_arg\":\"\"}"; + "\"error_code\":\"request_body_too_large\",\"error_arg\":\"\"}"; struct MHD_Response *resp = MHD_create_response_from_buffer(sizeof(json) - 1, (void *)json, MHD_RESPMEM_PERSISTENT);