mirror of
https://github.com/owendswang/ps5-web-file-manager.git
synced 2026-10-18 18:09:53 -05:00
Added ability to list share folders from the SMB server
This commit is contained in:
1 parent
e72bd6f1cf
commit
f105808ee4
9 files changed
+240
-31
No files matched your search
@@ -110,7 +110,7 @@ On startup the payload registers a `PS5 Web File Manager` web shortcut in the Me
|
||||
- Delete is recursive and permanent. There is no recycle bin.
|
||||
- Copy/move tasks can be canceled. A partially copied single file is removed, but partially copied folders are left in place to avoid deleting existing files when merging into an existing target folder.
|
||||
- Upload tasks can be canceled. A partially uploaded temporary file is removed when possible.
|
||||
- Downloading a folder or multiple selected items produces a tar stream. The tar archive is generated by the payload and is not written to PS5 storage first.
|
||||
- Selected files download individually, and each selected folder produces its own tar stream. The tar archives are generated by the payload and are not written to PS5 storage first.
|
||||
- The UI can recover the active task display if the browser is closed and reopened while the payload process is still running.
|
||||
- Text editing is limited to common text-file extensions. Non-UTF-8 and oversized files are rejected.
|
||||
- File names are transmitted as UTF-8 through the web API. The payload also attempts to preserve legacy byte-oriented names returned by mounted filesystems so mixed USB filename encodings can still be displayed and operated on.
|
||||
|
||||
+10
-1
@@ -153,7 +153,16 @@
|
||||
<section class="permission-panel smb-manager-panel" role="dialog" aria-modal="true" aria-labelledby="smbManagerTitle">
|
||||
<h2 id="smbManagerTitle" class="permission-title" data-i18n="manageSmb"></h2>
|
||||
<div id="smbConnectionList" class="smb-connection-list"></div>
|
||||
<div class="permission-actions">
|
||||
<div id="smbSharePicker" hidden>
|
||||
<label for="smbShareSelect" data-i18n="smbChooseShare"></label>
|
||||
<select id="smbShareSelect"></select>
|
||||
<p class="smb-connections-empty" data-i18n="smbShareHint"></p>
|
||||
<div class="permission-actions">
|
||||
<button id="smbShareConnectBtn" class="primary" type="button" data-i18n="smbConnect"></button>
|
||||
<button id="smbShareCancelBtn" type="button" data-i18n="cancel"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="smbManagerActions" class="permission-actions">
|
||||
<button id="smbManagerAddBtn" class="primary" type="button" data-i18n="addSmb"></button>
|
||||
<button id="smbManagerCloseBtn" type="button" data-i18n="close"></button>
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -85,7 +85,13 @@ window.WFM_LANG = {
|
||||
smbConnectionsEmpty: "No SMB connections yet. Add one below.",
|
||||
smbRemoveConfirm: "Remove SMB connection \"{path}\"?\n\nFiles on the share will not be deleted.",
|
||||
smbRemoveFailed: "Could not remove SMB connection: {error}",
|
||||
smbAddressPrompt: "Enter an SMB address:\nsmb://server/share[/path]\nWith authentication: smb://username:password@server/share",
|
||||
smbAddressPrompt: "Enter an SMB address:\nsmb://server/ (choose a share)\nsmb://server/share[/path]\nWith authentication: smb://username:password@server/share",
|
||||
smbChooseShare: "Choose a shared folder",
|
||||
smbConnect: "Connect",
|
||||
smbShareHint: "Share missing? Cancel and enter its full SMB address.",
|
||||
smbNoShares: "The server did not list any file shares. Try entering the full SMB address.",
|
||||
err_smb_share_enum_failed: "Could not list shares. Try entering the full SMB address: {arg}",
|
||||
err_smb_password_required: "Access denied. Check your credentials or enter the full SMB address: {arg}",
|
||||
smbPasswordPrompt: "Enter the SMB password (saved only in this browser):\n{path}",
|
||||
smbPasswordCanceled: "SMB password entry canceled",
|
||||
smbConnectFailed: "Could not connect to SMB: {error}",
|
||||
|
||||
+7
-1
@@ -85,7 +85,13 @@ window.WFM_LANG = {
|
||||
smbConnectionsEmpty: "还没有 SMB 连接,可点击下方添加。",
|
||||
smbRemoveConfirm: "移除 SMB 连接「{path}」?\n\n共享中的文件不会被删除。",
|
||||
smbRemoveFailed: "移除 SMB 连接失败:{error}",
|
||||
smbAddressPrompt: "输入 SMB 地址:\nsmb://服务器/共享目录[/路径]\n需要认证时:smb://用户名:密码@服务器/共享目录",
|
||||
smbAddressPrompt: "输入 SMB 地址:\nsmb://服务器/(选择共享)\nsmb://服务器/共享目录[/路径]\n需要认证时:smb://用户名:密码@服务器/共享目录",
|
||||
smbChooseShare: "选择共享目录",
|
||||
smbConnect: "连接",
|
||||
smbShareHint: "没有列出需要的共享?取消后可输入完整共享地址。",
|
||||
smbNoShares: "服务器未列出文件共享,可尝试输入完整共享地址。",
|
||||
err_smb_share_enum_failed: "无法列出共享,可尝试输入完整共享地址:{arg}",
|
||||
err_smb_password_required: "服务器拒绝访问,请检查账号密码或输入完整共享地址:{arg}",
|
||||
smbPasswordPrompt: "请输入 SMB 密码(仅保存在当前浏览器):\n{path}",
|
||||
smbPasswordCanceled: "已取消输入 SMB 密码",
|
||||
smbConnectFailed: "无法连接 SMB:{error}",
|
||||
|
||||
+28
-3
@@ -424,6 +424,11 @@ button.install-action:hover:not(:disabled) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* PS5 cursor navigation also targets scrollable containers behind overlays. */
|
||||
.shell.modal-open .content {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-loading {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -785,7 +790,7 @@ input[type="checkbox"] {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
padding: 9px;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
@@ -799,8 +804,8 @@ input[type="checkbox"] {
|
||||
|
||||
.row-tool img {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) and (hover: none) {
|
||||
@@ -809,6 +814,10 @@ input[type="checkbox"] {
|
||||
}
|
||||
}
|
||||
|
||||
.shell.interaction-locked .file-name-cell .row-tools {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #aab4be;
|
||||
padding: 20px 90px;
|
||||
@@ -1102,6 +1111,22 @@ input[type="checkbox"] {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#smbSharePicker {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#smbShareSelect {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #46515f;
|
||||
border-radius: 6px;
|
||||
background: #20262d;
|
||||
color: #edf0f2;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.permission-path {
|
||||
display: flex;
|
||||
margin: 10px 0 22px;
|
||||
|
||||
+98
-23
@@ -20,6 +20,7 @@ let lastCompletionId = null;
|
||||
let taskPollFailedAlertShown = false;
|
||||
let taskPollPromise = null;
|
||||
let localActionBusy = false;
|
||||
let modalBackgroundLocked = false;
|
||||
let specialStoragePaths = {};
|
||||
let hasSpecialMntStorage = false;
|
||||
let textEditorPath = null;
|
||||
@@ -72,6 +73,10 @@ const smbManagerOverlayEl = document.getElementById("smbManagerOverlay");
|
||||
const smbConnectionListEl = document.getElementById("smbConnectionList");
|
||||
const smbManagerAddBtn = document.getElementById("smbManagerAddBtn");
|
||||
const smbManagerCloseBtn = document.getElementById("smbManagerCloseBtn");
|
||||
const smbSharePickerEl = document.getElementById("smbSharePicker");
|
||||
const smbShareSelectEl = document.getElementById("smbShareSelect");
|
||||
const smbManagerActionsEl = document.getElementById("smbManagerActions");
|
||||
let smbShareChoice = null;
|
||||
let smbConnections = [];
|
||||
const statusEl = document.getElementById("statusText");
|
||||
const versionEl = document.getElementById("versionText");
|
||||
@@ -260,7 +265,7 @@ function hideContentLoading() {
|
||||
clearTimeout(directoryLoadingTimer);
|
||||
directoryLoadingTimer = 0;
|
||||
directoryLoadingStartedAt = 0;
|
||||
contentEl.classList.remove("loading");
|
||||
contentEl.classList.toggle("loading", modalBackgroundLocked);
|
||||
contentLoadingEl.hidden = true;
|
||||
updateButtons();
|
||||
}
|
||||
@@ -625,17 +630,24 @@ function setBusy(value) {
|
||||
}
|
||||
|
||||
function setModalBackgroundLocked(value) {
|
||||
contentEl.classList.toggle("loading", value);
|
||||
modalBackgroundLocked = value;
|
||||
contentEl.classList.toggle("loading", value || !contentLoadingEl.hidden);
|
||||
if (value) clearHoverPath();
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
function backgroundInteractionLocked() {
|
||||
return Boolean(modalBackgroundLocked || busy || loadingPath ||
|
||||
contentEl.classList.contains("loading"));
|
||||
}
|
||||
|
||||
function showTaskOverlay() {
|
||||
setBusy(true);
|
||||
if (!overlayEl.hidden || taskOverlayTimer) return;
|
||||
taskOverlayTimer = setTimeout(() => {
|
||||
taskOverlayTimer = 0;
|
||||
overlayEl.hidden = false;
|
||||
updateButtons();
|
||||
}, TASK_OVERLAY_DISPLAY_DELAY);
|
||||
}
|
||||
|
||||
@@ -643,6 +655,7 @@ function hideTaskOverlay() {
|
||||
clearTimeout(taskOverlayTimer);
|
||||
taskOverlayTimer = 0;
|
||||
overlayEl.hidden = true;
|
||||
document.querySelector(".shell").classList.toggle("modal-open", modalBackgroundLocked);
|
||||
}
|
||||
|
||||
function showActionFailed(label, error) {
|
||||
@@ -877,8 +890,7 @@ async function refreshSpaces() {
|
||||
}
|
||||
|
||||
function spaceNavigationLocked() {
|
||||
return Boolean(busy || loadingPath || !contentLoadingEl.hidden ||
|
||||
contentEl.classList.contains("loading"));
|
||||
return backgroundInteractionLocked() || !contentLoadingEl.hidden;
|
||||
}
|
||||
|
||||
function readSmbPassword(item) {
|
||||
@@ -986,6 +998,7 @@ function renderSmbConnections() {
|
||||
|
||||
function openSmbManager() {
|
||||
if (spaceNavigationLocked()) return;
|
||||
setModalBackgroundLocked(true);
|
||||
smbManagerOverlayEl.hidden = false;
|
||||
renderSmbConnections();
|
||||
smbManagerAddBtn.focus();
|
||||
@@ -993,13 +1006,19 @@ function openSmbManager() {
|
||||
}
|
||||
|
||||
function closeSmbManager() {
|
||||
if (smbShareChoice) { smbShareChoice(null); return; }
|
||||
if (localActionBusy) return;
|
||||
smbManagerOverlayEl.hidden = true;
|
||||
setModalBackgroundLocked(false);
|
||||
manageSmbBtn.focus();
|
||||
}
|
||||
|
||||
function smbManagerBusy() {
|
||||
return Boolean(busy || loadingPath || !contentLoadingEl.hidden);
|
||||
}
|
||||
|
||||
async function removeSmbConnection(path) {
|
||||
if (spaceNavigationLocked() || !confirm(t("smbRemoveConfirm", { path: displayPath(path) }))) return;
|
||||
if (smbManagerBusy() || !confirm(t("smbRemoveConfirm", { path: displayPath(path) }))) return;
|
||||
try {
|
||||
localActionBusy = true;
|
||||
setBusy(true);
|
||||
@@ -1011,14 +1030,40 @@ async function removeSmbConnection(path) {
|
||||
alert(t("smbRemoveFailed", { error: err.message }));
|
||||
} finally {
|
||||
localActionBusy = false;
|
||||
setModalBackgroundLocked(!smbManagerOverlayEl.hidden);
|
||||
setBusy(false);
|
||||
renderSmbConnections();
|
||||
smbManagerAddBtn.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function chooseSmbShare(shares) {
|
||||
smbShareSelectEl.textContent = "";
|
||||
for (const rawName of shares) {
|
||||
const name = decodeFsText(rawName);
|
||||
const option = document.createElement("option");
|
||||
option.value = name;
|
||||
option.textContent = name;
|
||||
smbShareSelectEl.appendChild(option);
|
||||
}
|
||||
smbConnectionListEl.hidden = true;
|
||||
smbManagerActionsEl.hidden = true;
|
||||
smbSharePickerEl.hidden = false;
|
||||
smbShareSelectEl.focus();
|
||||
return new Promise(resolve => {
|
||||
smbShareChoice = value => {
|
||||
smbShareChoice = null;
|
||||
smbSharePickerEl.hidden = true;
|
||||
smbShareSelectEl.textContent = "";
|
||||
smbConnectionListEl.hidden = false;
|
||||
smbManagerActionsEl.hidden = false;
|
||||
resolve(value);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function addSmbConnection() {
|
||||
if (spaceNavigationLocked()) return;
|
||||
if (smbManagerBusy()) return;
|
||||
const address = prompt(t("smbAddressPrompt"), "smb://");
|
||||
if (typeof address !== "string") return;
|
||||
const input = address.trim();
|
||||
@@ -1026,7 +1071,7 @@ async function addSmbConnection() {
|
||||
try {
|
||||
localActionBusy = true;
|
||||
setBusy(true);
|
||||
const authority = /^smb:\/\/([^/]+)\//.exec(input);
|
||||
const authority = /^smb:\/\/([^/]+)(?:\/|$)/.exec(input);
|
||||
let clean = input;
|
||||
let password = "";
|
||||
if (authority && authority[1].indexOf("@") >= 0) {
|
||||
@@ -1036,6 +1081,20 @@ async function addSmbConnection() {
|
||||
password = colon >= 0 ? decodeURIComponent(credentials.slice(colon + 1)) : askSmbPassword(input);
|
||||
if (colon >= 0) clean = input.replace(authority[1], credentials.slice(0, colon) + "@" + authority[1].slice(at + 1));
|
||||
}
|
||||
if (/^smb:\/\/[^/]+\/?$/.test(clean)) {
|
||||
let listing;
|
||||
try {
|
||||
listing = await apiForm("/api/smb/shares", { address: clean, password: password });
|
||||
} catch (err) {
|
||||
if (password || err.code !== "smb_password_required") throw err;
|
||||
password = askSmbPassword(clean);
|
||||
listing = await apiForm("/api/smb/shares", { address: clean, password: password });
|
||||
}
|
||||
if (!listing.shares.length) throw new Error(t("smbNoShares"));
|
||||
const share = await chooseSmbShare(listing.shares);
|
||||
if (share === null) return;
|
||||
clean = clean.replace(/\/$/, "") + "/" + share;
|
||||
}
|
||||
const data = await apiForm("/api/smb/add", { address: clean, password: password });
|
||||
saveSmbPassword(data, password);
|
||||
smbReady[data.path] = Promise.resolve();
|
||||
@@ -1046,8 +1105,10 @@ async function addSmbConnection() {
|
||||
if (err.code !== "smb_password_canceled") alert(t("smbConnectFailed", { error: err.message }));
|
||||
} finally {
|
||||
localActionBusy = false;
|
||||
setModalBackgroundLocked(!smbManagerOverlayEl.hidden);
|
||||
setBusy(false);
|
||||
if (smbManagerOverlayEl.hidden) manageSmbBtn.focus();
|
||||
else smbManagerAddBtn.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1751,7 +1812,7 @@ function renderTaskName(el, task) {
|
||||
|
||||
function renderClipboard() {
|
||||
const hasClipboard = Boolean(clipboard && clipboard.items.length);
|
||||
const locked = busy || contentEl.classList.contains("loading");
|
||||
const locked = backgroundInteractionLocked();
|
||||
pasteBtn.hidden = !hasClipboard;
|
||||
clearClipboardBtn.hidden = !hasClipboard;
|
||||
if (!hasClipboard) {
|
||||
@@ -1805,12 +1866,16 @@ function singleSelected() {
|
||||
|
||||
function updateButtons() {
|
||||
const items = selectedEntries();
|
||||
const locked = busy || contentEl.classList.contains("loading");
|
||||
const locked = backgroundInteractionLocked();
|
||||
const shell = document.querySelector(".shell");
|
||||
shell.classList.toggle("interaction-locked", locked);
|
||||
shell.classList.toggle("modal-open", modalBackgroundLocked || !overlayEl.hidden);
|
||||
versionEl.disabled = locked;
|
||||
manageSmbBtn.disabled = spaceNavigationLocked();
|
||||
smbManagerAddBtn.disabled = spaceNavigationLocked();
|
||||
smbManagerAddBtn.disabled = smbManagerBusy();
|
||||
smbManagerCloseBtn.disabled = localActionBusy;
|
||||
for (const button of smbConnectionListEl.querySelectorAll("button")) {
|
||||
button.disabled = spaceNavigationLocked();
|
||||
button.disabled = smbManagerBusy();
|
||||
}
|
||||
for (const select of spaceInfoEl.querySelectorAll(".space-select")) {
|
||||
select.disabled = spaceNavigationLocked();
|
||||
@@ -1824,6 +1889,8 @@ function updateButtons() {
|
||||
uploadBtn.disabled = locked;
|
||||
uploadMenuBtn.disabled = locked;
|
||||
uploadFolderBtn.disabled = locked;
|
||||
uploadFilesEl.disabled = locked;
|
||||
uploadFolderEl.disabled = locked;
|
||||
document.getElementById("mkdirBtn").disabled = locked;
|
||||
newTextBtn.disabled = locked;
|
||||
for (const button of filesEl.querySelectorAll(".row-action, .mode-action, .row-tool")) {
|
||||
@@ -1888,7 +1955,7 @@ function focusPath(path) {
|
||||
}
|
||||
|
||||
function hoverRow(row) {
|
||||
if (hoverPaused) return;
|
||||
if (hoverPaused || backgroundInteractionLocked()) return;
|
||||
if (hoveredRow === row) return;
|
||||
clearHoverPath();
|
||||
hoveredRow = row;
|
||||
@@ -1974,7 +2041,7 @@ function clearSelection(updateVisibleRows) {
|
||||
const FILE_ACTIONS = { download: actionDownload, copy: actionCopy, move: actionMove, rename: actionRename, delete: actionDelete };
|
||||
|
||||
function runRowAction(item, action) {
|
||||
if (busy || loadingPath || contentEl.classList.contains("loading")) return;
|
||||
if (backgroundInteractionLocked()) return;
|
||||
clearSelection();
|
||||
togglePath(item.path, true);
|
||||
focusPath(item.path);
|
||||
@@ -2732,7 +2799,7 @@ function uploadFileRequest(taskId, file, rel, overwrite, index) {
|
||||
}
|
||||
|
||||
async function uploadFiles(files, relativeNames) {
|
||||
if (busy || loadingPath || !files.length) return;
|
||||
if (backgroundInteractionLocked() || !files.length) return;
|
||||
const useLoading = files.length >= SELECT_ALL_LOADING_THRESHOLD;
|
||||
let list;
|
||||
let rels = [];
|
||||
@@ -2887,12 +2954,12 @@ function setupDropUpload() {
|
||||
if (!dataTransferHasFiles(event.dataTransfer)) return;
|
||||
event.preventDefault();
|
||||
dragDepth++;
|
||||
if (!busy && !loadingPath) dropUploadOverlayEl.hidden = false;
|
||||
if (!backgroundInteractionLocked()) dropUploadOverlayEl.hidden = false;
|
||||
});
|
||||
window.addEventListener("dragover", event => {
|
||||
if (!dataTransferHasFiles(event.dataTransfer)) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
event.dataTransfer.dropEffect = backgroundInteractionLocked() ? "none" : "copy";
|
||||
});
|
||||
window.addEventListener("dragleave", () => {
|
||||
dragDepth = Math.max(0, dragDepth - 1);
|
||||
@@ -2903,7 +2970,7 @@ function setupDropUpload() {
|
||||
event.preventDefault();
|
||||
dragDepth = 0;
|
||||
dropUploadOverlayEl.hidden = true;
|
||||
if (busy || loadingPath) return;
|
||||
if (backgroundInteractionLocked()) return;
|
||||
uploadDroppedItems(event.dataTransfer).catch(err => {
|
||||
const message = t("uploadFailed", { error: err.message || t("backendError") });
|
||||
setStatus(message);
|
||||
@@ -2970,10 +3037,11 @@ A file manager for PS5 with a web UI. It is primarily intended for quickly and s
|
||||
Credits:
|
||||
ps5-payload-dev/websrv, ps5-payload-dev/ftpsrv,
|
||||
seregonwar/zftpd, itsPLK/ps5-payload-manager,
|
||||
libmicrohttpd, ps5-payload-dev/sdk, etaHEN, ezremote.
|
||||
itsPLK/ps5-pkg-manager, libmicrohttpd, libsmb2,
|
||||
ps5-payload-dev/sdk, etaHEN, ezremote.
|
||||
|
||||
License:
|
||||
GPLv3 or later. Third-party licenses apply; libmicrohttpd is LGPL.`);
|
||||
GPLv3 or later. Third-party licenses apply; libmicrohttpd and libsmb2 are LGPL.`);
|
||||
});
|
||||
parentBtn.addEventListener("click", actionParentDirectory);
|
||||
nameFilterBtn.addEventListener("click", event => {
|
||||
@@ -3004,6 +3072,12 @@ pkgInfoImageEl.addEventListener("error", () => {
|
||||
manageSmbBtn.addEventListener("click", openSmbManager);
|
||||
smbManagerAddBtn.addEventListener("click", addSmbConnection);
|
||||
smbManagerCloseBtn.addEventListener("click", closeSmbManager);
|
||||
document.getElementById("smbShareConnectBtn").addEventListener("click", () => {
|
||||
if (smbShareChoice) smbShareChoice(smbShareSelectEl.value);
|
||||
});
|
||||
document.getElementById("smbShareCancelBtn").addEventListener("click", () => {
|
||||
if (smbShareChoice) smbShareChoice(null);
|
||||
});
|
||||
smbManagerOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === smbManagerOverlayEl) closeSmbManager();
|
||||
});
|
||||
@@ -3071,7 +3145,8 @@ document.addEventListener("keydown", event => {
|
||||
document.body.classList.add("keyboard-navigation");
|
||||
}
|
||||
if (!smbManagerOverlayEl.hidden && (event.key === "Tab" || event.keyCode === 9)) {
|
||||
const buttons = smbManagerOverlayEl.querySelectorAll("button:not(:disabled)");
|
||||
const buttons = Array.from(smbManagerOverlayEl.querySelectorAll("select, button:not(:disabled)"))
|
||||
.filter(button => button.offsetParent !== null);
|
||||
if (!buttons.length) { event.preventDefault(); return; }
|
||||
const first = buttons[0];
|
||||
const last = buttons[buttons.length - 1];
|
||||
@@ -3120,8 +3195,8 @@ permissionOverlayEl.addEventListener("click", event => {
|
||||
extractOverlayEl.addEventListener("click", event => {
|
||||
if (event.target === extractOverlayEl) closeExtractDialog();
|
||||
});
|
||||
contentEl.addEventListener("click", event => {
|
||||
if (!busy && !loadingPath) return;
|
||||
document.querySelector(".shell").addEventListener("click", event => {
|
||||
if (!backgroundInteractionLocked()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, true);
|
||||
@@ -3130,7 +3205,7 @@ filesEl.addEventListener("focusin", event => {
|
||||
if (row) focusPath(row.dataset.path);
|
||||
});
|
||||
filesEl.addEventListener("change", event => {
|
||||
if (busy || loadingPath) {
|
||||
if (backgroundInteractionLocked()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1952,6 +1952,11 @@ filemgr_api_request(struct MHD_Connection *conn, const char *url,
|
||||
if(!strcmp(url, "/api/list")) return api_list(conn);
|
||||
if(!strcmp(url, "/api/tasks")) return api_tasks(conn);
|
||||
if(!strcmp(url, "/api/space")) return api_space(conn);
|
||||
if(!strcmp(url, "/api/smb/shares")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_POST) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
api_smb_shares(conn, body, body_size);
|
||||
}
|
||||
if(!strcmp(url, "/api/smb/add") || !strcmp(url, "/api/smb/connect")) {
|
||||
return strcmp(method, MHD_HTTP_METHOD_POST) ?
|
||||
send_json_error(conn, MHD_HTTP_METHOD_NOT_ALLOWED, "invalid method") :
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -15,6 +16,9 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <smb2/libsmb2-dcerpc-srvsvc.h>
|
||||
#include <smb2/libsmb2-raw.h>
|
||||
#include <smb2/smb2-errors.h>
|
||||
|
||||
#define SMB_CONNECTION_MAX 64
|
||||
#if WFM_DEBUG
|
||||
@@ -237,8 +241,11 @@ static struct smb2_context *connect_address(const smb_address_t *a) {
|
||||
} else {
|
||||
snprintf(client_error, sizeof(client_error), "%s", smb2_get_error(ctx));
|
||||
}
|
||||
uint32_t status = smb2_get_nterror(ctx);
|
||||
int auth_error = status == SMB2_STATUS_LOGON_FAILURE ||
|
||||
status == SMB2_STATUS_WRONG_PASSWORD || status == SMB2_STATUS_ACCESS_DENIED;
|
||||
smb2_destroy_context(ctx);
|
||||
errno = -result;
|
||||
errno = auth_error ? EACCES : -result;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -263,6 +270,81 @@ struct smb2_context *smb_connect_path(const char *path, smb_address_t *a) {
|
||||
return connect_address(a);
|
||||
}
|
||||
|
||||
typedef struct share_enum_result {
|
||||
int done;
|
||||
int status;
|
||||
struct srvsvc_NetrShareEnum_rep *reply;
|
||||
} share_enum_result_t;
|
||||
|
||||
static void share_enum_cb(struct smb2_context *ctx, int status, void *data, void *opaque) {
|
||||
share_enum_result_t *result = opaque;
|
||||
result->status = status;
|
||||
result->reply = data;
|
||||
result->done = 1;
|
||||
}
|
||||
|
||||
enum MHD_Result api_smb_shares(struct MHD_Connection *conn, const char *body, size_t size) {
|
||||
char *input = body_form_value(body, size, "address");
|
||||
char *password = body_form_value(body, size, "password");
|
||||
char ipc[PATH_MAX];
|
||||
smb_address_t a;
|
||||
size_t len = input ? strlen(input) : 0;
|
||||
if(len && input[len - 1] == '/') input[--len] = 0;
|
||||
int valid = input && smb_path(input) && !strchr(input + 6, '/') &&
|
||||
snprintf(ipc, sizeof(ipc), "%s/IPC$", input) < (int)sizeof(ipc) &&
|
||||
!smb_parse_address(ipc, &a);
|
||||
free(input);
|
||||
if(!valid || (password && copy_field(a.password, sizeof(a.password), password, strlen(password)))) {
|
||||
free(password);
|
||||
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid SMB server address; use smb://server/");
|
||||
}
|
||||
free(password);
|
||||
if(has_active_task()) return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
|
||||
smb_lock();
|
||||
struct smb2_context *ctx = connect_address(&a);
|
||||
share_enum_result_t result = {0};
|
||||
int status = ctx ? smb2_share_enum_async(ctx, SHARE_INFO_1, share_enum_cb, &result) : -errno;
|
||||
struct timespec start, now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||
while(ctx && !status && !result.done) {
|
||||
struct pollfd pfd = {.fd = smb2_get_fd(ctx), .events = smb2_which_events(ctx)};
|
||||
int ready = poll(&pfd, 1, 1000);
|
||||
if(ready < 0 && errno == EINTR) continue;
|
||||
if(ready < 0) { status = -errno; break; }
|
||||
status = smb2_service(ctx, pfd.revents);
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
if(!result.done && now.tv_sec - start.tv_sec >= 15) status = -ETIMEDOUT;
|
||||
}
|
||||
if(!status) status = result.status;
|
||||
if(!status && !result.reply) status = -EIO;
|
||||
strbuf_t json = {0};
|
||||
if(!status) {
|
||||
strbuf_append(&json, "{\"ok\":true,\"shares\":[");
|
||||
struct srvsvc_SHARE_INFO_1_CONTAINER *shares = &result.reply->ses.ShareInfo.Level1;
|
||||
int first = 1;
|
||||
for(uint32_t i = 0; shares->Buffer && i < shares->EntriesRead; i++) {
|
||||
struct srvsvc_SHARE_INFO_1 *share = &shares->Buffer->share_info_1[i];
|
||||
if((share->type & 3) != SHARE_TYPE_DISKTREE || !share->netname.utf8) continue;
|
||||
if(!first) strbuf_append(&json, ",");
|
||||
first = 0;
|
||||
json_escape(&json, share->netname.utf8);
|
||||
}
|
||||
strbuf_append(&json, "]}");
|
||||
}
|
||||
char detail[512];
|
||||
snprintf(detail, sizeof(detail), "%s", ctx ? smb2_get_error(ctx) : client_error);
|
||||
if(status && !detail[0]) snprintf(detail, sizeof(detail), "share enumeration failed (%d)", status);
|
||||
SMB_DEBUG("share enumeration server=%s status=%d detail=%s", a.server, status, detail);
|
||||
int password_required = status == -EACCES || status == 5;
|
||||
if(result.reply) smb2_free_data(ctx, result.reply);
|
||||
/* Keep callback state alive until outstanding requests have been canceled. */
|
||||
if(ctx) smb2_destroy_context(ctx);
|
||||
smb_unlock();
|
||||
if(status) return send_json_error_detail(conn, MHD_HTTP_BAD_GATEWAY, "SMB share enumeration failed",
|
||||
password_required ? "smb_password_required" : "smb_share_enum_failed", detail);
|
||||
return send_buffer(conn, MHD_HTTP_OK, json.data, "application/json");
|
||||
}
|
||||
|
||||
enum MHD_Result api_smb_add(struct MHD_Connection *conn, const char *body, size_t size, int connecting) {
|
||||
char *input = body_form_value(body, size, "address");
|
||||
char *password = body_form_value(body, size, "password");
|
||||
|
||||
@@ -26,6 +26,7 @@ void smb_unlock(void);
|
||||
struct smb2_context *smb_connect_path(const char *path, smb_address_t *address);
|
||||
int smb_result(int result);
|
||||
void smb_append_spaces(strbuf_t *json, const char *current, int *first);
|
||||
enum MHD_Result api_smb_shares(struct MHD_Connection *conn, const char *body, size_t size);
|
||||
enum MHD_Result api_smb_add(struct MHD_Connection *conn, const char *body,
|
||||
size_t size, int connecting);
|
||||
enum MHD_Result api_smb_remove(struct MHD_Connection *conn, const char *body,
|
||||
|
||||
Reference in new issue
Block a user