refactor: add back isOffline and remove trashReason from asset, change sync job flow

This commit is contained in:
Zack Pollard 2024-09-17 17:55:27 +01:00
parent e071cc9ca8
commit ba0d5410cd
35 changed files with 253 additions and 557 deletions

View File

@ -1,4 +1,4 @@
import { LibraryResponseDto, LoginResponseDto, TrashReason, getAllLibraries, scanLibrary } from '@immich/sdk';
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@ -411,7 +411,7 @@ describe('/libraries', () => {
expect(assets.count).toBe(0);
});
it('should trash an asset if its file is missing', async () => {
it('should set an asset offline if its file is missing', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
@ -436,15 +436,14 @@ describe('/libraries', () => {
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.isTrashed).toBe(true);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(trashedAsset.trashReason).toEqual(TrashReason.Offline);
expect(trashedAsset.isOffline).toEqual(true);
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([]);
});
it('should trash an asset if its file is not in any import path', async () => {
it('should set an asset offline its file is not in any import path', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
@ -474,9 +473,8 @@ describe('/libraries', () => {
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.isTrashed).toBe(true);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(trashedAsset.trashReason).toEqual(TrashReason.Offline);
expect(trashedAsset.isOffline).toBe(true);
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
@ -486,7 +484,7 @@ describe('/libraries', () => {
utils.removeDirectory(`${testAssetDir}/temp/another-path/`);
});
it('should trash an asset if its file is covered by an exclusion pattern', async () => {
it('should set an asset offline if its file is covered by an exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
@ -512,7 +510,7 @@ describe('/libraries', () => {
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.isTrashed).toBe(true);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/directoryB/assetB.png`);
expect(trashedAsset.trashReason).toEqual(TrashReason.Offline);
expect(trashedAsset.isOffline).toBe(true);
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });

View File

@ -1,4 +1,4 @@
import { LoginResponseDto, TrashReason, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk';
import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk';
import { existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
@ -35,9 +35,7 @@ describe('/trash', () => {
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(
expect.objectContaining({ id: assetId, isTrashed: true, trashReason: TrashReason.Deleted }),
);
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
const { status, body } = await request(app)
.post('/trash/empty')
@ -59,9 +57,7 @@ describe('/trash', () => {
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(
expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true, trashReason: TrashReason.Deleted }),
);
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
const { status, body } = await request(app)
.post('/trash/empty')
@ -160,17 +156,13 @@ describe('/trash', () => {
await utils.waitForQueueFinish(admin.accessToken, 'library');
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(
expect.objectContaining({ id: assetId, isTrashed: true, trashReason: TrashReason.Offline }),
);
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(
expect.objectContaining({ id: assetId, isTrashed: true, trashReason: TrashReason.Offline }),
);
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
});
});

View File

@ -132,7 +132,6 @@ Class | Method | HTTP request | Description
*LibrariesApi* | [**getAllLibraries**](doc//LibrariesApi.md#getalllibraries) | **GET** /libraries |
*LibrariesApi* | [**getLibrary**](doc//LibrariesApi.md#getlibrary) | **GET** /libraries/{id} |
*LibrariesApi* | [**getLibraryStatistics**](doc//LibrariesApi.md#getlibrarystatistics) | **GET** /libraries/{id}/statistics |
*LibrariesApi* | [**removeOfflineFiles**](doc//LibrariesApi.md#removeofflinefiles) | **POST** /libraries/{id}/removeOffline |
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan |
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} |
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |
@ -384,7 +383,6 @@ Class | Method | HTTP request | Description
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
- [ScanLibraryDto](doc//ScanLibraryDto.md)
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
- [SearchExploreItem](doc//SearchExploreItem.md)

View File

@ -197,7 +197,6 @@ part 'model/ratings_update.dart';
part 'model/reaction_level.dart';
part 'model/reaction_type.dart';
part 'model/reverse_geocoding_state_response_dto.dart';
part 'model/scan_library_dto.dart';
part 'model/search_album_response_dto.dart';
part 'model/search_asset_response_dto.dart';
part 'model/search_explore_item.dart';

View File

@ -449,8 +449,6 @@ class ApiClient {
return ReactionTypeTypeTransformer().decode(value);
case 'ReverseGeocodingStateResponseDto':
return ReverseGeocodingStateResponseDto.fromJson(value);
case 'ScanLibraryDto':
return ScanLibraryDto.fromJson(value);
case 'SearchAlbumResponseDto':
return SearchAlbumResponseDto.fromJson(value);
case 'SearchAssetResponseDto':

View File

@ -15,7 +15,6 @@ class AssetBulkDeleteDto {
AssetBulkDeleteDto({
this.force,
this.ids = const [],
this.trashReason,
});
///
@ -28,23 +27,19 @@ class AssetBulkDeleteDto {
List<String> ids;
AssetBulkDeleteDtoTrashReasonEnum? trashReason;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkDeleteDto &&
other.force == force &&
_deepEquality.equals(other.ids, ids) &&
other.trashReason == trashReason;
_deepEquality.equals(other.ids, ids);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(force == null ? 0 : force!.hashCode) +
(ids.hashCode) +
(trashReason == null ? 0 : trashReason!.hashCode);
(ids.hashCode);
@override
String toString() => 'AssetBulkDeleteDto[force=$force, ids=$ids, trashReason=$trashReason]';
String toString() => 'AssetBulkDeleteDto[force=$force, ids=$ids]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -54,11 +49,6 @@ class AssetBulkDeleteDto {
// json[r'force'] = null;
}
json[r'ids'] = this.ids;
if (this.trashReason != null) {
json[r'trashReason'] = this.trashReason;
} else {
// json[r'trashReason'] = null;
}
return json;
}
@ -74,7 +64,6 @@ class AssetBulkDeleteDto {
ids: json[r'ids'] is Iterable
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
: const [],
trashReason: AssetBulkDeleteDtoTrashReasonEnum.fromJson(json[r'trashReason']),
);
}
return null;
@ -126,77 +115,3 @@ class AssetBulkDeleteDto {
};
}
class AssetBulkDeleteDtoTrashReasonEnum {
/// Instantiate a new enum with the provided [value].
const AssetBulkDeleteDtoTrashReasonEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const deleted = AssetBulkDeleteDtoTrashReasonEnum._(r'deleted');
static const offline = AssetBulkDeleteDtoTrashReasonEnum._(r'offline');
/// List of all possible values in this [enum][AssetBulkDeleteDtoTrashReasonEnum].
static const values = <AssetBulkDeleteDtoTrashReasonEnum>[
deleted,
offline,
];
static AssetBulkDeleteDtoTrashReasonEnum? fromJson(dynamic value) => AssetBulkDeleteDtoTrashReasonEnumTypeTransformer().decode(value);
static List<AssetBulkDeleteDtoTrashReasonEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetBulkDeleteDtoTrashReasonEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetBulkDeleteDtoTrashReasonEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [AssetBulkDeleteDtoTrashReasonEnum] to String,
/// and [decode] dynamic data back to [AssetBulkDeleteDtoTrashReasonEnum].
class AssetBulkDeleteDtoTrashReasonEnumTypeTransformer {
factory AssetBulkDeleteDtoTrashReasonEnumTypeTransformer() => _instance ??= const AssetBulkDeleteDtoTrashReasonEnumTypeTransformer._();
const AssetBulkDeleteDtoTrashReasonEnumTypeTransformer._();
String encode(AssetBulkDeleteDtoTrashReasonEnum data) => data.value;
/// Decodes a [dynamic value][data] to a AssetBulkDeleteDtoTrashReasonEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
AssetBulkDeleteDtoTrashReasonEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'deleted': return AssetBulkDeleteDtoTrashReasonEnum.deleted;
case r'offline': return AssetBulkDeleteDtoTrashReasonEnum.offline;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [AssetBulkDeleteDtoTrashReasonEnumTypeTransformer] instance.
static AssetBulkDeleteDtoTrashReasonEnumTypeTransformer? _instance;
}

View File

@ -25,6 +25,7 @@ class AssetResponseDto {
required this.id,
required this.isArchived,
required this.isFavorite,
required this.isOffline,
required this.isTrashed,
this.libraryId,
this.livePhotoVideoId,
@ -40,7 +41,6 @@ class AssetResponseDto {
this.stack,
this.tags = const [],
required this.thumbhash,
this.trashReason,
required this.type,
this.unassignedFaces = const [],
required this.updatedAt,
@ -77,6 +77,8 @@ class AssetResponseDto {
bool isFavorite;
bool isOffline;
bool isTrashed;
/// This property was deprecated in v1.106.0
@ -133,8 +135,6 @@ class AssetResponseDto {
String? thumbhash;
String? trashReason;
AssetTypeEnum type;
List<AssetFaceWithoutPersonResponseDto> unassignedFaces;
@ -155,6 +155,7 @@ class AssetResponseDto {
other.id == id &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.isOffline == isOffline &&
other.isTrashed == isTrashed &&
other.libraryId == libraryId &&
other.livePhotoVideoId == livePhotoVideoId &&
@ -170,7 +171,6 @@ class AssetResponseDto {
other.stack == stack &&
_deepEquality.equals(other.tags, tags) &&
other.thumbhash == thumbhash &&
other.trashReason == trashReason &&
other.type == type &&
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
other.updatedAt == updatedAt;
@ -190,6 +190,7 @@ class AssetResponseDto {
(id.hashCode) +
(isArchived.hashCode) +
(isFavorite.hashCode) +
(isOffline.hashCode) +
(isTrashed.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
@ -205,13 +206,12 @@ class AssetResponseDto {
(stack == null ? 0 : stack!.hashCode) +
(tags.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(trashReason == null ? 0 : trashReason!.hashCode) +
(type.hashCode) +
(unassignedFaces.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, trashReason=$trashReason, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -235,6 +235,7 @@ class AssetResponseDto {
json[r'id'] = this.id;
json[r'isArchived'] = this.isArchived;
json[r'isFavorite'] = this.isFavorite;
json[r'isOffline'] = this.isOffline;
json[r'isTrashed'] = this.isTrashed;
if (this.libraryId != null) {
json[r'libraryId'] = this.libraryId;
@ -281,11 +282,6 @@ class AssetResponseDto {
json[r'thumbhash'] = this.thumbhash;
} else {
// json[r'thumbhash'] = null;
}
if (this.trashReason != null) {
json[r'trashReason'] = this.trashReason;
} else {
// json[r'trashReason'] = null;
}
json[r'type'] = this.type;
json[r'unassignedFaces'] = this.unassignedFaces;
@ -313,6 +309,7 @@ class AssetResponseDto {
id: mapValueOfType<String>(json, r'id')!,
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
isOffline: mapValueOfType<bool>(json, r'isOffline')!,
isTrashed: mapValueOfType<bool>(json, r'isTrashed')!,
libraryId: mapValueOfType<String>(json, r'libraryId'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
@ -328,7 +325,6 @@ class AssetResponseDto {
stack: AssetStackResponseDto.fromJson(json[r'stack']),
tags: TagResponseDto.listFromJson(json[r'tags']),
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
trashReason: mapValueOfType<String>(json, r'trashReason'),
type: AssetTypeEnum.fromJson(json[r'type'])!,
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
@ -389,6 +385,7 @@ class AssetResponseDto {
'id',
'isArchived',
'isFavorite',
'isOffline',
'isTrashed',
'localDateTime',
'originalFileName',

View File

@ -7832,13 +7832,6 @@
"type": "string"
},
"type": "array"
},
"trashReason": {
"enum": [
"deleted",
"offline"
],
"type": "string"
}
},
"required": [
@ -8376,6 +8369,9 @@
"isFavorite": {
"type": "boolean"
},
"isOffline": {
"type": "boolean"
},
"isTrashed": {
"type": "boolean"
},
@ -8440,10 +8436,6 @@
"nullable": true,
"type": "string"
},
"trashReason": {
"nullable": true,
"type": "string"
},
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
},
@ -8469,6 +8461,7 @@
"id",
"isArchived",
"isFavorite",
"isOffline",
"isTrashed",
"localDateTime",
"originalFileName",

View File

@ -253,6 +253,7 @@ export type AssetResponseDto = {
id: string;
isArchived: boolean;
isFavorite: boolean;
isOffline: boolean;
isTrashed: boolean;
/** This property was deprecated in v1.106.0 */
libraryId?: string | null;
@ -270,7 +271,6 @@ export type AssetResponseDto = {
stack?: (AssetStackResponseDto) | null;
tags?: TagResponseDto[];
thumbhash: string | null;
trashReason?: string | null;
"type": AssetTypeEnum;
unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
updatedAt: string;
@ -356,7 +356,6 @@ export type ApiKeyUpdateDto = {
export type AssetBulkDeleteDto = {
force?: boolean;
ids: string[];
trashReason?: TrashReason;
};
export type AssetMediaCreateDto = {
assetData: Blob;
@ -3350,10 +3349,6 @@ export enum Permission {
AdminUserUpdate = "admin.user.update",
AdminUserDelete = "admin.user.delete"
}
export enum TrashReason {
Deleted = "deleted",
Offline = "offline"
}
export enum AssetMediaStatus {
Created = "created",
Replaced = "replaced",

View File

@ -43,7 +43,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
isFavorite!: boolean;
isArchived!: boolean;
isTrashed!: boolean;
trashReason?: string | null;
isOffline!: boolean;
exifInfo?: ExifResponseDto;
smartInfo?: SmartInfoResponseDto;
tags?: TagResponseDto[];
@ -139,7 +139,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
isArchived: entity.isArchived,
isTrashed: !!entity.deletedAt,
trashReason: entity.trashReason,
isOffline: entity.isOffline,
duration: entity.duration ?? '0:00:00.00000',
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,

View File

@ -84,9 +84,6 @@ export class RandomAssetsDto {
export class AssetBulkDeleteDto extends BulkIdsDto {
@ValidateBoolean({ optional: true })
force?: boolean;
@Optional()
trashReason?: AssetTrashReason;
}
export class AssetIdsDto {
@ -94,11 +91,6 @@ export class AssetIdsDto {
assetIds!: string[];
}
export enum AssetTrashReason {
DELETED = 'deleted',
OFFLINE = 'offline',
}
export enum AssetJobName {
REGENERATE_THUMBNAIL = 'regenerate-thumbnail',
REFRESH_METADATA = 'refresh-metadata',

View File

@ -1,4 +1,3 @@
import { AssetTrashReason } from 'src/dtos/asset.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetFileEntity } from 'src/entities/asset-files.entity';
@ -171,12 +170,8 @@ export class AssetEntity {
@OneToOne(() => AssetJobStatusEntity, (jobStatus) => jobStatus.asset, { nullable: true })
jobStatus?: AssetJobStatusEntity;
@Column({
type: 'enum',
enum: AssetTrashReason,
nullable: true,
})
trashReason!: AssetTrashReason | null;
@Column({ type: 'boolean', default: false })
isOffline!: boolean;
@Index('IDX_assets_duplicateId')
@Column({ type: 'uuid', nullable: true })

View File

@ -146,8 +146,6 @@ export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath'>;
export const IAssetRepository = 'IAssetRepository';
export interface IAssetRepository {
getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]>;
getUniqueOriginalPaths(userId: string): Promise<string[]>;
create(asset: AssetCreate): Promise<AssetEntity>;
getByIds(
ids: string[],
@ -156,13 +154,6 @@ export interface IAssetRepository {
): Promise<AssetEntity[]>;
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
getByIds(
ids: string[],
relations?: FindOptionsRelations<AssetEntity>,
select?: FindOptionsSelect<AssetEntity>,
): Promise<AssetEntity[]>;
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
getByChecksum(options: { ownerId: string; checksum: Buffer; libraryId?: string }): Promise<AssetEntity | null>;
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
@ -181,15 +172,6 @@ export interface IAssetRepository {
libraryId?: string,
withDeleted?: boolean,
): Paginated<AssetEntity>;
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
getWith(
pagination: PaginationOptions,
property: WithProperty,
libraryId?: string,
withDeleted?: boolean,
): Paginated<AssetEntity>;
getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity>;
@ -198,31 +180,12 @@ export interface IAssetRepository {
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
getLivePhotoCount(motionId: string): Promise<number>;
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
updateAll(ids: string[], options: Partial<AssetUpdateAllOptions>): Promise<void>;
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
update(asset: AssetUpdateOptions): Promise<void>;
remove(asset: AssetEntity): Promise<void>;
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
getUniqueOriginalPaths(userId: string): Promise<string[]>;
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated<AssetEntity>;
remove(asset: AssetEntity): Promise<void>;
restoreAll(ids: string[]): Promise<void>;
softDeleteAll(ids: string[]): Promise<void>;
update(asset: AssetUpdateOptions): Promise<void>;
updateAll(ids: string[], options: Partial<AssetUpdateAllOptions>): Promise<void>;
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
update(asset: AssetUpdateOptions): Promise<void>;
remove(asset: AssetEntity): Promise<void>;
softDeleteAll(ids: string[]): Promise<void>;
restoreAll(ids: string[]): Promise<void>;
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
@ -233,6 +196,4 @@ export interface IAssetRepository {
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise<void>;
restoreAllDeleted(userId: string): Promise<void>;
restoreAllDeletedById(ids: string[]): Promise<void>;
}

View File

@ -76,12 +76,12 @@ export enum JobName {
FACIAL_RECOGNITION = 'facial-recognition',
// library management
LIBRARY_QUEUE_SCAN = 'library-scan-new',
LIBRARY_QUEUE_OFFLINE_CHECK = 'library-queue-remove-deleted',
LIBRARY_REFRESH_ASSET = 'library-refresh-asset',
LIBRARY_OFFLINE_CHECK = 'library-remove-deleted',
LIBRARY_QUEUE_SYNC_FILES = 'library-scan-new',
LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-remove-deleted',
LIBRARY_SYNC_FILE = 'library-refresh-asset',
LIBRARY_SYNC_ASSET = 'library-remove-deleted',
LIBRARY_DELETE = 'library-delete',
LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh',
LIBRARY_QUEUE_SYNC_ALL = 'library-queue-all-refresh',
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
// cleanup
@ -272,12 +272,12 @@ export type JobItem =
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
// Library Management
| { name: JobName.LIBRARY_REFRESH_ASSET; data: ILibraryFileJob }
| { name: JobName.LIBRARY_QUEUE_SCAN; data: IEntityJob }
| { name: JobName.LIBRARY_QUEUE_OFFLINE_CHECK; data: IEntityJob }
| { name: JobName.LIBRARY_OFFLINE_CHECK; data: IEntityJob }
| { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob }
| { name: JobName.LIBRARY_SYNC_ASSET; data: IEntityJob }
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
| { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data?: IBaseJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
// Notification

View File

@ -1,4 +1,3 @@
import { AssetTrashReason } from 'src/dtos/asset.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
@ -58,13 +57,13 @@ export interface SearchStatusOptions {
isEncoded?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isVisible?: boolean;
isNotInAlbum?: boolean;
type?: AssetType;
status?: AssetStatus;
withArchived?: boolean;
withDeleted?: boolean;
trashReason?: AssetTrashReason;
}
export interface SearchOneToOneRelationOptions {

View File

@ -1,14 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class RemoveOfflineField1725282595231 implements MigrationInterface {
name = 'RemoveOfflineField1725282595231'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isOffline"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ADD "isOffline" boolean NOT NULL DEFAULT false`);
}
}

View File

@ -1,15 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddTrashReason1725444664102 implements MigrationInterface {
name = 'AddTrashReason1725444664102';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."assets_trashreason_enum" AS ENUM('deleted', 'offline')`);
await queryRunner.query(`ALTER TABLE "assets" ADD "trashReason" "public"."assets_trashreason_enum"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "trashReason"`);
await queryRunner.query(`DROP TYPE "public"."assets_trashreason_enum"`);
}
}

View File

@ -21,7 +21,6 @@ SELECT
"entity"."isFavorite" AS "entity_isFavorite",
"entity"."isArchived" AS "entity_isArchived",
"entity"."isExternal" AS "entity_isExternal",
"entity"."isOffline" AS "entity_isOffline",
"entity"."checksum" AS "entity_checksum",
"entity"."duration" AS "entity_duration",
"entity"."isVisible" AS "entity_isVisible",
@ -29,6 +28,7 @@ SELECT
"entity"."originalFileName" AS "entity_originalFileName",
"entity"."sidecarPath" AS "entity_sidecarPath",
"entity"."stackId" AS "entity_stackId",
"entity"."isOffline" AS "entity_isOffline",
"entity"."duplicateId" AS "entity_duplicateId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
@ -110,7 +110,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
@ -118,6 +117,7 @@ SELECT
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
FROM
"assets" "AssetEntity"
@ -145,7 +145,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
@ -153,6 +152,7 @@ SELECT
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId",
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
@ -234,7 +234,6 @@ SELECT
"bd93d5747511a4dad4923546c51365bf1a803774"."isFavorite" AS "bd93d5747511a4dad4923546c51365bf1a803774_isFavorite",
"bd93d5747511a4dad4923546c51365bf1a803774"."isArchived" AS "bd93d5747511a4dad4923546c51365bf1a803774_isArchived",
"bd93d5747511a4dad4923546c51365bf1a803774"."isExternal" AS "bd93d5747511a4dad4923546c51365bf1a803774_isExternal",
"bd93d5747511a4dad4923546c51365bf1a803774"."isOffline" AS "bd93d5747511a4dad4923546c51365bf1a803774_isOffline",
"bd93d5747511a4dad4923546c51365bf1a803774"."checksum" AS "bd93d5747511a4dad4923546c51365bf1a803774_checksum",
"bd93d5747511a4dad4923546c51365bf1a803774"."duration" AS "bd93d5747511a4dad4923546c51365bf1a803774_duration",
"bd93d5747511a4dad4923546c51365bf1a803774"."isVisible" AS "bd93d5747511a4dad4923546c51365bf1a803774_isVisible",
@ -242,6 +241,7 @@ SELECT
"bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName",
"bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId",
"bd93d5747511a4dad4923546c51365bf1a803774"."isOffline" AS "bd93d5747511a4dad4923546c51365bf1a803774_isOffline",
"bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId",
"AssetEntity__AssetEntity_files"."id" AS "AssetEntity__AssetEntity_files_id",
"AssetEntity__AssetEntity_files"."assetId" AS "AssetEntity__AssetEntity_files_assetId",
@ -275,8 +275,7 @@ FROM
(
SELECT
"AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline"
"AssetEntity"."originalPath" AS "AssetEntity_originalPath"
FROM
"assets" "AssetEntity"
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
@ -322,7 +321,6 @@ FROM
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
@ -330,6 +328,7 @@ FROM
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
FROM
"assets" "AssetEntity"
@ -366,18 +365,6 @@ WHERE
AND "originalPath" = path
);
-- AssetRepository.updateOfflineLibraryAssets
UPDATE "assets"
SET
"isOffline" = $1,
"updatedAt" = CURRENT_TIMESTAMP
WHERE
(
"libraryId" = $2
AND NOT ("originalPath" IN ($3))
AND "isOffline" = $4
)
-- AssetRepository.getAllByDeviceId
SELECT
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",
@ -420,7 +407,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
@ -428,6 +414,7 @@ SELECT
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
FROM
"assets" "AssetEntity"
@ -474,7 +461,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
@ -482,6 +468,7 @@ SELECT
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
FROM
"assets" "AssetEntity"
@ -547,7 +534,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible",
@ -555,6 +541,7 @@ SELECT
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
FROM
"assets" "AssetEntity"
@ -602,7 +589,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
@ -610,6 +596,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
@ -662,7 +649,6 @@ SELECT
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
@ -670,6 +656,7 @@ SELECT
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM
"assets" "asset"
@ -742,7 +729,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
@ -750,6 +736,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
@ -802,7 +789,6 @@ SELECT
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
@ -810,6 +796,7 @@ SELECT
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM
"assets" "asset"
@ -858,7 +845,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
@ -866,6 +852,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
@ -918,7 +905,6 @@ SELECT
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
@ -926,6 +912,7 @@ SELECT
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM
"assets" "asset"
@ -1024,7 +1011,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
@ -1032,6 +1018,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
@ -1100,7 +1087,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
@ -1108,6 +1094,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",
@ -1173,15 +1160,3 @@ RETURNING
"id",
"createdAt",
"updatedAt"
-- AssetRepository.restoreAllDeleted
UPDATE "assets"
SET
"deletedAt" = $1,
"trashReason" = $2,
"updatedAt" = CURRENT_TIMESTAMP
WHERE
(
"ownerId" = $3
AND "trashReason" = $4
)

View File

@ -179,6 +179,7 @@ FROM
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
"AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
FROM
"asset_faces" "AssetFaceEntity"
@ -280,6 +281,7 @@ FROM
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId",
"AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id",
"AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId",
@ -411,6 +413,7 @@ SELECT
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
"AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
FROM
"asset_faces" "AssetFaceEntity"

View File

@ -33,6 +33,7 @@ FROM
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
@ -63,6 +64,7 @@ FROM
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM
"assets" "asset"
@ -126,6 +128,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"stack"."id" AS "stack_id",
"stack"."ownerId" AS "stack_ownerId",
@ -156,6 +159,7 @@ SELECT
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
FROM
"assets" "asset"
@ -365,6 +369,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"exif"."assetId" AS "exif_assetId",
"exif"."description" AS "exif_description",

View File

@ -47,6 +47,7 @@ FROM
"SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName",
"SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath",
"SharedLinkEntity__SharedLinkEntity_assets"."stackId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackId",
"SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline",
"SharedLinkEntity__SharedLinkEntity_assets"."duplicateId" AS "SharedLinkEntity__SharedLinkEntity_assets_duplicateId",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."assetId" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_assetId",
"9b1d35b344d838023994a3233afd6ffe098be6d8"."description" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_description",
@ -113,6 +114,7 @@ FROM
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalFileName" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalFileName",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."sidecarPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_sidecarPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."stackId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_stackId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isOffline" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isOffline",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."duplicateId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_duplicateId",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."assetId" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_assetId",
"d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."description" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_description",
@ -234,6 +236,7 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName",
"SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath",
"SharedLinkEntity__SharedLinkEntity_assets"."stackId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackId",
"SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline",
"SharedLinkEntity__SharedLinkEntity_assets"."duplicateId" AS "SharedLinkEntity__SharedLinkEntity_assets_duplicateId",
"SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id",
"SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId",

View File

@ -21,7 +21,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
@ -29,6 +28,7 @@ SELECT
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId",
"asset"."isOffline" AS "asset_isOffline",
"asset"."duplicateId" AS "asset_duplicateId",
"exifInfo"."assetId" AS "exifInfo_assetId",
"exifInfo"."description" AS "exifInfo_description",

View File

@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AssetTrashReason } from 'src/dtos/asset.dto';
import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity';
@ -199,10 +198,11 @@ export class AssetRepository implements IAssetRepository {
async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise<string[]> {
const result = await this.repository.query(
`
WITH paths AS (SELECT unnest($2::text[]) AS path)
SELECT path FROM paths
WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path);
`,
WITH paths AS (SELECT unnest($2::text[]) AS path)
SELECT path
FROM paths
WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path);
`,
[libraryId, originalPaths],
);
return result.map((row: { path: string }) => row.path);
@ -287,16 +287,6 @@ export class AssetRepository implements IAssetRepository {
.execute();
}
@Chunked()
async softDeleteAll(ids: string[]): Promise<void> {
await this.repository.softDelete({ id: In(ids) });
}
@Chunked()
async restoreAll(ids: string[]): Promise<void> {
await this.updateAll(ids, { trashReason: null, deletedAt: null });
}
async update(asset: AssetUpdateOptions): Promise<void> {
await this.repository.update(asset.id, asset);
}
@ -820,32 +810,4 @@ export class AssetRepository implements IAssetRepository {
async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise<void> {
await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] });
}
@GenerateSql({
params: [
{
ownerId: DummyValue.UUID,
},
],
})
async restoreAllDeleted(ownerId?: string): Promise<void> {
await this.repository.update(
{ ownerId, trashReason: AssetTrashReason.DELETED },
{ deletedAt: null, trashReason: null },
);
}
@GenerateSql({
params: [
{
ownerId: DummyValue.UUID,
},
],
})
async restoreAllDeletedById(ids: string[]): Promise<void> {
await this.repository.update(
{ id: In(ids), trashReason: AssetTrashReason.DELETED },
{ deletedAt: null, trashReason: null },
);
}
}

View File

@ -79,12 +79,12 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
[JobName.SIDECAR_WRITE]: QueueName.SIDECAR,
// Library management
[JobName.LIBRARY_REFRESH_ASSET]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SCAN]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_OFFLINE_CHECK]: QueueName.LIBRARY,
[JobName.LIBRARY_SYNC_FILE]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_FILES]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_ASSETS]: QueueName.LIBRARY,
[JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
[JobName.LIBRARY_OFFLINE_CHECK]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY,
[JobName.LIBRARY_SYNC_ASSET]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_ALL]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
// Notification

