mirror of
https://github.com/immich-app/immich.git
synced 2026-03-22 15:59:30 +03:00
fix(server): sync all written files to disk
Add flush/fsync to all file write paths including thumbnails, transcoded videos, exif writes, file copies, and backups.
This commit is contained in:
@@ -6,6 +6,7 @@ import { SourceType } from 'src/enum';
|
|||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { BoundingBox } from 'src/repositories/machine-learning.repository';
|
import { BoundingBox } from 'src/repositories/machine-learning.repository';
|
||||||
import { MediaRepository } from 'src/repositories/media.repository';
|
import { MediaRepository } from 'src/repositories/media.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor';
|
import { checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor';
|
||||||
import { automock } from 'test/utils';
|
import { automock } from 'test/utils';
|
||||||
|
|
||||||
@@ -65,8 +66,11 @@ describe(MediaRepository.name, () => {
|
|||||||
let sut: MediaRepository;
|
let sut: MediaRepository;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// eslint-disable-next-line no-sparse-arrays
|
sut = new MediaRepository(
|
||||||
sut = new MediaRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }));
|
// eslint-disable-next-line no-sparse-arrays
|
||||||
|
automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }),
|
||||||
|
automock(StorageRepository, { args: [{ setContext: () => {} }], strict: false }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('applyEdits (single actions)', () => {
|
describe('applyEdits (single actions)', () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Exif } from 'src/database';
|
|||||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||||
import { Colorspace, LogLevel, RawExtractedFormat } from 'src/enum';
|
import { Colorspace, LogLevel, RawExtractedFormat } from 'src/enum';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import {
|
import {
|
||||||
DecodeToBufferOptions,
|
DecodeToBufferOptions,
|
||||||
GenerateThumbhashOptions,
|
GenerateThumbhashOptions,
|
||||||
@@ -45,7 +46,10 @@ export type ExtractResult = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MediaRepository {
|
export class MediaRepository {
|
||||||
constructor(private logger: LoggingRepository) {
|
constructor(
|
||||||
|
private logger: LoggingRepository,
|
||||||
|
private storageRepository: StorageRepository,
|
||||||
|
) {
|
||||||
this.logger.setContext(MediaRepository.name);
|
this.logger.setContext(MediaRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +120,7 @@ export class MediaRepository {
|
|||||||
ignoreMinorErrors: true,
|
ignoreMinorErrors: true,
|
||||||
writeArgs: ['-overwrite_original'],
|
writeArgs: ['-overwrite_original'],
|
||||||
});
|
});
|
||||||
|
await this.storageRepository.datasync(output);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.warn(`Could not write exif data to image: ${error.message}`);
|
this.logger.warn(`Could not write exif data to image: ${error.message}`);
|
||||||
@@ -133,6 +138,7 @@ export class MediaRepository {
|
|||||||
writeArgs: ['-TagsFromFile', source, `-${tagGroup}:all>${tagGroup}:all`, '-overwrite_original'],
|
writeArgs: ['-TagsFromFile', source, `-${tagGroup}:all>${tagGroup}:all`, '-overwrite_original'],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
await this.storageRepository.datasync(target);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.warn(`Could not copy tag data to image: ${error.message}`);
|
this.logger.warn(`Could not copy tag data to image: ${error.message}`);
|
||||||
@@ -180,6 +186,7 @@ export class MediaRepository {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await decoded.toFile(output);
|
await decoded.toFile(output);
|
||||||
|
await this.storageRepository.datasync(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
|
private async getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||||
@@ -274,14 +281,18 @@ export class MediaRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise<void> {
|
async transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise<void> {
|
||||||
if (!options.twoPass) {
|
if (!options.twoPass) {
|
||||||
return new Promise((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
this.configureFfmpegCall(input, output, options)
|
this.configureFfmpegCall(input, output, options)
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('end', () => resolve())
|
.on('end', () => resolve())
|
||||||
.run();
|
.run();
|
||||||
});
|
});
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
await this.storageRepository.datasync(output);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof output !== 'string') {
|
if (typeof output !== 'string') {
|
||||||
@@ -290,7 +301,7 @@ export class MediaRepository {
|
|||||||
|
|
||||||
// two-pass allows for precise control of bitrate at the cost of running twice
|
// two-pass allows for precise control of bitrate at the cost of running twice
|
||||||
// recommended for vp9 for better quality and compression
|
// recommended for vp9 for better quality and compression
|
||||||
return new Promise((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
// first pass output is not saved as only the .log file is needed
|
// first pass output is not saved as only the .log file is needed
|
||||||
this.configureFfmpegCall(input, '/dev/null', options)
|
this.configureFfmpegCall(input, '/dev/null', options)
|
||||||
.addOptions('-pass', '1')
|
.addOptions('-pass', '1')
|
||||||
@@ -310,6 +321,7 @@ export class MediaRepository {
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
});
|
});
|
||||||
|
await this.storageRepository.datasync(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageMetadata(input: string | Buffer): Promise<ImageDimensions & { isTransparent: boolean }> {
|
async getImageMetadata(input: string | Buffer): Promise<ImageDimensions & { isTransparent: boolean }> {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
||||||
import geotz from 'geo-tz';
|
import geotz from 'geo-tz';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
|
||||||
interface ExifDuration {
|
interface ExifDuration {
|
||||||
@@ -94,7 +95,10 @@ export class MetadataRepository {
|
|||||||
taskTimeoutMillis: 2 * 60 * 1000,
|
taskTimeoutMillis: 2 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private logger: LoggingRepository) {
|
constructor(
|
||||||
|
private logger: LoggingRepository,
|
||||||
|
private storageRepository: StorageRepository,
|
||||||
|
) {
|
||||||
this.logger.setContext(MetadataRepository.name);
|
this.logger.setContext(MetadataRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +125,7 @@ export class MetadataRepository {
|
|||||||
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.exiftool.write(path, tags);
|
await this.exiftool.write(path, tags);
|
||||||
|
await this.storageRepository.datasync(path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`Error writing exif data (${path}): ${error}`);
|
this.logger.warn(`Error writing exif data (${path}): ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,9 @@ export class StorageRepository {
|
|||||||
return fs.readdir(folder);
|
return fs.readdir(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
copyFile(source: string, target: string) {
|
async copyFile(source: string, target: string) {
|
||||||
return fs.copyFile(source, target);
|
await fs.copyFile(source, target);
|
||||||
|
await this.datasync(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
async datasync(filepath: string) {
|
async datasync(filepath: string) {
|
||||||
@@ -68,19 +69,19 @@ export class StorageRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createFile(filepath: string, buffer: Buffer) {
|
createFile(filepath: string, buffer: Buffer) {
|
||||||
return fs.writeFile(filepath, buffer, { flag: 'wx' });
|
return fs.writeFile(filepath, buffer, { flag: 'wx', flush: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
createWriteStream(filepath: string): Writable {
|
createWriteStream(filepath: string): Writable {
|
||||||
return createWriteStream(filepath, { flags: 'w' });
|
return createWriteStream(filepath, { flags: 'w', flush: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
createOrOverwriteFile(filepath: string, buffer: Buffer) {
|
createOrOverwriteFile(filepath: string, buffer: Buffer) {
|
||||||
return fs.writeFile(filepath, buffer, { flag: 'w' });
|
return fs.writeFile(filepath, buffer, { flag: 'w', flush: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
overwriteFile(filepath: string, buffer: Buffer) {
|
overwriteFile(filepath: string, buffer: Buffer) {
|
||||||
return fs.writeFile(filepath, buffer, { flag: 'r+' });
|
return fs.writeFile(filepath, buffer, { flag: 'r+', flush: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
rename(source: string, target: string) {
|
rename(source: string, target: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user