Skip to content

Commit 1796fdb

Browse files
authored
fix: misc fs issues (#2883)
* fix: deps * fix: FS issues
1 parent 25f0237 commit 1796fdb

10 files changed

Lines changed: 262 additions & 2204 deletions

File tree

package-lock.json

Lines changed: 166 additions & 2171 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"@stylistic/eslint-plugin": "^5.3.1",
1717
"@types/express": "^5.0.0",
1818
"@types/mime-types": "^3.0.1",
19-
"@types/uuid": "^10.0.0",
2019
"@typescript-eslint/eslint-plugin": "^8.46.1",
2120
"@typescript-eslint/parser": "^8.46.1",
2221
"@vitest/coverage-v8": "^4.0.14",
@@ -32,7 +31,6 @@
3231
"html-webpack-plugin": "^5.6.0",
3332
"husky": "^9.1.7",
3433
"license-check-and-add": "^4.0.5",
35-
"mocha": "^7.2.0",
3634
"nodemon": "^3.1.0",
3735
"prettier": "^3.8.3",
3836
"simple-git": "^3.32.3",

src/backend/controllers/fs/FSController.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,21 +1443,6 @@ export class FSController extends PuterController {
14431443
return candidate;
14441444
}
14451445

1446-
#isDedupeEnabled(fileMetadata: FSEntryWriteInput | undefined): boolean {
1447-
if (!fileMetadata) {
1448-
return false;
1449-
}
1450-
const metadataRecord = fileMetadata as unknown as Record<
1451-
string,
1452-
unknown
1453-
>;
1454-
const dedupeCandidate = this.#firstDefined(
1455-
fileMetadata.dedupeName,
1456-
metadataRecord.dedupe_name,
1457-
);
1458-
return this.#toBoolean(dedupeCandidate) ?? false;
1459-
}
1460-
14611446
#resolveWriteFileMetadata(
14621447
fileMetadata: FSEntryWriteInput | undefined,
14631448
fallbackSource?: unknown,
@@ -1834,9 +1819,8 @@ export class FSController extends PuterController {
18341819
throw new HttpError(400, 'Cannot write to root path');
18351820
}
18361821

