Fix wrong path encodings in push

This commit is contained in:
2026-03-30 15:35:42 +02:00
parent c8bae194ad
commit 81db115bff
4 changed files with 166 additions and 11 deletions
@@ -20,6 +20,84 @@ const OPERATIONS = {
STAT: 0xF, CHECKSUM: 0x10, FOLDER_SIZE: 0x11,
};
/** i18next escapes `/` etc. in interpolations by default (/); file paths must display literally. */
const i18nUnescapedInterpolation = { interpolation: { escapeValue: false } };
const readAllDirectoryEntries = (reader) => new Promise((resolve, reject) => {
const out = [];
const readBatch = () => {
try {
reader.readEntries((entries) => {
if (!entries || entries.length === 0) return resolve(out);
out.push(...entries);
readBatch();
}, reject);
} catch (e) {
reject(e);
}
};
readBatch();
});
const walkFileSystemEntry = async (entry, pathPrefix) => {
if (!entry) return [];
if (entry.isFile) {
const file = await new Promise((res, rej) => entry.file(res, rej));
const rel = `${pathPrefix}${file.name}`.replace(/^\/+/, "");
return [{ file, relativePath: rel }];
}
if (entry.isDirectory) {
const reader = entry.createReader();
const children = await readAllDirectoryEntries(reader);
const dirPrefix = `${pathPrefix}${entry.name}/`;
const acc = [];
for (const child of children) {
acc.push(...await walkFileSystemEntry(child, dirPrefix));
}
return acc;
}
return [];
};
/** Resolves files + relative POSIX paths for a drop (supports folders via webkitGetAsEntry). */
const collectDroppedFiles = async (dataTransfer) => {
const out = [];
const seen = new Set();
const add = (file, relativePath) => {
const rp = String(relativePath || "").replace(/^\/+/, "").split("/").filter(Boolean).join("/");
if (!rp || !file) return;
const key = `${rp}\0${file.size}\0${file.lastModified}`;
if (seen.has(key)) return;
seen.add(key);
out.push({ file, relativePath: rp });
};
// Snapshot synchronously before any await — the live FileList is invalidated after the handler yields.
const filesSnapshot = dataTransfer.files?.length ? Array.from(dataTransfer.files) : [];
const items = dataTransfer.items ? [...dataTransfer.items] : [];
for (const item of items) {
if (item.kind !== "file") continue;
const getAsEntry = typeof item.webkitGetAsEntry === "function" ? item.webkitGetAsEntry.bind(item) : null;
const entry = getAsEntry ? getAsEntry() : null;
if (entry) {
const walked = await walkFileSystemEntry(entry, "");
for (const w of walked) add(w.file, w.relativePath);
continue;
}
const file = item.getAsFile();
if (file) add(file, file.webkitRelativePath || file.name);
}
for (let i = 0; i < filesSnapshot.length; i++) {
const file = filesSnapshot[i];
add(file, file.webkitRelativePath || file.name);
}
return out;
};
export const FileRenderer = ({ session, disconnectFromServer, setOpenFileEditors, isActive, onOpenTerminal }) => {
const { t } = useTranslation();
const { sessionToken } = useContext(UserContext);
@@ -136,8 +214,14 @@ export const FileRenderer = ({ session, disconnectFromServer, setOpenFileEditors
}
};
const uploadFileHttp = async (file, targetDir) => {
const filePath = `${targetDir}/${file.name}`.replace(/\/+/g, '/');
const uploadFileHttp = async (file, targetDir, relativePath) => {
const rel = (relativePath || file.name || "").split("/").filter(Boolean).join("/");
if (!rel) {
sendToast(t("common.error"), t("servers.fileManager.toast.uploadFailed", { message: "Missing file path", ...i18nUnescapedInterpolation }));
return false;
}
const filePath = `${targetDir.replace(/\/+$/, "")}/${rel}`.replace(/\/+/g, "/");
const label = rel.includes("/") ? rel : file.name;
setIsUploading(true);
setUploadProgress(0);
@@ -151,11 +235,11 @@ export const FileRenderer = ({ session, disconnectFromServer, setOpenFileEditors
setIsUploading(false);
setUploadProgress(0);
listFiles();
sendToast(t("common.success"), t("servers.fileManager.toast.uploaded", { name: file.name }));
sendToast(t("common.success"), t("servers.fileManager.toast.uploaded", { name: label, ...i18nUnescapedInterpolation }));
return true;
} catch (err) {
console.error("Upload error:", err);
sendToast(t("common.error"), t("servers.fileManager.toast.uploadFailed", { message: err.message }));
sendToast(t("common.error"), t("servers.fileManager.toast.uploadFailed", { message: err.message, ...i18nUnescapedInterpolation }));
setIsUploading(false);
setUploadProgress(0);
return false;
@@ -164,14 +248,18 @@ export const FileRenderer = ({ session, disconnectFromServer, setOpenFileEditors
const processUploadQueue = async () => {
while (uploadQueueRef.current.length > 0) {
const { file, targetDir } = uploadQueueRef.current[0];
await uploadFileHttp(file, targetDir);
const { file, targetDir, relativePath } = uploadQueueRef.current[0];
await uploadFileHttp(file, targetDir, relativePath);
uploadQueueRef.current.shift();
}
};
const queueUpload = (file, targetDir) => {
uploadQueueRef.current.push({ file, targetDir });
const queueUpload = (file, targetDir, relativePath) => {
uploadQueueRef.current.push({
file,
targetDir,
relativePath: relativePath != null ? relativePath : file.name,
});
if (uploadQueueRef.current.length === 1) processUploadQueue();
};
@@ -345,7 +433,21 @@ export const FileRenderer = ({ session, disconnectFromServer, setOpenFileEditors
else if (e.type === "dragleave" && !dropZoneRef.current.contains(e.relatedTarget)) setDragging(false);
else if (e.type === "drop") {
setDragging(false);
for (let i = 0; i < e.dataTransfer.files.length; i++) queueUpload(e.dataTransfer.files[i], directory);
(async () => {
try {
const entries = await collectDroppedFiles(e.dataTransfer);
if (!entries.length) {
sendToast(t("common.error"), t("servers.fileManager.toast.error"));
return;
}
for (const { file, relativePath } of entries) {
queueUpload(file, directory, relativePath);
}
} catch (err) {
console.error("Drop handling error:", err);
sendToast(t("common.error"), t("servers.fileManager.toast.uploadFailed", { message: err.message || String(err), ...i18nUnescapedInterpolation }));
}
})();
}
};
+1
View File
@@ -28,6 +28,7 @@ const AUDIT_ACTIONS = {
FILE_UPLOAD: "file.upload",
FILE_DOWNLOAD: "file.download",
FILE_CREATE: "file.create",
FILE_DELETE: "file.delete",
FILE_RENAME: "file.rename",
FILE_CHMOD: "file.chmod",
+2 -1
View File
@@ -7,7 +7,7 @@ const Entry = require("../models/Entry");
const Identity = require("../models/Identity");
const { createAuditLog, AUDIT_ACTIONS, RESOURCE_TYPES } = require("../controllers/audit");
const { createSSH } = require("../utils/createSSH");
const { addFolderToArchive } = require("../utils/sftpHelpers");
const { addFolderToArchive, ensureSftpParentDirs } = require("../utils/sftpHelpers");
const logger = require("../utils/logger");
const archiver = require("archiver");
const sharp = require("sharp");
@@ -211,6 +211,7 @@ app.post("/upload", async (req, res) => {
}
sftp = await sftpConnect(ssh);
await ensureSftpParentDirs(sftp, remotePath);
await new Promise((resolve, reject) => {
sftp.fastPut(tempFile, remotePath, { concurrency: 64, chunkSize: 32768 }, (err) => err ? reject(err) : resolve());
});
+52 -1
View File
@@ -1,3 +1,48 @@
const posixDirname = (p) => {
if (!p || p === "/") return "/";
const normalized = String(p).replace(/\/+$/, "");
const i = normalized.lastIndexOf("/");
if (i <= 0) return "/";
return normalized.slice(0, i) || "/";
};
const isSftpDirStat = (st) => {
if (!st) return false;
if (typeof st.isDirectory === "function") return st.isDirectory();
return (st.mode & 0o170000) === 0o040000;
};
/**
* Create parent directories for a remote file path (mkdir -p semantics).
* Ignores if a path component already exists as a directory.
*/
const ensureSftpParentDirs = (sftp, remoteFilePath) => new Promise((resolve, reject) => {
const dir = posixDirname(remoteFilePath);
if (dir === "/" || dir === "") return resolve();
const mkdirOne = (target, cb) => {
sftp.mkdir(target, (err) => {
if (!err) return cb(null);
sftp.stat(target, (stErr, st) => {
if (!stErr && isSftpDirStat(st)) return cb(null);
cb(err);
});
});
};
const ensureDir = (target, cb) => {
if (target === "/" || target === "") return cb(null);
const parent = posixDirname(target);
if (parent === target) return cb(null);
ensureDir(parent, (err) => {
if (err) return cb(err);
mkdirOne(target, cb);
});
};
ensureDir(dir, (err) => (err ? reject(err) : resolve()));
});
const deleteFolderRecursive = (sftp, folderPath, callback) => {
sftp.readdir(folderPath, (err, list) => {
if (err) return err.code === 2 ? callback(null) : callback(err);
@@ -142,4 +187,10 @@ const addFolderToArchive = (sftp, folderPath, archive, basePath = "", activeStre
return addFolder(folderPath, basePath);
};
module.exports = { deleteFolderRecursive, searchDirectories, addFolderToArchive, OPERATIONS };
module.exports = {
deleteFolderRecursive,
searchDirectories,
addFolderToArchive,
ensureSftpParentDirs,
OPERATIONS,
};