mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 14:00:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259d0a6434 |
No files matched your search
@@ -13,7 +13,7 @@ ifeq ($(MAKECMDGOALS),)
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION_TAG := v0.3
|
||||
VERSION_TAG := v0.4
|
||||
TITLE_ID := FMGR88888
|
||||
PYTHON ?= python3
|
||||
STRIP ?= $(PS5_PAYLOAD_SDK)/bin/prospero-strip
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 139 B |
Binary file not shown.
|
After Width: | Height: | Size: 184 B |
+13
-7
@@ -49,6 +49,9 @@
|
||||
<tbody id="files"></tbody>
|
||||
</table>
|
||||
<div id="empty" class="empty" data-i18n="empty" hidden></div>
|
||||
<div id="contentLoading" class="content-loading" hidden>
|
||||
<div class="content-loading-text" data-i18n="readDir"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="status">
|
||||
@@ -70,19 +73,22 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="imagePreviewOverlay" class="text-editor-overlay" hidden>
|
||||
<section class="text-editor-panel image-preview-panel">
|
||||
<div class="image-preview-stage">
|
||||
<img id="imagePreview" class="image-preview" alt="">
|
||||
</div>
|
||||
<button id="imagePreviewCloseBtn" class="secondary" data-i18n="close"></button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="taskOverlay" class="task-overlay" hidden>
|
||||
<div class="task-panel">
|
||||
<div id="tasks" class="tasks"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="initLoading" class="init-loading" aria-hidden="true">
|
||||
<div class="loading-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="initLoading" class="init-loading" aria-hidden="true"></div>
|
||||
|
||||
<script src="/main.js"></script>
|
||||
</body>
|
||||
|
||||
+3
-1
@@ -61,6 +61,8 @@ window.WFM_LANG = {
|
||||
cancelTaskConfirm: "Cancel current {label} task?",
|
||||
cancelFailed: "Cancel failed: {error}",
|
||||
tasksPollFailed: "Failed to read task status: {error}",
|
||||
transferComplete: "{label} completed\n\nTotal time: {duration}\nTransferred: {size}",
|
||||
transferCompleteFiles: "{label} completed\n\nTotal time: {duration}\nTransferred: {size}\nFiles: {count}",
|
||||
renamePrompt: "New name",
|
||||
mkdirPrompt: "Folder name",
|
||||
newTextPrompt: "Text file name",
|
||||
@@ -84,7 +86,7 @@ window.WFM_LANG = {
|
||||
storageExtended: "Extended",
|
||||
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: "Target parent folder is not writable: {path}",
|
||||
err_target_parent_not_writable: "Current folder is not writable: {path}",
|
||||
err_target_check_failed: "Cannot check target path: {path}",
|
||||
err_target_path_too_long: "Target path is too long",
|
||||
err_space_check_failed: "Cannot read target free space: {path}",
|
||||
|
||||
+4
-2
@@ -41,7 +41,7 @@ window.WFM_LANG = {
|
||||
selectedItems: "{name} 等 {count} 项",
|
||||
countItems: "{count} 项",
|
||||
totalItems: "共 {count} 项",
|
||||
readDir: "读取目录...",
|
||||
readDir: "读取目录中...",
|
||||
actionBusy: "{label}...",
|
||||
taskCreated: "{label}任务已创建",
|
||||
actionDone: "{label}完成",
|
||||
@@ -61,6 +61,8 @@ window.WFM_LANG = {
|
||||
cancelTaskConfirm: "取消当前{label}任务?",
|
||||
cancelFailed: "取消失败: {error}",
|
||||
tasksPollFailed: "任务状态读取失败: {error}",
|
||||
transferComplete: "{label}完成\n\n总耗时:{duration}\n传输大小:{size}",
|
||||
transferCompleteFiles: "{label}完成\n\n总耗时:{duration}\n传输大小:{size}\n文件数量:{count}",
|
||||
renamePrompt: "新名称",
|
||||
mkdirPrompt: "目录名",
|
||||
newTextPrompt: "文本文件名",
|
||||
@@ -84,7 +86,7 @@ window.WFM_LANG = {
|
||||
storageExtended: "扩展存储",
|
||||
err_target_dir_not_writable: "目标目录不可写: {path}",
|
||||
err_target_file_not_writable: "目标文件不可写: {path}",
|
||||
err_target_parent_not_writable: "目标父目录不可写: {path}",
|
||||
err_target_parent_not_writable: "当前目录不可写: {path}",
|
||||
err_target_check_failed: "无法检查目标路径: {path}",
|
||||
err_target_path_too_long: "目标路径过长",
|
||||
err_space_check_failed: "无法读取目标剩余空间: {path}",
|
||||
|
||||
+54
-44
@@ -23,41 +23,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #111316;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.loading-dots span {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0 6px;
|
||||
border-radius: 50%;
|
||||
background: #8fa1b3;
|
||||
animation: loading-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(2) {
|
||||
animation-delay: .12s;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(3) {
|
||||
animation-delay: .24s;
|
||||
}
|
||||
|
||||
@keyframes loading-pulse {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
opacity: .35;
|
||||
transform: scale(.75);
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
color: #aab4be;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -268,14 +235,37 @@ button.primary:hover {
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.content.loading {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.content-loading {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(4, 6, 8, .72);
|
||||
}
|
||||
|
||||
.content-loading-text {
|
||||
color: #aab4be;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
@@ -354,6 +344,10 @@ tr.selected {
|
||||
background: #1d2d35;
|
||||
}
|
||||
|
||||
tr[data-type="parent"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.select-hit {
|
||||
min-height: 78px;
|
||||
width: 100%;
|
||||
@@ -373,14 +367,6 @@ input[type="checkbox"] {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex: 0 0 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -509,6 +495,30 @@ input[type="checkbox"] {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.image-preview-stage {
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.image-preview-panel > button {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.task-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
||||
+133
-52
@@ -10,17 +10,23 @@ let pendingAbortController = null;
|
||||
let taskRefreshPath = "/";
|
||||
let hoverPaused = false;
|
||||
let hoverResumeTimer = 0;
|
||||
let hoveredRow = null;
|
||||
let loadingPath = null;
|
||||
let directoryLoadingTimer = 0;
|
||||
let lastCompletionId = null;
|
||||
let textEditorPath = null;
|
||||
let textEditorVersion = null;
|
||||
let textEditorOriginal = "";
|
||||
let textEditorBusy = false;
|
||||
let L = {};
|
||||
|
||||
const APP_VERSION = "v0.3";
|
||||
const APP_VERSION = "v0.4";
|
||||
const LAST_PATH_KEY = "ps5-web-file-mgr:last-path";
|
||||
const DIRECTORY_LOADING_DELAY = 250;
|
||||
|
||||
const filesEl = document.getElementById("files");
|
||||
const contentEl = document.getElementById("content");
|
||||
const contentLoadingEl = document.getElementById("contentLoading");
|
||||
const emptyEl = document.getElementById("empty");
|
||||
const pathEl = document.getElementById("path");
|
||||
const spaceInfoEl = document.getElementById("spaceInfo");
|
||||
@@ -43,6 +49,9 @@ const textEditorStatusEl = document.getElementById("textEditorStatus");
|
||||
const textEditorCloseBtn = document.getElementById("textEditorCloseBtn");
|
||||
const textEditorSaveBtn = document.getElementById("textEditorSaveBtn");
|
||||
const newTextBtn = document.getElementById("newTextBtn");
|
||||
const imagePreviewOverlayEl = document.getElementById("imagePreviewOverlay");
|
||||
const imagePreviewEl = document.getElementById("imagePreview");
|
||||
const imagePreviewCloseBtn = document.getElementById("imagePreviewCloseBtn");
|
||||
|
||||
function chooseLanguage() {
|
||||
const langs = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language || ""];
|
||||
@@ -53,6 +62,7 @@ function chooseLanguage() {
|
||||
function loadLanguage() {
|
||||
return new Promise(resolve => {
|
||||
const lang = chooseLanguage();
|
||||
initLoadingEl.textContent = lang === "zh" ? "加载中..." : "Loading...";
|
||||
const script = document.createElement("script");
|
||||
script.src = "/lang-" + lang + ".js";
|
||||
script.onload = () => {
|
||||
@@ -314,6 +324,22 @@ function isEditableText(item) {
|
||||
/\.(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 isPreviewableImage(item) {
|
||||
return item.type === "-" && /\.(png|jpe?g|gif|bmp|webp)$/i.test(item.name);
|
||||
}
|
||||
|
||||
function openImagePreview(item) {
|
||||
if (busy) return;
|
||||
imagePreviewEl.src = "/fs?path=" + encodeURIComponent(item.path);
|
||||
imagePreviewOverlayEl.hidden = false;
|
||||
imagePreviewCloseBtn.focus();
|
||||
}
|
||||
|
||||
function closeImagePreview() {
|
||||
imagePreviewOverlayEl.hidden = true;
|
||||
imagePreviewEl.removeAttribute("src");
|
||||
}
|
||||
|
||||
function setTextEditorBusy(value, status) {
|
||||
textEditorBusy = value;
|
||||
textEditorEl.disabled = value;
|
||||
@@ -400,9 +426,11 @@ async function saveTextEditor() {
|
||||
}
|
||||
|
||||
async function actionNewText() {
|
||||
if (busy) return;
|
||||
if (busy || loadingPath) return;
|
||||
const dir = cwd;
|
||||
const name = (prompt(t("newTextPrompt"), "") || "").trim();
|
||||
const input = prompt(t("newTextPrompt"));
|
||||
if (input === null) return;
|
||||
const name = input.trim();
|
||||
if (!name) return;
|
||||
const item = { name, path: pathJoin(dir, name), type: "-" };
|
||||
|
||||
@@ -467,6 +495,7 @@ function renderTaskName(el, task) {
|
||||
|
||||
function renderClipboard() {
|
||||
const hasClipboard = Boolean(clipboard && clipboard.items.length);
|
||||
const locked = busy || contentEl.classList.contains("loading");
|
||||
pasteBtn.hidden = !hasClipboard;
|
||||
clearClipboardBtn.hidden = !hasClipboard;
|
||||
if (!hasClipboard) {
|
||||
@@ -482,8 +511,8 @@ function renderClipboard() {
|
||||
pasteVerbEl.textContent = clipboard.op === "move" ? t("move") : t("paste");
|
||||
pasteNameEl.textContent = title;
|
||||
pasteBtn.title = t("pasteTitle", { label: clipboard.op === "move" ? t("move") : t("paste"), name: title, path: cwd });
|
||||
pasteBtn.disabled = busy;
|
||||
clearClipboardBtn.disabled = busy;
|
||||
pasteBtn.disabled = locked;
|
||||
clearClipboardBtn.disabled = locked;
|
||||
}
|
||||
|
||||
function singleSelected() {
|
||||
@@ -493,12 +522,15 @@ function singleSelected() {
|
||||
|
||||
function updateButtons() {
|
||||
const items = selectedEntries();
|
||||
document.getElementById("copyBtn").disabled = busy || items.length === 0;
|
||||
document.getElementById("moveBtn").disabled = busy || items.length === 0;
|
||||
document.getElementById("renameBtn").disabled = busy || items.length !== 1;
|
||||
document.getElementById("deleteBtn").disabled = busy || items.length === 0;
|
||||
document.getElementById("mkdirBtn").disabled = busy;
|
||||
newTextBtn.disabled = busy;
|
||||
const locked = busy || contentEl.classList.contains("loading");
|
||||
document.getElementById("copyBtn").disabled = locked || items.length === 0;
|
||||
document.getElementById("moveBtn").disabled = locked || items.length === 0;
|
||||
document.getElementById("renameBtn").disabled = locked || items.length !== 1;
|
||||
document.getElementById("deleteBtn").disabled = locked || items.length === 0;
|
||||
document.getElementById("refreshBtn").disabled = locked;
|
||||
document.getElementById("mkdirBtn").disabled = locked;
|
||||
newTextBtn.disabled = locked;
|
||||
selectAllEl.disabled = locked;
|
||||
selectAllEl.checked = entries.length > 0 && items.length === entries.length;
|
||||
selectAllEl.indeterminate = items.length > 0 && items.length < entries.length;
|
||||
renderClipboard();
|
||||
@@ -513,18 +545,17 @@ function focusPath(path) {
|
||||
if (next) next.classList.add("focused");
|
||||
}
|
||||
|
||||
function hoverPath(path) {
|
||||
function hoverRow(row) {
|
||||
if (hoverPaused) return;
|
||||
const old = filesEl.querySelector("tr.hovered");
|
||||
if (old && old.dataset.path === path) return;
|
||||
const next = path ? filesEl.querySelector("tr[data-path=\"" + cssEscape(path) + "\"]") : null;
|
||||
if (old) old.classList.remove("hovered");
|
||||
if (next) next.classList.add("hovered");
|
||||
if (hoveredRow === row) return;
|
||||
clearHoverPath();
|
||||
hoveredRow = row;
|
||||
if (hoveredRow) hoveredRow.classList.add("hovered");
|
||||
}
|
||||
|
||||
function clearHoverPath() {
|
||||
const old = filesEl.querySelector("tr.hovered");
|
||||
if (old) old.classList.remove("hovered");
|
||||
if (hoveredRow) hoveredRow.classList.remove("hovered");
|
||||
hoveredRow = null;
|
||||
}
|
||||
|
||||
function cssEscape(value) {
|
||||
@@ -538,11 +569,6 @@ function appendChildren(parent) {
|
||||
}
|
||||
}
|
||||
|
||||
function setClass(el, name, enabled) {
|
||||
if (enabled) el.classList.add(name);
|
||||
else el.classList.remove(name);
|
||||
}
|
||||
|
||||
function bindPress(el, fn) {
|
||||
let handled = false;
|
||||
const run = event => {
|
||||
@@ -576,8 +602,10 @@ function togglePath(path, checked) {
|
||||
}
|
||||
|
||||
function render() {
|
||||
hoveredRow = null;
|
||||
filesEl.innerHTML = "";
|
||||
emptyEl.hidden = entries.length !== 0;
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
const rows = cwd === "/" ? entries : [{
|
||||
name: "..",
|
||||
@@ -592,8 +620,10 @@ function render() {
|
||||
const isParent = item.type === "parent";
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.path = item.path;
|
||||
setClass(tr, "selected", !isParent && selected.has(item.path));
|
||||
setClass(tr, "focused", focusedPath === item.path);
|
||||
tr.dataset.name = item.name;
|
||||
tr.dataset.type = item.type;
|
||||
tr.className = (!isParent && selected.has(item.path) ? "selected " : "") +
|
||||
(focusedPath === item.path ? "focused" : "");
|
||||
|
||||
const selectTd = document.createElement("td");
|
||||
selectTd.className = "select-cell";
|
||||
@@ -603,8 +633,6 @@ function render() {
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = selected.has(item.path);
|
||||
checkbox.addEventListener("focus", () => focusPath(item.path));
|
||||
checkbox.addEventListener("change", () => togglePath(item.path, checkbox.checked));
|
||||
label.appendChild(checkbox);
|
||||
selectTd.appendChild(label);
|
||||
}
|
||||
@@ -614,24 +642,18 @@ function render() {
|
||||
const nameBtn = document.createElement("button");
|
||||
nameBtn.className = "row-action";
|
||||
nameBtn.type = "button";
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "icon";
|
||||
const iconImg = document.createElement("img");
|
||||
iconImg.src = isParent ? "/icon-up.png" : item.type === "d" ? "/icon-folder.png" : "/icon-file.png";
|
||||
iconImg.alt = "";
|
||||
icon.appendChild(iconImg);
|
||||
const nameText = document.createElement("span");
|
||||
nameText.className = "name-text";
|
||||
nameText.textContent = item.name;
|
||||
nameBtn.title = item.path;
|
||||
nameBtn.addEventListener("focus", () => focusPath(item.path));
|
||||
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);
|
||||
const iconImg = document.createElement("img");
|
||||
iconImg.className = "icon";
|
||||
iconImg.src = isParent ? "/icon-up.png" : item.type === "d" ? "/icon-folder.png" :
|
||||
isPreviewableImage(item) ? "/icon-image.png" :
|
||||
isEditableText(item) ? "/icon-file.png" : "/icon-generic.png";
|
||||
iconImg.alt = "";
|
||||
nameBtn.appendChild(iconImg);
|
||||
nameBtn.appendChild(nameText);
|
||||
nameTd.appendChild(nameBtn);
|
||||
|
||||
appendChildren(
|
||||
@@ -643,8 +665,9 @@ function render() {
|
||||
classCell("time-col", formatTime(item.mtime)),
|
||||
classCell("mode-col", isParent ? "" : "0" + Number(item.mode).toString(8))
|
||||
);
|
||||
filesEl.appendChild(tr);
|
||||
fragment.appendChild(tr);
|
||||
}
|
||||
filesEl.appendChild(fragment);
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
@@ -670,7 +693,14 @@ function classCell(className, text) {
|
||||
|
||||
async function load(path, scrollTop, force) {
|
||||
const shouldScrollTop = scrollTop !== false;
|
||||
if (busy && !force) return;
|
||||
if ((busy && !force) || loadingPath) return;
|
||||
loadingPath = path;
|
||||
directoryLoadingTimer = setTimeout(() => {
|
||||
contentLoadingEl.style.top = contentEl.scrollTop + "px";
|
||||
contentLoadingEl.hidden = false;
|
||||
contentEl.classList.add("loading");
|
||||
updateButtons();
|
||||
}, DIRECTORY_LOADING_DELAY);
|
||||
try {
|
||||
setStatus(t("readDir"));
|
||||
const data = await api("/api/list", { path });
|
||||
@@ -690,6 +720,12 @@ async function load(path, scrollTop, force) {
|
||||
setStatus(t("totalItems", { count: entries.length }));
|
||||
} catch (err) {
|
||||
setStatus(err.message);
|
||||
} finally {
|
||||
clearTimeout(directoryLoadingTimer);
|
||||
contentEl.classList.remove("loading");
|
||||
contentLoadingEl.hidden = true;
|
||||
loadingPath = null;
|
||||
updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,7 +764,7 @@ async function runImmediateAction(label, fn) {
|
||||
}
|
||||
|
||||
function setClipboard(op) {
|
||||
if (busy || selected.size === 0) return;
|
||||
if (busy || loadingPath || selected.size === 0) return;
|
||||
const items = selectedEntries().map(item => ({ path: item.path, name: item.name, type: item.type }));
|
||||
if (items.length === 0) return;
|
||||
clipboard = { op, items };
|
||||
@@ -738,6 +774,7 @@ function setClipboard(op) {
|
||||
}
|
||||
|
||||
function clearClipboard() {
|
||||
if (busy || loadingPath) return;
|
||||
clipboard = null;
|
||||
renderClipboard();
|
||||
setStatus(t("clipboardCleared"));
|
||||
@@ -839,7 +876,7 @@ function showPendingOverlay(text) {
|
||||
}
|
||||
|
||||
async function actionPaste() {
|
||||
if (busy || !clipboard || clipboard.items.length === 0) return;
|
||||
if (busy || loadingPath || !clipboard || clipboard.items.length === 0) return;
|
||||
const op = clipboard.op;
|
||||
const label = clipboard.op === "move" ? t("move") : t("copy");
|
||||
const check = validatePasteTarget();
|
||||
@@ -971,6 +1008,18 @@ async function pollTasks() {
|
||||
try {
|
||||
const data = await api("/api/tasks");
|
||||
const tasks = data.tasks || [];
|
||||
const completion = data.completion;
|
||||
if (lastCompletionId === null) {
|
||||
lastCompletionId = completion ? completion.id : 0;
|
||||
} else if (completion && completion.id !== lastCompletionId) {
|
||||
lastCompletionId = completion.id;
|
||||
alert(t(completion.file_count ? "transferCompleteFiles" : "transferComplete", {
|
||||
label: opLabel(completion.op),
|
||||
duration: formatDuration(Number(completion.elapsed || 0)),
|
||||
size: formatBytes(completion.total, true),
|
||||
count: completion.file_count
|
||||
}));
|
||||
}
|
||||
const wasBusy = busy;
|
||||
renderTasks(tasks);
|
||||
let shouldRefresh = false;
|
||||
@@ -1010,7 +1059,7 @@ function actionMove() {
|
||||
}
|
||||
|
||||
function actionRename() {
|
||||
if (busy) return;
|
||||
if (busy || loadingPath) return;
|
||||
const item = singleSelected();
|
||||
if (!item) return;
|
||||
const name = (prompt(t("renamePrompt"), item.name) || "").trim();
|
||||
@@ -1019,7 +1068,7 @@ function actionRename() {
|
||||
}
|
||||
|
||||
function actionDelete() {
|
||||
if (busy || selected.size === 0) return;
|
||||
if (busy || loadingPath || selected.size === 0) return;
|
||||
const items = selectedEntries();
|
||||
const paths = items.map(item => item.path);
|
||||
const target = itemTitle(items);
|
||||
@@ -1040,7 +1089,7 @@ function actionExit() {
|
||||
|
||||
document.getElementById("refreshBtn").addEventListener("click", () => load(cwd, false));
|
||||
document.getElementById("mkdirBtn").addEventListener("click", () => {
|
||||
if (busy) return;
|
||||
if (busy || loadingPath) return;
|
||||
const path = cwd;
|
||||
const name = (prompt(t("mkdirPrompt")) || "").trim();
|
||||
if (!name) return;
|
||||
@@ -1056,16 +1105,48 @@ exitBtn.addEventListener("click", actionExit);
|
||||
textEditorCloseBtn.addEventListener("click", requestCloseTextEditor);
|
||||
textEditorSaveBtn.addEventListener("click", saveTextEditor);
|
||||
newTextBtn.addEventListener("click", actionNewText);
|
||||
imagePreviewCloseBtn.addEventListener("click", closeImagePreview);
|
||||
textEditorOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === textEditorOverlayEl) requestCloseTextEditor();
|
||||
});
|
||||
imagePreviewOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === imagePreviewOverlayEl) closeImagePreview();
|
||||
});
|
||||
contentEl.addEventListener("click", event => {
|
||||
if (!loadingPath) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, true);
|
||||
filesEl.addEventListener("focusin", event => {
|
||||
const row = findRowFromTarget(event.target);
|
||||
if (row) focusPath(row.dataset.path);
|
||||
});
|
||||
filesEl.addEventListener("change", event => {
|
||||
const row = findRowFromTarget(event.target);
|
||||
if (row && event.target.type === "checkbox") togglePath(row.dataset.path, event.target.checked);
|
||||
});
|
||||
filesEl.addEventListener("click", event => {
|
||||
const row = findRowFromTarget(event.target);
|
||||
if (!row) return;
|
||||
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 };
|
||||
focusPath(item.path);
|
||||
if (item.type === "parent" || item.type === "d") load(item.path);
|
||||
else if (isPreviewableImage(item)) openImagePreview(item);
|
||||
else if (isEditableText(item)) openTextEditor(item);
|
||||
else togglePath(item.path, !selected.has(item.path));
|
||||
});
|
||||
selectAllEl.addEventListener("change", () => {
|
||||
selected.clear();
|
||||
if (selectAllEl.checked) entries.forEach(item => selected.add(item.path));
|
||||
render();
|
||||
});
|
||||
contentEl.addEventListener("mousemove", event => {
|
||||
const row = findRowFromTarget(event.target);
|
||||
hoverPath(row ? row.getAttribute("data-path") : null);
|
||||
hoverRow(findRowFromTarget(event.target));
|
||||
});
|
||||
contentEl.addEventListener("mouseleave", () => hoverPath(null));
|
||||
contentEl.addEventListener("mouseleave", () => hoverRow(null));
|
||||
contentEl.addEventListener("scroll", () => {
|
||||
hoverPaused = true;
|
||||
clearHoverPath();
|
||||
|
||||
+187
-183
@@ -27,6 +27,7 @@
|
||||
|
||||
#define COPY_BUFFER_SIZE (8 * 1024 * 1024)
|
||||
#define TEXT_FILE_MAX_SIZE (1024 * 1024)
|
||||
#define TRANSFER_ALERT_THRESHOLD (10 * 60)
|
||||
typedef struct strbuf {
|
||||
char *data;
|
||||
size_t len;
|
||||
@@ -56,9 +57,11 @@ typedef struct file_task {
|
||||
char current[PATH_MAX];
|
||||
char error[160];
|
||||
char error_code[64];
|
||||
char error_arg[PATH_MAX];
|
||||
char error_arg[PATH_MAX + 96];
|
||||
char **srcs;
|
||||
size_t src_count;
|
||||
size_t file_count;
|
||||
size_t dir_count;
|
||||
unsigned long long total;
|
||||
unsigned long long done;
|
||||
unsigned long long speed;
|
||||
@@ -72,9 +75,18 @@ typedef struct file_task {
|
||||
struct file_task *next;
|
||||
} file_task_t;
|
||||
|
||||
typedef struct task_completion {
|
||||
unsigned long id;
|
||||
task_op_t op;
|
||||
unsigned long long total;
|
||||
size_t file_count;
|
||||
time_t elapsed;
|
||||
} task_completion_t;
|
||||
|
||||
static pthread_mutex_t g_tasks_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static file_task_t *g_tasks = NULL;
|
||||
static unsigned long g_next_task_id = 1;
|
||||
static task_completion_t g_last_completion;
|
||||
|
||||
static int
|
||||
strbuf_reserve(strbuf_t *b, size_t extra) {
|
||||
@@ -766,14 +778,18 @@ send_text_file(struct MHD_Connection *conn, char *data, size_t size,
|
||||
}
|
||||
|
||||
static int count_path_bytes(file_task_t *task, const char *path,
|
||||
const char *display,
|
||||
unsigned long long *total);
|
||||
const char *display, const char *target,
|
||||
unsigned long long *total,
|
||||
unsigned long long *reclaimable,
|
||||
size_t *file_count, size_t *dir_count);
|
||||
static int task_target_path(file_task_t *task, const char *src,
|
||||
char *out, size_t size);
|
||||
|
||||
static int
|
||||
count_dir_bytes(file_task_t *task, const char *path, const char *display,
|
||||
unsigned long long *total) {
|
||||
const char *target, unsigned long long *total,
|
||||
unsigned long long *reclaimable, size_t *file_count,
|
||||
size_t *dir_count) {
|
||||
DIR *dir = opendir(path);
|
||||
struct dirent *entry;
|
||||
int ret = -1;
|
||||
@@ -784,6 +800,7 @@ count_dir_bytes(file_task_t *task, const char *path, const char *display,
|
||||
while((entry = readdir(dir))) {
|
||||
char child[PATH_MAX];
|
||||
char display_child[PATH_MAX];
|
||||
char target_child[PATH_MAX];
|
||||
if(!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
@@ -793,7 +810,11 @@ count_dir_bytes(file_task_t *task, const char *path, const char *display,
|
||||
if(path_join(child, sizeof(child), path, entry->d_name) ||
|
||||
(display && path_join(display_child, sizeof(display_child), display,
|
||||
entry->d_name)) ||
|
||||
count_path_bytes(task, child, display ? display_child : NULL, total)) {
|
||||
(target && path_join(target_child, sizeof(target_child), target,
|
||||
entry->d_name)) ||
|
||||
count_path_bytes(task, child, display ? display_child : NULL,
|
||||
target ? target_child : NULL, total, reclaimable,
|
||||
file_count, dir_count)) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
@@ -805,8 +826,12 @@ done:
|
||||
|
||||
static int
|
||||
count_path_bytes(file_task_t *task, const char *path, const char *display,
|
||||
unsigned long long *total) {
|
||||
const char *target, unsigned long long *total,
|
||||
unsigned long long *reclaimable, size_t *file_count,
|
||||
size_t *dir_count) {
|
||||
struct stat st;
|
||||
struct stat target_st;
|
||||
int target_exists = 0;
|
||||
|
||||
if(task && task_cancel_requested(task)) {
|
||||
return -1;
|
||||
@@ -818,11 +843,25 @@ count_path_bytes(file_task_t *task, const char *path, const char *display,
|
||||
if(lstat(path, &st)) {
|
||||
return -1;
|
||||
}
|
||||
if(target) {
|
||||
if(!lstat(target, &target_st)) {
|
||||
target_exists = 1;
|
||||
} else if(errno != ENOENT) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if(S_ISDIR(st.st_mode)) {
|
||||
return count_dir_bytes(task, path, display, total);
|
||||
if(dir_count) (*dir_count)++;
|
||||
return count_dir_bytes(task, path, display,
|
||||
target_exists && S_ISDIR(target_st.st_mode) ? target : NULL,
|
||||
total, reclaimable, file_count, dir_count);
|
||||
}
|
||||
if(S_ISREG(st.st_mode)) {
|
||||
*total += (unsigned long long)st.st_size;
|
||||
if(reclaimable && target_exists && S_ISREG(target_st.st_mode)) {
|
||||
*reclaimable += (unsigned long long)target_st.st_size;
|
||||
}
|
||||
if(file_count) (*file_count)++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -988,6 +1027,7 @@ done:
|
||||
static int copy_path(file_task_t *task, const char *src, const char *dst);
|
||||
static int remove_path(file_task_t *task, const char *path);
|
||||
static int check_remove_path_writable(file_task_t *task, const char *path);
|
||||
static int mode_access(const char *path, int mode);
|
||||
|
||||
static int
|
||||
check_remove_entry_writable(const char *path) {
|
||||
@@ -996,7 +1036,7 @@ check_remove_entry_writable(const char *path) {
|
||||
if(path_dirname(path, parent, sizeof(parent))) {
|
||||
return -1;
|
||||
}
|
||||
return access(parent, W_OK | X_OK);
|
||||
return mode_access(parent, W_OK | X_OK);
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -1005,7 +1045,7 @@ check_remove_dir_writable(file_task_t *task, const char *path) {
|
||||
struct dirent *entry;
|
||||
int ret = -1;
|
||||
|
||||
if(access(path, R_OK | W_OK | X_OK)) {
|
||||
if(mode_access(path, R_OK | W_OK | X_OK)) {
|
||||
task_update(task, TASK_RUNNING, path, 0, NULL);
|
||||
return -1;
|
||||
}
|
||||
@@ -1358,6 +1398,73 @@ set_error_detail(char *error, size_t error_size, char *code, size_t code_size,
|
||||
snprintf(arg, arg_size, "%s", path);
|
||||
}
|
||||
|
||||
static void
|
||||
set_permission_error_detail(char *error, size_t error_size,
|
||||
char *code, size_t code_size,
|
||||
char *arg, size_t arg_size,
|
||||
const char *error_code, const char *message,
|
||||
const char *path) {
|
||||
struct stat st;
|
||||
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
error_code, message, path);
|
||||
if(!stat(path, &st)) {
|
||||
snprintf(arg, arg_size, "%s (mode=%04o, uid=%lu, gid=%lu)", path,
|
||||
(unsigned int)(st.st_mode & 07777),
|
||||
(unsigned long)st.st_uid, (unsigned long)st.st_gid);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
mode_access_stat(const struct stat *st, int mode) {
|
||||
mode_t allowed;
|
||||
uid_t uid = geteuid();
|
||||
|
||||
if(uid == 0) {
|
||||
if(!(mode & X_OK) || !S_ISREG(st->st_mode) ||
|
||||
(st->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) {
|
||||
return 0;
|
||||
}
|
||||
} else if(uid == st->st_uid) {
|
||||
allowed = (st->st_mode >> 6) & 7;
|
||||
if((allowed & mode) == (mode_t)mode) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
gid_t gid = getegid();
|
||||
int group_match = gid == st->st_gid;
|
||||
|
||||
if(!group_match) {
|
||||
int count = getgroups(0, NULL);
|
||||
gid_t *groups = count > 0 ? malloc((size_t)count * sizeof(*groups)) : NULL;
|
||||
|
||||
if(groups && getgroups(count, groups) == count) {
|
||||
int i;
|
||||
for(i = 0; i < count; i++) {
|
||||
if(groups[i] == st->st_gid) {
|
||||
group_match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(groups);
|
||||
}
|
||||
allowed = group_match ? (st->st_mode >> 3) & 7 : st->st_mode & 7;
|
||||
if((allowed & mode) == (mode_t)mode) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
mode_access(const char *path, int mode) {
|
||||
struct stat st;
|
||||
|
||||
return stat(path, &st) ? -1 : mode_access_stat(&st, mode);
|
||||
}
|
||||
|
||||
static int
|
||||
check_target_writable(const char *target, char *error, size_t error_size,
|
||||
char *code, size_t code_size, char *arg, size_t arg_size) {
|
||||
@@ -1366,23 +1473,20 @@ check_target_writable(const char *target, char *error, size_t error_size,
|
||||
|
||||
if(!stat(target, &st)) {
|
||||
if(S_ISDIR(st.st_mode)) {
|
||||
if(access(target, W_OK | X_OK)) {
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
"target_dir_not_writable", "target directory is not writable", target);
|
||||
if(mode_access_stat(&st, W_OK | X_OK)) {
|
||||
set_permission_error_detail(error, error_size, code, code_size,
|
||||
arg, arg_size, "target_dir_not_writable",
|
||||
"target directory is not writable", target);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
if(access(target, W_OK)) {
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
"target_file_not_writable", "target file is not writable", target);
|
||||
return -1;
|
||||
}
|
||||
if(path_dirname(target, parent, sizeof(parent)) ||
|
||||
access(parent, W_OK | X_OK)) {
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
"target_parent_not_writable", "target parent directory is not writable", target);
|
||||
if(mode_access_stat(&st, W_OK)) {
|
||||
set_permission_error_detail(error, error_size, code, code_size,
|
||||
arg, arg_size, "target_file_not_writable",
|
||||
"target file is not writable", target);
|
||||
return -1;
|
||||
}
|
||||
goto check_parent;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1392,10 +1496,17 @@ check_target_writable(const char *target, char *error, size_t error_size,
|
||||
"target_check_failed", "cannot check target path", target);
|
||||
return -1;
|
||||
}
|
||||
if(path_dirname(target, parent, sizeof(parent)) ||
|
||||
access(parent, W_OK | X_OK)) {
|
||||
|
||||
check_parent:
|
||||
if(path_dirname(target, parent, sizeof(parent))) {
|
||||
set_error_detail(error, error_size, code, code_size, arg, arg_size,
|
||||
"target_parent_not_writable", "target parent directory is not writable", target);
|
||||
"target_check_failed", "cannot check target path", target);
|
||||
return -1;
|
||||
}
|
||||
if(mode_access(parent, W_OK | X_OK)) {
|
||||
set_permission_error_detail(error, error_size, code, code_size,
|
||||
arg, arg_size, "target_parent_not_writable",
|
||||
"current directory is not writable", parent);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -1474,131 +1585,12 @@ task_request_error(struct MHD_Connection *conn, file_task_t *task,
|
||||
return send_json_error(conn, status, msg);
|
||||
}
|
||||
|
||||
static int
|
||||
count_required_move_space(file_task_t *task, unsigned long long *required) {
|
||||
size_t i;
|
||||
|
||||
*required = 0;
|
||||
for(i = 0; i < task->src_count; i++) {
|
||||
char target[PATH_MAX];
|
||||
int needs_space;
|
||||
|
||||
if(task_cancel_requested(task)) {
|
||||
return -1;
|
||||
}
|
||||
if(task_target_path(task, task->srcs[i], target, sizeof(target))) {
|
||||
return -1;
|
||||
}
|
||||
needs_space = move_requires_space_check(task->srcs[i], target);
|
||||
if(needs_space < 0) {
|
||||
return -1;
|
||||
}
|
||||
if(needs_space && count_path_bytes(task, task->srcs[i], target, required)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int count_reclaimable_bytes(file_task_t *task, const char *src,
|
||||
const char *dst,
|
||||
unsigned long long *reclaimable);
|
||||
|
||||
static int
|
||||
count_reclaimable_dir_bytes(file_task_t *task, const char *src,
|
||||
const char *dst,
|
||||
unsigned long long *reclaimable) {
|
||||
DIR *dir = opendir(src);
|
||||
struct dirent *entry;
|
||||
int ret = -1;
|
||||
|
||||
if(!dir) {
|
||||
return -1;
|
||||
}
|
||||
while((entry = readdir(dir))) {
|
||||
char src_child[PATH_MAX];
|
||||
char dst_child[PATH_MAX];
|
||||
|
||||
if(!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
if(task_cancel_requested(task)) {
|
||||
goto done;
|
||||
}
|
||||
if(path_join(src_child, sizeof(src_child), src, entry->d_name) ||
|
||||
path_join(dst_child, sizeof(dst_child), dst, entry->d_name) ||
|
||||
count_reclaimable_bytes(task, src_child, dst_child, reclaimable)) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
ret = 0;
|
||||
done:
|
||||
closedir(dir);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
count_reclaimable_bytes(file_task_t *task, const char *src, const char *dst,
|
||||
unsigned long long *reclaimable) {
|
||||
struct stat src_st;
|
||||
struct stat dst_st;
|
||||
|
||||
if(task_cancel_requested(task)) {
|
||||
return -1;
|
||||
}
|
||||
if(lstat(src, &src_st)) {
|
||||
return -1;
|
||||
}
|
||||
if(lstat(dst, &dst_st)) {
|
||||
if(errno == ENOENT) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if(S_ISREG(src_st.st_mode) && S_ISREG(dst_st.st_mode)) {
|
||||
*reclaimable += (unsigned long long)dst_st.st_size;
|
||||
return 0;
|
||||
}
|
||||
if(S_ISDIR(src_st.st_mode) && S_ISDIR(dst_st.st_mode)) {
|
||||
return count_reclaimable_dir_bytes(task, src, dst, reclaimable);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
subtract_reclaimable_space(file_task_t *task, unsigned long long *required,
|
||||
int copy_needed_only) {
|
||||
unsigned long long reclaimable = 0;
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < task->src_count; i++) {
|
||||
char target[PATH_MAX];
|
||||
|
||||
if(task_target_path(task, task->srcs[i], target, sizeof(target))) {
|
||||
return -1;
|
||||
}
|
||||
if(copy_needed_only) {
|
||||
int needs_space = move_requires_space_check(task->srcs[i], target);
|
||||
if(needs_space < 0) {
|
||||
return -1;
|
||||
}
|
||||
if(!needs_space) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(count_reclaimable_bytes(task, task->srcs[i], target, &reclaimable)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
*required = reclaimable >= *required ? 0 : *required - reclaimable;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *
|
||||
task_worker(void *arg) {
|
||||
file_task_t *task = arg;
|
||||
unsigned long long total = 0;
|
||||
unsigned long long required = 0;
|
||||
unsigned long long reclaimable = 0;
|
||||
int ret = -1;
|
||||
|
||||
task_update(task, TASK_RUNNING, "preparing", 0, NULL);
|
||||
@@ -1610,7 +1602,7 @@ task_worker(void *arg) {
|
||||
if(task->op == TASK_COPY || task->op == TASK_MOVE) {
|
||||
char error[160] = {0};
|
||||
char code[64] = {0};
|
||||
char arg[PATH_MAX] = {0};
|
||||
char arg[PATH_MAX + 96] = {0};
|
||||
|
||||
task_update(task, TASK_RUNNING, "checking target permissions", 0, NULL);
|
||||
if(check_task_targets_writable(task, error, sizeof(error),
|
||||
@@ -1629,56 +1621,49 @@ task_worker(void *arg) {
|
||||
if(task->op == TASK_COPY || task->op == TASK_MOVE || task->op == TASK_DELETE) {
|
||||
size_t i;
|
||||
for(i = 0; i < task->src_count; i++) {
|
||||
unsigned long long before = total;
|
||||
int needs_space = task->op == TASK_COPY;
|
||||
char target[PATH_MAX];
|
||||
const char *display = task->srcs[i];
|
||||
const char *compare_target = NULL;
|
||||
|
||||
if((task->op == TASK_COPY || task->op == TASK_MOVE) &&
|
||||
task_target_path(task, task->srcs[i], target, sizeof(target))) {
|
||||
task_update(task, TASK_FAILED, task->srcs[i], 0, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
if(task->op == TASK_COPY || task->op == TASK_MOVE) {
|
||||
display = target;
|
||||
if(task_target_path(task, task->srcs[i], target, sizeof(target))) {
|
||||
task_update(task, TASK_FAILED, task->srcs[i], 0, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if(count_path_bytes(task, task->srcs[i], display, &total)) {
|
||||
if(task->op == TASK_MOVE) {
|
||||
needs_space = move_requires_space_check(task->srcs[i], target);
|
||||
if(needs_space < 0) {
|
||||
task_update(task, TASK_FAILED, task->srcs[i], 0, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if(needs_space) {
|
||||
compare_target = target;
|
||||
}
|
||||
if(count_path_bytes(task, task->srcs[i], task->srcs[i], compare_target,
|
||||
&total, &reclaimable,
|
||||
&task->file_count, &task->dir_count)) {
|
||||
if(errno == ECANCELED || task_cancel_requested(task)) {
|
||||
task_update(task, TASK_CANCELED, display, 0, "canceled");
|
||||
task_update(task, TASK_CANCELED, task->srcs[i], 0, "canceled");
|
||||
} else {
|
||||
task_update(task, TASK_FAILED, display, 0, strerror(errno));
|
||||
task_update(task, TASK_FAILED, task->srcs[i], 0, strerror(errno));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if(needs_space) {
|
||||
required += total - before;
|
||||
}
|
||||
}
|
||||
task_set_total(task, total);
|
||||
required = reclaimable >= required ? 0 : required - reclaimable;
|
||||
|
||||
if(task->op == TASK_COPY || task->op == TASK_MOVE) {
|
||||
unsigned long long required = task->op == TASK_COPY ? total : 0;
|
||||
char error[128] = {0};
|
||||
char code[64] = {0};
|
||||
char arg[PATH_MAX] = {0};
|
||||
|
||||
task_update(task, TASK_RUNNING, "checking target space", 0, NULL);
|
||||
if(task->op == TASK_MOVE && count_required_move_space(task, &required)) {
|
||||
if(errno == ECANCELED || task_cancel_requested(task)) {
|
||||
task_update(task, TASK_CANCELED, task->current[0] ? task->current : task->src,
|
||||
0, "canceled");
|
||||
} else {
|
||||
task_update(task, TASK_FAILED, task->current[0] ? task->current : task->src,
|
||||
0, strerror(errno));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if(required &&
|
||||
subtract_reclaimable_space(task, &required, task->op == TASK_MOVE)) {
|
||||
if(errno == ECANCELED || task_cancel_requested(task)) {
|
||||
task_update(task, TASK_CANCELED, task->current[0] ? task->current : task->src,
|
||||
0, "canceled");
|
||||
} else {
|
||||
task_update(task, TASK_FAILED, task->current[0] ? task->current : task->src,
|
||||
0, strerror(errno));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if(check_target_space(task->dst, required, error, sizeof(error),
|
||||
code, sizeof(code), arg, sizeof(arg))) {
|
||||
task_set_error_code(task, code, arg);
|
||||
@@ -1730,12 +1715,21 @@ task_worker(void *arg) {
|
||||
0, strerror(errno));
|
||||
}
|
||||
} else {
|
||||
time_t completed_at = time(NULL);
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->state = TASK_DONE;
|
||||
if(task->total) {
|
||||
task->done = task->total;
|
||||
}
|
||||
task->updated_at = time(NULL);
|
||||
task->updated_at = completed_at;
|
||||
if((task->op == TASK_COPY || task->op == TASK_MOVE) &&
|
||||
completed_at - task->created_at >= TRANSFER_ALERT_THRESHOLD) {
|
||||
g_last_completion.id = task->id;
|
||||
g_last_completion.op = task->op;
|
||||
g_last_completion.total = task->total;
|
||||
g_last_completion.file_count = task->dir_count ? task->file_count : 0;
|
||||
g_last_completion.elapsed = completed_at - task->created_at;
|
||||
}
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
}
|
||||
|
||||
@@ -1919,9 +1913,19 @@ api_tasks(struct MHD_Connection *conn) {
|
||||
task->transfer_started_at ? (long long)(time(NULL) - task->transfer_started_at) : 0LL,
|
||||
(long long)task->updated_at);
|
||||
}
|
||||
strbuf_append(&b, "],\"completion\":");
|
||||
if(g_last_completion.id) {
|
||||
strbuf_printf(&b, "{\"id\":%lu,\"op\":\"%s\",\"elapsed\":%lld,"
|
||||
"\"total\":%llu,\"file_count\":%zu}",
|
||||
g_last_completion.id, task_op_name(g_last_completion.op),
|
||||
(long long)g_last_completion.elapsed,
|
||||
g_last_completion.total, g_last_completion.file_count);
|
||||
} else {
|
||||
strbuf_append(&b, "null");
|
||||
}
|
||||
remove_finished_tasks_locked();
|
||||
pthread_mutex_unlock(&g_tasks_lock);
|
||||
strbuf_append(&b, "]}");
|
||||
strbuf_append(&b, "}");
|
||||
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
|
||||
}
|
||||
|
||||
@@ -2209,7 +2213,7 @@ api_text_create(struct MHD_Connection *conn) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
|
||||
}
|
||||
if(access(path, W_OK | X_OK)) {
|
||||
if(mode_access(path, W_OK | X_OK)) {
|
||||
free(path); free(name);
|
||||
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
|
||||
"text file is not writable");
|
||||
@@ -2330,7 +2334,7 @@ api_text_save(struct MHD_Connection *conn, const char *body,
|
||||
"file changed since it was opened");
|
||||
}
|
||||
if(path_dirname(path, parent, sizeof(parent)) ||
|
||||
access(path, W_OK) || access(parent, W_OK | X_OK)) {
|
||||
mode_access(path, W_OK) || mode_access(parent, W_OK | X_OK)) {
|
||||
free(current);
|
||||
free(path); free(expected);
|
||||
return send_json_error(conn, MHD_HTTP_FORBIDDEN,
|
||||
|
||||
@@ -8,8 +8,10 @@ typedef struct mime_entry {
|
||||
} mime_entry_t;
|
||||
|
||||
static const mime_entry_t g_mimes[] = {
|
||||
{"bmp", "image/bmp"},
|
||||
{"css", "text/css"},
|
||||
{"elf", "application/octet-stream"},
|
||||
{"gif", "image/gif"},
|
||||
{"html", "text/html"},
|
||||
{"jpeg", "image/jpeg"},
|
||||
{"jpg", "image/jpeg"},
|
||||
@@ -20,6 +22,7 @@ static const mime_entry_t g_mimes[] = {
|
||||
{"png", "image/png"},
|
||||
{"txt", "text/plain"},
|
||||
{"xml", "text/xml"},
|
||||
{"webp", "image/webp"},
|
||||
{"zip", "application/zip"},
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user