Skip to content

Commit 0f3f963

Browse files
authored
Merge pull request #1833 from rktjmp/feat-upload-no-clobber
Dont clobber existing files when uploading
2 parents 924f7ca + 274780e commit 0f3f963

File tree

2 files changed

+131
-15
lines changed

2 files changed

+131
-15
lines changed

client/codemirror/editor_paste.ts

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,26 @@ function striptHtmlComments(s: string): string {
3636
return s.replace(/<!--[\s\S]*?-->/g, "");
3737
}
3838

39+
function ensureValidFilenameWithExtension(filename: string): string {
40+
if (isValidPath(filename)) {
41+
return filename;
42+
}
43+
const match = filename.match(/\.([^.]+)$/);
44+
return `file.${match ? match[1] : "txt"}`;
45+
}
46+
47+
async function doesFileExist(
48+
editor: Client,
49+
filePath: string,
50+
): Promise<boolean> {
51+
try {
52+
await editor.space.spacePrimitives.getFileMeta(filePath);
53+
return true;
54+
} catch {
55+
return false;
56+
}
57+
}
58+
3959
const urlRegexp =
4060
/^https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
4161

@@ -207,6 +227,9 @@ export function documentExtension(editor: Client) {
207227

208228
async function saveFile(file: UploadFile) {
209229
const maxSize = maximumDocumentSize;
230+
const invalidPathMessage =
231+
"Unable to upload file, invalid target filename or path";
232+
210233
if (file.content.length > maxSize * 1024 * 1024) {
211234
editor.flashNotification(
212235
`Document is too large, maximum is ${maxSize}MiB`,
@@ -215,21 +238,62 @@ export function documentExtension(editor: Client) {
215238
return;
216239
}
217240

218-
const finalFilePath = await editor.prompt(
241+
let desiredFilePath = await editor.prompt(
219242
"File name for pasted document",
220243
resolveMarkdownLink(
221244
client.currentPath(),
222-
isValidPath(file.name)
223-
? file.name
224-
: `file.${
225-
file.name.indexOf(".") !== -1 ? file.name.split(".").pop() : "txt"
226-
}`,
245+
ensureValidFilenameWithExtension(file.name),
227246
),
228247
);
229-
if (!finalFilePath || !isValidName(finalFilePath)) {
248+
if (desiredFilePath === undefined) {
249+
// User hit cancel, so they know why we stopped and dont need an notification.
250+
return;
251+
}
252+
desiredFilePath = desiredFilePath.trim();
253+
if (!isValidName(desiredFilePath)) {
254+
editor.flashNotification(invalidPathMessage, "error");
230255
return;
231256
}
232257

258+
// Check the given desired file path wont clobber an existing file. If it
259+
// would, ask the user to confirm or provide another filename. Repeat this
260+
// check for every new filename they give.
261+
// Note: duplicate any modifications here to client/code_mirror/editor_paste.ts
262+
let finalFilePath = null;
263+
while (finalFilePath == null) {
264+
if (await doesFileExist(editor, desiredFilePath)) {
265+
let confirmedFilePath = await editor.prompt(
266+
"A file with that name already exists, keep the same name to replace it, or rename your file",
267+
resolveMarkdownLink(
268+
client.currentPath(),
269+
ensureValidFilenameWithExtension(desiredFilePath),
270+
),
271+
);
272+
if (confirmedFilePath === undefined) {
273+
// Unlike the initial filename prompt, we're inside a workflow here
274+
// and should be explicit that the user action cancelled the whole
275+
// operation.
276+
editor.flashNotification("Upload cancelled by user", "info");
277+
return;
278+
}
279+
confirmedFilePath = confirmedFilePath.trim();
280+
if (!isValidPath(confirmedFilePath)) {
281+
editor.flashNotification(invalidPathMessage, "error");
282+
return;
283+
}
284+
if (desiredFilePath === confirmedFilePath) {
285+
// if we got back the same path, we're replacing and should accept the given name
286+
finalFilePath = desiredFilePath;
287+
} else {
288+
// we got a new path, so we must repeat the check
289+
desiredFilePath = confirmedFilePath;
290+
confirmedFilePath = undefined;
291+
}
292+
} else {
293+
finalFilePath = desiredFilePath;
294+
}
295+
}
296+
233297
await editor.space.writeDocument(finalFilePath, file.content);
234298
let documentMarkdown = `[[${finalFilePath}]]`;
235299
if (file.contentType.startsWith("image/")) {

plugs/editor/upload.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,22 @@ import {
1010
} from "@silverbulletmd/silverbullet/lib/ref";
1111
import type { UploadFile } from "@silverbulletmd/silverbullet/type/client";
1212

13+
function ensureValidFilenameWithExtension(filename: string): string {
14+
if (isValidPath(filename)) {
15+
return filename;
16+
}
17+
const match = filename.match(/\.([^.]+)$/);
18+
return `file.${match ? match[1] : "txt"}`;
19+
}
20+
1321
export async function saveFile(file: UploadFile) {
22+
const invalidPathMessage =
23+
"Unable to upload file, invalid target filename or path";
1424
const maxSize = await system.getConfig<number>(
1525
"maximumDocumentSize",
1626
maximumDocumentSize,
1727
);
28+
1829
if (typeof maxSize !== "number") {
1930
await editor.flashNotification(
2031
"The setting 'maximumDocumentSize' must be a number",
@@ -29,21 +40,62 @@ export async function saveFile(file: UploadFile) {
2940
return;
3041
}
3142

32-
const finalFilePath = await editor.prompt(
33-
"File name for pasted document",
43+
let desiredFilePath = await editor.prompt(
44+
"File name for uploaded document",
3445
resolveMarkdownLink(
3546
await editor.getCurrentPath(),
36-
isValidPath(file.name)
37-
? file.name
38-
: `file.${
39-
file.name.indexOf(".") !== -1 ? file.name.split(".").pop() : "txt"
40-
}`,
47+
ensureValidFilenameWithExtension(file.name),
4148
),
4249
);
43-
if (!finalFilePath || !isValidPath(finalFilePath)) {
50+
if (desiredFilePath === undefined) {
51+
// User hit cancel, so they know why we stopped and dont need an notification.
52+
return;
53+
}
54+
desiredFilePath = desiredFilePath.trim();
55+
if (!isValidPath(desiredFilePath)) {
56+
editor.flashNotification(invalidPathMessage, "error");
4457
return;
4558
}
4659

60+
// Check the given desired file path wont clobber an existing file. If it
61+
// would, ask the user to confirm or provide another filename. Repeat this
62+
// check for every new filename they give.
63+
// Note: duplicate any modifications here to client/code_mirror/editor_paste.ts
64+
let finalFilePath = null;
65+
while (finalFilePath == null) {
66+
if (await space.fileExists(desiredFilePath)) {
67+
let confirmedFilePath = await editor.prompt(
68+
"A file with that name already exists, keep the same name to replace it, or rename your file",
69+
resolveMarkdownLink(
70+
await editor.getCurrentPath(),
71+
ensureValidFilenameWithExtension(desiredFilePath),
72+
),
73+
);
74+
if (confirmedFilePath === undefined) {
75+
// Unlike the initial filename prompt, we're inside a workflow here
76+
// and should be explicit that the user action cancelled the whole
77+
// operation.
78+
editor.flashNotification("Upload cancelled by user", "info");
79+
return;
80+
}
81+
confirmedFilePath = confirmedFilePath.trim();
82+
if (!isValidPath(confirmedFilePath)) {
83+
editor.flashNotification(invalidPathMessage, "error");
84+
return;
85+
}
86+
if (desiredFilePath === confirmedFilePath) {
87+
// if we got back the same path, we're replacing and should accept the given name
88+
finalFilePath = desiredFilePath;
89+
} else {
90+
// we got a new path, so we must repeat the check
91+
desiredFilePath = confirmedFilePath;
92+
confirmedFilePath = undefined;
93+
}
94+
} else {
95+
finalFilePath = desiredFilePath;
96+
}
97+
}
98+
4799
await space.writeDocument(finalFilePath, file.content);
48100

49101
if (await editor.getCurrentEditor() === "page") {

0 commit comments

Comments
 (0)