mirror of
https://github.com/filebrowser/filebrowser.git
synced 2026-07-17 16:36:49 +00:00
fix: copy/move allow overwrite
This commit is contained in:
parent
5f7311d324
commit
a1a514dcbb
8 changed files with 70 additions and 13 deletions
|
|
@ -179,6 +179,7 @@ const drop = async (event: Event) => {
|
|||
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
||||
name: fileStore.req?.items[i].name,
|
||||
size: fileStore.req?.items[i].size,
|
||||
isDir: fileStore.req?.items[i].isDir,
|
||||
modified: fileStore.req?.items[i].modified,
|
||||
overwrite: false,
|
||||
rename: false,
|
||||
|
|
@ -204,7 +205,7 @@ const drop = async (event: Event) => {
|
|||
.catch($showError);
|
||||
};
|
||||
|
||||
const conflict = await upload.checkConflict(items, path);
|
||||
const conflict = await upload.checkConflict(items, path, true);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
layoutStore.showHover({
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export default {
|
|||
});
|
||||
};
|
||||
|
||||
const conflict = await upload.checkConflict(items, this.dest);
|
||||
const conflict = await upload.checkConflict(items, this.dest, true);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
this.showHover({
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export default {
|
|||
});
|
||||
};
|
||||
|
||||
const conflict = await upload.checkConflict(items, this.dest);
|
||||
const conflict = await upload.checkConflict(items, this.dest, true);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
this.showHover({
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ describe("copy and move conflict prompts", () => {
|
|||
isDir: false,
|
||||
}),
|
||||
],
|
||||
"/files/target/"
|
||||
"/files/target/",
|
||||
true
|
||||
);
|
||||
expect(context.showHover).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -109,7 +110,8 @@ describe("copy and move conflict prompts", () => {
|
|||
isDir: false,
|
||||
}),
|
||||
],
|
||||
"/files/target/"
|
||||
"/files/target/",
|
||||
true
|
||||
);
|
||||
expect(context.showHover).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
|
|||
1
frontend/src/types/file.d.ts
vendored
1
frontend/src/types/file.d.ts
vendored
|
|
@ -53,6 +53,7 @@ interface ClipItem {
|
|||
from: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
isDir?: boolean;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,49 @@ describe("checkConflict", () => {
|
|||
expect(conflicts[0].name).toBe("/target/folder/deep/file.txt");
|
||||
});
|
||||
|
||||
// Copy/move stats the whole destination, so a same-named directory is a
|
||||
// conflict in its own right (regression for the directory case of #5957).
|
||||
it("reports a directory conflict for copy/move (includeDirectories)", async () => {
|
||||
vi.mocked(api.fetchAll).mockResolvedValue([
|
||||
{
|
||||
path: "/target/folder",
|
||||
name: "folder",
|
||||
size: 0,
|
||||
modified: "2026-06-04T00:00:00Z",
|
||||
isDir: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const items = [{ ...moveItem("folder", "/files/target/", 0), isDir: true }];
|
||||
|
||||
const conflicts = await checkConflict(items, "/files/target/", true);
|
||||
|
||||
expect(conflicts).toHaveLength(1);
|
||||
expect(conflicts[0].name).toBe("/target/folder");
|
||||
});
|
||||
|
||||
// Uploads merge into an existing folder, so the directory itself must not be
|
||||
// reported — only the files inside it can conflict.
|
||||
it("ignores a directory conflict for uploads (default)", async () => {
|
||||
vi.mocked(api.fetchAll).mockResolvedValue([
|
||||
{
|
||||
path: "/target/folder",
|
||||
name: "folder",
|
||||
size: 0,
|
||||
modified: "2026-06-04T00:00:00Z",
|
||||
isDir: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const files = [
|
||||
{ name: "folder", size: 0, isDir: true, fullPath: "folder" },
|
||||
];
|
||||
|
||||
const conflicts = await checkConflict(files, "/files/target/");
|
||||
|
||||
expect(conflicts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns no conflicts when the recursive listing fails", async () => {
|
||||
vi.mocked(api.fetchAll).mockRejectedValue(new Error("404"));
|
||||
|
||||
|
|
|
|||
|
|
@ -26,16 +26,24 @@ function conflictKey(item: UploadEntry): string {
|
|||
* server, so the caller can prompt the user to overwrite/rename/skip.
|
||||
*
|
||||
* The whole destination tree is fetched once and indexed by path relative to
|
||||
* the destination, then every file is looked up directly — no need to mirror
|
||||
* the upload's folder structure. Directories are never reported: an existing
|
||||
* folder is merged on upload, and a copy/move onto one is rejected server-side.
|
||||
* the destination, then every entry is looked up directly — no need to mirror
|
||||
* the upload's folder structure.
|
||||
*
|
||||
* @param files - flat upload list to check
|
||||
* @param basePath - server destination path (e.g. "/files/uploads/")
|
||||
* Directory handling differs by action, hence `includeDirectories`:
|
||||
* - Upload (false): an existing folder is silently merged, so only the
|
||||
* individual files inside it can conflict.
|
||||
* - Copy/move (true): the server stats the destination and rejects it whole if
|
||||
* a same-named entry exists, so the directory itself is a conflict. The list
|
||||
* only holds the top-level items being moved, so each is reported once.
|
||||
*
|
||||
* @param files - flat upload list to check
|
||||
* @param basePath - server destination path (e.g. "/files/uploads/")
|
||||
* @param includeDirectories - report directories as conflicts (copy/move)
|
||||
*/
|
||||
export async function checkConflict(
|
||||
files: UploadList,
|
||||
basePath: string
|
||||
basePath: string,
|
||||
includeDirectories = false
|
||||
): Promise<ConflictingResource[]> {
|
||||
if (files.length === 0) return [];
|
||||
|
||||
|
|
@ -62,7 +70,7 @@ export async function checkConflict(
|
|||
|
||||
const conflicts: ConflictingResource[] = [];
|
||||
files.forEach((file, index) => {
|
||||
if (file.isDir) return; // see directory note above
|
||||
if (file.isDir && !includeDirectories) return; // see directory note above
|
||||
|
||||
const server = serverMap.get(conflictKey(file));
|
||||
if (!server) return;
|
||||
|
|
|
|||
|
|
@ -632,6 +632,7 @@ const copyCut = (event: Event | KeyboardEvent): void => {
|
|||
from: fileStore.req.items[i].url,
|
||||
name: fileStore.req.items[i].name,
|
||||
size: fileStore.req.items[i].size,
|
||||
isDir: fileStore.req.items[i].isDir,
|
||||
modified: fileStore.req.items[i].modified,
|
||||
});
|
||||
}
|
||||
|
|
@ -661,6 +662,7 @@ const paste = async (event: Event) => {
|
|||
to,
|
||||
name: item.name,
|
||||
size: item.size,
|
||||
isDir: item.isDir,
|
||||
modified: item.modified,
|
||||
overwrite: false,
|
||||
rename: clipboardStore.path == route.path,
|
||||
|
|
@ -697,7 +699,7 @@ const paste = async (event: Event) => {
|
|||
}
|
||||
|
||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||
const conflict = await upload.checkConflict(items, path);
|
||||
const conflict = await upload.checkConflict(items, path, true);
|
||||
|
||||
if (conflict.length > 0) {
|
||||
layoutStore.showHover({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue