mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 18:19:10 +03:00
refactor: extract isEdited into its own column in asset_file (#25358)
This commit is contained in:
@@ -1,7 +1,15 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
import { StorageAsset } from 'src/database';
|
import { StorageAsset } from 'src/database';
|
||||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
import {
|
||||||
|
AssetFileType,
|
||||||
|
AssetPathType,
|
||||||
|
ImageFormat,
|
||||||
|
PathType,
|
||||||
|
PersonPathType,
|
||||||
|
RawExtractedFormat,
|
||||||
|
StorageFolder,
|
||||||
|
} from 'src/enum';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
@@ -24,15 +32,6 @@ export interface MoveRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GeneratedImageType =
|
|
||||||
| AssetPathType.Preview
|
|
||||||
| AssetPathType.Thumbnail
|
|
||||||
| AssetPathType.FullSize
|
|
||||||
| AssetPathType.EditedPreview
|
|
||||||
| AssetPathType.EditedThumbnail
|
|
||||||
| AssetPathType.EditedFullSize;
|
|
||||||
export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo;
|
|
||||||
|
|
||||||
export type ThumbnailPathEntity = { id: string; ownerId: string };
|
export type ThumbnailPathEntity = { id: string; ownerId: string };
|
||||||
|
|
||||||
let instance: StorageCore | null;
|
let instance: StorageCore | null;
|
||||||
@@ -111,8 +110,19 @@ export class StorageCore {
|
|||||||
return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`);
|
return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') {
|
static getImagePath(
|
||||||
return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`);
|
asset: ThumbnailPathEntity,
|
||||||
|
{
|
||||||
|
fileType,
|
||||||
|
format,
|
||||||
|
isEdited,
|
||||||
|
}: { fileType: AssetFileType; format: ImageFormat | RawExtractedFormat; isEdited: boolean },
|
||||||
|
) {
|
||||||
|
return StorageCore.getNestedPath(
|
||||||
|
StorageFolder.Thumbnails,
|
||||||
|
asset.ownerId,
|
||||||
|
`${asset.id}_${fileType}${isEdited ? '_edited' : ''}.${format}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getEncodedVideoPath(asset: ThumbnailPathEntity) {
|
static getEncodedVideoPath(asset: ThumbnailPathEntity) {
|
||||||
@@ -137,14 +147,14 @@ export class StorageCore {
|
|||||||
return normalizedPath.startsWith(normalizedAppMediaLocation);
|
return normalizedPath.startsWith(normalizedAppMediaLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveAssetImage(asset: StorageAsset, pathType: GeneratedImageType, format: ImageFormat) {
|
async moveAssetImage(asset: StorageAsset, fileType: AssetFileType, format: ImageFormat) {
|
||||||
const { id: entityId, files } = asset;
|
const { id: entityId, files } = asset;
|
||||||
const oldFile = getAssetFile(files, pathType);
|
const oldFile = getAssetFile(files, fileType, { isEdited: false });
|
||||||
return this.moveFile({
|
return this.moveFile({
|
||||||
entityId,
|
entityId,
|
||||||
pathType,
|
pathType: fileType,
|
||||||
oldPath: oldFile?.path || null,
|
oldPath: oldFile?.path || null,
|
||||||
newPath: StorageCore.getImagePath(asset, pathType, format),
|
newPath: StorageCore.getImagePath(asset, { fileType, format, isEdited: false }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,19 +308,19 @@ export class StorageCore {
|
|||||||
case AssetPathType.Original: {
|
case AssetPathType.Original: {
|
||||||
return this.assetRepository.update({ id, originalPath: newPath });
|
return this.assetRepository.update({ id, originalPath: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.FullSize: {
|
case AssetFileType.FullSize: {
|
||||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath });
|
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.Preview: {
|
case AssetFileType.Preview: {
|
||||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath });
|
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.Thumbnail: {
|
case AssetFileType.Thumbnail: {
|
||||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath });
|
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.EncodedVideo: {
|
case AssetPathType.EncodedVideo: {
|
||||||
return this.assetRepository.update({ id, encodedVideoPath: newPath });
|
return this.assetRepository.update({ id, encodedVideoPath: newPath });
|
||||||
}
|
}
|
||||||
case AssetPathType.Sidecar: {
|
case AssetFileType.Sidecar: {
|
||||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: newPath });
|
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: newPath });
|
||||||
}
|
}
|
||||||
case PersonPathType.Face: {
|
case PersonPathType.Face: {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type AssetFile = {
|
|||||||
id: string;
|
id: string;
|
||||||
type: AssetFileType;
|
type: AssetFileType;
|
||||||
path: string;
|
path: string;
|
||||||
|
isEdited: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Library = {
|
export type Library = {
|
||||||
@@ -344,7 +345,7 @@ export const columns = {
|
|||||||
'asset.width',
|
'asset.width',
|
||||||
'asset.height',
|
'asset.height',
|
||||||
],
|
],
|
||||||
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
|
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type', 'asset_file.isEdited'],
|
||||||
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
||||||
authApiKey: ['api_key.id', 'api_key.permissions'],
|
authApiKey: ['api_key.id', 'api_key.permissions'],
|
||||||
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.appVersion'],
|
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.appVersion'],
|
||||||
|
|||||||
@@ -45,9 +45,6 @@ export enum AssetFileType {
|
|||||||
Preview = 'preview',
|
Preview = 'preview',
|
||||||
Thumbnail = 'thumbnail',
|
Thumbnail = 'thumbnail',
|
||||||
Sidecar = 'sidecar',
|
Sidecar = 'sidecar',
|
||||||
FullSizeEdited = 'fullsize_edited',
|
|
||||||
PreviewEdited = 'preview_edited',
|
|
||||||
ThumbnailEdited = 'thumbnail_edited',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AlbumUserRole {
|
export enum AlbumUserRole {
|
||||||
@@ -369,14 +366,7 @@ export enum ManualJobName {
|
|||||||
|
|
||||||
export enum AssetPathType {
|
export enum AssetPathType {
|
||||||
Original = 'original',
|
Original = 'original',
|
||||||
FullSize = 'fullsize',
|
|
||||||
Preview = 'preview',
|
|
||||||
EditedFullSize = 'edited_fullsize',
|
|
||||||
EditedPreview = 'edited_preview',
|
|
||||||
EditedThumbnail = 'edited_thumbnail',
|
|
||||||
Thumbnail = 'thumbnail',
|
|
||||||
EncodedVideo = 'encoded_video',
|
EncodedVideo = 'encoded_video',
|
||||||
Sidecar = 'sidecar',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PersonPathType {
|
export enum PersonPathType {
|
||||||
@@ -387,7 +377,7 @@ export enum UserPathType {
|
|||||||
Profile = 'profile',
|
Profile = 'profile',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PathType = AssetPathType | PersonPathType | UserPathType;
|
export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType;
|
||||||
|
|
||||||
export enum TranscodePolicy {
|
export enum TranscodePolicy {
|
||||||
All = 'all',
|
All = 'all',
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -72,7 +73,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -99,7 +101,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -145,7 +148,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -174,7 +178,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -244,7 +249,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -269,7 +275,8 @@ where
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -318,7 +325,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -357,7 +365,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -444,7 +453,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -536,7 +546,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
@@ -575,7 +586,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -286,7 +286,8 @@ select
|
|||||||
select
|
select
|
||||||
"asset_file"."id",
|
"asset_file"."id",
|
||||||
"asset_file"."path",
|
"asset_file"."path",
|
||||||
"asset_file"."type"
|
"asset_file"."type",
|
||||||
|
"asset_file"."isEdited"
|
||||||
from
|
from
|
||||||
"asset_file"
|
"asset_file"
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -903,20 +903,22 @@ export class AssetRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertFile(file: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type'>): Promise<void> {
|
async upsertFile(file: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type' | 'isEdited'>): Promise<void> {
|
||||||
const value = { ...file, assetId: asUuid(file.assetId) };
|
const value = { ...file, assetId: asUuid(file.assetId) };
|
||||||
await this.db
|
await this.db
|
||||||
.insertInto('asset_file')
|
.insertInto('asset_file')
|
||||||
.values(value)
|
.values(value)
|
||||||
.onConflict((oc) =>
|
.onConflict((oc) =>
|
||||||
oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({
|
oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({
|
||||||
path: eb.ref('excluded.path'),
|
path: eb.ref('excluded.path'),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertFiles(files: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type'>[]): Promise<void> {
|
async upsertFiles(
|
||||||
|
files: Pick<Insertable<AssetFileTable>, 'assetId' | 'path' | 'type' | 'isEdited'>[],
|
||||||
|
): Promise<void> {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -926,7 +928,7 @@ export class AssetRepository {
|
|||||||
.insertInto('asset_file')
|
.insertInto('asset_file')
|
||||||
.values(values)
|
.values(values)
|
||||||
.onConflict((oc) =>
|
.onConflict((oc) =>
|
||||||
oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({
|
oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({
|
||||||
path: eb.ref('excluded.path'),
|
path: eb.ref('excluded.path'),
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_uq";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_file" ADD "isEdited" boolean NOT NULL DEFAULT false;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_isEdited_uq" UNIQUE ("assetId", "type", "isEdited");`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_isEdited_uq";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_uq" UNIQUE ("assetId", "type");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset_file" DROP COLUMN "isEdited";`.execute(db);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from 'src/sql-tools';
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
@Table('asset_file')
|
@Table('asset_file')
|
||||||
@Unique({ columns: ['assetId', 'type'] })
|
@Unique({ columns: ['assetId', 'type', 'isEdited'] })
|
||||||
@UpdatedAtTrigger('asset_file_updatedAt')
|
@UpdatedAtTrigger('asset_file_updatedAt')
|
||||||
export class AssetFileTable {
|
export class AssetFileTable {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
@@ -37,4 +37,7 @@ export class AssetFileTable {
|
|||||||
|
|
||||||
@UpdateIdColumn({ index: true })
|
@UpdateIdColumn({ index: true })
|
||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isEdited!: Generated<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -529,9 +529,10 @@ describe(AssetMediaService.name, () => {
|
|||||||
...assetStub.withCropEdit.files,
|
...assetStub.withCropEdit.files,
|
||||||
{
|
{
|
||||||
id: 'edited-file',
|
id: 'edited-file',
|
||||||
type: AssetFileType.FullSizeEdited,
|
type: AssetFileType.FullSize,
|
||||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||||
} as AssetFile,
|
isEdited: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
@@ -554,9 +555,10 @@ describe(AssetMediaService.name, () => {
|
|||||||
...assetStub.withCropEdit.files,
|
...assetStub.withCropEdit.files,
|
||||||
{
|
{
|
||||||
id: 'edited-file',
|
id: 'edited-file',
|
||||||
type: AssetFileType.FullSizeEdited,
|
type: AssetFileType.FullSize,
|
||||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||||
} as AssetFile,
|
isEdited: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
@@ -579,9 +581,10 @@ describe(AssetMediaService.name, () => {
|
|||||||
...assetStub.withCropEdit.files,
|
...assetStub.withCropEdit.files,
|
||||||
{
|
{
|
||||||
id: 'edited-file',
|
id: 'edited-file',
|
||||||
type: AssetFileType.FullSizeEdited,
|
type: AssetFileType.FullSize,
|
||||||
path: '/uploads/user-id/fullsize/edited.jpg',
|
path: '/uploads/user-id/fullsize/edited.jpg',
|
||||||
} as AssetFile,
|
isEdited: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
|
||||||
@@ -656,6 +659,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
id: '42',
|
id: '42',
|
||||||
path: '/path/to/preview',
|
path: '/path/to/preview',
|
||||||
type: AssetFileType.Thumbnail,
|
type: AssetFileType.Thumbnail,
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -673,6 +677,7 @@ describe(AssetMediaService.name, () => {
|
|||||||
id: '42',
|
id: '42',
|
||||||
path: '/path/to/preview.jpg',
|
path: '/path/to/preview.jpg',
|
||||||
type: AssetFileType.Preview,
|
type: AssetFileType.Preview,
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { DateTime, Duration } from 'luxon';
|
|||||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||||
import { AssetFile } from 'src/database';
|
import { AssetFile } from 'src/database';
|
||||||
import { OnJob } from 'src/decorators';
|
import { OnJob } from 'src/decorators';
|
||||||
import { AssetResponseDto, MapAsset, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import {
|
import {
|
||||||
AssetBulkDeleteDto,
|
AssetBulkDeleteDto,
|
||||||
AssetBulkUpdateDto,
|
AssetBulkUpdateDto,
|
||||||
@@ -112,7 +112,7 @@ export class AssetService extends BaseService {
|
|||||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||||
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||||
|
|
||||||
let previousMotion: MapAsset | null = null;
|
let previousMotion: { id: string } | null = null;
|
||||||
if (rest.livePhotoVideoId) {
|
if (rest.livePhotoVideoId) {
|
||||||
await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId });
|
await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId });
|
||||||
} else if (rest.livePhotoVideoId === null) {
|
} else if (rest.livePhotoVideoId === null) {
|
||||||
|
|||||||
@@ -241,21 +241,21 @@ describe(MediaService.name, () => {
|
|||||||
await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success);
|
await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success);
|
||||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||||
entityId: assetStub.image.id,
|
entityId: assetStub.image.id,
|
||||||
pathType: AssetPathType.FullSize,
|
pathType: AssetFileType.FullSize,
|
||||||
oldPath: '/uploads/user-id/fullsize/path.webp',
|
oldPath: '/uploads/user-id/fullsize/path.webp',
|
||||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-fullsize.jpeg'),
|
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_fullsize.jpeg'),
|
||||||
});
|
});
|
||||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||||
entityId: assetStub.image.id,
|
entityId: assetStub.image.id,
|
||||||
pathType: AssetPathType.Preview,
|
pathType: AssetFileType.Preview,
|
||||||
oldPath: '/uploads/user-id/thumbs/path.jpg',
|
oldPath: '/uploads/user-id/thumbs/path.jpg',
|
||||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-preview.jpeg'),
|
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_preview.jpeg'),
|
||||||
});
|
});
|
||||||
expect(mocks.move.create).toHaveBeenCalledWith({
|
expect(mocks.move.create).toHaveBeenCalledWith({
|
||||||
entityId: assetStub.image.id,
|
entityId: assetStub.image.id,
|
||||||
pathType: AssetPathType.Thumbnail,
|
pathType: AssetFileType.Thumbnail,
|
||||||
oldPath: '/uploads/user-id/webp/path.ext',
|
oldPath: '/uploads/user-id/webp/path.ext',
|
||||||
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-thumbnail.webp'),
|
newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_thumbnail.webp'),
|
||||||
});
|
});
|
||||||
expect(mocks.move.create).toHaveBeenCalledTimes(3);
|
expect(mocks.move.create).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
@@ -385,11 +385,13 @@ describe(MediaService.name, () => {
|
|||||||
assetId: 'asset-id',
|
assetId: 'asset-id',
|
||||||
type: AssetFileType.Preview,
|
type: AssetFileType.Preview,
|
||||||
path: expect.any(String),
|
path: expect.any(String),
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
assetId: 'asset-id',
|
assetId: 'asset-id',
|
||||||
type: AssetFileType.Thumbnail,
|
type: AssetFileType.Thumbnail,
|
||||||
path: expect.any(String),
|
path: expect.any(String),
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
|
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
|
||||||
@@ -421,11 +423,13 @@ describe(MediaService.name, () => {
|
|||||||
assetId: 'asset-id',
|
assetId: 'asset-id',
|
||||||
type: AssetFileType.Preview,
|
type: AssetFileType.Preview,
|
||||||
path: expect.any(String),
|
path: expect.any(String),
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
assetId: 'asset-id',
|
assetId: 'asset-id',
|
||||||
type: AssetFileType.Thumbnail,
|
type: AssetFileType.Thumbnail,
|
||||||
path: expect.any(String),
|
path: expect.any(String),
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -456,11 +460,13 @@ describe(MediaService.name, () => {
|
|||||||
assetId: 'asset-id',
|
assetId: 'asset-id',
|
||||||
type: AssetFileType.Preview,
|
type: AssetFileType.Preview,
|
||||||
path: expect.any(String),
|
path: expect.any(String),
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
assetId: 'asset-id',
|
assetId: 'asset-id',
|
||||||
type: AssetFileType.Thumbnail,
|
type: AssetFileType.Thumbnail,
|
||||||
path: expect.any(String),
|
path: expect.any(String),
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -548,8 +554,8 @@ describe(MediaService.name, () => {
|
|||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||||
const previewPath = `/data/thumbs/user-id/as/se/asset-id-preview.${format}`;
|
const previewPath = `/data/thumbs/user-id/as/se/asset-id_preview.${format}`;
|
||||||
const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id-thumbnail.webp`;
|
const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id_thumbnail.webp`;
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
@@ -595,8 +601,8 @@ describe(MediaService.name, () => {
|
|||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer);
|
||||||
const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-preview.jpeg`);
|
const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_preview.jpeg`);
|
||||||
const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-thumbnail.${format}`);
|
const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id_thumbnail.${format}`);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
@@ -1026,9 +1032,9 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith(
|
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ type: AssetFileType.FullSizeEdited }),
|
expect.objectContaining({ type: AssetFileType.FullSize, isEdited: true }),
|
||||||
expect.objectContaining({ type: AssetFileType.PreviewEdited }),
|
expect.objectContaining({ type: AssetFileType.Preview, isEdited: true }),
|
||||||
expect.objectContaining({ type: AssetFileType.ThumbnailEdited }),
|
expect.objectContaining({ type: AssetFileType.Thumbnail, isEdited: true }),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1098,17 +1104,17 @@ describe(MediaService.name, () => {
|
|||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
rawBuffer,
|
rawBuffer,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.stringContaining('edited_preview.jpeg'),
|
expect.stringContaining('preview_edited.jpeg'),
|
||||||
);
|
);
|
||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
rawBuffer,
|
rawBuffer,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.stringContaining('edited_thumbnail.webp'),
|
expect.stringContaining('thumbnail_edited.webp'),
|
||||||
);
|
);
|
||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
rawBuffer,
|
rawBuffer,
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.stringContaining('edited_fullsize.jpeg'),
|
expect.stringContaining('fullsize_edited.jpeg'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3254,13 +3260,13 @@ describe(MediaService.name, () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await sut['syncFiles'](asset, [
|
await sut['syncFiles'](asset, [
|
||||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg' },
|
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false },
|
||||||
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg' },
|
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg', isEdited: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
||||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview },
|
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false },
|
||||||
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail },
|
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail, isEdited: false },
|
||||||
]);
|
]);
|
||||||
expect(mocks.asset.deleteFiles).not.toHaveBeenCalled();
|
expect(mocks.asset.deleteFiles).not.toHaveBeenCalled();
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||||
@@ -3270,19 +3276,31 @@ describe(MediaService.name, () => {
|
|||||||
const asset = {
|
const asset = {
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
files: [
|
files: [
|
||||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
{
|
||||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
id: 'file-1',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Preview,
|
||||||
|
path: '/old/preview.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/old/thumbnail.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await sut['syncFiles'](asset, [
|
await sut['syncFiles'](asset, [
|
||||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg' },
|
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false },
|
||||||
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg' },
|
{ type: AssetFileType.Thumbnail, newPath: '/new/thumbnail.jpg', isEdited: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
||||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview },
|
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false },
|
||||||
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail },
|
{ assetId: 'asset-id', path: '/new/thumbnail.jpg', type: AssetFileType.Thumbnail, isEdited: false },
|
||||||
]);
|
]);
|
||||||
expect(mocks.asset.deleteFiles).not.toHaveBeenCalled();
|
expect(mocks.asset.deleteFiles).not.toHaveBeenCalled();
|
||||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||||
@@ -3295,17 +3313,38 @@ describe(MediaService.name, () => {
|
|||||||
const asset = {
|
const asset = {
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
files: [
|
files: [
|
||||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
{
|
||||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
id: 'file-1',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Preview,
|
||||||
|
path: '/old/preview.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/old/thumbnail.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await sut['syncFiles'](asset, [{ type: AssetFileType.Preview }, { type: AssetFileType.Thumbnail }]);
|
await sut['syncFiles'](asset, [
|
||||||
|
{ type: AssetFileType.Preview, isEdited: false },
|
||||||
|
{ type: AssetFileType.Thumbnail, isEdited: false },
|
||||||
|
]);
|
||||||
|
|
||||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||||
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([
|
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([
|
||||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg', isEdited: false },
|
||||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/old/thumbnail.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.FileDelete,
|
name: JobName.FileDelete,
|
||||||
@@ -3317,14 +3356,26 @@ describe(MediaService.name, () => {
|
|||||||
const asset = {
|
const asset = {
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
files: [
|
files: [
|
||||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/same/preview.jpg' },
|
{
|
||||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/same/thumbnail.jpg' },
|
id: 'file-1',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Preview,
|
||||||
|
path: '/same/preview.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/same/thumbnail.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await sut['syncFiles'](asset, [
|
await sut['syncFiles'](asset, [
|
||||||
{ type: AssetFileType.Preview, newPath: '/same/preview.jpg' },
|
{ type: AssetFileType.Preview, newPath: '/same/preview.jpg', isEdited: false },
|
||||||
{ type: AssetFileType.Thumbnail, newPath: '/same/thumbnail.jpg' },
|
{ type: AssetFileType.Thumbnail, newPath: '/same/thumbnail.jpg', isEdited: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||||
@@ -3336,23 +3387,41 @@ describe(MediaService.name, () => {
|
|||||||
const asset = {
|
const asset = {
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
files: [
|
files: [
|
||||||
{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' },
|
{
|
||||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
id: 'file-1',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Preview,
|
||||||
|
path: '/old/preview.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/old/thumbnail.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await sut['syncFiles'](asset, [
|
await sut['syncFiles'](asset, [
|
||||||
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg' }, // replace
|
{ type: AssetFileType.Preview, newPath: '/new/preview.jpg', isEdited: false }, // replace
|
||||||
{ type: AssetFileType.Thumbnail }, // delete
|
{ type: AssetFileType.Thumbnail, isEdited: false }, // delete
|
||||||
{ type: AssetFileType.FullSize, newPath: '/new/fullsize.jpg' }, // new
|
{ type: AssetFileType.FullSize, newPath: '/new/fullsize.jpg', isEdited: false }, // new
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([
|
||||||
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview },
|
{ assetId: 'asset-id', path: '/new/preview.jpg', type: AssetFileType.Preview, isEdited: false },
|
||||||
{ assetId: 'asset-id', path: '/new/fullsize.jpg', type: AssetFileType.FullSize },
|
{ assetId: 'asset-id', path: '/new/fullsize.jpg', type: AssetFileType.FullSize, isEdited: false },
|
||||||
]);
|
]);
|
||||||
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([
|
expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([
|
||||||
{ id: 'file-2', assetId: 'asset-id', type: AssetFileType.Thumbnail, path: '/old/thumbnail.jpg' },
|
{
|
||||||
|
id: 'file-2',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Thumbnail,
|
||||||
|
path: '/old/thumbnail.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.FileDelete,
|
name: JobName.FileDelete,
|
||||||
@@ -3376,11 +3445,19 @@ describe(MediaService.name, () => {
|
|||||||
it('should delete non-existent file types when newPath is not provided', async () => {
|
it('should delete non-existent file types when newPath is not provided', async () => {
|
||||||
const asset = {
|
const asset = {
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
files: [{ id: 'file-1', assetId: 'asset-id', type: AssetFileType.Preview, path: '/old/preview.jpg' }],
|
files: [
|
||||||
|
{
|
||||||
|
id: 'file-1',
|
||||||
|
assetId: 'asset-id',
|
||||||
|
type: AssetFileType.Preview,
|
||||||
|
path: '/old/preview.jpg',
|
||||||
|
isEdited: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await sut['syncFiles'](asset, [
|
await sut['syncFiles'](asset, [
|
||||||
{ type: AssetFileType.Thumbnail }, // file doesn't exist, newPath not provided
|
{ type: AssetFileType.Thumbnail, isEdited: false }, // file doesn't exist, newPath not provided
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
expect(mocks.asset.upsertFiles).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto';
|
|||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
import {
|
import {
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
AssetPathType,
|
|
||||||
AssetType,
|
AssetType,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
@@ -50,6 +49,7 @@ interface UpsertFileOptions {
|
|||||||
assetId: string;
|
assetId: string;
|
||||||
type: AssetFileType;
|
type: AssetFileType;
|
||||||
path: string;
|
path: string;
|
||||||
|
isEdited: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThumbnailAsset = NonNullable<Awaited<ReturnType<AssetJobRepository['getForGenerateThumbnailJob']>>>;
|
type ThumbnailAsset = NonNullable<Awaited<ReturnType<AssetJobRepository['getForGenerateThumbnailJob']>>>;
|
||||||
@@ -160,9 +160,9 @@ export class MediaService extends BaseService {
|
|||||||
return JobStatus.Failed;
|
return JobStatus.Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storageCore.moveAssetImage(asset, AssetPathType.FullSize, image.fullsize.format);
|
await this.storageCore.moveAssetImage(asset, AssetFileType.FullSize, image.fullsize.format);
|
||||||
await this.storageCore.moveAssetImage(asset, AssetPathType.Preview, image.preview.format);
|
await this.storageCore.moveAssetImage(asset, AssetFileType.Preview, image.preview.format);
|
||||||
await this.storageCore.moveAssetImage(asset, AssetPathType.Thumbnail, image.thumbnail.format);
|
await this.storageCore.moveAssetImage(asset, AssetFileType.Thumbnail, image.thumbnail.format);
|
||||||
await this.storageCore.moveAssetVideo(asset);
|
await this.storageCore.moveAssetVideo(asset);
|
||||||
|
|
||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
@@ -236,9 +236,9 @@ export class MediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.syncFiles(asset, [
|
await this.syncFiles(asset, [
|
||||||
{ type: AssetFileType.Preview, newPath: generated.previewPath },
|
{ type: AssetFileType.Preview, newPath: generated.previewPath, isEdited: false },
|
||||||
{ type: AssetFileType.Thumbnail, newPath: generated.thumbnailPath },
|
{ type: AssetFileType.Thumbnail, newPath: generated.thumbnailPath, isEdited: false },
|
||||||
{ type: AssetFileType.FullSize, newPath: generated.fullsizePath },
|
{ type: AssetFileType.FullSize, newPath: generated.fullsizePath, isEdited: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const editiedGenerated = await this.generateEditedThumbnails(asset);
|
const editiedGenerated = await this.generateEditedThumbnails(asset);
|
||||||
@@ -307,16 +307,16 @@ export class MediaService extends BaseService {
|
|||||||
|
|
||||||
private async generateImageThumbnails(asset: ThumbnailAsset, useEdits: boolean = false) {
|
private async generateImageThumbnails(asset: ThumbnailAsset, useEdits: boolean = false) {
|
||||||
const { image } = await this.getConfig({ withCache: true });
|
const { image } = await this.getConfig({ withCache: true });
|
||||||
const previewPath = StorageCore.getImagePath(
|
const previewPath = StorageCore.getImagePath(asset, {
|
||||||
asset,
|
fileType: AssetFileType.Preview,
|
||||||
useEdits ? AssetPathType.EditedPreview : AssetPathType.Preview,
|
isEdited: useEdits,
|
||||||
image.preview.format,
|
format: image.preview.format,
|
||||||
);
|
});
|
||||||
const thumbnailPath = StorageCore.getImagePath(
|
const thumbnailPath = StorageCore.getImagePath(asset, {
|
||||||
asset,
|
fileType: AssetFileType.Thumbnail,
|
||||||
useEdits ? AssetPathType.EditedThumbnail : AssetPathType.Thumbnail,
|
isEdited: useEdits,
|
||||||
image.thumbnail.format,
|
format: image.thumbnail.format,
|
||||||
);
|
});
|
||||||
this.storageCore.ensureFolders(previewPath);
|
this.storageCore.ensureFolders(previewPath);
|
||||||
|
|
||||||
// Handle embedded preview extraction for RAW files
|
// Handle embedded preview extraction for RAW files
|
||||||
@@ -343,11 +343,11 @@ export class MediaService extends BaseService {
|
|||||||
|
|
||||||
if (convertFullsize) {
|
if (convertFullsize) {
|
||||||
// convert a new fullsize image from the same source as the thumbnail
|
// convert a new fullsize image from the same source as the thumbnail
|
||||||
fullsizePath = StorageCore.getImagePath(
|
fullsizePath = StorageCore.getImagePath(asset, {
|
||||||
asset,
|
fileType: AssetFileType.FullSize,
|
||||||
useEdits ? AssetPathType.EditedFullSize : AssetPathType.FullSize,
|
isEdited: useEdits,
|
||||||
image.fullsize.format,
|
format: image.fullsize.format,
|
||||||
);
|
});
|
||||||
const fullsizeOptions = {
|
const fullsizeOptions = {
|
||||||
format: image.fullsize.format,
|
format: image.fullsize.format,
|
||||||
quality: image.fullsize.quality,
|
quality: image.fullsize.quality,
|
||||||
@@ -355,7 +355,11 @@ export class MediaService extends BaseService {
|
|||||||
};
|
};
|
||||||
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
||||||
} else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) {
|
} else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) {
|
||||||
fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, extracted.format);
|
fullsizePath = StorageCore.getImagePath(asset, {
|
||||||
|
fileType: AssetFileType.FullSize,
|
||||||
|
format: extracted.format,
|
||||||
|
isEdited: false,
|
||||||
|
});
|
||||||
this.storageCore.ensureFolders(fullsizePath);
|
this.storageCore.ensureFolders(fullsizePath);
|
||||||
|
|
||||||
// Write the buffer to disk with essential EXIF data
|
// Write the buffer to disk with essential EXIF data
|
||||||
@@ -489,8 +493,16 @@ export class MediaService extends BaseService {
|
|||||||
|
|
||||||
private async generateVideoThumbnails(asset: ThumbnailPathEntity & { originalPath: string }) {
|
private async generateVideoThumbnails(asset: ThumbnailPathEntity & { originalPath: string }) {
|
||||||
const { image, ffmpeg } = await this.getConfig({ withCache: true });
|
const { image, ffmpeg } = await this.getConfig({ withCache: true });
|
||||||
const previewPath = StorageCore.getImagePath(asset, AssetPathType.Preview, image.preview.format);
|
const previewPath = StorageCore.getImagePath(asset, {
|
||||||
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.Thumbnail, image.thumbnail.format);
|
fileType: AssetFileType.Preview,
|
||||||
|
format: image.preview.format,
|
||||||
|
isEdited: false,
|
||||||
|
});
|
||||||
|
const thumbnailPath = StorageCore.getImagePath(asset, {
|
||||||
|
fileType: AssetFileType.Thumbnail,
|
||||||
|
format: image.thumbnail.format,
|
||||||
|
isEdited: false,
|
||||||
|
});
|
||||||
this.storageCore.ensureFolders(previewPath);
|
this.storageCore.ensureFolders(previewPath);
|
||||||
|
|
||||||
const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||||
@@ -779,18 +791,18 @@ export class MediaService extends BaseService {
|
|||||||
|
|
||||||
private async syncFiles(
|
private async syncFiles(
|
||||||
asset: { id: string; files: AssetFile[] },
|
asset: { id: string; files: AssetFile[] },
|
||||||
files: { type: AssetFileType; newPath?: string }[],
|
files: { type: AssetFileType; newPath?: string; isEdited: boolean }[],
|
||||||
) {
|
) {
|
||||||
const toUpsert: UpsertFileOptions[] = [];
|
const toUpsert: UpsertFileOptions[] = [];
|
||||||
const pathsToDelete: string[] = [];
|
const pathsToDelete: string[] = [];
|
||||||
const toDelete: AssetFile[] = [];
|
const toDelete: AssetFile[] = [];
|
||||||
|
|
||||||
for (const { type, newPath } of files) {
|
for (const { type, newPath, isEdited } of files) {
|
||||||
const existingFile = asset.files.find((file) => file.type === type);
|
const existingFile = asset.files.find((file) => file.type === type && file.isEdited === isEdited);
|
||||||
|
|
||||||
// upsert new file path
|
// upsert new file path
|
||||||
if (newPath && existingFile?.path !== newPath) {
|
if (newPath && existingFile?.path !== newPath) {
|
||||||
toUpsert.push({ assetId: asset.id, path: newPath, type });
|
toUpsert.push({ assetId: asset.id, path: newPath, type, isEdited });
|
||||||
|
|
||||||
// delete old file from disk
|
// delete old file from disk
|
||||||
if (existingFile) {
|
if (existingFile) {
|
||||||
@@ -829,9 +841,9 @@ export class MediaService extends BaseService {
|
|||||||
const generated = asset.edits.length > 0 ? await this.generateImageThumbnails(asset, true) : undefined;
|
const generated = asset.edits.length > 0 ? await this.generateImageThumbnails(asset, true) : undefined;
|
||||||
|
|
||||||
await this.syncFiles(asset, [
|
await this.syncFiles(asset, [
|
||||||
{ type: AssetFileType.PreviewEdited, newPath: generated?.previewPath },
|
{ type: AssetFileType.Preview, newPath: generated?.previewPath, isEdited: true },
|
||||||
{ type: AssetFileType.ThumbnailEdited, newPath: generated?.thumbnailPath },
|
{ type: AssetFileType.Thumbnail, newPath: generated?.thumbnailPath, isEdited: true },
|
||||||
{ type: AssetFileType.FullSizeEdited, newPath: generated?.fullsizePath },
|
{ type: AssetFileType.FullSize, newPath: generated?.fullsizePath, isEdited: true },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const crop = asset.edits.find((e) => e.action === AssetEditAction.Crop);
|
const crop = asset.edits.find((e) => e.action === AssetEditAction.Crop);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const forSidecarJob = (
|
|||||||
asset: {
|
asset: {
|
||||||
id?: string;
|
id?: string;
|
||||||
originalPath?: string;
|
originalPath?: string;
|
||||||
files?: { id: string; type: AssetFileType; path: string }[];
|
files?: { id: string; type: AssetFileType; path: string; isEdited: boolean }[];
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
@@ -1084,6 +1084,7 @@ describe(MetadataService.name, () => {
|
|||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
type: AssetFileType.Sidecar,
|
type: AssetFileType.Sidecar,
|
||||||
path: '/path/to/something',
|
path: '/path/to/something',
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1691,7 +1692,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should unset sidecar path if file no longer exist', async () => {
|
it('should unset sidecar path if file no longer exist', async () => {
|
||||||
const asset = forSidecarJob({
|
const asset = forSidecarJob({
|
||||||
originalPath: '/path/to/IMG_123.jpg',
|
originalPath: '/path/to/IMG_123.jpg',
|
||||||
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }],
|
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }],
|
||||||
});
|
});
|
||||||
mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset);
|
mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset);
|
||||||
mocks.storage.checkFileExists.mockResolvedValue(false);
|
mocks.storage.checkFileExists.mockResolvedValue(false);
|
||||||
@@ -1704,7 +1705,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should do nothing if the sidecar file still exists', async () => {
|
it('should do nothing if the sidecar file still exists', async () => {
|
||||||
const asset = forSidecarJob({
|
const asset = forSidecarJob({
|
||||||
originalPath: '/path/to/IMG_123.jpg',
|
originalPath: '/path/to/IMG_123.jpg',
|
||||||
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }],
|
files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }],
|
||||||
});
|
});
|
||||||
|
|
||||||
mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset);
|
mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset);
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ describe(NotificationService.name, () => {
|
|||||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([
|
||||||
{ id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' },
|
{ id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg', isEdited: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||||
@@ -403,7 +403,7 @@ describe(NotificationService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
mocks.systemMetadata.get.mockResolvedValue({ server: {} });
|
||||||
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
mocks.notification.create.mockResolvedValue(notificationStub.albumEvent);
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]);
|
mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([{ ...assetStub.image.files[2], isEdited: false }]);
|
||||||
|
|
||||||
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success);
|
||||||
expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith(
|
expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -240,11 +240,11 @@ export class StorageTemplateService extends BaseService {
|
|||||||
assetInfo: { sizeInBytes: fileSizeInByte, checksum },
|
assetInfo: { sizeInBytes: fileSizeInByte, checksum },
|
||||||
});
|
});
|
||||||
|
|
||||||
const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar)?.path;
|
const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar, { isEdited: false })?.path;
|
||||||
if (sidecarPath) {
|
if (sidecarPath) {
|
||||||
await this.storageCore.moveFile({
|
await this.storageCore.moveFile({
|
||||||
entityId: id,
|
entityId: id,
|
||||||
pathType: AssetPathType.Sidecar,
|
pathType: AssetFileType.Sidecar,
|
||||||
oldPath: sidecarPath,
|
oldPath: sidecarPath,
|
||||||
newPath: `${newPath}.xmp`,
|
newPath: `${newPath}.xmp`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { GeneratedImageType, StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { AssetFile, Exif } from 'src/database';
|
import { AssetFile, Exif } from 'src/database';
|
||||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||||
@@ -14,19 +14,19 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
|
|||||||
import { IBulkAsset, ImmichFile, UploadFile, UploadRequest } from 'src/types';
|
import { IBulkAsset, ImmichFile, UploadFile, UploadRequest } from 'src/types';
|
||||||
import { checkAccess } from 'src/utils/access';
|
import { checkAccess } from 'src/utils/access';
|
||||||
|
|
||||||
export const getAssetFile = (files: AssetFile[], type: AssetFileType | GeneratedImageType) => {
|
export const getAssetFile = (files: AssetFile[], type: AssetFileType, { isEdited }: { isEdited: boolean }) => {
|
||||||
return files.find((file) => file.type === type);
|
return files.find((file) => file.type === type && file.isEdited === isEdited);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssetFiles = (files: AssetFile[]) => ({
|
export const getAssetFiles = (files: AssetFile[]) => ({
|
||||||
fullsizeFile: getAssetFile(files, AssetFileType.FullSize),
|
fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }),
|
||||||
previewFile: getAssetFile(files, AssetFileType.Preview),
|
previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }),
|
||||||
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail),
|
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { isEdited: false }),
|
||||||
sidecarFile: getAssetFile(files, AssetFileType.Sidecar),
|
sidecarFile: getAssetFile(files, AssetFileType.Sidecar, { isEdited: false }),
|
||||||
|
|
||||||
editedFullsizeFile: getAssetFile(files, AssetFileType.FullSizeEdited),
|
editedFullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: true }),
|
||||||
editedPreviewFile: getAssetFile(files, AssetFileType.PreviewEdited),
|
editedPreviewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }),
|
||||||
editedThumbnailFile: getAssetFile(files, AssetFileType.ThumbnailEdited),
|
editedThumbnailFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const addAssets = async (
|
export const addAssets = async (
|
||||||
|
|||||||
9
server/test/fixtures/asset.stub.ts
vendored
9
server/test/fixtures/asset.stub.ts
vendored
@@ -31,18 +31,21 @@ const sidecarFileWithoutExt = factory.assetFile({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const editedPreviewFile = factory.assetFile({
|
const editedPreviewFile = factory.assetFile({
|
||||||
type: AssetFileType.PreviewEdited,
|
type: AssetFileType.Preview,
|
||||||
path: '/uploads/user-id/preview/path_edited.jpg',
|
path: '/uploads/user-id/preview/path_edited.jpg',
|
||||||
|
isEdited: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const editedThumbnailFile = factory.assetFile({
|
const editedThumbnailFile = factory.assetFile({
|
||||||
type: AssetFileType.ThumbnailEdited,
|
type: AssetFileType.Thumbnail,
|
||||||
path: '/uploads/user-id/thumbnail/path_edited.jpg',
|
path: '/uploads/user-id/thumbnail/path_edited.jpg',
|
||||||
|
isEdited: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const editedFullsizeFile = factory.assetFile({
|
const editedFullsizeFile = factory.assetFile({
|
||||||
type: AssetFileType.FullSizeEdited,
|
type: AssetFileType.FullSize,
|
||||||
path: '/uploads/user-id/fullsize/path_edited.jpg',
|
path: '/uploads/user-id/fullsize/path_edited.jpg',
|
||||||
|
isEdited: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile];
|
const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile];
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ const assetSidecarWriteFactory = () => {
|
|||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
path: '/path/to/original-path.jpg.xmp',
|
path: '/path/to/original-path.jpg.xmp',
|
||||||
type: AssetFileType.Sidecar,
|
type: AssetFileType.Sidecar,
|
||||||
|
isEdited: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exifInfo: {
|
exifInfo: {
|
||||||
@@ -386,6 +387,7 @@ const assetFileFactory = (file: Partial<AssetFile> = {}): AssetFile => ({
|
|||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
type: AssetFileType.Preview,
|
type: AssetFileType.Preview,
|
||||||
path: '/uploads/user-id/thumbs/path.jpg',
|
path: '/uploads/user-id/thumbs/path.jpg',
|
||||||
|
isEdited: false,
|
||||||
...file,
|
...file,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user