mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
feat(server): add checksum algorithm field (#26573)
* feat: add checksum algorithm field * fix comments * chore: rename migration --------- Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
47b45453c8
commit
22bf7c2005
14 changed files with 69 additions and 6 deletions
|
|
@ -5,6 +5,7 @@ import {
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
AssetType,
|
AssetType,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
|
ChecksumAlgorithm,
|
||||||
MemoryType,
|
MemoryType,
|
||||||
Permission,
|
Permission,
|
||||||
PluginContext,
|
PluginContext,
|
||||||
|
|
@ -112,6 +113,7 @@ export type Memory = {
|
||||||
export type Asset = {
|
export type Asset = {
|
||||||
id: string;
|
id: string;
|
||||||
checksum: Buffer<ArrayBufferLike>;
|
checksum: Buffer<ArrayBufferLike>;
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm;
|
||||||
deviceAssetId: string;
|
deviceAssetId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
fileCreatedAt: Date;
|
fileCreatedAt: Date;
|
||||||
|
|
@ -330,6 +332,7 @@ export const columns = {
|
||||||
asset: [
|
asset: [
|
||||||
'asset.id',
|
'asset.id',
|
||||||
'asset.checksum',
|
'asset.checksum',
|
||||||
|
'asset.checksumAlgorithm',
|
||||||
'asset.deviceAssetId',
|
'asset.deviceAssetId',
|
||||||
'asset.deviceId',
|
'asset.deviceId',
|
||||||
'asset.fileCreatedAt',
|
'asset.fileCreatedAt',
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from 'src/dtos/person.dto';
|
} from 'src/dtos/person.dto';
|
||||||
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
|
||||||
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
import { ImageDimensions, MaybeDehydrated } from 'src/types';
|
||||||
import { getDimensions } from 'src/utils/asset.util';
|
import { getDimensions } from 'src/utils/asset.util';
|
||||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||||
|
|
@ -148,6 +148,7 @@ export type MapAsset = {
|
||||||
updateId: string;
|
updateId: string;
|
||||||
status: AssetStatus;
|
status: AssetStatus;
|
||||||
checksum: Buffer<ArrayBufferLike>;
|
checksum: Buffer<ArrayBufferLike>;
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm;
|
||||||
deviceAssetId: string;
|
deviceAssetId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
duplicateId: string | null;
|
duplicateId: string | null;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ export enum AssetType {
|
||||||
Other = 'OTHER',
|
Other = 'OTHER',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ChecksumAlgorithm {
|
||||||
|
sha1File = 'sha1', // sha1 checksum of the whole file contents
|
||||||
|
sha1Path = 'sha1-path', // sha1 checksum of "path:" plus the file path, currently used in external libraries, deprecated
|
||||||
|
}
|
||||||
|
|
||||||
export enum AssetFileType {
|
export enum AssetFileType {
|
||||||
/**
|
/**
|
||||||
* An full/large-size image extracted/converted from RAW photos
|
* An full/large-size image extracted/converted from RAW photos
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,7 @@ where
|
||||||
select
|
select
|
||||||
"asset"."id",
|
"asset"."id",
|
||||||
"asset"."checksum",
|
"asset"."checksum",
|
||||||
|
"asset"."checksumAlgorithm",
|
||||||
"asset"."deviceAssetId",
|
"asset"."deviceAssetId",
|
||||||
"asset"."deviceId",
|
"asset"."deviceId",
|
||||||
"asset"."fileCreatedAt",
|
"asset"."fileCreatedAt",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { registerEnum } from '@immich/sql-tools';
|
import { registerEnum } from '@immich/sql-tools';
|
||||||
import { AssetStatus, AssetVisibility, SourceType } from 'src/enum';
|
import { AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType } from 'src/enum';
|
||||||
|
|
||||||
export const assets_status_enum = registerEnum({
|
export const assets_status_enum = registerEnum({
|
||||||
name: 'assets_status_enum',
|
name: 'assets_status_enum',
|
||||||
|
|
@ -15,3 +15,8 @@ export const asset_visibility_enum = registerEnum({
|
||||||
name: 'asset_visibility_enum',
|
name: 'asset_visibility_enum',
|
||||||
values: Object.values(AssetVisibility),
|
values: Object.values(AssetVisibility),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const asset_checksum_algorithm_enum = registerEnum({
|
||||||
|
name: 'asset_checksum_algorithm_enum',
|
||||||
|
values: Object.values(ChecksumAlgorithm),
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE TYPE "asset_checksum_algorithm_enum" AS ENUM ('sha1','sha1-path');`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset" ADD "checksumAlgorithm" asset_checksum_algorithm_enum;`.execute(db);
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
UPDATE "asset"
|
||||||
|
SET "checksumAlgorithm" = CASE
|
||||||
|
WHEN "isExternal" = true THEN 'sha1-path'::asset_checksum_algorithm_enum
|
||||||
|
ELSE 'sha1'::asset_checksum_algorithm_enum
|
||||||
|
END
|
||||||
|
`.execute(db);
|
||||||
|
|
||||||
|
await sql`ALTER TABLE "asset" ALTER COLUMN "checksumAlgorithm" SET NOT NULL;`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "asset" DROP COLUMN "checksumAlgorithm";`.execute(db);
|
||||||
|
await sql`DROP TYPE "asset_checksum_algorithm_enum";`.execute(db);
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,8 @@ import {
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from '@immich/sql-tools';
|
} from '@immich/sql-tools';
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
|
||||||
import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
|
import { asset_checksum_algorithm_enum, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
|
||||||
import { asset_delete_audit } from 'src/schema/functions';
|
import { asset_delete_audit } from 'src/schema/functions';
|
||||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||||
import { StackTable } from 'src/schema/tables/stack.table';
|
import { StackTable } from 'src/schema/tables/stack.table';
|
||||||
|
|
@ -95,6 +95,9 @@ export class AssetTable {
|
||||||
@Column({ type: 'bytea', index: true })
|
@Column({ type: 'bytea', index: true })
|
||||||
checksum!: Buffer; // sha1 checksum
|
checksum!: Buffer; // sha1 checksum
|
||||||
|
|
||||||
|
@Column({ enum: asset_checksum_algorithm_enum })
|
||||||
|
checksumAlgorithm!: ChecksumAlgorithm;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
|
||||||
livePhotoVideoId!: string | null;
|
livePhotoVideoId!: string | null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
AssetStatus,
|
AssetStatus,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
CacheControl,
|
CacheControl,
|
||||||
|
ChecksumAlgorithm,
|
||||||
JobName,
|
JobName,
|
||||||
Permission,
|
Permission,
|
||||||
StorageFolder,
|
StorageFolder,
|
||||||
|
|
@ -425,6 +426,7 @@ export class AssetMediaService extends BaseService {
|
||||||
deviceId: asset.deviceId,
|
deviceId: asset.deviceId,
|
||||||
type: asset.type,
|
type: asset.type,
|
||||||
checksum: asset.checksum,
|
checksum: asset.checksum,
|
||||||
|
checksumAlgorithm: asset.checksumAlgorithm,
|
||||||
fileCreatedAt: asset.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
localDateTime: asset.localDateTime,
|
localDateTime: asset.localDateTime,
|
||||||
fileModifiedAt: asset.fileModifiedAt,
|
fileModifiedAt: asset.fileModifiedAt,
|
||||||
|
|
@ -446,6 +448,7 @@ export class AssetMediaService extends BaseService {
|
||||||
libraryId: null,
|
libraryId: null,
|
||||||
|
|
||||||
checksum: file.checksum,
|
checksum: file.checksum,
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
originalPath: file.originalPath,
|
originalPath: file.originalPath,
|
||||||
|
|
||||||
deviceAssetId: dto.deviceAssetId,
|
deviceAssetId: dto.deviceAssetId,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,17 @@ import {
|
||||||
ValidateLibraryImportPathResponseDto,
|
ValidateLibraryImportPathResponseDto,
|
||||||
ValidateLibraryResponseDto,
|
ValidateLibraryResponseDto,
|
||||||
} from 'src/dtos/library.dto';
|
} from 'src/dtos/library.dto';
|
||||||
import { AssetStatus, AssetType, CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
import {
|
||||||
|
AssetStatus,
|
||||||
|
AssetType,
|
||||||
|
ChecksumAlgorithm,
|
||||||
|
CronJob,
|
||||||
|
DatabaseLock,
|
||||||
|
ImmichWorker,
|
||||||
|
JobName,
|
||||||
|
JobStatus,
|
||||||
|
QueueName,
|
||||||
|
} from 'src/enum';
|
||||||
import { ArgOf } from 'src/repositories/event.repository';
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { AssetSyncResult } from 'src/repositories/library.repository';
|
import { AssetSyncResult } from 'src/repositories/library.repository';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
|
|
@ -400,6 +410,7 @@ export class LibraryService extends BaseService {
|
||||||
ownerId,
|
ownerId,
|
||||||
libraryId,
|
libraryId,
|
||||||
checksum: this.cryptoRepository.hashSha1(`path:${assetPath}`),
|
checksum: this.cryptoRepository.hashSha1(`path:${assetPath}`),
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1Path,
|
||||||
originalPath: assetPath,
|
originalPath: assetPath,
|
||||||
|
|
||||||
fileCreatedAt: stat.mtime,
|
fileCreatedAt: stat.mtime,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
AssetType,
|
AssetType,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
|
ChecksumAlgorithm,
|
||||||
ExifOrientation,
|
ExifOrientation,
|
||||||
ImmichWorker,
|
ImmichWorker,
|
||||||
JobName,
|
JobName,
|
||||||
|
|
@ -652,6 +653,7 @@ describe(MetadataService.name, () => {
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||||
checksum: expect.any(Buffer),
|
checksum: expect.any(Buffer),
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
deviceAssetId: 'NONE',
|
deviceAssetId: 'NONE',
|
||||||
deviceId: 'NONE',
|
deviceId: 'NONE',
|
||||||
fileCreatedAt: asset.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
|
|
@ -705,6 +707,7 @@ describe(MetadataService.name, () => {
|
||||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||||
checksum: expect.any(Buffer),
|
checksum: expect.any(Buffer),
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
deviceAssetId: 'NONE',
|
deviceAssetId: 'NONE',
|
||||||
deviceId: 'NONE',
|
deviceId: 'NONE',
|
||||||
fileCreatedAt: asset.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
|
|
@ -758,6 +761,7 @@ describe(MetadataService.name, () => {
|
||||||
expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object));
|
expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object));
|
||||||
expect(mocks.asset.create).toHaveBeenCalledWith({
|
expect(mocks.asset.create).toHaveBeenCalledWith({
|
||||||
checksum: expect.any(Buffer),
|
checksum: expect.any(Buffer),
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
deviceAssetId: 'NONE',
|
deviceAssetId: 'NONE',
|
||||||
deviceId: 'NONE',
|
deviceId: 'NONE',
|
||||||
fileCreatedAt: asset.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
AssetType,
|
AssetType,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
|
ChecksumAlgorithm,
|
||||||
DatabaseLock,
|
DatabaseLock,
|
||||||
ExifOrientation,
|
ExifOrientation,
|
||||||
ImmichWorker,
|
ImmichWorker,
|
||||||
|
|
@ -675,6 +676,7 @@ export class MetadataService extends BaseService {
|
||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
localDateTime: dates.localDateTime,
|
localDateTime: dates.localDateTime,
|
||||||
checksum,
|
checksum,
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
ownerId: asset.ownerId,
|
ownerId: asset.ownerId,
|
||||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||||
originalFileName: `${parse(asset.originalFileName).name}.mp4`,
|
originalFileName: `${parse(asset.originalFileName).name}.mp4`,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Selectable } from 'kysely';
|
import { Selectable } from 'kysely';
|
||||||
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
import { AssetFileType, AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { StackTable } from 'src/schema/tables/stack.table';
|
import { StackTable } from 'src/schema/tables/stack.table';
|
||||||
import { AssetEditFactory } from 'test/factories/asset-edit.factory';
|
import { AssetEditFactory } from 'test/factories/asset-edit.factory';
|
||||||
|
|
@ -53,6 +53,7 @@ export class AssetFactory {
|
||||||
updateId: newUuidV7(),
|
updateId: newUuidV7(),
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
checksum: newSha1(),
|
checksum: newSha1(),
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
deviceAssetId: '',
|
deviceAssetId: '',
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ export const getForMemory = (memory: ReturnType<MemoryFactory['build']>) => ({
|
||||||
export const getForMetadataExtraction = (asset: ReturnType<AssetFactory['build']>) => ({
|
export const getForMetadataExtraction = (asset: ReturnType<AssetFactory['build']>) => ({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
checksum: asset.checksum,
|
checksum: asset.checksum,
|
||||||
|
checksumAlgorithm: asset.checksumAlgorithm,
|
||||||
deviceAssetId: asset.deviceAssetId,
|
deviceAssetId: asset.deviceAssetId,
|
||||||
deviceId: asset.deviceId,
|
deviceId: asset.deviceId,
|
||||||
fileCreatedAt: asset.fileCreatedAt,
|
fileCreatedAt: asset.fileCreatedAt,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
AssetType,
|
AssetType,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
|
ChecksumAlgorithm,
|
||||||
MemoryType,
|
MemoryType,
|
||||||
SourceType,
|
SourceType,
|
||||||
SyncEntityType,
|
SyncEntityType,
|
||||||
|
|
@ -547,6 +548,7 @@ const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
originalFileName: '',
|
originalFileName: '',
|
||||||
checksum: randomBytes(32),
|
checksum: randomBytes(32),
|
||||||
|
checksumAlgorithm: ChecksumAlgorithm.sha1File,
|
||||||
type: AssetType.Image,
|
type: AssetType.Image,
|
||||||
originalPath: '/path/to/something.jpg',
|
originalPath: '/path/to/something.jpg',
|
||||||
ownerId: 'not-a-valid-uuid',
|
ownerId: 'not-a-valid-uuid',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue