mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix: motion video extraction race condition (#21285)
fix: motion video extraction race ccondition
This commit is contained in:
parent
88072910da
commit
868d5f56e2
3 changed files with 56 additions and 36 deletions
|
|
@ -27,7 +27,7 @@ import { BaseService } from 'src/services/base.service';
|
|||
import { UploadFile } from 'src/types';
|
||||
import { requireUploadAccess } from 'src/utils/access';
|
||||
import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
|
||||
import { isAssetChecksumConstraint } from 'src/utils/database';
|
||||
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { fromChecksum } from 'src/utils/request';
|
||||
|
|
@ -318,7 +318,7 @@ export class AssetMediaService extends BaseService {
|
|||
});
|
||||
|
||||
// handle duplicates with a success response
|
||||
if (error.constraint_name === ASSET_CHECKSUM_CONSTRAINT) {
|
||||
if (isAssetChecksumConstraint(error)) {
|
||||
const duplicateId = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, file.checksum);
|
||||
if (!duplicateId) {
|
||||
this.logger.error(`Error locating duplicate for checksum constraint`);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
|||
import { PersonTable } from 'src/schema/tables/person.table';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobItem, JobOf } from 'src/types';
|
||||
import { isAssetChecksumConstraint } from 'src/utils/database';
|
||||
import { isFaceImportEnabled } from 'src/utils/misc';
|
||||
import { upsertTags } from 'src/utils/tag';
|
||||
|
||||
|
|
@ -545,47 +546,62 @@ export class MetadataService extends BaseService {
|
|||
});
|
||||
}
|
||||
const checksum = this.cryptoRepository.hashSha1(video);
|
||||
const checksumQuery = { ownerId: asset.ownerId, libraryId: asset.libraryId ?? undefined, checksum };
|
||||
|
||||
let motionAsset = await this.assetRepository.getByChecksum({
|
||||
ownerId: asset.ownerId,
|
||||
libraryId: asset.libraryId ?? undefined,
|
||||
checksum,
|
||||
});
|
||||
if (motionAsset) {
|
||||
let motionAsset = await this.assetRepository.getByChecksum(checksumQuery);
|
||||
let isNewMotionAsset = false;
|
||||
|
||||
if (!motionAsset) {
|
||||
try {
|
||||
const motionAssetId = this.cryptoRepository.randomUUID();
|
||||
motionAsset = await this.assetRepository.create({
|
||||
id: motionAssetId,
|
||||
libraryId: asset.libraryId,
|
||||
type: AssetType.Video,
|
||||
fileCreatedAt: dates.dateTimeOriginal,
|
||||
fileModifiedAt: stats.mtime,
|
||||
localDateTime: dates.localDateTime,
|
||||
checksum,
|
||||
ownerId: asset.ownerId,
|
||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||
originalFileName: `${path.parse(asset.originalFileName).name}.mp4`,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
});
|
||||
|
||||
isNewMotionAsset = true;
|
||||
|
||||
if (!asset.isExternal) {
|
||||
await this.userRepository.updateUsage(asset.ownerId, video.byteLength);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isAssetChecksumConstraint(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
motionAsset = await this.assetRepository.getByChecksum(checksumQuery);
|
||||
if (!motionAsset) {
|
||||
this.logger.warn(`Unable to find existing motion video asset for ${asset.id}: ${asset.originalPath}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNewMotionAsset) {
|
||||
this.logger.debugFn(() => {
|
||||
const base64Checksum = checksum.toString('base64');
|
||||
return `Motion asset with checksum ${base64Checksum} already exists for asset ${asset.id}: ${asset.originalPath}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Hide the motion photo video asset if it's not already hidden to prepare for linking
|
||||
if (motionAsset.visibility === AssetVisibility.Timeline) {
|
||||
await this.assetRepository.update({
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
});
|
||||
this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`);
|
||||
}
|
||||
} else {
|
||||
const motionAssetId = this.cryptoRepository.randomUUID();
|
||||
motionAsset = await this.assetRepository.create({
|
||||
id: motionAssetId,
|
||||
libraryId: asset.libraryId,
|
||||
type: AssetType.Video,
|
||||
fileCreatedAt: dates.dateTimeOriginal,
|
||||
fileModifiedAt: stats.mtime,
|
||||
localDateTime: dates.localDateTime,
|
||||
checksum,
|
||||
ownerId: asset.ownerId,
|
||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||
originalFileName: `${path.parse(asset.originalFileName).name}.mp4`,
|
||||
// Hide the motion photo video asset if it's not already hidden to prepare for linking
|
||||
if (motionAsset.visibility === AssetVisibility.Timeline) {
|
||||
await this.assetRepository.update({
|
||||
id: motionAsset.id,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
});
|
||||
|
||||
if (!asset.isExternal) {
|
||||
await this.userRepository.updateUsage(asset.ownerId, video.byteLength);
|
||||
}
|
||||
this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`);
|
||||
}
|
||||
|
||||
if (asset.livePhotoVideoId !== motionAsset.id) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { parse } from 'pg-connection-string';
|
||||
import postgres, { Notice } from 'postgres';
|
||||
import postgres, { Notice, PostgresError } from 'postgres';
|
||||
import { columns, Exif, Person } from 'src/database';
|
||||
import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
|
|
@ -153,6 +153,10 @@ export function toJson<DB, TB extends keyof DB & string, T extends TB | Expressi
|
|||
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||
|
||||
export const isAssetChecksumConstraint = (error: unknown) => {
|
||||
return (error as PostgresError)?.constraint_name === 'UQ_assets_owner_checksum';
|
||||
};
|
||||
|
||||
export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
|
||||
return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.Archive), sql.lit(AssetVisibility.Timeline)]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue