Skip to content

Commit 15134c9

Browse files
Fix mkdir dedupeName for existing directories (HeyPuter#3215)
1 parent 8262e31 commit 15134c9

5 files changed

Lines changed: 187 additions & 7 deletions

File tree

src/backend/controllers/fs/FSController.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,82 @@ describe('FSController.mkdirEntry', () => {
953953
expect(body.isDir).toBe(true);
954954
});
955955

956+
it('dedupes an existing directory when dedupe_name is true', async () => {
957+
const { actor } = await makeUser();
958+
const username = actor.user!.username!;
959+
const target = `/${username}/Documents/hello`;
960+
961+
await withActor(actor, () =>
962+
controller.mkdirEntry(
963+
makeReq({
964+
body: { path: target },
965+
actor,
966+
}),
967+
makeRes().res,
968+
),
969+
);
970+
971+
const { res, captured } = makeRes();
972+
await withActor(actor, () =>
973+
controller.mkdirEntry(
974+
makeReq({
975+
body: { path: target, dedupe_name: true },
976+
actor,
977+
}),
978+
res,
979+
),
980+
);
981+
982+
const body = captured.body as {
983+
path: string;
984+
name: string;
985+
isDir: boolean;
986+
};
987+
expect(body.path).toBe(`/${username}/Documents/hello (1)`);
988+
expect(body.name).toBe('hello (1)');
989+
expect(body.isDir).toBe(true);
990+
expect(
991+
await server.stores.fsEntry.getEntryByPath(
992+
`/${username}/Documents/hello (1)`,
993+
),
994+
).toMatchObject({ isDir: true });
995+
});
996+
997+
it('requires parent write when deduping an existing directory', async () => {
998+
const { actor: userActor } = await makeUser();
999+
const username = userActor.user!.username!;
1000+
const appUid = `app-mkdir-${uuidv4()}`;
1001+
const appActor: Actor = { ...userActor, app: { uid: appUid } };
1002+
const target = `/${username}/AppData/${appUid}`;
1003+
1004+
await withActor(userActor, () =>
1005+
controller.mkdirEntry(
1006+
makeReq({
1007+
body: { path: target },
1008+
actor: userActor,
1009+
}),
1010+
makeRes().res,
1011+
),
1012+
);
1013+
1014+
await expect(
1015+
withActor(appActor, () =>
1016+
controller.mkdirEntry(
1017+
makeReq({
1018+
body: { path: target, dedupe_name: true },
1019+
actor: appActor,
1020+
}),
1021+
makeRes().res,
1022+
),
1023+
),
1024+
).rejects.toMatchObject({ statusCode: 404 });
1025+
expect(
1026+
await server.stores.fsEntry.getEntryByPath(
1027+
`/${username}/AppData/${appUid} (1)`,
1028+
),
1029+
).toBeNull();
1030+
});
1031+
9561032
it('expands ~/ in the path to the user home', async () => {
9571033
const { actor } = await makeUser();
9581034
const username = actor.user!.username!;

src/backend/controllers/fs/FSController.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,13 +1039,15 @@ export class FSController extends PuterController {
10391039
legacyCode: 'bad_request',
10401040
});
10411041

1042+
const dedupeName =
1043+
this.#toBoolean(body.dedupe_name ?? body.dedupeName) ?? false;
10421044
await this.#assertCanCreate(actor, path);
1045+
if (dedupeName) await this.#assertCanDedupeCreate(actor, path);
10431046

10441047
const entry = await this.services.fs.mkdir(userId, {
10451048
path,
10461049
overwrite: this.#toBoolean(body.overwrite) ?? false,
1047-
dedupeName:
1048-
this.#toBoolean(body.dedupe_name ?? body.dedupeName) ?? false,
1050+
dedupeName,
10491051
createMissingParents:
10501052
this.#toBoolean(
10511053
body.create_missing_parents ??
@@ -1310,6 +1312,17 @@ export class FSController extends PuterController {
13101312
await this.#assertAccess(actor, parentForCheck, 'write');
13111313
}
13121314

1315+
async #assertCanDedupeCreate(actor: Actor, targetPath: string) {
1316+
const existing = await this.stores.fsEntry.getEntryByPath(targetPath);
1317+
if (!existing) return;
1318+
const parent = pathPosix.dirname(targetPath);
1319+
await this.#assertAccess(
1320+
actor,
1321+
parent === '/' ? targetPath : parent,
1322+
'write',
1323+
);
1324+
}
1325+
13131326
async #assertAccess(
13141327
actor: Actor,
13151328
path: string,

src/backend/controllers/fs/LegacyFSController.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,78 @@ describe('LegacyFSController.mkdir', () => {
206206
expect(fetched?.isDir).toBe(true);
207207
});
208208

209+
it('dedupes an existing directory when dedupe_name is true', async () => {
210+
const { actor } = await makeUser();
211+
const username = actor.user!.username!;
212+
const parent = `/${username}/Documents`;
213+
214+
await withActor(actor, () =>
215+
controller.mkdir(
216+
makeReq({
217+
body: { parent, path: 'hello' },
218+
actor,
219+
}),
220+
makeRes().res,
221+
),
222+
);
223+
224+
const { res, captured } = makeRes();
225+
await withActor(actor, () =>
226+
controller.mkdir(
227+
makeReq({
228+
body: { parent, path: 'hello', dedupe_name: true },
229+
actor,
230+
}),
231+
res,
232+
),
233+
);
234+
235+
const body = captured.body as Record<string, unknown>;
236+
expect(body).toMatchObject({
237+
path: `${parent}/hello (1)`,
238+
name: 'hello (1)',
239+
is_dir: true,
240+
});
241+
expect(
242+
await server.stores.fsEntry.getEntryByPath(`${parent}/hello (1)`),
243+
).toMatchObject({ isDir: true });
244+
});
245+
246+
it('requires parent write when deduping an existing directory', async () => {
247+
const { actor: userActor } = await makeUser();
248+
const username = userActor.user!.username!;
249+
const appUid = `app-legacy-mkdir-${uuidv4()}`;
250+
const appActor: Actor = { ...userActor, app: { uid: appUid } };
251+
const parent = `/${username}/AppData`;
252+
253+
await withActor(userActor, () =>
254+
controller.mkdir(
255+
makeReq({
256+
body: { parent, path: appUid },
257+
actor: userActor,
258+
}),
259+
makeRes().res,
260+
),
261+
);
262+
263+
await expect(
264+
withActor(appActor, () =>
265+
controller.mkdir(
266+
makeReq({
267+
body: { parent, path: appUid, dedupe_name: true },
268+
actor: appActor,
269+
}),
270+
makeRes().res,
271+
),
272+
),
273+
).rejects.toMatchObject({ statusCode: 404 });
274+
expect(
275+
await server.stores.fsEntry.getEntryByPath(
276+
`${parent}/${appUid} (1)`,
277+
),
278+
).toBeNull();
279+
});
280+
209281
it("rejects writing into another user's home with a 4xx", async () => {
210282
const a = await makeUser();
211283
const b = await makeUser();

src/backend/controllers/fs/LegacyFSController.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,17 +427,33 @@ export class LegacyFSController extends PuterController {
427427
const normalizedTarget = targetPath.startsWith('/')
428428
? targetPath
429429
: `/${targetPath}`;
430+
const dedupeName =
431+
getBoolean(body, 'dedupe_name', 'change_name') ?? false;
430432
await assertCanCreate(
431433
this.services.acl,
432434
this.services.fs,
433435
actor,
434436
normalizedTarget,
435437
);
438+
if (dedupeName) {
439+
const existing =
440+
await this.stores.fsEntry.getEntryByPath(normalizedTarget);
441+
if (existing) {
442+
const parent = pathPosix.dirname(normalizedTarget);
443+
await assertAccess(
444+
this.services.acl,
445+
this.services.fs,
446+
actor,
447+
parent === '/' ? normalizedTarget : parent,
448+
'write',
449+
);
450+
}
451+
}
436452

437453
const entry = await this.services.fs.mkdir(userId, {
438454
path: targetPath,
439455
overwrite: getBoolean(body, 'overwrite') ?? false,
440-
dedupeName: getBoolean(body, 'dedupe_name', 'change_name') ?? false,
456+
dedupeName,
441457
createMissingParents:
442458
getBoolean(
443459
body,

src/backend/services/fs/FSService.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2840,10 +2840,13 @@ export class FSService extends PuterService {
28402840
const existing = await this.stores.fsEntry.getEntryByPath(targetPath);
28412841
if (existing) {
28422842
if (existing.isDir) {
2843-
// A directory already exists at path: idempotent success.
2844-
return existing;
2845-
}
2846-
if (input.overwrite) {
2843+
if (input.dedupeName) {
2844+
name = await this.#findDedupedName(parent, name);
2845+
} else {
2846+
// A directory already exists at path: idempotent success.
2847+
return existing;
2848+
}
2849+
} else if (input.overwrite) {
28472850
// Remove the non-directory occupant then create the dir.
28482851
await this.remove(userId, {
28492852
entry: existing,

0 commit comments

Comments
 (0)