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),
|
to: props.url + encodeURIComponent(fileStore.req?.items[i].name),
|
||||||
name: fileStore.req?.items[i].name,
|
name: fileStore.req?.items[i].name,
|
||||||
size: fileStore.req?.items[i].size,
|
size: fileStore.req?.items[i].size,
|
||||||
|
isDir: fileStore.req?.items[i].isDir,
|
||||||
modified: fileStore.req?.items[i].modified,
|
modified: fileStore.req?.items[i].modified,
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
rename: false,
|
rename: false,
|
||||||
|
|
@ -204,7 +205,7 @@ const drop = async (event: Event) => {
|
||||||
.catch($showError);
|
.catch($showError);
|
||||||
};
|
};
|
||||||
|
|
||||||
const conflict = await upload.checkConflict(items, path);
|
const conflict = await upload.checkConflict(items, path, true);
|
||||||
|
|
||||||
if (conflict.length > 0) {
|
if (conflict.length > 0) {
|
||||||
layoutStore.showHover({
|
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) {
|
if (conflict.length > 0) {
|
||||||
this.showHover({
|
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) {
|
if (conflict.length > 0) {
|
||||||
this.showHover({
|
this.showHover({
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,8 @@ describe("copy and move conflict prompts", () => {
|
||||||
isDir: false,
|
isDir: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
"/files/target/"
|
"/files/target/",
|
||||||
|
true
|
||||||
);
|
);
|
||||||
expect(context.showHover).toHaveBeenCalledWith(
|
expect(context.showHover).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|
@ -109,7 +110,8 @@ describe("copy and move conflict prompts", () => {
|
||||||
isDir: false,
|
isDir: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
"/files/target/"
|
"/files/target/",
|
||||||
|
true
|
||||||
);
|
);
|
||||||
expect(context.showHover).toHaveBeenCalledWith(
|
expect(context.showHover).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
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;
|
from: string;
|
||||||
name: string;
|
name: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
isDir?: boolean;
|
||||||
modified?: string;
|
modified?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,49 @@ describe("checkConflict", () => {
|
||||||
expect(conflicts[0].name).toBe("/target/folder/deep/file.txt");
|
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 () => {
|
it("returns no conflicts when the recursive listing fails", async () => {
|
||||||
vi.mocked(api.fetchAll).mockRejectedValue(new Error("404"));
|
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.
|
* 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 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 destination, then every entry is looked up directly — no need to mirror
|
||||||
* the upload's folder structure. Directories are never reported: an existing
|
* the upload's folder structure.
|
||||||
* folder is merged on upload, and a copy/move onto one is rejected server-side.
|
|
||||||
*
|
*
|
||||||
* @param files - flat upload list to check
|
* Directory handling differs by action, hence `includeDirectories`:
|
||||||
* @param basePath - server destination path (e.g. "/files/uploads/")
|
* - 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(
|
export async function checkConflict(
|
||||||
files: UploadList,
|
files: UploadList,
|
||||||
basePath: string
|
basePath: string,
|
||||||
|
includeDirectories = false
|
||||||
): Promise<ConflictingResource[]> {
|
): Promise<ConflictingResource[]> {
|
||||||
if (files.length === 0) return [];
|
if (files.length === 0) return [];
|
||||||
|
|
||||||
|
|
@ -62,7 +70,7 @@ export async function checkConflict(
|
||||||
|
|
||||||
const conflicts: ConflictingResource[] = [];
|
const conflicts: ConflictingResource[] = [];
|
||||||
files.forEach((file, index) => {
|
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));
|
const server = serverMap.get(conflictKey(file));
|
||||||
if (!server) return;
|
if (!server) return;
|
||||||
|
|
|
||||||
|
|
@ -632,6 +632,7 @@ const copyCut = (event: Event | KeyboardEvent): void => {
|
||||||
from: fileStore.req.items[i].url,
|
from: fileStore.req.items[i].url,
|
||||||
name: fileStore.req.items[i].name,
|
name: fileStore.req.items[i].name,
|
||||||
size: fileStore.req.items[i].size,
|
size: fileStore.req.items[i].size,
|
||||||
|
isDir: fileStore.req.items[i].isDir,
|
||||||
modified: fileStore.req.items[i].modified,
|
modified: fileStore.req.items[i].modified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -661,6 +662,7 @@ const paste = async (event: Event) => {
|
||||||
to,
|
to,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
|
isDir: item.isDir,
|
||||||
modified: item.modified,
|
modified: item.modified,
|
||||||
overwrite: false,
|
overwrite: false,
|
||||||
rename: clipboardStore.path == route.path,
|
rename: clipboardStore.path == route.path,
|
||||||
|
|
@ -697,7 +699,7 @@ const paste = async (event: Event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
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) {
|
if (conflict.length > 0) {
|
||||||
layoutStore.showHover({
|
layoutStore.showHover({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue