mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-29 21:01:27 +00:00
fix: fsEntry and tscofnig (#2923)
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@
|
||||
"build:workerLib": "cd src/puter-js && npm run build && cd ../worker && npm run build",
|
||||
"check-translations": "node tools/check-translations.js",
|
||||
"prepare": "husky",
|
||||
"build:ts": "tsc -p tsconfig.json && node ./tools/write-dist-package-json.mjs",
|
||||
"build:ts": "tsc -p tsconfig.build.json && node ./tools/write-dist-package-json.mjs",
|
||||
"postinstall": "node ./tools/extensionSetup.mjs"
|
||||
},
|
||||
"workspaces": [
|
||||
|
||||
@@ -72,7 +72,7 @@ The Puter Team
|
||||
Please update <a href="https://apps.puter.com/app/{{app_name}}">{{app_title}}</a>.
|
||||
</p>
|
||||
<p><strong>Requested updates:</strong></p>
|
||||
<blockquote>{{message}}</blockquote>
|
||||
<blockquote>{{nl2br message}}</blockquote>
|
||||
<p>Best,<br />
|
||||
The Puter Team
|
||||
</p>
|
||||
|
||||
@@ -1836,8 +1836,14 @@ export class FSEntryStore extends PuterStore {
|
||||
);
|
||||
}
|
||||
|
||||
// Use INSERT IGNORE so a row that lost the (parent_id,
|
||||
// name) race against another writer doesn't take down
|
||||
// the whole chunk. Skipped rows are recovered below by
|
||||
// detecting uuids missing from the post-INSERT SELECT
|
||||
// and running the same overwrite-or-409 logic the
|
||||
// upfront existence check uses.
|
||||
await this.clients.db.write(
|
||||
`INSERT INTO fsentries (
|
||||
`${this.#insertIgnoreIntoFsentriesSql()} (
|
||||
uuid,
|
||||
bucket,
|
||||
bucket_region,
|
||||
@@ -1908,6 +1914,132 @@ export class FSEntryStore extends PuterStore {
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery for INSERT IGNORE skips. An insertCandidate whose
|
||||
// uuid is missing from the post-INSERT SELECT lost the
|
||||
// (parent_id, name) race against a concurrent writer. Re-check
|
||||
// primary-aware and either UPDATE (overwrite=true) or surface
|
||||
// a clear 409. Other rows in the batch already persisted.
|
||||
const racedCandidates = insertCandidates.filter(
|
||||
(entry) => !insertedEntriesByUuid.has(entry.input.uuid),
|
||||
);
|
||||
const insertConflictErrors: Error[] = [];
|
||||
if (racedCandidates.length > 0) {
|
||||
const racedExistingByPath =
|
||||
await this.#readEntriesByPathsForUser(
|
||||
userId,
|
||||
racedCandidates.map((entry) => entry.targetPath),
|
||||
{ useTryHardRead: true, skipCache: true },
|
||||
);
|
||||
for (const entry of racedCandidates) {
|
||||
const parentEntry = parentByPath.get(entry.parentPath);
|
||||
if (!parentEntry) {
|
||||
throw new Error(
|
||||
`Failed to resolve parent directory for ${entry.targetPath}`,
|
||||
);
|
||||
}
|
||||
const existing = racedExistingByPath.get(entry.targetPath);
|
||||
if (!existing) {
|
||||
insertConflictErrors.push(
|
||||
new HttpError(
|
||||
409,
|
||||
`Entry already exists at ${entry.targetPath}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (existing.isDir) {
|
||||
insertConflictErrors.push(
|
||||
new HttpError(
|
||||
409,
|
||||
`Cannot overwrite a directory at ${entry.targetPath}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!entry.input.overwrite) {
|
||||
insertConflictErrors.push(
|
||||
new HttpError(
|
||||
409,
|
||||
`Entry already exists at ${entry.targetPath}`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updatedEntry: FSEntry = {
|
||||
...existing,
|
||||
bucket: entry.bucket,
|
||||
bucketRegion: entry.bucketRegion,
|
||||
parentId: parentEntry.id,
|
||||
parentUid: parentEntry.uuid,
|
||||
associatedAppId: entry.input.associatedAppId ?? null,
|
||||
isPublic:
|
||||
entry.input.isPublic === undefined
|
||||
? null
|
||||
: Boolean(entry.input.isPublic),
|
||||
thumbnail: entry.input.thumbnail ?? null,
|
||||
immutable: Boolean(entry.input.immutable),
|
||||
name: entry.fileName,
|
||||
path: entry.targetPath,
|
||||
metadata: entry.metadataJson,
|
||||
modified: now,
|
||||
accessed: now,
|
||||
size: entry.size,
|
||||
};
|
||||
await this.clients.db.write(
|
||||
`UPDATE fsentries
|
||||
SET bucket = ?,
|
||||
bucket_region = ?,
|
||||
parent_id = ?,
|
||||
parent_uid = ?,
|
||||
associated_app_id = ?,
|
||||
is_public = ?,
|
||||
thumbnail = ?,
|
||||
immutable = ?,
|
||||
name = ?,
|
||||
path = ?,
|
||||
metadata = ?,
|
||||
modified = ?,
|
||||
accessed = ?,
|
||||
size = ?
|
||||
WHERE id = ?`,
|
||||
[
|
||||
entry.bucket,
|
||||
entry.bucketRegion,
|
||||
parentEntry.id,
|
||||
parentEntry.uuid,
|
||||
entry.input.associatedAppId ?? null,
|
||||
entry.input.isPublic === undefined
|
||||
? null
|
||||
: entry.input.isPublic
|
||||
? 1
|
||||
: 0,
|
||||
entry.input.thumbnail ?? null,
|
||||
entry.input.immutable ? 1 : 0,
|
||||
entry.fileName,
|
||||
entry.targetPath,
|
||||
entry.metadataJson,
|
||||
now,
|
||||
now,
|
||||
entry.size,
|
||||
existing.id,
|
||||
],
|
||||
);
|
||||
await this.#invalidateEntryCache(existing);
|
||||
await this.#writeEntryToCache(updatedEntry);
|
||||
updatedResultsByIndex.set(entry.index, updatedEntry);
|
||||
}
|
||||
}
|
||||
|
||||
if (insertConflictErrors.length > 0) {
|
||||
// Other rows in this user's batch already persisted (the
|
||||
// INSERT IGNORE landed them and overwrite=true racers ran
|
||||
// UPDATE above). Surface the first non-recoverable conflict
|
||||
// as a clean 409 instead of letting ER_DUP_ENTRY escape
|
||||
// through SQLBatcher as a 500.
|
||||
throw insertConflictErrors[0]!;
|
||||
}
|
||||
|
||||
for (const entry of userEntries) {
|
||||
const updatedResult = updatedResultsByIndex.get(entry.index);
|
||||
if (updatedResult) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"strict": false,
|
||||
"noCheck": true,
|
||||
"noImplicitAny": true
|
||||
}
|
||||
}
|
||||
+4
-15
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"noCheck": true,
|
||||
"noCheck": false,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
@@ -29,7 +29,7 @@
|
||||
"removeComments": true,
|
||||
"noEmit": false,
|
||||
"noEmitOnError": false,
|
||||
"noImplicitAny": false
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": [
|
||||
"src/backend/**/*",
|
||||
@@ -44,17 +44,6 @@
|
||||
"**/node_modules/**",
|
||||
"dist/**",
|
||||
"volatile/**",
|
||||
"extensions/**/node_modules/**",
|
||||
"src/backend/test/**",
|
||||
"src/backend/tools/**",
|
||||
"src/backend/vitest.config.ts",
|
||||
"src/backend/vitest.bench.config.ts",
|
||||
"src/backend/vitest.bench.config.js",
|
||||
"src/backend/services/worker/template/puter-portable.js",
|
||||
"src/backend/services/DynamoKVStore/DynamoKVStore.ts",
|
||||
"src/backend/clients/s3/S3Client.js",
|
||||
"src/backend/clients/s3/s3ClientProvider.js",
|
||||
"src/backend/clients/redis/RedisClient.js",
|
||||
"src/backend/clients/dynamodb/DDBClient.js"
|
||||
]
|
||||
"extensions/**/node_modules/**"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user