1837-
const dedupeEnabled = this.#isDedupeEnabled(normalizedFileMetadata);
18381822
let pathToCheck = parentPath;
1839-
if (Boolean(normalizedFileMetadata.overwrite) && !dedupeEnabled) {
1823+
if (Boolean(normalizedFileMetadata.overwrite)) {
18401824
const destinationExists =
18411825
await this.services.fs.entryExistsByPath(targetPath);
18421826
if (destinationExists) {

src/backend/controllers/fs/LegacyFSController.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,16 +448,31 @@ export class LegacyFSController extends PuterController {
448448
if (!rawPath) throw new HttpError(400, '`path` is required');
449449

450450
// Supports `{ parent, path }` where `path` is a relative suffix.
451+
// When `parent` is a path string, use it directly without requiring
452+
// the entry to exist — `services.fs.mkdir` honors `create_missing_parents`
453+
// and will materialize any missing intermediate directories.
451454
let targetPath = rawPath;
452455
if (body.parent !== undefined && !rawPath.startsWith('/')) {
453-
const parent = await resolveV1Selector(
454-
this.stores.fsEntry,
455-
body.parent,
456-
);
456+
let parentPath: string;
457+
if (
458+
typeof body.parent === 'string' &&
459+
(body.parent.startsWith('/') || body.parent.startsWith('~'))
460+
) {
461+
parentPath = this.#expandTilde(
462+
body.parent,
463+
actor.user?.username,
464+
);
465+
} else {
466+
const parent = await resolveV1Selector(
467+
this.stores.fsEntry,
468+
body.parent,
469+
);
470+
parentPath = parent.path;
471+
}
457472
targetPath =
458-
parent.path === '/'
473+
parentPath === '/'
459474
? `/${rawPath}`
460-
: `${parent.path}/${rawPath}`;
475+
: `${parentPath.replace(/\/+$/, '')}/${rawPath}`;
461476
}
462477

463478
const parentPath = pathPosix.dirname(

src/backend/package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
"@aws-sdk/client-polly": "^3.1028.0",
1515
"@aws-sdk/client-s3": "^3.1028.0",
1616
"@aws-sdk/client-textract": "^3.1028.0",
17-
"@aws-sdk/s3-request-presigner": "^3.1028.0",
1817
"@aws-sdk/credential-providers": "^3.1021.0",
1918
"@aws-sdk/lib-dynamodb": "^3.490.0",
19+
"@aws-sdk/s3-request-presigner": "^3.1028.0",
2020
"@google/genai": "^1.19.0",
2121
"@heyputer/kv.js": "^0.2.1",
2222
"@heyputer/putility": "^1.0.0",
@@ -57,7 +57,7 @@
5757
"mime-types": "^2.1.35",
5858
"murmurhash": "^2.0.1",
5959
"mysql2": "^3.21.1",
60-
"nodemailer": "^7.0.13",
60+
"nodemailer": "^8.0.7",
6161
"openai": "^6.34.0",
6262
"otpauth": "^9.2.4",
6363
"parse-domain": "^8.2.2",
@@ -69,13 +69,12 @@
6969
"together-ai": "^0.33.0",
7070
"ua-parser-js": "^1.0.41",
7171
"uglify-js": "^3.17.4",
72-
"uuid": "^9.0.1",
72+
"uuid": "^14.0.0",
7373
"validator": "^13.15.35"
7474
},
7575
"devDependencies": {
7676
"@types/node": "^24.0.0",
7777
"chai": "^4.3.7",
78-
"mocha": "^7.2.0",
7978
"nodemon": "^3.1.0",
8079
"typescript": "^5.9.3",
8180
"vite": "^8.0.0",

src/backend/services/fs/FSService.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,14 @@ export class FSService extends PuterService {
417417
const pathReservedInBatch = reservedPaths.has(normalizedInput.path);
418418

419419
if (pathReservedInBatch || existingEntry) {
420-
if (normalizedInput.dedupeName) {
420+
if (normalizedInput.overwrite) {
421+
if (pathReservedInBatch) {
422+
throw new HttpError(
423+
409,
424+
`Batch contains duplicate target path: ${normalizedInput.path}`,
425+
);
426+
}
427+
} else if (normalizedInput.dedupeName) {
421428
const dedupedPath = await this.#findDedupedPath(
422429
normalizedInput.path,
423430
reservedPaths,

src/backend/stores/fs/FSEntryStore.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,36 @@ export class FSEntryStore extends PuterStore {
345345
});
346346
}
347347

348+
async invalidateEntryCacheById(id: number): Promise<void> {
349+
if (typeof id !== 'number' || !Number.isFinite(id)) {
350+
return;
351+
}
352+
353+
const rows = (await this.clients.db.read(
354+
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE id = ? LIMIT 1`,
355+
[id],
356+
)) as unknown as FSEntryRow[];
357+
const row = rows[0];
358+
359+
if (row) {
360+
const entry = this.#mapFSEntryRow(row);
361+
await this.#invalidateEntryCache(entry);
362+
return;
363+
}
364+
365+
const cached = await this.#readEntryFromCache(
366+
`prodfsv2:fsentry:id:${id}`,
367+
);
368+
if (cached) {
369+
await this.#invalidateEntryCache(cached);
370+
return;
371+
}
372+
373+
await this.publishCacheKeys({
374+
keys: [`prodfsv2:fsentry:id:${id}`],
375+
});
376+
}
377+
348378
#chunk<T>(values: T[], size: number): T[][] {
349379
if (values.length === 0) {
350380
return [];

src/backend/stores/subdomain/SubdomainStore.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export class SubdomainStore extends PuterStore {
218218
};
219219
await this.#refreshCache(row);
220220
await this.#invalidatePrefixListsForUser(userId);
221+
await this.#invalidateRootDirEntry(row.root_dir_id);
221222

222223
return row;
223224
}
@@ -268,6 +269,15 @@ export class SubdomainStore extends PuterStore {
268269
for (const uid of affectedUsers) {
269270
await this.#invalidatePrefixListsForUser(uid);
270271
}
272+
// FSEntry rows embed a `subdomains_agg` JSON of associated subdomains,
273+
// so any rename / root_dir reassignment must drop the stale entry
274+
// caches on both the old and new root_dir_id.
275+
const affectedRootDirIds = new Set(
276+
[before?.root_dir_id, after?.root_dir_id].filter((v) => v != null),
277+
);
278+
for (const id of affectedRootDirIds) {
279+
await this.#invalidateRootDirEntry(id);
280+
}
271281
return after;
272282
}
273283

@@ -293,6 +303,7 @@ export class SubdomainStore extends PuterStore {
293303
if (row.user_id != null) {
294304
await this.#invalidatePrefixListsForUser(row.user_id);
295305
}
306+
await this.#invalidateRootDirEntry(row.root_dir_id);
296307
}
297308
return affected;
298309
}
@@ -317,6 +328,25 @@ export class SubdomainStore extends PuterStore {
317328
});
318329
}
319330

331+
// FSEntryStore caches each row with an embedded `subdomains_agg` JSON
332+
// (uuid + subdomain) keyed on `root_dir_id`. Without this, a deleted or
333+
// renamed subdomain keeps showing up under its old folder in the GUI
334+
// (website badge, "associated websites" popover) until the entry's
335+
// independent TTL expires.
336+
async #invalidateRootDirEntry(rootDirId) {
337+
if (rootDirId == null) return;
338+
const id =
339+
typeof rootDirId === 'number' ? rootDirId : Number(rootDirId);
340+
if (!Number.isFinite(id)) return;
341+
const fsEntry = this.stores?.fsEntry;
342+
if (!fsEntry?.invalidateEntryCacheById) return;
343+
try {
344+
await fsEntry.invalidateEntryCacheById(id);
345+
} catch {
346+
/* best-effort */
347+
}
348+
}
349+
320350
async #invalidatePrefixListsForUser(userId) {
321351
if (userId == null) return;
322352
const trackerKey = this.#prefixListTrackerKey(userId);

src/gui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
"dependencies": {
4848
"file-type": "21.3.3",
4949
"json-colorizer": "^3.0.1",
50-
"mocha": "7.2.0",
5150
"music-metadata": "11.12.3",
51+
"nodemailer": "8.0.7",
5252
"string-template": "^1.0.0",
53-
"uuid": "^9.0.1"
53+
"uuid": "^14.0.0"
5454
}
5555
}

src/puter-js/src/modules/FileSystem/operations/upload.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ const upload = async function (items, dirPath, options = {}) {
716716
size: file.size,
717717
contentType: file.type || 'application/octet-stream',
718718
overwrite: overwriteEnabled,
719-
dedupeName: options.dedupeName ?? true,
719+
dedupeName: overwriteEnabled? false: options.dedupeName ?? true,
720720
createMissingParents: shouldCreateMissingParents,
721721
app_uid: options.appUID,
722722
};

0 commit comments

Comments
 (0)