View File

@ -300,7 +300,6 @@ export class AssetService {
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto;
let { trashReason } = dto;
await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
await this.assetRepository.updateAll(ids, {

View File

@ -164,7 +164,7 @@ export class JobService {
}
case QueueName.LIBRARY: {
return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } });
return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, data: { force } });
}
default: {

View File

@ -2,7 +2,6 @@ import { BadRequestException } from '@nestjs/common';
import { Stats } from 'node:fs';
import { SystemConfig } from 'src/config';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetTrashReason } from 'src/dtos/asset.dto';
import { mapLibrary } from 'src/dtos/library.dto';
import { UserEntity } from 'src/entities/user.entity';
import { AssetType } from 'src/enum';
@ -172,11 +171,11 @@ describe(LibraryService.name, () => {
});
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueAssetRefresh({ id: libraryStub.externalLibrary1.id });
await sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id });
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_REFRESH_ASSET,
name: JobName.LIBRARY_SYNC_FILE,
data: {
id: libraryStub.externalLibrary1.id,
ownerId: libraryStub.externalLibrary1.owner.id,
@ -189,9 +188,7 @@ describe(LibraryService.name, () => {
it("should fail when library can't be found", async () => {
libraryMock.get.mockResolvedValue(null);
await expect(sut.handleQueueAssetRefresh({ id: libraryStub.externalLibrary1.id })).resolves.toBe(
JobStatus.SKIPPED,
);
await expect(sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED);
});
it('should ignore import paths that do not exist', async () => {
@ -212,7 +209,7 @@ describe(LibraryService.name, () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueAssetRefresh({ id: libraryStub.externalLibraryWithImportPaths1.id });
await sut.handleQueueSyncFiles({ id: libraryStub.externalLibraryWithImportPaths1.id });
expect(storageMock.walk).toHaveBeenCalledWith({
pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]],
@ -229,11 +226,11 @@ describe(LibraryService.name, () => {
storageMock.walk.mockImplementation(async function* generator() {});
assetMock.getAll.mockResolvedValue({ items: [assetStub.external], hasNextPage: false });
await sut.handleQueueAssetOfflineCheck({ id: libraryStub.externalLibrary1.id });
await sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id });
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_OFFLINE_CHECK,
name: JobName.LIBRARY_SYNC_ASSET,
data: {
id: assetStub.external.id,
importPaths: libraryStub.externalLibrary1.importPaths,
@ -246,9 +243,7 @@ describe(LibraryService.name, () => {
it("should fail when library can't be found", async () => {
libraryMock.get.mockResolvedValue(null);
await expect(sut.handleQueueAssetOfflineCheck({ id: libraryStub.externalLibrary1.id })).resolves.toBe(
JobStatus.SKIPPED,
);
await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED);
});
});
@ -262,7 +257,7 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(null);
await expect(sut.handleAssetOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED);
expect(assetMock.remove).not.toHaveBeenCalled();
});
@ -276,10 +271,10 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(assetStub.external);
await expect(sut.handleAssetOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
trashReason: AssetTrashReason.OFFLINE,
isOffline: true,
});
expect(assetMock.softDeleteAll).toHaveBeenCalledWith([assetStub.external.id]);
});
@ -293,9 +288,9 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(assetStub.external);
await expect(sut.handleAssetOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
trashReason: AssetTrashReason.OFFLINE,
isOffline: true,
});
expect(assetMock.softDeleteAll).toHaveBeenCalledWith([assetStub.external.id]);
});
@ -310,10 +305,10 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(assetStub.external);
storageMock.checkFileExists.mockResolvedValue(true);
await expect(sut.handleAssetOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], {
trashReason: AssetTrashReason.OFFLINE,
isOffline: true,
});
expect(assetMock.softDeleteAll).toHaveBeenCalledWith([assetStub.external.id]);
});
@ -328,7 +323,7 @@ describe(LibraryService.name, () => {
assetMock.getById.mockResolvedValue(assetStub.external);
storageMock.checkFileExists.mockResolvedValue(true);
await expect(sut.handleAssetOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.remove).not.toHaveBeenCalled();
});
@ -356,7 +351,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.handleSyncFile(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
});
it('should reject an unknown file type', async () => {
@ -366,7 +361,7 @@ describe(LibraryService.name, () => {
assetPath: '/data/user1/file.xyz',
};
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.handleSyncFile(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
});
it('should import a new asset', async () => {
@ -379,7 +374,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([
[
@ -425,7 +420,7 @@ describe(LibraryService.name, () => {
assetMock.create.mockResolvedValue(assetStub.image);
storageMock.checkFileExists.mockResolvedValue(true);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([
[
@ -470,7 +465,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.video);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([
[
@ -524,7 +519,7 @@ describe(LibraryService.name, () => {
assetMock.create.mockResolvedValue(assetStub.image);
libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() });
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
expect(assetMock.create.mock.calls).toEqual([]);
});
@ -544,7 +539,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
expect(jobMock.queue).not.toHaveBeenCalled();
expect(jobMock.queueAll).not.toHaveBeenCalled();
@ -560,7 +555,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.METADATA_EXTRACTION,
@ -587,7 +582,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.trashed);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
expect(jobMock.queue).not.toHaveBeenCalled();
expect(jobMock.queueAll).not.toHaveBeenCalled();
@ -602,7 +597,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.trashedOffline);
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.restoreAll).toHaveBeenCalledWith([assetStub.trashedOffline.id]);
});
@ -619,7 +614,7 @@ describe(LibraryService.name, () => {
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
await expect(sut.handleSyncFile(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException);
});
});
@ -926,7 +921,7 @@ describe(LibraryService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_REFRESH_ASSET,
name: JobName.LIBRARY_SYNC_FILE,
data: {
id: libraryStub.externalLibraryWithImportPaths1.id,
assetPath: '/foo/photo.jpg',
@ -947,7 +942,7 @@ describe(LibraryService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_REFRESH_ASSET,
name: JobName.LIBRARY_SYNC_FILE,
data: {
id: libraryStub.externalLibraryWithImportPaths1.id,
assetPath: '/foo/photo.jpg',
@ -1059,7 +1054,7 @@ describe(LibraryService.name, () => {
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.LIBRARY_QUEUE_SCAN,
name: JobName.LIBRARY_QUEUE_SYNC_FILES,
data: {
id: libraryStub.externalLibrary1.id,
},
@ -1067,24 +1062,7 @@ describe(LibraryService.name, () => {
],
[
{
name: JobName.LIBRARY_QUEUE_OFFLINE_CHECK,
data: {
id: libraryStub.externalLibrary1.id,
},
},
],
]);
});
});
describe('queueOfflineCheck', () => {
it('should queue the trash job', async () => {
await sut.queueOfflineCheck(libraryStub.externalLibrary1.id);
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.LIBRARY_OFFLINE_CHECK,
name: JobName.LIBRARY_QUEUE_SYNC_ASSETS,
data: {
id: libraryStub.externalLibrary1.id,
},
@ -1098,7 +1076,7 @@ describe(LibraryService.name, () => {
it('should queue the refresh job', async () => {
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
await expect(sut.handleQueueAllScan()).resolves.toBe(JobStatus.SUCCESS);
await expect(sut.handleQueueSyncAll()).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queue.mock.calls).toEqual([
[
@ -1110,7 +1088,7 @@ describe(LibraryService.name, () => {
]);
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_QUEUE_SCAN,
name: JobName.LIBRARY_QUEUE_SYNC_FILES,
data: {
id: libraryStub.externalLibrary1.id,
},
@ -1125,13 +1103,11 @@ describe(LibraryService.name, () => {
assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
assetMock.getById.mockResolvedValue(assetStub.image1);
await expect(sut.handleQueueAssetOfflineCheck({ id: libraryStub.externalLibrary1.id })).resolves.toBe(
JobStatus.SUCCESS,
);
await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.LIBRARY_OFFLINE_CHECK,
name: JobName.LIBRARY_SYNC_ASSET,
data: {
id: assetStub.image1.id,
importPaths: libraryStub.externalLibrary1.importPaths,

View File

@ -5,16 +5,15 @@ import picomatch from 'picomatch';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { OnEmit } from 'src/decorators';
import { AssetTrashReason } from 'src/dtos/asset.dto';
import {
CreateLibraryDto,
LibraryResponseDto,
LibraryStatsResponseDto,
mapLibrary,
UpdateLibraryDto,
ValidateLibraryDto,
ValidateLibraryImportPathResponseDto,
ValidateLibraryResponseDto,
mapLibrary,
} from 'src/dtos/library.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetType } from 'src/enum';
@ -27,8 +26,8 @@ import {
IJobRepository,
ILibraryFileJob,
ILibraryOfflineJob,
JOBS_LIBRARY_PAGINATION_SIZE,
JobName,
JOBS_LIBRARY_PAGINATION_SIZE,
JobStatus,
} from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface';
@ -76,7 +75,7 @@ export class LibraryService {
this.jobRepository.addCronJob(
'libraryScan',
scan.cronExpression,
() => handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger),
() => handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger),
scan.enabled,
);
@ -247,7 +246,7 @@ export class LibraryService {
private async scanAssets(libraryId: string, assetPaths: string[], ownerId: string) {
await this.jobRepository.queueAll(
assetPaths.map((assetPath) => ({
name: JobName.LIBRARY_REFRESH_ASSET,
name: JobName.LIBRARY_SYNC_FILE,
data: {
id: libraryId,
assetPath,
@ -359,134 +358,94 @@ export class LibraryService {
return JobStatus.SUCCESS;
}
private async getMtime(path: string): Promise<Date> {
try {
const stat = await this.storageRepository.stat(path);
return stat.mtime;
} catch (error: Error | any) {
throw new BadRequestException(`Cannot access file ${path}`, { cause: error });
}
}
private async refreshExistingAsset(asset: AssetEntity) {
if (asset.trashReason == AssetTrashReason.DELETED) {
this.logger.debug(`Asset is previously trashed by user, won't refresh: ${asset.originalPath}`);
return JobStatus.SKIPPED;
} else if (asset.trashReason == AssetTrashReason.OFFLINE) {
this.logger.debug(`Asset is previously trashed as offline, restoring from trash: ${asset.originalPath}`);
await this.assetRepository.restoreAll([asset.id]);
return JobStatus.SUCCESS;
}
const mtime = await this.getMtime(asset.originalPath);
if (mtime.toISOString() === asset.fileModifiedAt.toISOString()) {
this.logger.debug(`Asset already exists in database and on disk, will not import: ${asset.originalPath}`);
return JobStatus.SKIPPED;
}
this.logger.debug(
`File modification time has changed, re-importing asset: ${asset.originalPath}. Old mtime: ${asset.fileModifiedAt}. New mtime: ${mtime}`,
);
await this.assetRepository.updateAll([asset.id], {
fileCreatedAt: mtime,
fileModifiedAt: mtime,
originalFileName: parse(asset.originalPath).base,
deletedAt: null,
trashReason: null,
});
}
async handleAssetRefresh(job: ILibraryFileJob): Promise<JobStatus> {
async handleSyncFile(job: ILibraryFileJob): Promise<JobStatus> {
// Only needs to handle new assets
const assetPath = path.normalize(job.assetPath);
let asset = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath);
if (asset) {
const status = await this.refreshExistingAsset(asset);
if (status) {
return status;
}
} else {
// This asset is new to us, read it from disk
this.logger.log(`Importing new library asset: ${assetPath}`);
const library = await this.repository.get(job.id, true);
if (library?.deletedAt) {
this.logger.error('Cannot import asset into deleted library');
return JobStatus.FAILED;
}
// TODO: device asset id is deprecated, remove it
const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, '');
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
// TODO: doesn't xmp replace the file extension? Will need investigation
let sidecarPath: string | null = null;
if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) {
sidecarPath = `${assetPath}.xmp`;
}
let assetType: AssetType;
if (mimeTypes.isImage(assetPath)) {
assetType = AssetType.IMAGE;
} else if (mimeTypes.isVideo(assetPath)) {
assetType = AssetType.VIDEO;
} else {
throw new BadRequestException(`Unsupported file type ${assetPath}`);
}
const mtime = await this.getMtime(assetPath);
// TODO: In wait of refactoring the domain asset service, this function is just manually written like this
asset = await this.assetRepository.create({
ownerId: job.ownerId,
libraryId: job.id,
checksum: pathHash,
originalPath: assetPath,
deviceAssetId,
deviceId: 'Library Import',
fileCreatedAt: mtime,
fileModifiedAt: mtime,
localDateTime: mtime,
type: assetType,
originalFileName: parse(assetPath).base,
sidecarPath,
isExternal: true,
});
return JobStatus.SKIPPED;
}
this.logger.debug(`Queueing metadata extraction for: ${assetPath}`);
let stat;
try {
stat = await this.storageRepository.stat(assetPath);
} catch (error: any) {
if (error.code === 'ENOENT') {
this.logger.error(`File not found: ${assetPath}`);
return JobStatus.SKIPPED;
}
this.logger.error(`Error reading file: ${assetPath}. Error: ${error}`);
return JobStatus.FAILED;
}
this.logger.log(`Importing new library asset: ${assetPath}`);
const library = await this.repository.get(job.id, true);
if (library?.deletedAt) {
this.logger.error('Cannot import asset into deleted library');
return JobStatus.FAILED;
}
// TODO: device asset id is deprecated, remove it
const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, '');
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
// TODO: doesn't xmp replace the file extension? Will need investigation
let sidecarPath: string | null = null;
if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) {
sidecarPath = `${assetPath}.xmp`;
}
const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE;
const mtime = stat.mtime;
asset = await this.assetRepository.create({
ownerId: job.ownerId,
libraryId: job.id,
checksum: pathHash,
originalPath: assetPath,
deviceAssetId,
deviceId: 'Library Import',
fileCreatedAt: mtime,
fileModifiedAt: mtime,
localDateTime: mtime,
type: assetType,
originalFileName: parse(assetPath).base,
sidecarPath,
isExternal: true,
});
await this.queuePostSyncJobs(asset);
return JobStatus.SUCCESS;
}
async queuePostSyncJobs(asset: AssetEntity) {
this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`);
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
if (asset.type === AssetType.VIDEO) {
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: asset.id } });
}
return JobStatus.SUCCESS;
}
async queueScan(id: string) {
await this.findOrFail(id);
await this.jobRepository.queue({
name: JobName.LIBRARY_QUEUE_SCAN,
name: JobName.LIBRARY_QUEUE_SYNC_FILES,
data: {
id,
},
});
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_OFFLINE_CHECK, data: { id } });
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
}
async queueOfflineCheck(id: string) {
this.logger.verbose(`Queueing offline file removal from library ${id}`);
await this.jobRepository.queue({ name: JobName.LIBRARY_OFFLINE_CHECK, data: { id } });
}
async handleQueueAllScan(): Promise<JobStatus> {
async handleQueueSyncAll(): Promise<JobStatus> {
this.logger.debug(`Refreshing all external libraries`);
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} });
@ -494,7 +453,7 @@ export class LibraryService {
const libraries = await this.repository.getAll(true);
await this.jobRepository.queueAll(
libraries.map((library) => ({
name: JobName.LIBRARY_QUEUE_SCAN,
name: JobName.LIBRARY_QUEUE_SYNC_FILES,
data: {
id: library.id,
},
@ -502,7 +461,7 @@ export class LibraryService {
);
await this.jobRepository.queueAll(
libraries.map((library) => ({
name: JobName.LIBRARY_QUEUE_OFFLINE_CHECK,
name: JobName.LIBRARY_QUEUE_SYNC_ASSETS,
data: {
id: library.id,
},
@ -511,20 +470,17 @@ export class LibraryService {
return JobStatus.SUCCESS;
}
async handleAssetOfflineCheck(job: ILibraryOfflineJob): Promise<JobStatus> {
async handleSyncAsset(job: ILibraryOfflineJob): Promise<JobStatus> {
const asset = await this.assetRepository.getById(job.id);
if (!asset || asset.trashReason) {
// Skip if asset is missing or already trashed
// We don't want to trash an asset that has already been trashed because it can otherwise re-appear on the timeline if an asset is re-imported
if (!asset) {
return JobStatus.SKIPPED;
}
const markOffline = async (explanation: string) => {
this.logger.debug(`${explanation}, removing: ${asset.originalPath}`);
await this.assetRepository.updateAll([asset.id], { trashReason: AssetTrashReason.OFFLINE });
await this.assetRepository.softDeleteAll([asset.id]);
if (!asset.isOffline) {
this.logger.debug(`${explanation}, removing: ${asset.originalPath}`);
await this.assetRepository.updateAll([asset.id], { isOffline: true, deletedAt: new Date() });
}
};
const isInPath = job.importPaths.find((path) => asset.originalPath.startsWith(path));
@ -539,20 +495,37 @@ export class LibraryService {
return JobStatus.SUCCESS;
}
const fileExists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK);
if (!fileExists) {
await markOffline('Asset is no longer on disk');
let stat;
try {
stat = await this.storageRepository.stat(asset.originalPath);
} catch {
await markOffline('Asset is no longer on disk or is inaccessible because of permissions');
return JobStatus.SUCCESS;
}
this.logger.verbose(
`Asset is found on disk, not covered by an exclusion pattern, and is in an import path, doing nothing: ${asset.originalPath}`,
);
const mtime = stat.mtime;
const isAssetModified = mtime.toISOString() !== asset.fileModifiedAt.toISOString();
if (asset.isOffline || isAssetModified) {
this.logger.debug(`Asset was offline or modified, updating asset record ${asset.originalPath}`);
//TODO: When we have asset status, we need to leave deletedAt as is when status is trashed
await this.assetRepository.updateAll([asset.id], {
isOffline: false,
deletedAt: null,
fileCreatedAt: mtime,
fileModifiedAt: mtime,
originalFileName: parse(asset.originalPath).base,
});
}
if (isAssetModified) {
this.logger.debug(`Asset was modified, queuing metadata extraction for: ${asset.originalPath}`);
await this.queuePostSyncJobs(asset);
}
return JobStatus.SUCCESS;
}
async handleQueueAssetRefresh(job: IEntityJob): Promise<JobStatus> {
async handleQueueSyncFiles(job: IEntityJob): Promise<JobStatus> {
const library = await this.repository.get(job.id);
if (!library) {
this.logger.debug(`Library ${job.id} not found, skipping refresh`);
@ -603,7 +576,7 @@ export class LibraryService {
return JobStatus.SUCCESS;
}
async handleQueueAssetOfflineCheck(job: IEntityJob): Promise<JobStatus> {
async handleQueueSyncAssets(job: IEntityJob): Promise<JobStatus> {
const library = await this.repository.get(job.id);
if (!library) {
return JobStatus.SKIPPED;
@ -621,7 +594,7 @@ export class LibraryService {
this.logger.debug(`Discovered ${assetCount} asset(s) in library ${library.id}...`);
await this.jobRepository.queueAll(
assets.map((asset) => ({
name: JobName.LIBRARY_OFFLINE_CHECK,
name: JobName.LIBRARY_SYNC_ASSET,
data: { id: asset.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns },
})),
);

View File

@ -86,12 +86,12 @@ export class MicroservicesService {
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
[JobName.SIDECAR_SYNC]: (data) => this.metadataService.handleSidecarSync(data),
[JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data),
[JobName.LIBRARY_REFRESH_ASSET]: (data) => this.libraryService.handleAssetRefresh(data),
[JobName.LIBRARY_QUEUE_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data),
[JobName.LIBRARY_QUEUE_OFFLINE_CHECK]: (data) => this.libraryService.handleQueueAssetOfflineCheck(data),
[JobName.LIBRARY_QUEUE_SYNC_ALL]: () => this.libraryService.handleQueueSyncAll(),
[JobName.LIBRARY_QUEUE_SYNC_FILES]: (data) => this.libraryService.handleQueueSyncFiles(data), //Queues all files paths on disk
[JobName.LIBRARY_SYNC_FILE]: (data) => this.libraryService.handleSyncFile(data), //Handles a single path on disk //Watcher calls for new files
[JobName.LIBRARY_QUEUE_SYNC_ASSETS]: (data) => this.libraryService.handleQueueSyncAssets(data), //Queues all library assets
[JobName.LIBRARY_SYNC_ASSET]: (data) => this.libraryService.handleSyncAsset(data), //Handles all library assets // Watcher calls for unlink and changed
[JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data),
[JobName.LIBRARY_OFFLINE_CHECK]: (data) => this.libraryService.handleAssetOfflineCheck(data),
[JobName.LIBRARY_QUEUE_SCAN_ALL]: () => this.libraryService.handleQueueAllScan(),
[JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(),
[JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data),
[JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data),

View File

@ -6,7 +6,7 @@ import { TrashResponseDto } from 'src/dtos/trash.dto';
import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface';
import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { requireAccess } from 'src/utils/access';

View File

@ -1,4 +1,3 @@
import { AssetTrashReason } from 'src/dtos/asset.dto';
import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
@ -73,7 +72,7 @@ export const assetStub = {
deletedAt: null,
isExternal: false,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
noWebpPath: Object.freeze<AssetEntity>({
@ -111,7 +110,7 @@ export const assetStub = {
} as ExifEntity,
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
noThumbhash: Object.freeze<AssetEntity>({
@ -146,7 +145,7 @@ export const assetStub = {
sidecarPath: null,
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
primaryImage: Object.freeze<AssetEntity>({
@ -191,7 +190,7 @@ export const assetStub = {
{ id: 'stack-child-asset-2' } as AssetEntity,
]),
duplicateId: null,
trashReason: null,
isOffline: false,
}),
image: Object.freeze<AssetEntity>({
@ -231,7 +230,7 @@ export const assetStub = {
exifImageWidth: 2160,
} as ExifEntity,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
trashed: Object.freeze<AssetEntity>({
@ -251,7 +250,6 @@ export const assetStub = {
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
trashReason: AssetTrashReason.DELETED,
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: false,
isArchived: false,
@ -271,6 +269,8 @@ export const assetStub = {
exifImageWidth: 2160,
} as ExifEntity,
duplicateId: null,
isOffline: false,
status: AssetStatus.TRASHED,
}),
trashedOffline: Object.freeze<AssetEntity>({
@ -291,7 +291,6 @@ export const assetStub = {
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
trashReason: AssetTrashReason.OFFLINE,
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: false,
isArchived: false,
@ -311,6 +310,7 @@ export const assetStub = {
exifImageWidth: 2160,
} as ExifEntity,
duplicateId: null,
isOffline: true,
}),
archived: Object.freeze<AssetEntity>({
id: 'asset-id',
@ -349,7 +349,7 @@ export const assetStub = {
exifImageWidth: 2160,
} as ExifEntity,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
external: Object.freeze<AssetEntity>({
@ -389,7 +389,7 @@ export const assetStub = {
fileSizeInByte: 5000,
} as ExifEntity,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
image1: Object.freeze<AssetEntity>({
@ -427,7 +427,7 @@ export const assetStub = {
fileSizeInByte: 5000,
} as ExifEntity,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
imageFrom2015: Object.freeze<AssetEntity>({
@ -465,7 +465,7 @@ export const assetStub = {
} as ExifEntity,
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
video: Object.freeze<AssetEntity>({
@ -505,7 +505,7 @@ export const assetStub = {
} as ExifEntity,
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
livePhotoMotionAsset: Object.freeze({
@ -643,7 +643,7 @@ export const assetStub = {
} as ExifEntity,
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
sidecar: Object.freeze<AssetEntity>({
id: 'asset-id',
@ -677,7 +677,7 @@ export const assetStub = {
sidecarPath: '/original/path.ext.xmp',
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
sidecarWithoutExt: Object.freeze<AssetEntity>({
id: 'asset-id',
@ -711,7 +711,7 @@ export const assetStub = {
sidecarPath: '/original/path.xmp',
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
hasEncodedVideo: Object.freeze<AssetEntity>({
@ -749,7 +749,7 @@ export const assetStub = {
} as ExifEntity,
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
missingFileExtension: Object.freeze<AssetEntity>({
id: 'asset-id',
@ -788,7 +788,7 @@ export const assetStub = {
fileSizeInByte: 5000,
} as ExifEntity,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
hasFileExtension: Object.freeze<AssetEntity>({
id: 'asset-id',
@ -827,7 +827,7 @@ export const assetStub = {
fileSizeInByte: 5000,
} as ExifEntity,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
imageDng: Object.freeze<AssetEntity>({
id: 'asset-id',
@ -866,7 +866,7 @@ export const assetStub = {
bitsPerSample: 14,
} as ExifEntity,
duplicateId: null,
trashReason: null,
isOffline: false,
}),
hasEmbedding: Object.freeze<AssetEntity>({
id: 'asset-id-embedding',
@ -907,7 +907,7 @@ export const assetStub = {
assetId: 'asset-id',
embedding: Array.from({ length: 512 }, Math.random),
},
trashReason: null,
isOffline: false,
}),
hasDupe: Object.freeze<AssetEntity>({
id: 'asset-id-dupe',
@ -948,6 +948,6 @@ export const assetStub = {
assetId: 'asset-id',
embedding: Array.from({ length: 512 }, Math.random),
},
trashReason: null,
isOffline: false,
}),
};

View File

@ -74,6 +74,7 @@ const assetResponse: AssetResponseDto = {
isTrashed: false,
libraryId: 'library-id',
hasMetadata: true,
isOffline: false,
};
const assetResponseWithoutMetadata = {
@ -255,7 +256,7 @@ export const sharedLinkStub = {
sidecarPath: null,
deletedAt: null,
duplicateId: null,
trashReason: null,
isOffline: false,
},
],
},

View File

@ -42,7 +42,5 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
upsertFile: vitest.fn(),
getAssetsByOriginalPath: vitest.fn(),
getUniqueOriginalPaths: vitest.fn(),
restoreAllDeleted: vitest.fn(),
restoreAllDeletedById: vitest.fn(),
};
};

View File

@ -24,7 +24,6 @@
import {
AssetJobName,
AssetTypeEnum,
TrashReason,
type AlbumResponseDto,
type AssetResponseDto,
type StackResponseDto,
@ -60,7 +59,7 @@
export let onClose: () => void;
const sharedLink = getSharedLink();
$: isOffline = asset.trashReason === TrashReason.Offline;
$: isOffline = asset.isOffline;
$: isOwner = $user && asset.ownerId === $user?.id;
$: showDownloadButton = sharedLink ? sharedLink.allowDownload : !isOffline;
// $: showEditorButton =
@ -137,7 +136,7 @@
{#if showDownloadButton}
<DownloadAction {asset} menuItem />
{/if}
{#if asset.trashReason === TrashReason.Deleted}
{#if asset.status === AssetStatus.TRASHED}
<RestoreAction {asset} {onAction} />
{:else}
<AddToAlbumAction {asset} {onAction} />

View File

@ -12,7 +12,6 @@
import {
AssetMediaSize,
getAssetInfo,
TrashReason,
updateAsset,
type AlbumResponseDto,
type AssetResponseDto,
@ -73,7 +72,7 @@
}
}
$: isOffline = asset.trashReason === TrashReason.Offline;
$: isOffline = asset.isOffline;
$: isOwner = $user?.id === asset.ownerId;
const handleNewAsset = async (newAsset: AssetResponseDto) => {