Compare commits

...
1 Commits
Author SHA1 Message Date
owendswang 1a24160b8b Release v0.9 2026-07-19 00:20:15 +08:00
8 changed files with 349 additions and 74 deletions

No files matched your search

+1 -1
View File
@@ -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
+19 -2
View File
@@ -4,6 +4,23 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PS5 Web File Manager</title>
<style>
html, body { margin: 0; min-height: 100%; background: #111316; color: #edf0f2; }
.init-loading {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
background: #111316;
color: #aab4be;
font: 18px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
</style>
<link rel="stylesheet" href="/main.css">
</head>
<body>
@@ -26,7 +43,7 @@
<button id="deleteBtn" class="danger" data-i18n="delete"></button>
<button id="pasteBtn" class="paste-action" hidden>
<span id="pasteVerb" data-i18n="paste"></span>
<span id="pasteName" class="paste-name"></span>
<span id="pasteName" class="paste-name"></span><span id="pasteCount" class="paste-count"></span>
<span id="pasteTargetText" data-i18n="pasteToCurrent"></span>
</button>
<button id="clearClipboardBtn" data-i18n="cancel" hidden></button>
@@ -93,7 +110,7 @@
</div>
</div>
<div id="initLoading" class="init-loading" aria-hidden="true"></div>
<div id="initLoading" class="init-loading" aria-hidden="true">Loading...</div>
<script src="/main.js"></script>
</body>
+3 -1
View File
@@ -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",
+3 -1
View File
@@ -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: "没有源路径",
+7
View File
@@ -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;
+179 -50
View File
@@ -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));
+135 -17
View File
@@ -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")) {
+2 -2
View File
@@ -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);