mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 14:00:24 +02:00
Release v0.3
This commit is contained in:
1 parent
91375e0537
commit
6f46847df2
15 files changed
+849
-27
No files matched your search
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION_TAG := v0.2
|
||||
VERSION_TAG := v0.3
|
||||
TITLE_ID := FMGR88888
|
||||
PYTHON ?= python3
|
||||
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
|
||||
@@ -63,7 +63,7 @@ gen:
|
||||
clean:
|
||||
rm -rf $(BIN) $(LEGACY_BIN) $(LINUX_BIN) gen
|
||||
|
||||
gen/%.c: assets/% gen-asset-module.py gen
|
||||
gen/%.c: assets/% gen-asset-module.py | gen
|
||||
$(PYTHON) gen-asset-module.py --path $* $< > $@
|
||||
|
||||
$(BIN): $(PS5_SRCS) $(GEN_SRCS)
|
||||
|
||||
@@ -10,16 +10,18 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
|
||||
## Screenshots
|
||||
|
||||
<p>
|
||||
<a href="docs/screenshots/20260617_231827.376.jpg" target="_blank"><img src="docs/screenshots/20260617_231827.376.jpg" width="23%" alt="PS5 Web File Manager screenshot 1"></a>
|
||||
<a href="docs/screenshots/20260617_231842.645.jpg" target="_blank"><img src="docs/screenshots/20260617_231842.645.jpg" width="23%" alt="PS5 Web File Manager screenshot 2"></a>
|
||||
<a href="docs/screenshots/20260617_232348.855.jpg" target="_blank"><img src="docs/screenshots/20260617_232348.855.jpg" width="23%" alt="PS5 Web File Manager screenshot 3"></a>
|
||||
<a href="docs/screenshots/20260617_231943.120.jpg" target="_blank"><img src="docs/screenshots/20260617_231943.120.jpg" width="23%" alt="PS5 Web File Manager screenshot 4"></a>
|
||||
<a href="docs/screenshots/20260617_231827.376.jpg" target="_blank"><img src="docs/screenshots/20260617_231827.376.jpg" width="31%" alt="PS5 Web File Manager screenshot 1"></a>
|
||||
<a href="docs/screenshots/20260619_131432.399.jpg" target="_blank"><img src="docs/screenshots/20260619_131432.399.jpg" width="31%" alt="PS5 Web File Manager screenshot 2"></a>
|
||||
<a href="docs/screenshots/20260617_232348.855.jpg" target="_blank"><img src="docs/screenshots/20260617_232348.855.jpg" width="31%" alt="PS5 Web File Manager screenshot 3"></a>
|
||||
<a href="docs/screenshots/20260619_131811.644.jpg" target="_blank"><img src="docs/screenshots/20260619_131811.644.jpg" width="31%" alt="PS5 Web File Manager screenshot 4"></a>
|
||||
<a href="docs/screenshots/20260619_131535.239.jpg" target="_blank"><img src="docs/screenshots/20260619_131535.239.jpg" width="31%" alt="PS5 Web File Manager screenshot 5"></a>
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- List files and folders.
|
||||
- Copy, move, delete, rename, and create folders.
|
||||
- Edit UTF-8 text files up to 1 MiB using the built-in plain text editor.
|
||||
- Multi-select operations.
|
||||
- Copy/move by choosing sources first, then pasting or moving them into the current folder.
|
||||
- Conflict prompts for overwriting files and merging folders.
|
||||
@@ -27,6 +29,7 @@ PS5 web file manager payload. It runs an HTTP UI starting at port `8888`, instal
|
||||
- Copied/moved files and folders are set to `0777` where the filesystem supports Unix permissions. FAT/exFAT-style filesystems may ignore chmod.
|
||||
- Chinese and English UI. The browser language is read from `navigator.languages` / `navigator.language`; Chinese uses `zh`, everything else uses English.
|
||||
- Startup notification showing the app name, version, and listen port.
|
||||
- Create/edit text files ending with `.txt`, `.json`, `.xml`, `.ini`, `.cfg`, `.conf`, `.md`, `.log`, `.lua`, `.js`, `.css`, `.html`, `.htm`, `.c`, `.h`, `.cpp`, `.hpp`, `.sh`, `.csv`, `.yaml`, `.yml`.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -86,6 +89,7 @@ On first startup the payload installs a `PS5 Web File Manager` web shortcut in t
|
||||
- Delete is recursive and permanent. There is no recycle bin.
|
||||
- Copy/move tasks can be canceled. A partially copied single file is removed, but partially copied folders are left in place to avoid deleting existing files when merging into an existing target folder.
|
||||
- The UI can recover the active task display if the browser is closed and reopened while the payload process is still running.
|
||||
- Text editing is limited to common text-file extensions. Non-UTF-8 and oversized files are rejected.
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
</div>
|
||||
<div class="tool-right">
|
||||
<button id="refreshBtn" data-i18n="refresh"></button>
|
||||
<button id="newTextBtn" data-i18n="newText"></button>
|
||||
<button id="mkdirBtn" data-i18n="mkdir"></button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -56,6 +57,19 @@
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<div id="textEditorOverlay" class="text-editor-overlay" hidden>
|
||||
<section class="text-editor-panel">
|
||||
<div id="textEditorPath" class="text-editor-path"></div>
|
||||
<textarea id="textEditor" class="text-editor" wrap="off" spellcheck="false"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off"></textarea>
|
||||
<div id="textEditorStatus" class="text-editor-status"></div>
|
||||
<div class="text-editor-actions">
|
||||
<button id="textEditorCloseBtn" class="secondary" data-i18n="close"></button>
|
||||
<button id="textEditorSaveBtn" class="primary" data-i18n="save"></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="taskOverlay" class="task-overlay" hidden>
|
||||
<div class="task-panel">
|
||||
<div id="tasks" class="tasks"></div>
|
||||
|
||||
@@ -6,10 +6,13 @@ window.WFM_LANG = {
|
||||
rename: "Rename",
|
||||
paste: "Paste",
|
||||
cancel: "Cancel",
|
||||
close: "Close",
|
||||
save: "Save",
|
||||
exit: "Exit",
|
||||
exiting: "Exiting...",
|
||||
refresh: "Refresh",
|
||||
mkdir: "New Folder",
|
||||
newText: "New Text",
|
||||
file: "File",
|
||||
dir: "Folder",
|
||||
parent: "Parent",
|
||||
@@ -60,6 +63,15 @@ window.WFM_LANG = {
|
||||
tasksPollFailed: "Failed to read task status: {error}",
|
||||
renamePrompt: "New name",
|
||||
mkdirPrompt: "Folder name",
|
||||
newTextPrompt: "Text file name",
|
||||
creatingText: "Creating text file...",
|
||||
createTextFailed: "Failed to create text file: {error}",
|
||||
unsavedSaveConfirm: "The text has unsaved changes. Save before closing?\n\nCancel discards the changes and closes.",
|
||||
loadingText: "Loading text...",
|
||||
savingText: "Saving...",
|
||||
textSaved: "Text saved",
|
||||
openTextFailed: "Failed to open text: {error}",
|
||||
saveTextFailed: "Failed to save text: {error}",
|
||||
deleteConfirm: "Delete {name}?",
|
||||
deleteConfirmRecursive: "Delete {name}?\n\nFolders will be deleted recursively.",
|
||||
activeTask: "A task is running",
|
||||
@@ -82,6 +94,14 @@ window.WFM_LANG = {
|
||||
err_source_destination_same: "Source and destination are the same",
|
||||
err_destination_inside_source: "Cannot copy or move a folder inside itself",
|
||||
err_invalid_path: "Invalid path",
|
||||
err_file_not_found: "File not found",
|
||||
err_file_already_exists: "A file with the same name already exists",
|
||||
err_invalid_method: "Invalid request method",
|
||||
err_text_type_not_editable: "This file type is not supported by the text editor",
|
||||
err_text_file_too_large: "The text file is too large. The maximum size is 1 MiB",
|
||||
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_unknown_api: "Unknown API",
|
||||
err_out_of_memory: "Out of memory",
|
||||
err_no_source_paths: "No source paths",
|
||||
|
||||
@@ -6,10 +6,13 @@ window.WFM_LANG = {
|
||||
rename: "重命名",
|
||||
paste: "粘贴",
|
||||
cancel: "取消",
|
||||
close: "关闭",
|
||||
save: "保存",
|
||||
exit: "退出",
|
||||
exiting: "正在退出...",
|
||||
refresh: "刷新",
|
||||
mkdir: "新建目录",
|
||||
newText: "新建文本",
|
||||
file: "文件",
|
||||
dir: "目录",
|
||||
parent: "上级目录",
|
||||
@@ -60,6 +63,15 @@ window.WFM_LANG = {
|
||||
tasksPollFailed: "任务状态读取失败: {error}",
|
||||
renamePrompt: "新名称",
|
||||
mkdirPrompt: "目录名",
|
||||
newTextPrompt: "文本文件名",
|
||||
creatingText: "正在新建文本...",
|
||||
createTextFailed: "新建文本失败: {error}",
|
||||
unsavedSaveConfirm: "文本有未保存的修改,是否保存后关闭?\n\n取消将放弃修改并关闭。",
|
||||
loadingText: "正在读取文本...",
|
||||
savingText: "正在保存...",
|
||||
textSaved: "文本已保存",
|
||||
openTextFailed: "打开文本失败: {error}",
|
||||
saveTextFailed: "保存文本失败: {error}",
|
||||
deleteConfirm: "确认删除 {name}?",
|
||||
deleteConfirmRecursive: "确认删除 {name}?\n\n目录会递归删除。",
|
||||
activeTask: "有任务正在执行",
|
||||
@@ -82,6 +94,14 @@ window.WFM_LANG = {
|
||||
err_source_destination_same: "源和目标相同",
|
||||
err_destination_inside_source: "不能复制或移动目录到它自己的内部",
|
||||
err_invalid_path: "路径无效",
|
||||
err_file_not_found: "文件不存在",
|
||||
err_file_already_exists: "同名文件已存在",
|
||||
err_invalid_method: "请求方式无效",
|
||||
err_text_type_not_editable: "该文件类型不支持文本编辑",
|
||||
err_text_file_too_large: "文本文件过大,最大支持 1 MiB",
|
||||
err_text_invalid_utf8: "文件不是有效的 UTF-8 文本",
|
||||
err_text_file_changed: "文件在打开后已发生变化,请关闭后重新打开",
|
||||
err_text_file_not_writable: "文本文件不可写",
|
||||
err_unknown_api: "未知接口",
|
||||
err_out_of_memory: "内存不足",
|
||||
err_no_source_paths: "没有源路径",
|
||||
|
||||
@@ -237,6 +237,20 @@ button:disabled,
|
||||
background: #402326;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border-color: #4f9f7b;
|
||||
background: #285943;
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: #347358;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
border-color: #59636e;
|
||||
background: #20262d;
|
||||
}
|
||||
|
||||
.paste-action {
|
||||
max-width: 480px;
|
||||
}
|
||||
@@ -419,6 +433,82 @@ input[type="checkbox"] {
|
||||
color: #7f8a94;
|
||||
}
|
||||
|
||||
.text-editor-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 9000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
background: rgba(4, 6, 8, .9);
|
||||
}
|
||||
|
||||
.text-editor-panel {
|
||||
width: 1200px;
|
||||
height: calc(100vh - 96px);
|
||||
max-width: calc(100vw - 96px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
border: 1px solid #46515f;
|
||||
border-radius: 10px;
|
||||
background: #15191e;
|
||||
box-shadow: 0 18px 60px rgba(0, 0, 0, .45);
|
||||
}
|
||||
|
||||
.text-editor-path {
|
||||
margin-bottom: 14px;
|
||||
overflow: hidden;
|
||||
color: #c9d3dc;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 20px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.text-editor {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
padding: 18px;
|
||||
border: 1px solid #46515f;
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
resize: none;
|
||||
background: #0f1215;
|
||||
color: #edf0f2;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.text-editor:focus {
|
||||
border-color: #6fb1ff;
|
||||
}
|
||||
|
||||
.text-editor-status {
|
||||
min-height: 28px;
|
||||
padding-top: 8px;
|
||||
color: #aab4be;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-editor-actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.text-editor-actions button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.text-editor-actions button + button {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.task-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
+162
-13
@@ -10,9 +10,13 @@ let pendingAbortController = null;
|
||||
let taskRefreshPath = "/";
|
||||
let hoverPaused = false;
|
||||
let hoverResumeTimer = 0;
|
||||
let textEditorPath = null;
|
||||
let textEditorVersion = null;
|
||||
let textEditorOriginal = "";
|
||||
let textEditorBusy = false;
|
||||
let L = {};
|
||||
|
||||
const APP_VERSION = "v0.2";
|
||||
const APP_VERSION = "v0.3";
|
||||
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
|
||||
|
||||
const filesEl = document.getElementById("files");
|
||||
@@ -32,6 +36,13 @@ const pasteTargetTextEl = document.getElementById("pasteTargetText");
|
||||
const clearClipboardBtn = document.getElementById("clearClipboardBtn");
|
||||
const initLoadingEl = document.getElementById("initLoading");
|
||||
const exitBtn = document.getElementById("exitBtn");
|
||||
const textEditorOverlayEl = document.getElementById("textEditorOverlay");
|
||||
const textEditorPathEl = document.getElementById("textEditorPath");
|
||||
const textEditorEl = document.getElementById("textEditor");
|
||||
const textEditorStatusEl = document.getElementById("textEditorStatus");
|
||||
const textEditorCloseBtn = document.getElementById("textEditorCloseBtn");
|
||||
const textEditorSaveBtn = document.getElementById("textEditorSaveBtn");
|
||||
const newTextBtn = document.getElementById("newTextBtn");
|
||||
|
||||
function chooseLanguage() {
|
||||
const langs = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language || ""];
|
||||
@@ -88,17 +99,29 @@ function applyStaticText() {
|
||||
if (initLoadingEl) initLoadingEl.hidden = true;
|
||||
}
|
||||
|
||||
function api(path, params, options) {
|
||||
async function request(path, params, options) {
|
||||
const qs = new URLSearchParams(params || {});
|
||||
const fetchOptions = Object.assign({ method: "POST" }, options || {});
|
||||
return fetch(path + (qs.toString() ? "?" + qs.toString() : ""), fetchOptions)
|
||||
.then(async response => {
|
||||
const data = await response.json();
|
||||
if (!response.ok || !data.ok) {
|
||||
throw new Error(backendErrorText(data.error_code, data.error_arg, data.error));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
const response = await fetch(path + (qs.toString() ? "?" + qs.toString() : ""), fetchOptions);
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(backendErrorText(data.error_code, data.error_arg, data.error));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function api(path, params, options) {
|
||||
const data = await (await request(path, params, options)).json();
|
||||
if (!data.ok) throw new Error(backendErrorText(data.error_code, data.error_arg, data.error));
|
||||
return data;
|
||||
}
|
||||
|
||||
async function fetchText(path, params) {
|
||||
const response = await request(path, params, { method: "GET" });
|
||||
return {
|
||||
text: await response.text(),
|
||||
version: response.headers.get("X-Text-Version") || ""
|
||||
};
|
||||
}
|
||||
|
||||
function formatSize(size, type) {
|
||||
@@ -119,7 +142,16 @@ function formatBytes(size, decimals) {
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
return seconds ? new Date(seconds * 1000).toLocaleString() : "";
|
||||
if (!seconds) return "";
|
||||
const date = new Date(seconds * 1000);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const time = [date.getHours(), date.getMinutes(), date.getSeconds()]
|
||||
.map(value => value < 10 ? "0" + value : value).join(":");
|
||||
return document.documentElement.lang === "en" ?
|
||||
month + "/" + day + "/" + year + ", " + time :
|
||||
year + "/" + month + "/" + day + " " + time;
|
||||
}
|
||||
|
||||
function formatSpeed(bytes) {
|
||||
@@ -277,6 +309,118 @@ function typeLabel(type) {
|
||||
return type === "d" ? t("dir") : t("file");
|
||||
}
|
||||
|
||||
function isEditableText(item) {
|
||||
return item.type === "-" &&
|
||||
/\.(txt|json|xml|ini|cfg|conf|md|log|lua|js|css|html?|c|h|cpp|hpp|sh|csv|ya?ml)$/i.test(item.name);
|
||||
}
|
||||
|
||||
function setTextEditorBusy(value, status) {
|
||||
textEditorBusy = value;
|
||||
textEditorEl.disabled = value;
|
||||
textEditorCloseBtn.disabled = value;
|
||||
textEditorSaveBtn.disabled = value;
|
||||
textEditorStatusEl.textContent = status || "";
|
||||
}
|
||||
|
||||
function closeTextEditor() {
|
||||
if (textEditorBusy) return;
|
||||
textEditorOverlayEl.hidden = true;
|
||||
textEditorPath = null;
|
||||
textEditorVersion = null;
|
||||
textEditorOriginal = "";
|
||||
textEditorEl.value = "";
|
||||
textEditorStatusEl.textContent = "";
|
||||
return load(cwd, false, true);
|
||||
}
|
||||
|
||||
function requestCloseTextEditor() {
|
||||
if (textEditorBusy) return;
|
||||
if (textEditorEl.value !== textEditorOriginal &&
|
||||
confirm(t("unsavedSaveConfirm"))) return saveTextEditor();
|
||||
return closeTextEditor();
|
||||
}
|
||||
|
||||
function showTextEditor(item, text, version) {
|
||||
textEditorPath = item.path;
|
||||
textEditorVersion = version;
|
||||
textEditorPathEl.textContent = item.path;
|
||||
textEditorPathEl.title = item.path;
|
||||
textEditorEl.value = text;
|
||||
textEditorOriginal = textEditorEl.value;
|
||||
textEditorOverlayEl.hidden = false;
|
||||
setTextEditorBusy(false, "");
|
||||
textEditorEl.focus();
|
||||
}
|
||||
|
||||
async function openTextEditor(item) {
|
||||
if (busy || textEditorBusy) return;
|
||||
textEditorPath = item.path;
|
||||
textEditorVersion = null;
|
||||
textEditorOriginal = "";
|
||||
textEditorPathEl.textContent = item.path;
|
||||
textEditorPathEl.title = item.path;
|
||||
textEditorEl.value = "";
|
||||
textEditorOverlayEl.hidden = false;
|
||||
setTextEditorBusy(true, t("loadingText"));
|
||||
try {
|
||||
const data = await fetchText("/api/text", { path: item.path });
|
||||
showTextEditor(item, data.text, data.version);
|
||||
} catch (err) {
|
||||
setTextEditorBusy(false, "");
|
||||
textEditorOverlayEl.hidden = true;
|
||||
textEditorPath = null;
|
||||
const message = t("openTextFailed", { error: err.message });
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTextEditor() {
|
||||
if (textEditorBusy || !textEditorPath || !textEditorVersion) return;
|
||||
if (textEditorEl.value === textEditorOriginal) {
|
||||
await closeTextEditor();
|
||||
return;
|
||||
}
|
||||
const path = textEditorPath;
|
||||
setTextEditorBusy(true, t("savingText"));
|
||||
try {
|
||||
await api("/api/text/save", { path, version: textEditorVersion }, {
|
||||
body: textEditorEl.value,
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
||||
});
|
||||
setTextEditorBusy(false, "");
|
||||
await closeTextEditor();
|
||||
setStatus(t("textSaved"));
|
||||
} catch (err) {
|
||||
setTextEditorBusy(false, "");
|
||||
const message = t("saveTextFailed", { error: err.message });
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function actionNewText() {
|
||||
if (busy) return;
|
||||
const dir = cwd;
|
||||
const name = (prompt(t("newTextPrompt"), "") || "").trim();
|
||||
if (!name) return;
|
||||
const item = { name, path: pathJoin(dir, name), type: "-" };
|
||||
|
||||
try {
|
||||
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) {
|
||||
setBusy(false);
|
||||
const message = t("createTextFailed", { error: err.message });
|
||||
setStatus(message);
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
function resetScrollTop() {
|
||||
const apply = () => {
|
||||
contentEl.scrollTop = 0;
|
||||
@@ -354,6 +498,7 @@ function updateButtons() {
|
||||
document.getElementById("renameBtn").disabled = busy || items.length !== 1;
|
||||
document.getElementById("deleteBtn").disabled = busy || items.length === 0;
|
||||
document.getElementById("mkdirBtn").disabled = busy;
|
||||
newTextBtn.disabled = busy;
|
||||
selectAllEl.checked = entries.length > 0 && items.length === entries.length;
|
||||
selectAllEl.indeterminate = items.length > 0 && items.length < entries.length;
|
||||
renderClipboard();
|
||||
@@ -483,6 +628,7 @@ function render() {
|
||||
nameBtn.addEventListener("click", () => {
|
||||
focusPath(item.path);
|
||||
if (isParent || item.type === "d") load(item.path);
|
||||
else if (isEditableText(item)) openTextEditor(item);
|
||||
else togglePath(item.path, !selected.has(item.path));
|
||||
});
|
||||
appendChildren(nameBtn, icon, nameText);
|
||||
@@ -867,7 +1013,7 @@ function actionRename() {
|
||||
if (busy) return;
|
||||
const item = singleSelected();
|
||||
if (!item) return;
|
||||
const name = prompt(t("renamePrompt"), item.name);
|
||||
const name = (prompt(t("renamePrompt"), item.name) || "").trim();
|
||||
if (!name || name === item.name) return;
|
||||
runImmediateAction(t("rename"), () => api("/api/rename", { path: item.path, name }));
|
||||
}
|
||||
@@ -896,7 +1042,7 @@ document.getElementById("refreshBtn").addEventListener("click", () => load(cwd,
|
||||
document.getElementById("mkdirBtn").addEventListener("click", () => {
|
||||
if (busy) return;
|
||||
const path = cwd;
|
||||
const name = prompt(t("mkdirPrompt"));
|
||||
const name = (prompt(t("mkdirPrompt")) || "").trim();
|
||||
if (!name) return;
|
||||
runImmediateAction(t("mkdir"), () => api("/api/mkdir", { path, name }));
|
||||
});
|
||||
@@ -907,6 +1053,9 @@ clearClipboardBtn.addEventListener("click", clearClipboard);
|
||||
document.getElementById("renameBtn").addEventListener("click", actionRename);
|
||||
document.getElementById("deleteBtn").addEventListener("click", actionDelete);
|
||||
exitBtn.addEventListener("click", actionExit);
|
||||
textEditorCloseBtn.addEventListener("click", requestCloseTextEditor);
|
||||
textEditorSaveBtn.addEventListener("click", saveTextEditor);
|
||||
newTextBtn.addEventListener("click", actionNewText);
|
||||
selectAllEl.addEventListener("change", () => {
|
||||
selected.clear();
|
||||
if (selectAllEl.checked) entries.forEach(item => selected.add(item.path));
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 150 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 119 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
+467
-2
@@ -1,5 +1,6 @@
|
||||
#include "filemgr.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -9,6 +10,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <sys/types.h>
|
||||
@@ -24,6 +26,7 @@
|
||||
#include "websrv.h"
|
||||
|
||||
#define COPY_BUFFER_SIZE (8 * 1024 * 1024)
|
||||
#define TEXT_FILE_MAX_SIZE (1024 * 1024)
|
||||
typedef struct strbuf {
|
||||
char *data;
|
||||
size_t len;
|
||||
@@ -131,7 +134,18 @@ strbuf_printf(strbuf_t *b, const char *fmt, ...) {
|
||||
static char *
|
||||
query_value(struct MHD_Connection *conn, const char *key) {
|
||||
const char *raw = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, key);
|
||||
return raw ? strdup(raw) : NULL;
|
||||
char *value = raw ? strdup(raw) : NULL;
|
||||
|
||||
if(value && !strcmp(key, "name")) {
|
||||
char *start = value;
|
||||
char *end;
|
||||
while(isspace((unsigned char)*start)) start++;
|
||||
end = start + strlen(start);
|
||||
while(end > start && isspace((unsigned char)end[-1])) end--;
|
||||
memmove(value, start, (size_t)(end - start));
|
||||
value[end - start] = 0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -484,6 +498,16 @@ send_json_ok(struct MHD_Connection *conn) {
|
||||
"application/json");
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
send_json_version(struct MHD_Connection *conn, unsigned long long version) {
|
||||
char *data;
|
||||
|
||||
if(asprintf(&data, "{\"ok\":true,\"version\":\"%016llx\"}", version) < 0) {
|
||||
return MHD_NO;
|
||||
}
|
||||
return send_buffer(conn, MHD_HTTP_OK, data, "application/json");
|
||||
}
|
||||
|
||||
static const char *
|
||||
api_error_code(const char *msg) {
|
||||
if(!msg) return "system_error";
|
||||
@@ -492,9 +516,17 @@ api_error_code(const char *msg) {
|
||||
if(!strcmp(msg, "source and destination are the same")) return "source_destination_same";
|
||||
if(!strcmp(msg, "destination is inside source directory")) return "destination_inside_source";
|
||||
if(!strcmp(msg, "invalid path")) return "invalid_path";
|
||||
if(!strcmp(msg, "file not found")) return "file_not_found";
|
||||
if(!strcmp(msg, "invalid method")) return "invalid_method";
|
||||
if(!strcmp(msg, "unknown api")) return "unknown_api";
|
||||
if(!strcmp(msg, "out of memory")) return "out_of_memory";
|
||||
if(!strcmp(msg, "no source paths")) return "no_source_paths";
|
||||
if(!strcmp(msg, "file type is not editable")) return "text_type_not_editable";
|
||||
if(!strcmp(msg, "text file is too large")) return "text_file_too_large";
|
||||
if(!strcmp(msg, "file is not valid UTF-8")) return "text_invalid_utf8";
|
||||
if(!strcmp(msg, "file changed since it was opened")) return "text_file_changed";
|
||||
if(!strcmp(msg, "text file is not writable")) return "text_file_not_writable";
|
||||
if(!strcmp(msg, "file already exists")) return "file_already_exists";
|
||||
if(!strcmp(msg, "destination must be a directory for multiple items")) {
|
||||
return "destination_must_be_directory";
|
||||
}
|
||||
@@ -517,6 +549,222 @@ send_json_error(struct MHD_Connection *conn, unsigned int status,
|
||||
return send_buffer(conn, status, b.data, "application/json");
|
||||
}
|
||||
|
||||
static int
|
||||
text_extension_allowed(const char *path) {
|
||||
static const char *extensions[] = {
|
||||
".txt", ".json", ".xml", ".ini", ".cfg", ".conf", ".md",
|
||||
".log", ".lua", ".js", ".css", ".html", ".htm", ".c", ".h",
|
||||
".cpp", ".hpp", ".sh", ".csv", ".yaml", ".yml"
|
||||
};
|
||||
const char *extension = strrchr(path, '.');
|
||||
size_t i;
|
||||
|
||||
if(!extension) {
|
||||
return 0;
|
||||
}
|
||||
for(i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) {
|
||||
if(!strcasecmp(extension, extensions[i])) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
valid_utf8(const unsigned char *data, size_t size) {
|
||||
size_t i = 0;
|
||||
|
||||
while(i < size) {
|
||||
unsigned char c = data[i++];
|
||||
size_t trailing;
|
||||
unsigned int codepoint;
|
||||
|
||||
if(!c) return 0;
|
||||
if(c < 0x80) continue;
|
||||
if(c >= 0xc2 && c <= 0xdf) {
|
||||
trailing = 1;
|
||||
codepoint = c & 0x1f;
|
||||
} else if(c >= 0xe0 && c <= 0xef) {
|
||||
trailing = 2;
|
||||
codepoint = c & 0x0f;
|
||||
} else if(c >= 0xf0 && c <= 0xf4) {
|
||||
trailing = 3;
|
||||
codepoint = c & 0x07;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if(trailing > size - i) return 0;
|
||||
while(trailing--) {
|
||||
unsigned char next = data[i++];
|
||||
if((next & 0xc0) != 0x80) return 0;
|
||||
codepoint = (codepoint << 6) | (next & 0x3f);
|
||||
}
|
||||
if((codepoint >= 0xd800 && codepoint <= 0xdfff) || codepoint > 0x10ffff ||
|
||||
(codepoint < 0x800 && c >= 0xe0) ||
|
||||
(codepoint < 0x10000 && c >= 0xf0)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static unsigned long long
|
||||
text_version(const unsigned char *data, size_t size) {
|
||||
unsigned long long hash = 1469598103934665603ULL;
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < size; i++) {
|
||||
hash ^= data[i];
|
||||
hash *= 1099511628211ULL;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
typedef enum text_newline {
|
||||
TEXT_NEWLINE_LF,
|
||||
TEXT_NEWLINE_CRLF,
|
||||
TEXT_NEWLINE_CR,
|
||||
} text_newline_t;
|
||||
|
||||
static text_newline_t
|
||||
detect_text_newline(const unsigned char *data, size_t size) {
|
||||
size_t crlf = 0;
|
||||
size_t lf = 0;
|
||||
size_t cr = 0;
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < size; i++) {
|
||||
if(data[i] == '\r') {
|
||||
if(i + 1 < size && data[i + 1] == '\n') {
|
||||
crlf++;
|
||||
i++;
|
||||
} else {
|
||||
cr++;
|
||||
}
|
||||
} else if(data[i] == '\n') {
|
||||
lf++;
|
||||
}
|
||||
}
|
||||
if(crlf > lf && crlf >= cr) return TEXT_NEWLINE_CRLF;
|
||||
if(cr > lf && cr > crlf) return TEXT_NEWLINE_CR;
|
||||
return TEXT_NEWLINE_LF;
|
||||
}
|
||||
|
||||
static int
|
||||
format_text_for_save(const char *body, size_t body_size,
|
||||
const unsigned char *current, size_t current_size,
|
||||
char **output, size_t *output_size) {
|
||||
static const unsigned char bom[] = {0xef, 0xbb, 0xbf};
|
||||
int keep_bom = current_size >= sizeof(bom) &&
|
||||
!memcmp(current, bom, sizeof(bom));
|
||||
text_newline_t newline = detect_text_newline(
|
||||
current + (keep_bom ? sizeof(bom) : 0),
|
||||
current_size - (keep_bom ? sizeof(bom) : 0));
|
||||
const unsigned char *input = (const unsigned char *)(body ? body : "");
|
||||
size_t input_size = body_size;
|
||||
size_t capacity = body_size * (newline == TEXT_NEWLINE_CRLF ? 2 : 1) +
|
||||
sizeof(bom) + 1;
|
||||
char *formatted;
|
||||
size_t i;
|
||||
size_t len = 0;
|
||||
|
||||
if(input_size >= sizeof(bom) && !memcmp(input, bom, sizeof(bom))) {
|
||||
input += sizeof(bom);
|
||||
input_size -= sizeof(bom);
|
||||
}
|
||||
if(!(formatted = malloc(capacity))) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
if(keep_bom) {
|
||||
memcpy(formatted + len, bom, sizeof(bom));
|
||||
len += sizeof(bom);
|
||||
}
|
||||
for(i = 0; i < input_size; i++) {
|
||||
unsigned char c = input[i];
|
||||
|
||||
if(c != '\r' && c != '\n') {
|
||||
formatted[len++] = (char)c;
|
||||
continue;
|
||||
}
|
||||
if(c == '\r' && i + 1 < input_size && input[i + 1] == '\n') {
|
||||
i++;
|
||||
}
|
||||
if(newline == TEXT_NEWLINE_CRLF) {
|
||||
formatted[len++] = '\r';
|
||||
formatted[len++] = '\n';
|
||||
} else {
|
||||
formatted[len++] = newline == TEXT_NEWLINE_CR ? '\r' : '\n';
|
||||
}
|
||||
}
|
||||
if(len > TEXT_FILE_MAX_SIZE) {
|
||||
free(formatted);
|
||||
errno = EFBIG;
|
||||
return -1;
|
||||
}
|
||||
formatted[len] = 0;
|
||||
*output = formatted;
|
||||
*output_size = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
read_text_file(const char *path, char **data, size_t *size,
|
||||
struct stat *st) {
|
||||
FILE *file;
|
||||
size_t read_size;
|
||||
|
||||
*data = NULL;
|
||||
*size = 0;
|
||||
if(lstat(path, st) || !S_ISREG(st->st_mode)) {
|
||||
return -1;
|
||||
}
|
||||
if(st->st_size < 0 || (unsigned long long)st->st_size > TEXT_FILE_MAX_SIZE) {
|
||||
errno = EFBIG;
|
||||
return -1;
|
||||
}
|
||||
if(!(file = fopen(path, "rb"))) {
|
||||
return -1;
|
||||
}
|
||||
if(!(*data = malloc((size_t)st->st_size + 1))) {
|
||||
fclose(file);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
read_size = fread(*data, 1, (size_t)st->st_size, file);
|
||||
if(read_size != (size_t)st->st_size || ferror(file)) {
|
||||
free(*data);
|
||||
*data = NULL;
|
||||
fclose(file);
|
||||
return -1;
|
||||
}
|
||||
fclose(file);
|
||||
(*data)[read_size] = 0;
|
||||
*size = read_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
send_text_file(struct MHD_Connection *conn, char *data, size_t size,
|
||||
unsigned long long version) {
|
||||
struct MHD_Response *resp;
|
||||
enum MHD_Result ret;
|
||||
char version_text[24];
|
||||
|
||||
if(!(resp = MHD_create_response_from_buffer(size, data,
|
||||
MHD_RESPMEM_MUST_FREE))) {
|
||||
free(data);
|
||||
return MHD_NO;
|
||||
}
|
||||
snprintf(version_text, sizeof(version_text), "%016llx", version);
|
||||
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
|
||||
"text/plain; charset=utf-8");
|
||||
MHD_add_response_header(resp, "X-Text-Version", version_text);
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_OK, resp);
|
||||
MHD_destroy_response(resp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int count_path_bytes(file_task_t *task, const char *path,
|
||||
const char *display,
|
||||
unsigned long long *total);
|
||||
@@ -1906,8 +2154,210 @@ api_mkdir(struct MHD_Connection *conn) {
|
||||
: send_json_ok(conn);
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
api_text(struct MHD_Connection *conn) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *data;
|
||||
size_t size;
|
||||
struct stat st;
|
||||
unsigned long long version;
|
||||
|
||||
if(has_active_task()) {
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
if(!path || !text_extension_allowed(path)) {
|
||||
free(path);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST,
|
||||
"file type is not editable");
|
||||
}
|
||||
if(read_text_file(path, &data, &size, &st)) {
|
||||
int error = errno;
|
||||
free(path);
|
||||
if(error == EFBIG) {
|
||||
return send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
|
||||
"text file is too large");
|
||||
}
|
||||
errno = error;
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
free(path);
|
||||
if(!valid_utf8((const unsigned char *)data, size)) {
|
||||
free(data);
|
||||
return send_json_error(conn, MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
|
||||
"file is not valid UTF-8");
|
||||
}
|
||||
version = text_version((const unsigned char *)data, size);
|
||||
return send_text_file(conn, data, size, version);
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
api_text_create(struct MHD_Connection *conn) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *name = query_value(conn, "name");
|
||||
char target[PATH_MAX];
|
||||
int fd = -1;
|
||||
int ret = -1;
|
||||
int error = 0;
|
||||
int created = 0;
|
||||
|
||||
if(has_active_task()) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
if(!path || !name || path_join(target, sizeof(target), path, name)) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(access(path, W_OK | X_OK)) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
|
||||
"text file is not writable");
|
||||
}
|
||||
if((fd = open(target, O_WRONLY | O_CREAT | O_EXCL, 0777)) >= 0) {
|
||||
created = 1;
|
||||
ret = fchmod_0777(fd);
|
||||
if(close(fd) && !ret) ret = -1;
|
||||
fd = -1;
|
||||
}
|
||||
if(ret) {
|
||||
error = errno;
|
||||
if(fd >= 0) close(fd);
|
||||
if(created) unlink(target);
|
||||
}
|
||||
free(path); free(name);
|
||||
if(!ret) {
|
||||
return send_json_version(conn,
|
||||
text_version((const unsigned char *)"", 0));
|
||||
}
|
||||
errno = error;
|
||||
return error == EEXIST ?
|
||||
send_json_error(conn, MHD_HTTP_CONFLICT, "file already exists") :
|
||||
send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
|
||||
}
|
||||
|
||||
static int
|
||||
write_text_atomic(const char *path, const char *body, size_t body_size,
|
||||
mode_t mode) {
|
||||
struct timespec now;
|
||||
char temp[PATH_MAX];
|
||||
size_t written = 0;
|
||||
int fd = -1;
|
||||
int ret = -1;
|
||||
int n;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
n = snprintf(temp, sizeof(temp), "%s.wfm-%ld-%ld.tmp", path,
|
||||
(long)getpid(), now.tv_nsec);
|
||||
if(n < 0 || (size_t)n >= sizeof(temp)) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
if((fd = open(temp, O_WRONLY | O_CREAT | O_EXCL, 0600)) < 0) {
|
||||
return -1;
|
||||
}
|
||||
while(written < body_size) {
|
||||
ssize_t count = write(fd, body + written, body_size - written);
|
||||
if(count <= 0) {
|
||||
goto done;
|
||||
}
|
||||
written += (size_t)count;
|
||||
}
|
||||
if(fchmod(fd, mode & 07777) && !ignore_chmod_error(errno)) {
|
||||
goto done;
|
||||
}
|
||||
if(fsync(fd)) {
|
||||
goto done;
|
||||
}
|
||||
if(close(fd)) {
|
||||
fd = -1;
|
||||
goto done;
|
||||
}
|
||||
fd = -1;
|
||||
if(rename(temp, path)) {
|
||||
goto done;
|
||||
}
|
||||
ret = 0;
|
||||
|
||||
done:
|
||||
if(fd >= 0) close(fd);
|
||||
if(ret) unlink(temp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static enum MHD_Result
|
||||
api_text_save(struct MHD_Connection *conn, const char *body,
|
||||
size_t body_size) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *expected = query_value(conn, "version");
|
||||
char *current = NULL;
|
||||
size_t current_size = 0;
|
||||
struct stat st;
|
||||
char version_text[24];
|
||||
char parent[PATH_MAX];
|
||||
char *formatted = NULL;
|
||||
size_t formatted_size = 0;
|
||||
int ret;
|
||||
|
||||
if(has_active_task()) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
}
|
||||
if(!path || !expected) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(body_size > TEXT_FILE_MAX_SIZE) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
|
||||
"text file is too large");
|
||||
}
|
||||
if(!valid_utf8((const unsigned char *)(body ? body : ""), body_size)) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_UNSUPPORTED_MEDIA_TYPE,
|
||||
"file is not valid UTF-8");
|
||||
}
|
||||
if(read_text_file(path, ¤t, ¤t_size, &st)) {
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "file not found");
|
||||
}
|
||||
snprintf(version_text, sizeof(version_text), "%016llx",
|
||||
text_version((const unsigned char *)current, current_size));
|
||||
if(strcmp(expected, version_text)) {
|
||||
free(current);
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_CONFLICT,
|
||||
"file changed since it was opened");
|
||||
}
|
||||
if(path_dirname(path, parent, sizeof(parent)) ||
|
||||
access(path, W_OK) || access(parent, W_OK | X_OK)) {
|
||||
free(current);
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
|
||||
"text file is not writable");
|
||||
}
|
||||
if(format_text_for_save(body, body_size, (const unsigned char *)current,
|
||||
current_size, &formatted, &formatted_size)) {
|
||||
int error = errno;
|
||||
free(current);
|
||||
free(path); free(expected);
|
||||
errno = error;
|
||||
return error == EFBIG ?
|
||||
send_json_error(conn, MHD_HTTP_CONTENT_TOO_LARGE,
|
||||
"text file is too large") :
|
||||
send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL);
|
||||
}
|
||||
free(current);
|
||||
ret = write_text_atomic(path, formatted, formatted_size, st.st_mode);
|
||||
free(formatted);
|
||||
free(path); free(expected);
|
||||
return ret ? send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, NULL)
|
||||
: send_json_ok(conn);
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
filemgr_api_request(struct MHD_Connection *conn, const char *url) {
|
||||
filemgr_api_request(struct MHD_Connection *conn, const char *url,
|
||||
const char *method, const char *body, size_t body_size) {
|
||||
if(!strcmp(url, "/api/list")) return api_list(conn);
|
||||
if(!strcmp(url, "/api/tasks")) return api_tasks(conn);
|
||||
if(!strcmp(url, "/api/space")) return api_space(conn);
|
||||
@@ -1918,6 +2368,21 @@ filemgr_api_request(struct MHD_Connection *conn, const char *url) {
|
||||
if(!strcmp(url, "/api/delete")) return api_delete(conn);
|
||||
if(!strcmp(url, "/api/rename")) return api_rename(conn);
|
||||
if(!strcmp(url, "/api/mkdir")) return api_mkdir(conn);
|
||||
if(!strcmp(url, "/api/text")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_GET) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
api_text(conn);
|
||||
}
|
||||
if(!strcmp(url, "/api/text/create")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_POST) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
api_text_create(conn);
|
||||
}
|
||||
if(!strcmp(url, "/api/text/save")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_POST) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
api_text_save(conn, body, body_size);
|
||||
}
|
||||
return send_json_error(conn, MHD_HTTP_NOT_FOUND, "unknown api");
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -3,5 +3,6 @@
|
||||
#include <microhttpd.h>
|
||||
|
||||
enum MHD_Result filemgr_api_request(struct MHD_Connection *conn,
|
||||
const char *url);
|
||||
const char *url, const char *method,
|
||||
const char *body, size_t body_size);
|
||||
enum MHD_Result filemgr_fs_request(struct MHD_Connection *conn);
|
||||
+64
-5
@@ -13,6 +13,34 @@
|
||||
#include "filemgr.h"
|
||||
#include "websrv.h"
|
||||
|
||||
#define REQUEST_BODY_MAX (1024 * 1024)
|
||||
|
||||
typedef struct request_context {
|
||||
char *body;
|
||||
size_t size;
|
||||
int too_large;
|
||||
} request_context_t;
|
||||
|
||||
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\":\"\"}";
|
||||
struct MHD_Response *resp =
|
||||
MHD_create_response_from_buffer(sizeof(json) - 1, (void *)json,
|
||||
MHD_RESPMEM_PERSISTENT);
|
||||
enum MHD_Result ret;
|
||||
|
||||
if(!resp) {
|
||||
return MHD_NO;
|
||||
}
|
||||
MHD_add_response_header(resp, MHD_HTTP_HEADER_CONTENT_TYPE,
|
||||
"application/json");
|
||||
ret = websrv_queue_response(conn, MHD_HTTP_CONTENT_TOO_LARGE, resp);
|
||||
MHD_destroy_response(resp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
enum MHD_Result
|
||||
websrv_queue_response(struct MHD_Connection *conn, unsigned int status,
|
||||
struct MHD_Response *resp) {
|
||||
@@ -26,10 +54,9 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
const char *method, const char *version,
|
||||
const char *upload_data, size_t *upload_data_size,
|
||||
void **con_cls) {
|
||||
request_context_t *ctx = *con_cls;
|
||||
(void)cls;
|
||||
(void)version;
|
||||
(void)upload_data;
|
||||
(void)upload_data_size;
|
||||
|
||||
if(strcmp(method, MHD_HTTP_METHOD_GET) &&
|
||||
strcmp(method, MHD_HTTP_METHOD_POST) &&
|
||||
@@ -37,13 +64,39 @@ websrv_on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
||||
return MHD_NO;
|
||||
}
|
||||
|
||||
if(!*con_cls) {
|
||||
*con_cls = (void *)1;
|
||||
if(!ctx) {
|
||||
if(!(ctx = calloc(1, sizeof(*ctx)))) {
|
||||
return MHD_NO;
|
||||
}
|
||||
*con_cls = ctx;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
if(*upload_data_size) {
|
||||
size_t chunk_size = *upload_data_size;
|
||||
|
||||
if(chunk_size > REQUEST_BODY_MAX - ctx->size) {
|
||||
ctx->too_large = 1;
|
||||
} else if(!ctx->too_large) {
|
||||
char *body = realloc(ctx->body, ctx->size + chunk_size + 1);
|
||||
if(!body) {
|
||||
return MHD_NO;
|
||||
}
|
||||
ctx->body = body;
|
||||
memcpy(ctx->body + ctx->size, upload_data, chunk_size);
|
||||
ctx->size += chunk_size;
|
||||
ctx->body[ctx->size] = 0;
|
||||
}
|
||||
*upload_data_size = 0;
|
||||
return MHD_YES;
|
||||
}
|
||||
|
||||
if(ctx->too_large) {
|
||||
return websrv_body_too_large(conn);
|
||||
}
|
||||
|
||||
if(!strncmp(url, "/api/", 5)) {
|
||||
return filemgr_api_request(conn, url);
|
||||
return filemgr_api_request(conn, url, method, ctx->body, ctx->size);
|
||||
}
|
||||
if(!strcmp(url, "/fs")) {
|
||||
return filemgr_fs_request(conn);
|
||||
@@ -60,6 +113,12 @@ websrv_on_completed(void *cls, struct MHD_Connection *connection,
|
||||
(void)cls;
|
||||
(void)connection;
|
||||
(void)toe;
|
||||
request_context_t *ctx = *con_cls;
|
||||
|
||||
if(ctx) {
|
||||
free(ctx->body);
|
||||
free(ctx);
|
||||
}
|
||||
*con_cls = NULL;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user