mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 09:00:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a24160b8b | ||
|
|
3696468611 | ||
|
|
777bb4abd4 |
No files matched your search
+2
-1
@@ -3,4 +3,5 @@ gen/
|
||||
web-file-mgr-linux
|
||||
*.o
|
||||
*.d
|
||||
tools/
|
||||
tools/
|
||||
test/
|
||||
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION_TAG := v0.7
|
||||
VERSION_TAG := v0.9
|
||||
TITLE_ID := FMGR88888
|
||||
PYTHON ?= python3
|
||||
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
|
||||
|
||||
+25
-4
@@ -4,13 +4,34 @@
|
||||
<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>
|
||||
<main class="shell">
|
||||
<header class="topbar">
|
||||
<div id="path" class="path">/</div>
|
||||
<div id="spaceInfo" class="space-info"></div>
|
||||
<div class="top-left">
|
||||
<div id="path" class="path">/</div>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<div id="spaceInfo" class="space-info"></div>
|
||||
</div>
|
||||
<button id="exitBtn" class="icon-button power-button" type="button" aria-label="Exit"></button>
|
||||
</header>
|
||||
|
||||
@@ -22,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>
|
||||
@@ -89,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>
|
||||
|
||||
+5
-1
@@ -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?",
|
||||
@@ -91,6 +92,8 @@ window.WFM_LANG = {
|
||||
storageUsb: "USB",
|
||||
storageM2: "M.2",
|
||||
storageExtended: "Extended",
|
||||
storageRoot: "Root",
|
||||
storageCurrent: "Current",
|
||||
err_target_dir_not_writable: "Target folder is not writable: {path}",
|
||||
err_target_file_not_writable: "Target file is not writable: {path}",
|
||||
err_target_parent_not_writable: "Current folder is not writable: {path}",
|
||||
@@ -112,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",
|
||||
|
||||
+5
-1
@@ -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}任务?",
|
||||
@@ -91,6 +92,8 @@ window.WFM_LANG = {
|
||||
storageUsb: "USB存储",
|
||||
storageM2: "M2扩充存储",
|
||||
storageExtended: "扩展存储",
|
||||
storageRoot: "根分区",
|
||||
storageCurrent: "当前目录",
|
||||
err_target_dir_not_writable: "目标目录不可写: {path}",
|
||||
err_target_file_not_writable: "目标文件不可写: {path}",
|
||||
err_target_parent_not_writable: "当前目录不可写: {path}",
|
||||
@@ -112,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: "没有源路径",
|
||||
|
||||
+277
-35
@@ -43,23 +43,48 @@ body {
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
height: 54px;
|
||||
height: auto;
|
||||
min-height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30px;
|
||||
align-items: start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
padding: 0 18px;
|
||||
background: #1b2026;
|
||||
border-bottom: 1px solid #2d343d;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.top-left,
|
||||
.top-right {
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
flex: 1 1 420px;
|
||||
min-width: 0;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
flex: 1 1 360px;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
padding: 12px 40px 12px 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.path {
|
||||
height: 54px;
|
||||
color: #aab4be;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 22px;
|
||||
line-height: 54px;
|
||||
line-height: 24px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -68,21 +93,25 @@ body {
|
||||
}
|
||||
|
||||
.space-info {
|
||||
height: 54px;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
flex: 0 1 auto;
|
||||
max-width: 58vw;
|
||||
flex: 1 1 auto;
|
||||
max-width: none;
|
||||
color: #c9d3dc;
|
||||
font-size: 18px;
|
||||
line-height: 54px;
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.space-item {
|
||||
margin-left: 14px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -103,8 +132,6 @@ body {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
flex: 0 0 24px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.power-button {
|
||||
@@ -134,6 +161,13 @@ body {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
#exitBtn {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 18px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.toolbar,
|
||||
.tool-left,
|
||||
.tool-right {
|
||||
@@ -144,13 +178,13 @@ body {
|
||||
|
||||
.toolbar {
|
||||
flex: 0 0 auto;
|
||||
height: 84px;
|
||||
height: auto;
|
||||
min-height: 84px;
|
||||
justify-content: space-between;
|
||||
padding: 14px 30px;
|
||||
padding: 15px 18px 0;
|
||||
background: #15191e;
|
||||
border-bottom: 1px solid #2d343d;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.tool-left {
|
||||
@@ -160,14 +194,13 @@ body {
|
||||
|
||||
.toolbar .tool-left,
|
||||
.toolbar .tool-right {
|
||||
height: 54px;
|
||||
min-height: 54px;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-left > *,
|
||||
.tool-right > * {
|
||||
margin: 0 12px 0 0;
|
||||
margin: 0 12px 15px 0;
|
||||
}
|
||||
|
||||
.tool-right > * {
|
||||
@@ -177,7 +210,7 @@ body {
|
||||
|
||||
.tool-right {
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -242,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;
|
||||
@@ -283,6 +323,7 @@ button.primary:hover {
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 1034px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
@@ -297,6 +338,7 @@ td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
th {
|
||||
@@ -314,7 +356,7 @@ th {
|
||||
}
|
||||
|
||||
.name-col {
|
||||
min-width: 360px;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
td.name-col {
|
||||
@@ -351,6 +393,7 @@ tr {
|
||||
}
|
||||
|
||||
thead tr {
|
||||
height: 78px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -368,6 +411,7 @@ tr[data-type="parent"] {
|
||||
}
|
||||
|
||||
.select-hit {
|
||||
height: 78px;
|
||||
min-height: 78px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -422,10 +466,11 @@ input[type="checkbox"] {
|
||||
|
||||
.status {
|
||||
color: #aab4be;
|
||||
padding: 10px 25px;
|
||||
}
|
||||
|
||||
.status {
|
||||
flex: 0 0 auto;
|
||||
height: 46px;
|
||||
min-height: 46px;
|
||||
padding: 0 25px;
|
||||
line-height: 46px;
|
||||
border-top: 1px solid #2d343d;
|
||||
background: #15191e;
|
||||
font-size: 18px;
|
||||
@@ -718,18 +763,215 @@ input[type="checkbox"] {
|
||||
background: #8a929b;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.topbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
@media (max-width: 815px) {
|
||||
.top-left {
|
||||
padding: 0 40px 0 0;
|
||||
}
|
||||
|
||||
th:nth-child(4),
|
||||
td:nth-child(4),
|
||||
th:nth-child(5),
|
||||
td:nth-child(5),
|
||||
th:nth-child(6),
|
||||
td:nth-child(6) {
|
||||
.top-right {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
min-height: 42px;
|
||||
padding: 0 10px;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
flex: 1 1 auto;
|
||||
margin-top: 0;
|
||||
padding-right: 34px;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.path {
|
||||
font-size: 16px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
#exitBtn {
|
||||
top: 9px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
min-height: 0;
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
|
||||
.toolbar .tool-left,
|
||||
.toolbar .tool-right {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tool-left,
|
||||
.tool-right {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.tool-right {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.tool-left > *,
|
||||
.tool-right > * {
|
||||
margin: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.tool-right > * {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
min-width: 76px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.paste-action {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.paste-name {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 960px;
|
||||
}
|
||||
|
||||
.name-col {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
tr,
|
||||
thead tr,
|
||||
.select-hit,
|
||||
.row-action {
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.row-action {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
flex-basis: 26px;
|
||||
}
|
||||
|
||||
.status {
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
line-height: 34px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.task-overlay {
|
||||
padding: 18px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-panel {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
padding: 18px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.task > * + * {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.task-head {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
display: flex;
|
||||
max-width: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-name > * {
|
||||
margin: 0 8px 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-name-subject {
|
||||
flex: 1 1 auto;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-elapsed {
|
||||
margin: 8px 0 0;
|
||||
text-align: left;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.task-current {
|
||||
font-size: 18px;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.task-meta div,
|
||||
.task-meta div:nth-child(2),
|
||||
.task-meta div:nth-child(3) {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.task-meta div:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.task .danger {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+278
-68
@@ -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.7";
|
||||
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,92 @@ 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;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const code = text.charCodeAt(i);
|
||||
if (code > 0xff) return text;
|
||||
if (code >= 0x80) needsDecode = true;
|
||||
}
|
||||
if (!needsDecode || typeof TextDecoder === "undefined" || typeof Uint8Array === "undefined") return text;
|
||||
|
||||
const bytes = new Uint8Array(text.length);
|
||||
for (let i = 0; i < text.length; i++) bytes[i] = text.charCodeAt(i) & 0xff;
|
||||
if (isUtf8Bytes(bytes)) {
|
||||
try {
|
||||
return new TextDecoder("utf-8").decode(bytes);
|
||||
} catch (err) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return new TextDecoder("gbk").decode(bytes);
|
||||
} catch (err) {
|
||||
try {
|
||||
return new TextDecoder("gb18030").decode(bytes);
|
||||
} catch (err2) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isUtf8Bytes(bytes) {
|
||||
for (let i = 0; i < bytes.length;) {
|
||||
const c = bytes[i];
|
||||
let n = 0;
|
||||
let cp = 0;
|
||||
|
||||
if (c < 0x80) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if ((c & 0xe0) === 0xc0) {
|
||||
n = 2;
|
||||
cp = c & 0x1f;
|
||||
} else if ((c & 0xf0) === 0xe0) {
|
||||
n = 3;
|
||||
cp = c & 0x0f;
|
||||
} else if ((c & 0xf8) === 0xf0) {
|
||||
n = 4;
|
||||
cp = c & 0x07;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (i + n > bytes.length) return false;
|
||||
for (let j = 1; j < n; j++) {
|
||||
const t = bytes[i + j];
|
||||
if ((t & 0xc0) !== 0x80) return false;
|
||||
cp = (cp << 6) | (t & 0x3f);
|
||||
}
|
||||
if ((n === 2 && cp < 0x80) ||
|
||||
(n === 3 && cp < 0x800) ||
|
||||
(n === 4 && (cp < 0x10000 || cp > 0x10ffff)) ||
|
||||
(cp >= 0xd800 && cp <= 0xdfff)) return false;
|
||||
i += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function displayName(item) {
|
||||
return item.displayName || decodeFsText(item.name);
|
||||
}
|
||||
|
||||
function displayPath(path) {
|
||||
return decodeFsText(path);
|
||||
}
|
||||
|
||||
async function fetchText(path, params) {
|
||||
const response = await request(path, params, { method: "GET" });
|
||||
return {
|
||||
@@ -209,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"
|
||||
};
|
||||
@@ -252,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) }));
|
||||
}
|
||||
@@ -267,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;
|
||||
@@ -356,8 +503,8 @@ function isPreviewableImage(item) {
|
||||
|
||||
function openImagePreview(item) {
|
||||
if (busy) return;
|
||||
imagePreviewNameEl.textContent = item.name;
|
||||
imagePreviewNameEl.title = item.name;
|
||||
imagePreviewNameEl.textContent = displayName(item);
|
||||
imagePreviewNameEl.title = displayName(item);
|
||||
imagePreviewEl.src = "/fs?path=" + encodeURIComponent(item.path);
|
||||
imagePreviewOverlayEl.hidden = false;
|
||||
imagePreviewCloseBtn.focus();
|
||||
@@ -399,8 +546,8 @@ function requestCloseTextEditor() {
|
||||
function showTextEditor(item, text, version) {
|
||||
textEditorPath = item.path;
|
||||
textEditorVersion = version;
|
||||
textEditorPathEl.textContent = item.path;
|
||||
textEditorPathEl.title = item.path;
|
||||
textEditorPathEl.textContent = displayPath(item.path);
|
||||
textEditorPathEl.title = displayPath(item.path);
|
||||
textEditorEl.value = text;
|
||||
textEditorOriginal = textEditorEl.value;
|
||||
textEditorOverlayEl.hidden = false;
|
||||
@@ -413,8 +560,8 @@ async function openTextEditor(item) {
|
||||
textEditorPath = item.path;
|
||||
textEditorVersion = null;
|
||||
textEditorOriginal = "";
|
||||
textEditorPathEl.textContent = item.path;
|
||||
textEditorPathEl.title = item.path;
|
||||
textEditorPathEl.textContent = displayPath(item.path);
|
||||
textEditorPathEl.title = displayPath(item.path);
|
||||
textEditorEl.value = "";
|
||||
textEditorOverlayEl.hidden = false;
|
||||
setTextEditorBusy(true, t("loadingText"));
|
||||
@@ -468,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) {
|
||||
@@ -498,13 +644,19 @@ function clipboardTitle() {
|
||||
|
||||
function itemTitle(items) {
|
||||
if (!items.length) return "";
|
||||
return items.length === 1 ? items[0].name : t("selectedItems", { name: items[0].name, count: items.length });
|
||||
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("/");
|
||||
return pos >= 0 ? clean.slice(pos + 1) || "/" : clean;
|
||||
return displayPath(pos >= 0 ? clean.slice(pos + 1) || "/" : clean);
|
||||
}
|
||||
|
||||
function taskSubject(task) {
|
||||
@@ -532,6 +684,7 @@ function renderClipboard() {
|
||||
if (!hasClipboard) {
|
||||
pasteVerbEl.textContent = t("paste");
|
||||
pasteNameEl.textContent = "";
|
||||
pasteCountEl.textContent = "";
|
||||
pasteBtn.title = "";
|
||||
pasteBtn.disabled = true;
|
||||
clearClipboardBtn.disabled = true;
|
||||
@@ -540,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;
|
||||
@@ -629,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() {
|
||||
@@ -675,8 +852,8 @@ function render() {
|
||||
nameBtn.type = "button";
|
||||
const nameText = document.createElement("span");
|
||||
nameText.className = "name-text";
|
||||
nameText.textContent = item.name;
|
||||
nameBtn.title = item.path;
|
||||
nameText.textContent = displayName(item);
|
||||
nameBtn.title = displayPath(item.path);
|
||||
const iconImg = document.createElement("img");
|
||||
iconImg.className = "icon";
|
||||
iconImg.src = isParent ? "/icon-up.png" : item.type === "d" ? "/icon-folder.png" :
|
||||
@@ -726,23 +903,22 @@ 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 = cwd;
|
||||
pathEl.textContent = displayPath(cwd);
|
||||
savePath(cwd);
|
||||
refreshSpaces();
|
||||
entries = data.entries.sort((a, b) => {
|
||||
if (a.type === "d" && b.type !== "d") return -1;
|
||||
if (a.type !== "d" && b.type === "d") return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
return displayName(a).localeCompare(displayName(b));
|
||||
});
|
||||
selected.clear();
|
||||
focusedPath = null;
|
||||
@@ -752,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);
|
||||
@@ -783,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) {
|
||||
@@ -799,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") }));
|
||||
}
|
||||
|
||||
@@ -811,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 = [];
|
||||
|
||||
@@ -820,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 {
|
||||
@@ -847,17 +1026,26 @@ 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 };
|
||||
}
|
||||
|
||||
function conflictText(names) {
|
||||
if (!names.length) return "";
|
||||
const shown = names.slice(0, 4).join(", ");
|
||||
const shown = names.slice(0, 4).map(decodeFsText).join(", ");
|
||||
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";
|
||||
@@ -866,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");
|
||||
@@ -890,6 +1078,7 @@ function renderPendingOverlay(text) {
|
||||
if (pendingAbortController) pendingAbortController.abort();
|
||||
pendingAbortController = null;
|
||||
pendingOverlayText = "";
|
||||
pendingOverlayLabel = "";
|
||||
hideTaskOverlay();
|
||||
setStatus(t("cancelPrepare"));
|
||||
setBusy(false);
|
||||
@@ -899,13 +1088,15 @@ 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) {
|
||||
path = displayPath(path);
|
||||
element.title = path;
|
||||
element.textContent = path;
|
||||
if (element.scrollWidth <= element.clientWidth) return;
|
||||
@@ -948,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);
|
||||
@@ -981,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;
|
||||
}
|
||||
@@ -1025,7 +1219,7 @@ function renderTasks(tasks) {
|
||||
current.className += " task-current-plain";
|
||||
current.textContent = task.error;
|
||||
}
|
||||
else current.textContent = task.current || task.src;
|
||||
else current.textContent = displayPath(task.current || task.src);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "task-meta";
|
||||
@@ -1088,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) {
|
||||
@@ -1103,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;
|
||||
}
|
||||
@@ -1129,8 +1325,8 @@ function actionRename() {
|
||||
if (busy || loadingPath) return;
|
||||
const item = singleSelected();
|
||||
if (!item) return;
|
||||
const name = (prompt(t("renamePrompt"), item.name) || "").trim();
|
||||
if (!name || name === item.name) return;
|
||||
const name = (prompt(t("renamePrompt"), displayName(item)) || "").trim();
|
||||
if (!name || name === displayName(item)) return;
|
||||
runImmediateAction(t("rename"), () => api("/api/rename", { path: item.path, name }));
|
||||
}
|
||||
|
||||
@@ -1142,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() {
|
||||
@@ -1198,17 +1396,29 @@ filesEl.addEventListener("click", event => {
|
||||
let target = event.target;
|
||||
while (target && target !== row && !target.classList.contains("row-action")) target = target.parentNode;
|
||||
if (target === row && row.dataset.type !== "parent") return;
|
||||
const item = { path: row.dataset.path, name: row.dataset.name, type: row.dataset.type };
|
||||
const item = {
|
||||
path: row.dataset.path,
|
||||
name: row.dataset.name,
|
||||
type: row.dataset.type,
|
||||
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));
|
||||
@@ -1228,7 +1438,7 @@ async function init() {
|
||||
applyStaticText();
|
||||
const savedPath = readSavedPath();
|
||||
cwd = savedPath;
|
||||
pathEl.textContent = cwd;
|
||||
pathEl.textContent = displayPath(cwd);
|
||||
taskRefreshPath = savedPath;
|
||||
refreshSpaces();
|
||||
await pollTasks();
|
||||
|
||||
+284
-38
@@ -102,6 +102,7 @@ typedef struct task_completion {
|
||||
time_t elapsed;
|
||||
} task_completion_t;
|
||||
|
||||
static char *fs_path_value(char *path);
|
||||
#if FILEMGR_PIPELINE_COPY
|
||||
typedef struct copy_pipeline_slot {
|
||||
char *data;
|
||||
@@ -226,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;
|
||||
@@ -271,7 +337,7 @@ parse_paths(const char *raw, char ***out_paths, size_t *out_count) {
|
||||
return -1;
|
||||
}
|
||||
paths = tmp;
|
||||
if(!(paths[count] = strdup(line))) {
|
||||
if(!(paths[count] = fs_path_value(strdup(line)))) {
|
||||
free_paths(paths, count);
|
||||
free(copy);
|
||||
errno = ENOMEM;
|
||||
@@ -576,27 +642,122 @@ task_finish_bytes(file_task_t *task, const char *current) {
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
|
||||
static int
|
||||
utf8_decode_char(const char **ps, unsigned int *out) {
|
||||
const unsigned char *s = (const unsigned char *)*ps;
|
||||
unsigned char c = s[0];
|
||||
unsigned int cp;
|
||||
size_t n;
|
||||
size_t i;
|
||||
|
||||
if(c < 0x80) {
|
||||
*out = c;
|
||||
*ps += 1;
|
||||
return 0;
|
||||
}
|
||||
if((c & 0xe0) == 0xc0) {
|
||||
cp = c & 0x1f;
|
||||
n = 2;
|
||||
} else if((c & 0xf0) == 0xe0) {
|
||||
cp = c & 0x0f;
|
||||
n = 3;
|
||||
} else if((c & 0xf8) == 0xf0) {
|
||||
cp = c & 0x07;
|
||||
n = 4;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for(i = 1; i < n; i++) {
|
||||
unsigned char t = s[i];
|
||||
if(!t || (t & 0xc0) != 0x80) {
|
||||
return -1;
|
||||
}
|
||||
cp = (cp << 6) | (t & 0x3f);
|
||||
}
|
||||
if((n == 2 && cp < 0x80) ||
|
||||
(n == 3 && cp < 0x800) ||
|
||||
(n == 4 && (cp < 0x10000 || cp > 0x10ffff)) ||
|
||||
(cp >= 0xd800 && cp <= 0xdfff)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out = cp;
|
||||
*ps += n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *
|
||||
fs_path_value(char *path) {
|
||||
const char *p;
|
||||
char *out;
|
||||
size_t len;
|
||||
size_t pos = 0;
|
||||
int has_byte_token = 0;
|
||||
|
||||
if(!path) {
|
||||
return path;
|
||||
}
|
||||
for(p = path; *p;) {
|
||||
unsigned int cp;
|
||||
const char *next = p;
|
||||
|
||||
if(utf8_decode_char(&next, &cp)) {
|
||||
return path;
|
||||
}
|
||||
if(cp >= 0x80 && cp <= 0xff) {
|
||||
has_byte_token = 1;
|
||||
} else if(cp > 0xff) {
|
||||
return path;
|
||||
}
|
||||
p = next;
|
||||
}
|
||||
if(!has_byte_token) {
|
||||
return path;
|
||||
}
|
||||
|
||||
len = strlen(path);
|
||||
if(!(out = malloc(len + 1))) {
|
||||
return path;
|
||||
}
|
||||
for(p = path; *p;) {
|
||||
unsigned int cp;
|
||||
const char *next = p;
|
||||
|
||||
if(utf8_decode_char(&next, &cp)) {
|
||||
free(out);
|
||||
return path;
|
||||
}
|
||||
out[pos++] = (char)(unsigned char)cp;
|
||||
p = next;
|
||||
}
|
||||
out[pos] = 0;
|
||||
free(path);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int
|
||||
json_escape(strbuf_t *b, const char *s) {
|
||||
if(strbuf_append(b, "\"")) return -1;
|
||||
for(; *s; s++) {
|
||||
while(*s) {
|
||||
unsigned char c = (unsigned char)*s;
|
||||
switch(c) {
|
||||
case '"': if(strbuf_append(b, "\\\"")) return -1; break;
|
||||
case '\\': if(strbuf_append(b, "\\\\")) return -1; break;
|
||||
case '\b': if(strbuf_append(b, "\\b")) return -1; break;
|
||||
case '\f': if(strbuf_append(b, "\\f")) return -1; break;
|
||||
case '\n': if(strbuf_append(b, "\\n")) return -1; break;
|
||||
case '\r': if(strbuf_append(b, "\\r")) return -1; break;
|
||||
case '\t': if(strbuf_append(b, "\\t")) return -1; break;
|
||||
case '"': if(strbuf_append(b, "\\\"")) return -1; s++; break;
|
||||
case '\\': if(strbuf_append(b, "\\\\")) return -1; s++; break;
|
||||
case '\b': if(strbuf_append(b, "\\b")) return -1; s++; break;
|
||||
case '\f': if(strbuf_append(b, "\\f")) return -1; s++; break;
|
||||
case '\n': if(strbuf_append(b, "\\n")) return -1; s++; break;
|
||||
case '\r': if(strbuf_append(b, "\\r")) return -1; s++; break;
|
||||
case '\t': if(strbuf_append(b, "\\t")) return -1; s++; break;
|
||||
default:
|
||||
if(c < 0x20) {
|
||||
if(c < 0x20 || c >= 0x80) {
|
||||
if(strbuf_printf(b, "\\u%04x", c)) return -1;
|
||||
} else {
|
||||
if(strbuf_reserve(b, 1)) return -1;
|
||||
b->data[b->len++] = (char)c;
|
||||
b->data[b->len] = 0;
|
||||
}
|
||||
s++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2369,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);
|
||||
@@ -2406,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);
|
||||
@@ -2418,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;
|
||||
}
|
||||
|
||||
@@ -2443,6 +2657,7 @@ vfs_bytes(fsblkcnt_t blocks, unsigned long block_size) {
|
||||
return (unsigned long long)blocks * (unsigned long long)block_size;
|
||||
}
|
||||
|
||||
#ifdef __SCE__
|
||||
static int
|
||||
path_is_mounted(const char *path, const struct stat *st) {
|
||||
char parent[PATH_MAX];
|
||||
@@ -2456,6 +2671,7 @@ path_is_mounted(const char *path, const struct stat *st) {
|
||||
}
|
||||
return st->st_dev != parent_st.st_dev;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int
|
||||
task_target_path(file_task_t *task, const char *src, char *out, size_t size) {
|
||||
@@ -2737,7 +2953,7 @@ create_task_response(struct MHD_Connection *conn, task_op_t op,
|
||||
|
||||
static enum MHD_Result
|
||||
api_list(struct MHD_Connection *conn) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
struct stat st;
|
||||
@@ -2852,6 +3068,7 @@ api_tasks(struct MHD_Connection *conn) {
|
||||
|
||||
static enum MHD_Result
|
||||
api_space(struct MHD_Connection *conn) {
|
||||
#ifdef __SCE__
|
||||
static const struct {
|
||||
const char *label_key;
|
||||
const char *path;
|
||||
@@ -2865,14 +3082,16 @@ api_space(struct MHD_Connection *conn) {
|
||||
{"storageUsb", "/mnt/usb5"},
|
||||
{"storageUsb", "/mnt/usb6"},
|
||||
{"storageUsb", "/mnt/usb7"},
|
||||
{"storageM2", "/mnt/ex1"},
|
||||
{"storageM2", "/mnt/ext1"},
|
||||
{"storageExtended", "/mnt/ext0"},
|
||||
};
|
||||
char *current = query_value(conn, "path");
|
||||
#endif
|
||||
char *current = fs_path_value(query_value(conn, "path"));
|
||||
strbuf_t b = {0};
|
||||
int first = 1;
|
||||
|
||||
strbuf_append(&b, "{\"ok\":true,\"spaces\":[");
|
||||
#ifdef __SCE__
|
||||
for(size_t i = 0; i < sizeof(mounts) / sizeof(mounts[0]); i++) {
|
||||
struct statvfs vfs;
|
||||
struct stat st;
|
||||
@@ -2909,6 +3128,33 @@ api_space(struct MHD_Connection *conn) {
|
||||
strbuf_printf(&b, ",\"free\":%llu,\"total\":%llu,\"current\":%s}",
|
||||
free_bytes, total_bytes, is_current ? "true" : "false");
|
||||
}
|
||||
#else
|
||||
const char *paths[] = {"/", current && strcmp(current, "/") ? current : NULL};
|
||||
const char *labels[] = {"storageRoot", "storageCurrent"};
|
||||
for(size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
|
||||
struct statvfs vfs;
|
||||
unsigned long block_size;
|
||||
unsigned long long free_bytes;
|
||||
unsigned long long total_bytes;
|
||||
|
||||
if(!paths[i] || statvfs(paths[i], &vfs)) {
|
||||
continue;
|
||||
}
|
||||
block_size = vfs.f_frsize ? vfs.f_frsize : vfs.f_bsize;
|
||||
free_bytes = vfs_bytes(vfs.f_bavail, block_size);
|
||||
total_bytes = vfs_bytes(vfs.f_blocks, block_size);
|
||||
if(!first) {
|
||||
strbuf_append(&b, ",");
|
||||
}
|
||||
first = 0;
|
||||
strbuf_append(&b, "{\"label_key\":");
|
||||
json_escape(&b, labels[i]);
|
||||
strbuf_append(&b, ",\"path\":");
|
||||
json_escape(&b, paths[i]);
|
||||
strbuf_printf(&b, ",\"free\":%llu,\"total\":%llu,\"current\":%s}",
|
||||
free_bytes, total_bytes, i ? "true" : "false");
|
||||
}
|
||||
#endif
|
||||
free(current);
|
||||
strbuf_append(&b, "]}");
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
@@ -2956,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 = 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;
|
||||
@@ -2983,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 = 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;
|
||||
@@ -3002,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;
|
||||
@@ -3025,8 +3271,8 @@ api_delete(struct MHD_Connection *conn) {
|
||||
|
||||
static enum MHD_Result
|
||||
api_rename(struct MHD_Connection *conn) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *name = query_value(conn, "name");
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
char *name = fs_path_value(query_value(conn, "name"));
|
||||
char parent[PATH_MAX];
|
||||
char target[PATH_MAX];
|
||||
int ret;
|
||||
@@ -3053,8 +3299,8 @@ api_rename(struct MHD_Connection *conn) {
|
||||
|
||||
static enum MHD_Result
|
||||
api_mkdir(struct MHD_Connection *conn) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *name = query_value(conn, "name");
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
char *name = fs_path_value(query_value(conn, "name"));
|
||||
char target[PATH_MAX];
|
||||
int ret;
|
||||
|
||||
@@ -3081,7 +3327,7 @@ api_mkdir(struct MHD_Connection *conn) {
|
||||
|
||||
static enum MHD_Result
|
||||
api_text(struct MHD_Connection *conn) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
char *data;
|
||||
size_t size;
|
||||
struct stat st;
|
||||
@@ -3118,8 +3364,8 @@ api_text(struct MHD_Connection *conn) {
|
||||
|
||||
static enum MHD_Result
|
||||
api_text_create(struct MHD_Connection *conn) {
|
||||
char *path = query_value(conn, "path");
|
||||
char *name = query_value(conn, "name");
|
||||
char *path = fs_path_value(query_value(conn, "path"));
|
||||
char *name = fs_path_value(query_value(conn, "name"));
|
||||
char target[PATH_MAX];
|
||||
int fd = -1;
|
||||
int ret = -1;
|
||||
@@ -3213,7 +3459,7 @@ done:
|
||||
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 *path = fs_path_value(query_value(conn, "path"));
|
||||
char *expected = query_value(conn, "version");
|
||||
char *current = NULL;
|
||||
size_t current_size = 0;
|
||||
@@ -3288,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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user