fix: misc fs issues (#2883)

* fix: deps

* fix: FS issues
This commit is contained in:
Daniel Salazar
2026-05-01 18:54:10 -07:00
committed by GitHub
parent 25f02378cb
commit 1796fdb37b
10 changed files with 172 additions and 2114 deletions
+76 -2081
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -16,7 +16,6 @@
"@stylistic/eslint-plugin": "^5.3.1",
"@types/express": "^5.0.0",
"@types/mime-types": "^3.0.1",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.46.1",
"@typescript-eslint/parser": "^8.46.1",
"@vitest/coverage-v8": "^4.0.14",
@@ -32,7 +31,6 @@
"html-webpack-plugin": "^5.6.0",
"husky": "^9.1.7",
"license-check-and-add": "^4.0.5",
"mocha": "^7.2.0",
"nodemon": "^3.1.0",
"prettier": "^3.8.3",
"simple-git": "^3.32.3",
+1 -17
View File
@@ -1443,21 +1443,6 @@ export class FSController extends PuterController {
return candidate;
}
#isDedupeEnabled(fileMetadata: FSEntryWriteInput | undefined): boolean {
if (!fileMetadata) {
return false;
}
const metadataRecord = fileMetadata as unknown as Record<
string,
unknown
>;
const dedupeCandidate = this.#firstDefined(
fileMetadata.dedupeName,
metadataRecord.dedupe_name,
);
return this.#toBoolean(dedupeCandidate) ?? false;
}
#resolveWriteFileMetadata(
fileMetadata: FSEntryWriteInput | undefined,
fallbackSource?: unknown,
@@ -1834,9 +1819,8 @@ export class FSController extends PuterController {
throw new HttpError(400, 'Cannot write to root path');
}
const dedupeEnabled = this.#isDedupeEnabled(normalizedFileMetadata);
let pathToCheck = parentPath;
if (Boolean(normalizedFileMetadata.overwrite) && !dedupeEnabled) {
if (Boolean(normalizedFileMetadata.overwrite)) {
const destinationExists =
await this.services.fs.entryExistsByPath(targetPath);
if (destinationExists) {
@@ -448,16 +448,31 @@ export class LegacyFSController extends PuterController {
if (!rawPath) throw new HttpError(400, '`path` is required');
// Supports `{ parent, path }` where `path` is a relative suffix.
// When `parent` is a path string, use it directly without requiring
// the entry to exist — `services.fs.mkdir` honors `create_missing_parents`
// and will materialize any missing intermediate directories.
let targetPath = rawPath;
if (body.parent !== undefined && !rawPath.startsWith('/')) {
const parent = await resolveV1Selector(
this.stores.fsEntry,
body.parent,
);
let parentPath: string;
if (
typeof body.parent === 'string' &&
(body.parent.startsWith('/') || body.parent.startsWith('~'))
) {
parentPath = this.#expandTilde(
body.parent,
actor.user?.username,
);
} else {
const parent = await resolveV1Selector(
this.stores.fsEntry,
body.parent,
);
parentPath = parent.path;
}
targetPath =
parent.path === '/'
parentPath === '/'
? `/${rawPath}`
: `${parent.path}/${rawPath}`;
: `${parentPath.replace(/\/+$/, '')}/${rawPath}`;
}
const parentPath = pathPosix.dirname(
+3 -4
View File
@@ -14,9 +14,9 @@
"@aws-sdk/client-polly": "^3.1028.0",
"@aws-sdk/client-s3": "^3.1028.0",
"@aws-sdk/client-textract": "^3.1028.0",
"@aws-sdk/s3-request-presigner": "^3.1028.0",
"@aws-sdk/credential-providers": "^3.1021.0",
"@aws-sdk/lib-dynamodb": "^3.490.0",
"@aws-sdk/s3-request-presigner": "^3.1028.0",
"@google/genai": "^1.19.0",
"@heyputer/kv.js": "^0.2.1",
"@heyputer/putility": "^1.0.0",
@@ -57,7 +57,7 @@
"mime-types": "^2.1.35",
"murmurhash": "^2.0.1",
"mysql2": "^3.21.1",
"nodemailer": "^7.0.13",
"nodemailer": "^8.0.7",
"openai": "^6.34.0",
"otpauth": "^9.2.4",
"parse-domain": "^8.2.2",
@@ -69,13 +69,12 @@
"together-ai": "^0.33.0",
"ua-parser-js": "^1.0.41",
"uglify-js": "^3.17.4",
"uuid": "^9.0.1",
"uuid": "^14.0.0",
"validator": "^13.15.35"
},
"devDependencies": {
"@types/node": "^24.0.0",
"chai": "^4.3.7",
"mocha": "^7.2.0",
"nodemon": "^3.1.0",
"typescript": "^5.9.3",
"vite": "^8.0.0",
+8 -1
View File
@@ -417,7 +417,14 @@ export class FSService extends PuterService {
const pathReservedInBatch = reservedPaths.has(normalizedInput.path);
if (pathReservedInBatch || existingEntry) {
if (normalizedInput.dedupeName) {
if (normalizedInput.overwrite) {
if (pathReservedInBatch) {
throw new HttpError(
409,
`Batch contains duplicate target path: ${normalizedInput.path}`,
);
}
} else if (normalizedInput.dedupeName) {
const dedupedPath = await this.#findDedupedPath(
normalizedInput.path,
reservedPaths,
+30
View File
@@ -345,6 +345,36 @@ export class FSEntryStore extends PuterStore {
});
}
async invalidateEntryCacheById(id: number): Promise<void> {
if (typeof id !== 'number' || !Number.isFinite(id)) {
return;
}
const rows = (await this.clients.db.read(
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE id = ? LIMIT 1`,
[id],
)) as unknown as FSEntryRow[];
const row = rows[0];
if (row) {
const entry = this.#mapFSEntryRow(row);
await this.#invalidateEntryCache(entry);
return;
}
const cached = await this.#readEntryFromCache(
`prodfsv2:fsentry:id:${id}`,
);
if (cached) {
await this.#invalidateEntryCache(cached);
return;
}
await this.publishCacheKeys({
keys: [`prodfsv2:fsentry:id:${id}`],
});
}
#chunk<T>(values: T[], size: number): T[][] {
if (values.length === 0) {
return [];
@@ -218,6 +218,7 @@ export class SubdomainStore extends PuterStore {
};
await this.#refreshCache(row);
await this.#invalidatePrefixListsForUser(userId);
await this.#invalidateRootDirEntry(row.root_dir_id);
return row;
}
@@ -268,6 +269,15 @@ export class SubdomainStore extends PuterStore {
for (const uid of affectedUsers) {
await this.#invalidatePrefixListsForUser(uid);
}
// FSEntry rows embed a `subdomains_agg` JSON of associated subdomains,
// so any rename / root_dir reassignment must drop the stale entry
// caches on both the old and new root_dir_id.
const affectedRootDirIds = new Set(
[before?.root_dir_id, after?.root_dir_id].filter((v) => v != null),
);
for (const id of affectedRootDirIds) {
await this.#invalidateRootDirEntry(id);
}
return after;
}
@@ -293,6 +303,7 @@ export class SubdomainStore extends PuterStore {
if (row.user_id != null) {
await this.#invalidatePrefixListsForUser(row.user_id);
}
await this.#invalidateRootDirEntry(row.root_dir_id);
}
return affected;
}
@@ -317,6 +328,25 @@ export class SubdomainStore extends PuterStore {
});
}
// FSEntryStore caches each row with an embedded `subdomains_agg` JSON
// (uuid + subdomain) keyed on `root_dir_id`. Without this, a deleted or
// renamed subdomain keeps showing up under its old folder in the GUI
// (website badge, "associated websites" popover) until the entry's
// independent TTL expires.
async #invalidateRootDirEntry(rootDirId) {
if (rootDirId == null) return;
const id =
typeof rootDirId === 'number' ? rootDirId : Number(rootDirId);
if (!Number.isFinite(id)) return;
const fsEntry = this.stores?.fsEntry;
if (!fsEntry?.invalidateEntryCacheById) return;
try {
await fsEntry.invalidateEntryCacheById(id);
} catch {
/* best-effort */
}
}
async #invalidatePrefixListsForUser(userId) {
if (userId == null) return;
const trackerKey = this.#prefixListTrackerKey(userId);
+2 -2
View File
@@ -47,9 +47,9 @@
"dependencies": {
"file-type": "21.3.3",
"json-colorizer": "^3.0.1",
"mocha": "7.2.0",
"music-metadata": "11.12.3",
"nodemailer": "8.0.7",
"string-template": "^1.0.0",
"uuid": "^9.0.1"
"uuid": "^14.0.0"
}
}
@@ -716,7 +716,7 @@ const upload = async function (items, dirPath, options = {}) {
size: file.size,
contentType: file.type || 'application/octet-stream',
overwrite: overwriteEnabled,
dedupeName: options.dedupeName ?? true,
dedupeName: overwriteEnabled? false: options.dedupeName ?? true,
createMissingParents: shouldCreateMissingParents,
app_uid: options.appUID,
};