From 0ab057f453472e7cb85bb677cce111c9c339427b Mon Sep 17 00:00:00 2001 From: Thomas Way Date: Sun, 15 Mar 2026 11:53:38 +0000 Subject: [PATCH] fix(server): sync files to disk Ensure that all files are flushed after they've been written. At current, files are not explicitly flushed to disk, which can cause data corruption. In extreme circumstances, it's possible that uploaded files may not ever be persisted at all. --- .../src/middleware/file-upload.interceptor.ts | 23 +++++++++++++++++-- server/src/repositories/storage.repository.ts | 9 ++++++++ .../repositories/storage.repository.mock.ts | 1 + 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index 6dfd11ee4b..eb4cfb0a29 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -10,6 +10,7 @@ import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { RouteKey } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { AssetMediaService } from 'src/services/asset-media.service'; import { ImmichFile, UploadFile, UploadFiles } from 'src/types'; import { asUploadRequest, mapToUploadFile } from 'src/utils/asset.util'; @@ -54,6 +55,7 @@ export class FileUploadInterceptor implements NestInterceptor { constructor( private reflect: Reflector, private assetService: AssetMediaService, + private storageRepository: StorageRepository, private logger: LoggingRepository, ) { this.logger.setContext(FileUploadInterceptor.name); @@ -125,7 +127,18 @@ export class FileUploadInterceptor implements NestInterceptor { }); if (!this.isAssetUploadFile(file)) { - this.defaultStorage._handleFile(request, file, callback); + this.defaultStorage._handleFile(request, file, (error, info) => { + if (error) { + return callback(error); + } + // Multer does not sync files to disk after writing. + // + // TODO: use `flush: true` in multer when available: https://github.com/expressjs/multer/issues/1381 + this.storageRepository + .datasync(info!.path!) + .then(() => callback(null, info!)) + .catch((error) => callback(error)); + }); return; } @@ -136,7 +149,13 @@ export class FileUploadInterceptor implements NestInterceptor { hash.destroy(); callback(error); } else { - callback(null, { ...info, checksum: hash.digest() }); + this.storageRepository + .datasync(info!.path!) + .then(() => callback(null, { ...info, checksum: hash.digest() })) + .catch((error) => { + hash.destroy(); + callback(error); + }); } }); } diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 5a1a936e77..64dd6aa5e5 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -54,6 +54,15 @@ export class StorageRepository { return fs.copyFile(source, target); } + async datasync(filepath: string) { + const handle = await fs.open(filepath, 'r'); + try { + await handle.datasync(); + } finally { + await handle.close(); + } + } + stat(filepath: string) { return fs.stat(filepath); } diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 85c72b6c10..522cfa0f7e 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -72,6 +72,7 @@ export const newStorageRepositoryMock = (): Mocked