mirror of
https://github.com/swissmakers/infram.git
synced 2026-05-08 22:49:00 +02:00
Fix wrong path encodings in push
This commit is contained in:
+111
-9
@@ -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 }));
|
||||
}
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user