同步异常兜底

This commit is contained in:
tianmo committed 2026-10-17 15:08:21 +08:00
1 parent 9cb8552a99
commit a1b4a63552
20 files changed
+754 -54

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
export { getActiveBackend, getBackendForKey, getActiveBackendRow, getBackendRowForKey } from './resolver'
export { getActiveBackend, getBackendById, getBackendForKey, getActiveBackendRow, getBackendRowForKey } from './resolver'
export { LocalStorageBackend } from './local'
export { S3StorageBackend, buildObjectKey, parseS3Config, validateS3Config } from './s3'
export {
+59 -1
View File
@@ -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
}
+12
View File
@@ -53,6 +53,18 @@ export async function getBackendForKey(key: string): Promise<StorageBackend> {
}
}
export async function getBackendById(
backendId: string
): Promise<StorageBackend | null> {
const fromEnv = createBackendFromEnv()
if (fromEnv) return fromEnv.id === backendId ? fromEnv : null
ensureDefaultBackends()
const row = getStorageBackendRow(backendId)
if (!row) return null
return createBackendInstance(row)
}
export function getActiveBackendRow(): StorageBackendRow | null {
const fromEnv = createBackendFromEnv()
if (fromEnv) {