mirror of
https://github.com/immich-app/immich.git
synced 2024-09-21 10:37:20 +00:00
refactor: add back isOffline and remove trashReason from asset, change sync job flow
This commit is contained in:
parent
e071cc9ca8
commit
ba0d5410cd
@ -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 });
|
||||
|
||||
|
@ -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 }));
|
||||
});
|
||||
});
|
||||
|
||||
|
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -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)
|
||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -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';
|
||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -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':
|
||||
|
91
mobile/openapi/lib/model/asset_bulk_delete_dto.dart
generated
91
mobile/openapi/lib/model/asset_bulk_delete_dto.dart
generated
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
21
mobile/openapi/lib/model/asset_response_dto.dart
generated
21
mobile/openapi/lib/model/asset_response_dto.dart
generated
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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 })
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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`);
|
||||
}
|
||||
|
||||
}
|
@ -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"`);
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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, {
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
|
@ -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 },
|
||||
})),
|
||||
);
|
||||
|
@ -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),
|
||||
|
@ -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';
|
||||
|
44
server/test/fixtures/asset.stub.ts
vendored
44
server/test/fixtures/asset.stub.ts
vendored
@ -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,
|
||||
}),
|
||||
};
|
||||
|
3
server/test/fixtures/shared-link.stub.ts
vendored
3
server/test/fixtures/shared-link.stub.ts
vendored
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -42,7 +42,5 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => {
|
||||
upsertFile: vitest.fn(),
|
||||
getAssetsByOriginalPath: vitest.fn(),
|
||||
getUniqueOriginalPaths: vitest.fn(),
|
||||
restoreAllDeleted: vitest.fn(),
|
||||
restoreAllDeletedById: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
@ -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} />
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user