同步异常兜底
This commit is contained in:
1 parent
9cb8552a99
commit
a1b4a63552
20 files changed
+754
-54
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { createReadStream, promises as fs } from 'node:fs'
|
||||
import type { ReadStream } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
@@ -7,6 +8,7 @@ import { validateStorageKey } from '../image-key'
|
||||
import type { StoredImage, StoredImageMeta, StorageBackend } from './types'
|
||||
|
||||
const META_SUFFIX = '.meta.json'
|
||||
const ATOMIC_TEMP_DIR = '.pichost-tmp'
|
||||
|
||||
function keyToFilePath(key: string): string {
|
||||
return join(getDataDir(), ...key.split('/'))
|
||||
@@ -39,8 +41,36 @@ export class LocalStorageBackend implements StorageBackend {
|
||||
|
||||
async put(key: string, bytes: Uint8Array, _meta: StoredImageMeta): Promise<void> {
|
||||
const filePath = keyToFilePath(key)
|
||||
const tempDir = join(getDataDir(), ATOMIC_TEMP_DIR)
|
||||
const tempPath = join(tempDir, `${randomUUID()}.part`)
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
await fs.mkdir(dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, bytes)
|
||||
|
||||
const handle = await fs.open(tempPath, 'w')
|
||||
try {
|
||||
await handle.writeFile(bytes)
|
||||
await handle.sync()
|
||||
} finally {
|
||||
await handle.close()
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.rename(tempPath, filePath)
|
||||
} catch (error) {
|
||||
await fs.rm(tempPath, { force: true }).catch(() => {})
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
const directory = await fs.open(dirname(filePath), 'r')
|
||||
try {
|
||||
await directory.sync()
|
||||
} finally {
|
||||
await directory.close()
|
||||
}
|
||||
} catch {
|
||||
// The rename already completed; directory fsync is best-effort on macOS.
|
||||
}
|
||||
}
|
||||
|
||||
async head(key: string): Promise<StoredImage | null> {
|
||||
@@ -93,3 +123,31 @@ export class LocalStorageBackend implements StorageBackend {
|
||||
return { count: 0, bytes: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupLocalAtomicTempFiles(
|
||||
maxAgeMs = 60 * 60 * 1000
|
||||
): Promise<number> {
|
||||
const tempDir = join(getDataDir(), ATOMIC_TEMP_DIR)
|
||||
let entries
|
||||
try {
|
||||
entries = await fs.readdir(tempDir, { withFileTypes: true })
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
|
||||
const cutoff = Date.now() - maxAgeMs
|
||||
let removed = 0
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.part')) continue
|
||||
const path = join(tempDir, entry.name)
|
||||
try {
|
||||
const stat = await fs.stat(path)
|
||||
if (stat.mtimeMs > cutoff) continue
|
||||
await fs.rm(path, { force: true })
|
||||
removed++
|
||||
} catch {
|
||||
// A later startup will retry stale temporary files.
|
||||
}
|
||||
}
|
||||
return removed
|
||||
}
|
||||
Reference in new issue
Block a user