mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 00:06:17 +00:00
fix: give a restored file with a clashing WebDAV name a free name the server accepts
This commit is contained in:
@@ -96,6 +96,8 @@ function signIn(username: string, filesAccount = 'acc-1') {
|
||||
|
||||
beforeEach(() => {
|
||||
signIn('me@example.org');
|
||||
// No naming rules published unless a test says so.
|
||||
(jmapClient.getAccountCapability as ReturnType<typeof vi.fn>).mockReturnValue(undefined);
|
||||
nextId = 1;
|
||||
calls = [];
|
||||
refuseUpdate = new Set();
|
||||
@@ -309,6 +311,18 @@ describe('restoreFromTrash', () => {
|
||||
expect(byId('a')!.parentId).toBeNull();
|
||||
});
|
||||
|
||||
it('gives a clashing WebDAV name a free one the server accepts', async () => {
|
||||
// Stored by WebDAV with a ":" escaped, which JMAP names refuse.
|
||||
store.push({ id: 'w', parentId: 'docs', name: 'a%3Ab.txt', blobId: 'bw' });
|
||||
await moveToTrash(listed(), listed().filter((n) => n.id === 'w'), 1730112000007);
|
||||
store.push({ id: 'w2', parentId: 'docs', name: 'a%3Ab.txt', blobId: 'bw2' });
|
||||
(jmapClient.getAccountCapability as ReturnType<typeof vi.fn>).mockReturnValue({ forbiddenNameChars: ':*' });
|
||||
await restoreFromTrash(trashedItems(listed()));
|
||||
const update = calls.filter((c) => c.update).pop()!.update as Record<string, Record<string, unknown>>;
|
||||
expect(update.w).toEqual({ parentId: 'docs', name: 'a_b (1).txt' });
|
||||
expect(byId('w')).toMatchObject({ parentId: 'docs', name: 'a_b (1).txt' });
|
||||
});
|
||||
|
||||
it('gives a folder named .Trash that goes back to the root another name', async () => {
|
||||
store.push({ id: 'x', parentId: 'docs', name: '.Trash', blobId: null }, { id: 'xin', parentId: 'x', name: 'in.txt', blobId: 'bi' });
|
||||
let nodes = listed();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { FileNode } from './types';
|
||||
import { jmapClient } from './jmap-client';
|
||||
import {
|
||||
createFolder, deleteFileNodes, destroyEmptyFolders, filesAccountId, getAllFileNodes, isCrossAccountId,
|
||||
isFolder, updateFileNodes,
|
||||
createFolder, deleteFileNodes, destroyEmptyFolders, filesAccountId, getAllFileNodes, getFileNameRules,
|
||||
isCrossAccountId, isFolder, updateFileNodes,
|
||||
} from './files';
|
||||
import { JMAPSetError } from './jmap-result';
|
||||
import { getUniqueName } from '../lib/filenode-name';
|
||||
import { acceptedFileName } from '../lib/file-name-rules';
|
||||
import {
|
||||
TRASH_FOLDER_NAME, findTrashFolder, restoreTarget, trashBatchName, trashPlaces, trashSubtreeIds, trashedItems,
|
||||
type TrashedItem,
|
||||
@@ -226,6 +227,7 @@ export async function restoreFromTrash(items: TrashedItem[], drive?: TrashDrive)
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const rules = getFileNameRules();
|
||||
const restored = new Set<string>();
|
||||
const tried = new Set<string>();
|
||||
const updates: Record<string, Partial<Pick<FileNode, 'name' | 'parentId'>>> = {};
|
||||
@@ -249,7 +251,11 @@ export async function restoreFromTrash(items: TrashedItem[], drive?: TrashDrive)
|
||||
? from
|
||||
: restoreTarget(item, places);
|
||||
const used = namesIn(target);
|
||||
const name = getUniqueName(item.node.name, used);
|
||||
let name = getUniqueName(item.node.name, used);
|
||||
// A new name goes to the server as it is, so it must pass its rules: a
|
||||
// WebDAV name lists decoded ("a%3Ab.txt" as "a:b.txt"), and the ":"
|
||||
// in "a:b (1).txt" would be refused.
|
||||
if (name !== item.node.name) name = getUniqueName(acceptedFileName(name, rules), used);
|
||||
used.add(name);
|
||||
// The name only goes along when it has to change: the listing shows a
|
||||
// WebDAV name decoded, and sending that back would rename the file.
|
||||
|
||||
Reference in New Issue
Block a user