1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-28 09:33:27 +02:00

feat(server): read-write external assets (#9235)

* refactor: remove isReadOnly and isExternal usages

* chore: open api

* fix: linting

* remove mobile isReadOnly dependency

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2024-05-03 15:34:57 -04:00 committed by GitHub
parent d26ac431b8
commit 5b87abb021
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 99 additions and 386 deletions

View File

@ -102,7 +102,6 @@ describe('/asset', () => {
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken, {
isFavorite: true,
isReadOnly: true,
fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(),
assetData: { filename: 'example.mp4' },

View File

@ -24,12 +24,12 @@ describe('/search', () => {
// let assetRidge: AssetFileUploadResponseDto;
// let assetPolemonium: AssetFileUploadResponseDto;
// let assetWood: AssetFileUploadResponseDto;
// let assetGlarus: AssetFileUploadResponseDto;
let assetHeic: AssetFileUploadResponseDto;
let assetRocks: AssetFileUploadResponseDto;
let assetOneJpg6: AssetFileUploadResponseDto;
let assetOneHeic6: AssetFileUploadResponseDto;
let assetOneJpg5: AssetFileUploadResponseDto;
let assetGlarus: AssetFileUploadResponseDto;
let assetSprings: AssetFileUploadResponseDto;
let assetLast: AssetFileUploadResponseDto;
let cities: string[];
@ -52,11 +52,12 @@ describe('/search', () => {
{ filename: '/formats/motionphoto/Samsung One UI 6.jpg' },
{ filename: '/formats/motionphoto/Samsung One UI 6.heic' },
{ filename: '/formats/motionphoto/Samsung One UI 5.jpg' },
{ filename: '/formats/raw/Nikon/D80/glarus.nef', dto: { isReadOnly: true } },
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
// used for search suggestions
{ filename: '/formats/png/density_plot.png' },
{ filename: '/formats/raw/Nikon/D80/glarus.nef' },
{ filename: '/formats/raw/Nikon/D700/philadelphia.nef' },
{ filename: '/albums/nature/orychophragmus_violaceus.jpg' },
{ filename: '/albums/nature/tanners_ridge.jpg' },
@ -93,9 +94,9 @@ describe('/search', () => {
{ latitude: 23.133_02, longitude: -82.383_04 }, // havana
{ latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi
{ latitude: 31.222_22, longitude: 121.458_06 }, // shanghai
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
{ latitude: 40.714_27, longitude: -74.005_97 }, // new york
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
{ latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
@ -123,9 +124,9 @@ describe('/search', () => {
assetOneJpg6,
assetOneHeic6,
assetOneJpg5,
assetGlarus,
assetSprings,
assetDensity,
// assetGlarus,
// assetPhiladelphia,
// assetOrychophragmus,
// assetRidge,
@ -190,16 +191,7 @@ describe('/search', () => {
dto: { size: -1.5 },
expected: ['size must not be less than 1', 'size must be an integer number'],
},
...[
'isArchived',
'isFavorite',
'isReadOnly',
'isExternal',
'isEncoded',
'isMotion',
'isOffline',
'isVisible',
].map((value) => ({
...['isArchived', 'isFavorite', 'isEncoded', 'isMotion', 'isOffline', 'isVisible'].map((value) => ({
should: `should reject ${value} not a boolean`,
dto: { [value]: 'immich' },
expected: [`${value} must be a boolean value`],
@ -255,14 +247,6 @@ describe('/search', () => {
should: 'should search by isArchived (false)',
deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }),
},
{
should: 'should search by isReadOnly (true)',
deferred: () => ({ dto: { isReadOnly: true }, assets: [assetGlarus] }),
},
{
should: 'should search by isReadOnly (false)',
deferred: () => ({ dto: { size: 1, isReadOnly: false }, assets: [assetLast] }),
},
{
should: 'should search by type (image)',
deferred: () => ({ dto: { size: 1, type: 'IMAGE' }, assets: [assetLast] }),

View File

@ -34,7 +34,6 @@ describe('/timeline', () => {
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken, {
isFavorite: true,
isReadOnly: true,
fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(),
assetData: { filename: 'example.mp4' },

View File

@ -32,7 +32,6 @@ class Asset {
isFavorite = remote.isFavorite,
isArchived = remote.isArchived,
isTrashed = remote.isTrashed,
isReadOnly = remote.isReadOnly,
isOffline = remote.isOffline,
// workaround to nullify stackParentId for the parent asset until we refactor the mobile app
// stack handling to properly handle it
@ -55,7 +54,6 @@ class Asset {
isFavorite = local.isFavorite,
isArchived = false,
isTrashed = false,
isReadOnly = false,
isOffline = false,
stackCount = 0,
fileCreatedAt = local.createDateTime {
@ -90,7 +88,6 @@ class Asset {
this.isTrashed = false,
this.stackParentId,
this.stackCount = 0,
this.isReadOnly = false,
this.isOffline = false,
this.thumbhash,
});
@ -161,8 +158,6 @@ class Asset {
bool isTrashed;
bool isReadOnly;
bool isOffline;
@ignore
@ -278,7 +273,6 @@ class Asset {
isFavorite != a.isFavorite ||
isArchived != a.isArchived ||
isTrashed != a.isTrashed ||
isReadOnly != a.isReadOnly ||
isOffline != a.isOffline ||
a.exifInfo?.latitude != exifInfo?.latitude ||
a.exifInfo?.longitude != exifInfo?.longitude ||
@ -324,7 +318,6 @@ class Asset {
isFavorite: isFavorite,
isArchived: isArchived,
isTrashed: isTrashed,
isReadOnly: isReadOnly,
isOffline: isOffline,
);
}
@ -345,7 +338,6 @@ class Asset {
isFavorite: a.isFavorite,
isArchived: a.isArchived,
isTrashed: a.isTrashed,
isReadOnly: a.isReadOnly,
isOffline: a.isOffline,
exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo,
thumbhash: a.thumbhash,
@ -380,7 +372,6 @@ class Asset {
bool? isFavorite,
bool? isArchived,
bool? isTrashed,
bool? isReadOnly,
bool? isOffline,
ExifInfo? exifInfo,
String? stackParentId,
@ -405,7 +396,6 @@ class Asset {
isFavorite: isFavorite ?? this.isFavorite,
isArchived: isArchived ?? this.isArchived,
isTrashed: isTrashed ?? this.isTrashed,
isReadOnly: isReadOnly ?? this.isReadOnly,
isOffline: isOffline ?? this.isOffline,
exifInfo: exifInfo ?? this.exifInfo,
stackParentId: stackParentId ?? this.stackParentId,
@ -470,7 +460,6 @@ class Asset {
"height": ${height ?? "N/A"},
"isArchived": $isArchived,
"isTrashed": $isTrashed,
"isReadOnly": $isReadOnly,
"isOffline": $isOffline,
}""";
}

Binary file not shown.

View File

@ -71,19 +71,6 @@ extension AssetListExtension on Iterable<Asset> {
return this;
}
/// Returns the assets that are present on a file system which has write permission
/// This filters out assets on readOnly external library to which we cannot perform any write operation
Iterable<Asset> writableOnly({
void Function()? errorCallback,
}) {
final bool onlyWritable = every((e) => !e.isReadOnly);
if (!onlyWritable) {
if (errorCallback != null) errorCallback();
return where((a) => !a.isReadOnly);
}
return this;
}
/// Filters out offline assets and returns those that are still accessible by the Immich server
Iterable<Asset> nonOfflineOnly({
void Function()? errorCallback,

View File

@ -102,16 +102,6 @@ class BottomGalleryBar extends ConsumerWidget {
}
void handleDelete() async {
// Cannot delete readOnly / external assets. They are handled through library offline jobs
if (asset.isReadOnly) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_delete_err_read_only'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
Future<bool> onDelete(bool force) async {
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
{asset},

View File

@ -42,7 +42,7 @@ class ExifBottomSheet extends HookConsumerWidget {
fontSize: 14,
),
),
if (asset.isRemote && !asset.isReadOnly)
if (asset.isRemote)
IconButton(
onPressed: () => handleEditDateTime(
ref,

View File

@ -24,7 +24,7 @@ class ExifLocation extends StatelessWidget {
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
// Guard no lat/lng
if (!hasCoordinates) {
return asset.isRemote && !asset.isReadOnly
return asset.isRemote
? ListTile(
minLeadingWidth: 0,
contentPadding: const EdgeInsets.all(0),
@ -57,7 +57,7 @@ class ExifLocation extends StatelessWidget {
fontWeight: FontWeight.w600,
),
).tr(),
if (asset.isRemote && !asset.isReadOnly)
if (asset.isRemote)
IconButton(
onPressed: editLocation,
icon: const Icon(Icons.edit_outlined),

View File

@ -63,6 +63,12 @@ abstract class _$AppRouter extends RootStackRouter {
child: const AllPeoplePage(),
);
},
AllPlacesRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const AllPlacesPage(),
);
},
AllVideosRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
@ -138,12 +144,6 @@ abstract class _$AppRouter extends RootStackRouter {
),
);
},
AllPlacesRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
child: const AllPlacesPage(),
);
},
FailedBackupStatusRoute.name: (routeData) {
return AutoRoutePage<dynamic>(
routeData: routeData,
@ -525,6 +525,20 @@ class AllPeopleRoute extends PageRouteInfo<void> {
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [AllPlacesPage]
class AllPlacesRoute extends PageRouteInfo<void> {
const AllPlacesRoute({List<PageRouteInfo>? children})
: super(
AllPlacesRoute.name,
initialChildren: children,
);
static const String name = 'AllPlacesRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [AllVideosPage]
class AllVideosRoute extends PageRouteInfo<void> {
@ -752,20 +766,6 @@ class CreateAlbumRouteArgs {
}
}
/// generated route for
/// [AllPlacesPage]
class AllPlacesRoute extends PageRouteInfo<void> {
const AllPlacesRoute({List<PageRouteInfo>? children})
: super(
AllPlacesRoute.name,
initialChildren: children,
);
static const String name = 'CuratedLocationRoute';
static const PageInfo<void> page = PageInfo<void>(name);
}
/// generated route for
/// [FailedBackupStatusPage]
class FailedBackupStatusRoute extends PageRouteInfo<void> {

View File

@ -184,11 +184,6 @@ class MultiselectGrid extends HookConsumerWidget {
currentUser,
errorCallback: errorBuilder('home_page_delete_err_partner'.tr()),
)
// Cannot delete readOnly / external assets. They are handled through library offline jobs
.writableOnly(
errorCallback:
errorBuilder('asset_action_delete_err_read_only'.tr()),
)
.toList();
final isDeleted = await ref
.read(assetProvider.notifier)
@ -238,13 +233,7 @@ class MultiselectGrid extends HookConsumerWidget {
final toDelete = ownedRemoteSelection(
localErrorMessage: 'home_page_delete_remote_err_local'.tr(),
ownerErrorMessage: 'home_page_delete_err_partner'.tr(),
)
// Cannot delete readOnly / external assets. They are handled through library offline jobs
.writableOnly(
errorCallback:
errorBuilder('asset_action_delete_err_read_only'.tr()),
)
.toList();
).toList();
final isDeleted = await ref
.read(assetProvider.notifier)
@ -372,12 +361,8 @@ class MultiselectGrid extends HookConsumerWidget {
final remoteAssets = ownedRemoteSelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(),
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
).writableOnly(
// Assume readOnly assets to be present in a read-only mount. So do not write sidecar
errorCallback: errorBuilder(
'multiselect_grid_edit_date_time_err_read_only'.tr(),
),
);
if (remoteAssets.isNotEmpty) {
handleEditDateTime(ref, context, remoteAssets.toList());
}
@ -391,12 +376,8 @@ class MultiselectGrid extends HookConsumerWidget {
final remoteAssets = ownedRemoteSelection(
localErrorMessage: 'home_page_favorite_err_local'.tr(),
ownerErrorMessage: 'home_page_favorite_err_partner'.tr(),
).writableOnly(
// Assume readOnly assets to be present in a read-only mount. So do not write sidecar
errorCallback: errorBuilder(
'multiselect_grid_edit_gps_err_read_only'.tr(),
),
);
if (remoteAssets.isNotEmpty) {
handleEditLocation(ref, context, remoteAssets.toList());
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7297,6 +7297,8 @@
"type": "boolean"
},
"isExternal": {
"deprecated": true,
"description": "This property was deprecated in v1.104.0",
"type": "boolean"
},
"isFavorite": {
@ -7306,6 +7308,8 @@
"type": "boolean"
},
"isReadOnly": {
"deprecated": true,
"description": "This property was deprecated in v1.104.0",
"type": "boolean"
},
"isTrashed": {
@ -7388,10 +7392,8 @@
"hasMetadata",
"id",
"isArchived",
"isExternal",
"isFavorite",
"isOffline",
"isReadOnly",
"isTrashed",
"libraryId",
"localDateTime",
@ -7652,9 +7654,6 @@
"isOffline": {
"type": "boolean"
},
"isReadOnly": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
@ -8599,9 +8598,6 @@
"isEncoded": {
"type": "boolean"
},
"isExternal": {
"type": "boolean"
},
"isFavorite": {
"type": "boolean"
},
@ -8614,9 +8610,6 @@
"isOffline": {
"type": "boolean"
},
"isReadOnly": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
@ -9821,9 +9814,6 @@
"isEncoded": {
"type": "boolean"
},
"isExternal": {
"type": "boolean"
},
"isFavorite": {
"type": "boolean"
},
@ -9836,9 +9826,6 @@
"isOffline": {
"type": "boolean"
},
"isReadOnly": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},

View File

@ -122,10 +122,12 @@ export type AssetResponseDto = {
hasMetadata: boolean;
id: string;
isArchived: boolean;
isExternal: boolean;
/** This property was deprecated in v1.104.0 */
isExternal?: boolean;
isFavorite: boolean;
isOffline: boolean;
isReadOnly: boolean;
/** This property was deprecated in v1.104.0 */
isReadOnly?: boolean;
isTrashed: boolean;
libraryId: string;
livePhotoVideoId?: string | null;
@ -296,7 +298,6 @@ export type CreateAssetDto = {
isArchived?: boolean;
isFavorite?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
libraryId?: string;
livePhotoData?: Blob;
@ -622,12 +623,10 @@ export type MetadataSearchDto = {
id?: string;
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isNotInAlbum?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
lensModel?: string;
libraryId?: string;
@ -699,12 +698,10 @@ export type SmartSearchDto = {
deviceId?: string;
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isNotInAlbum?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
lensModel?: string;
libraryId?: string;

View File

@ -113,6 +113,7 @@ export const DummyValue = {
PAGINATION: { take: 10, skip: 0 },
EMAIL: 'user@immich.app',
STRING: 'abcdefghi',
NUMBER: 50,
BUFFER: Buffer.from('abcdefghi'),
DATE: new Date(),
TIME_BUCKET: '2024-01-01T00:00:00.000Z',

View File

@ -36,8 +36,10 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
isArchived!: boolean;
isTrashed!: boolean;
isOffline!: boolean;
isExternal!: boolean;
isReadOnly!: boolean;
@PropertyLifecycle({ deprecatedAt: 'v1.104.0' })
isExternal?: boolean;
@PropertyLifecycle({ deprecatedAt: 'v1.104.0' })
isReadOnly?: boolean;
exifInfo?: ExifResponseDto;
smartInfo?: SmartInfoResponseDto;
tags?: TagResponseDto[];
@ -124,9 +126,9 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
: undefined,
stackCount: entity.stack?.assets?.length ?? null,
isExternal: entity.isExternal,
isOffline: entity.isOffline,
isReadOnly: entity.isReadOnly,
isExternal: false,
isReadOnly: false,
hasMetadata: true,
};
}

View File

@ -97,9 +97,6 @@ export class CreateAssetDto {
@ValidateBoolean({ optional: true })
isOffline?: boolean;
@ValidateBoolean({ optional: true })
isReadOnly?: boolean;
// The properties below are added to correctly generate the API docs
// and client SDKs. Validation should be handled in the controller.
@ApiProperty({ type: 'string', format: 'binary' })

View File

@ -33,9 +33,6 @@ class BaseSearchDto {
@ValidateBoolean({ optional: true })
isEncoded?: boolean;
@ValidateBoolean({ optional: true })
isExternal?: boolean;
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ -45,9 +42,6 @@ class BaseSearchDto {
@ValidateBoolean({ optional: true })
isOffline?: boolean;
@ValidateBoolean({ optional: true })
isReadOnly?: boolean;
@ValidateBoolean({ optional: true })
isVisible?: boolean;

View File

@ -106,9 +106,6 @@ export class AssetEntity {
@Column({ type: 'boolean', default: false })
isExternal!: boolean;
@Column({ type: 'boolean', default: false })
isReadOnly!: boolean;
@Column({ type: 'boolean', default: false })
isOffline!: boolean;

View File

@ -108,10 +108,6 @@ export interface IEntityJob extends IBaseJob {
source?: 'upload' | 'sidecar-write';
}
export interface IAssetDeletionJob extends IEntityJob {
fromExternal?: boolean;
}
export interface ILibraryFileJob extends IEntityJob {
ownerId: string;
assetPath: string;
@ -225,7 +221,7 @@ export type JobItem =
// Asset Deletion
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
| { name: JobName.ASSET_DELETION; data: IAssetDeletionJob }
| { name: JobName.ASSET_DELETION; data: IEntityJob }
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
// Library Management

View File

@ -56,11 +56,9 @@ export type SearchIdOptions = SearchAssetIDOptions & SearchUserIdOptions;
export interface SearchStatusOptions {
isArchived?: boolean;
isEncoded?: boolean;
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
isNotInAlbum?: boolean;
type?: AssetType;

View File

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

View File

@ -22,7 +22,6 @@ SELECT
"entity"."isFavorite" AS "entity_isFavorite",
"entity"."isArchived" AS "entity_isArchived",
"entity"."isExternal" AS "entity_isExternal",
"entity"."isReadOnly" AS "entity_isReadOnly",
"entity"."isOffline" AS "entity_isOffline",
"entity"."checksum" AS "entity_checksum",
"entity"."duration" AS "entity_duration",
@ -105,7 +104,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
@ -141,7 +139,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
@ -226,7 +223,6 @@ SELECT
"bd93d5747511a4dad4923546c51365bf1a803774"."isFavorite" AS "bd93d5747511a4dad4923546c51365bf1a803774_isFavorite",
"bd93d5747511a4dad4923546c51365bf1a803774"."isArchived" AS "bd93d5747511a4dad4923546c51365bf1a803774_isArchived",
"bd93d5747511a4dad4923546c51365bf1a803774"."isExternal" AS "bd93d5747511a4dad4923546c51365bf1a803774_isExternal",
"bd93d5747511a4dad4923546c51365bf1a803774"."isReadOnly" AS "bd93d5747511a4dad4923546c51365bf1a803774_isReadOnly",
"bd93d5747511a4dad4923546c51365bf1a803774"."isOffline" AS "bd93d5747511a4dad4923546c51365bf1a803774_isOffline",
"bd93d5747511a4dad4923546c51365bf1a803774"."checksum" AS "bd93d5747511a4dad4923546c51365bf1a803774_checksum",
"bd93d5747511a4dad4923546c51365bf1a803774"."duration" AS "bd93d5747511a4dad4923546c51365bf1a803774_duration",
@ -308,7 +304,6 @@ FROM
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
@ -405,7 +400,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
@ -451,7 +445,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
@ -519,7 +512,6 @@ SELECT
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
@ -608,7 +600,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
@ -667,7 +658,6 @@ SELECT
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
@ -784,7 +774,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
@ -843,7 +832,6 @@ SELECT
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
@ -891,7 +879,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
@ -950,7 +937,6 @@ SELECT
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",

View File

@ -165,7 +165,6 @@ FROM
"AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
"AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
"AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
"AssetFaceEntity__AssetFaceEntity_asset"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly",
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
"AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
"AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
@ -263,7 +262,6 @@ FROM
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration",
@ -393,7 +391,6 @@ SELECT
"AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
"AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
"AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
"AssetFaceEntity__AssetFaceEntity_asset"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly",
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
"AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
"AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",

View File

@ -27,7 +27,6 @@ FROM
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
@ -58,7 +57,6 @@ FROM
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
@ -123,7 +121,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
@ -154,7 +151,6 @@ SELECT
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
@ -333,7 +329,6 @@ SELECT
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",

View File

@ -41,7 +41,6 @@ FROM
"SharedLinkEntity__SharedLinkEntity_assets"."isFavorite" AS "SharedLinkEntity__SharedLinkEntity_assets_isFavorite",
"SharedLinkEntity__SharedLinkEntity_assets"."isArchived" AS "SharedLinkEntity__SharedLinkEntity_assets_isArchived",
"SharedLinkEntity__SharedLinkEntity_assets"."isExternal" AS "SharedLinkEntity__SharedLinkEntity_assets_isExternal",
"SharedLinkEntity__SharedLinkEntity_assets"."isReadOnly" AS "SharedLinkEntity__SharedLinkEntity_assets_isReadOnly",
"SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline",
"SharedLinkEntity__SharedLinkEntity_assets"."checksum" AS "SharedLinkEntity__SharedLinkEntity_assets_checksum",
"SharedLinkEntity__SharedLinkEntity_assets"."duration" AS "SharedLinkEntity__SharedLinkEntity_assets_duration",
@ -108,7 +107,6 @@ FROM
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isFavorite" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isFavorite",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isArchived" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isArchived",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isExternal" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isExternal",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isReadOnly" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isReadOnly",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isOffline" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isOffline",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."checksum" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_checksum",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."duration" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_duration",
@ -231,7 +229,6 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_assets"."isFavorite" AS "SharedLinkEntity__SharedLinkEntity_assets_isFavorite",
"SharedLinkEntity__SharedLinkEntity_assets"."isArchived" AS "SharedLinkEntity__SharedLinkEntity_assets_isArchived",
"SharedLinkEntity__SharedLinkEntity_assets"."isExternal" AS "SharedLinkEntity__SharedLinkEntity_assets_isExternal",
"SharedLinkEntity__SharedLinkEntity_assets"."isReadOnly" AS "SharedLinkEntity__SharedLinkEntity_assets_isReadOnly",
"SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline",
"SharedLinkEntity__SharedLinkEntity_assets"."checksum" AS "SharedLinkEntity__SharedLinkEntity_assets_checksum",
"SharedLinkEntity__SharedLinkEntity_assets"."duration" AS "SharedLinkEntity__SharedLinkEntity_assets_duration",

View File

@ -151,6 +151,14 @@ GROUP BY
ORDER BY
"users"."createdAt" ASC
-- UserRepository.updateUsage
UPDATE "users"
SET
"quotaUsageInBytes" = "quotaUsageInBytes" + 50,
"updatedAt" = CURRENT_TIMESTAMP
WHERE
"id" = $1
-- UserRepository.syncUsage
UPDATE "users"
SET

View File

@ -253,7 +253,7 @@ export class AssetRepository implements IAssetRepository {
@Chunked()
async softDeleteAll(ids: string[]): Promise<void> {
await this.repository.softDelete({ id: In(ids), isExternal: false });
await this.repository.softDelete({ id: In(ids) });
}
@Chunked()

View File

@ -112,6 +112,7 @@ export class UserRepository implements IUserRepository {
return stats;
}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] })
async updateUsage(id: string, delta: number): Promise<void> {
await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta);
}

View File

@ -295,7 +295,6 @@ export class AssetServiceV1 {
livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity),
originalFileName: file.originalName,
sidecarPath: sidecarPath || null,
isReadOnly: dto.isReadOnly ?? false,
isOffline: dto.isOffline ?? false,
});

View File

@ -685,61 +685,6 @@ describe(AssetService.name, () => {
});
});
it('should only delete generated files for readonly assets', async () => {
assetMock.getById.mockResolvedValue(assetStub.readOnly);
await sut.handleAssetDeletion({ id: assetStub.readOnly.id });
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.DELETE_FILES,
data: {
files: [
assetStub.readOnly.thumbnailPath,
assetStub.readOnly.previewPath,
assetStub.readOnly.encodedVideoPath,
],
},
},
],
]);
expect(assetMock.remove).toHaveBeenCalledWith(assetStub.readOnly);
});
it('should not process assets from external library without fromExternal flag', async () => {
assetMock.getById.mockResolvedValue(assetStub.external);
await sut.handleAssetDeletion({ id: assetStub.external.id });
expect(jobMock.queue).not.toHaveBeenCalled();
expect(jobMock.queueAll).not.toHaveBeenCalled();
expect(assetMock.remove).not.toHaveBeenCalled();
});
it('should process assets from external library with fromExternal flag', async () => {
assetMock.getById.mockResolvedValue(assetStub.external);
await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true });
expect(assetMock.remove).toHaveBeenCalledWith(assetStub.external);
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.DELETE_FILES,
data: {
files: [
assetStub.external.thumbnailPath,
assetStub.external.previewPath,
assetStub.external.encodedVideoPath,
],
},
},
],
]);
});
it('should delete a live photo', async () => {
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);

View File

@ -33,7 +33,7 @@ import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import {
IAssetDeletionJob,
IEntityJob,
IJobRepository,
ISidecarWriteJob,
JOBS_ASSET_PAGINATION_SIZE,
@ -371,8 +371,8 @@ export class AssetService {
return JobStatus.SUCCESS;
}
async handleAssetDeletion(job: IAssetDeletionJob): Promise<JobStatus> {
const { id, fromExternal } = job;
async handleAssetDeletion(job: IEntityJob): Promise<JobStatus> {
const { id } = job;
const asset = await this.assetRepository.getById(id, {
faces: {
@ -387,11 +387,6 @@ export class AssetService {
return JobStatus.FAILED;
}
// Ignore requests that are not from external library job but is for an external asset
if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) {
return JobStatus.SKIPPED;
}
// Replace the parent of the stack children with a new asset
if (asset.stack?.primaryAssetId === id) {
const stackAssetIds = asset.stack.assets.map((a) => a.id);
@ -414,18 +409,15 @@ export class AssetService {
// TODO refactor this to use cascades
if (asset.livePhotoVideoId) {
await this.jobRepository.queue({
name: JobName.ASSET_DELETION,
data: { id: asset.livePhotoVideoId, fromExternal },
});
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
}
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
if (!(asset.isExternal || asset.isReadOnly)) {
files.push(asset.sidecarPath, asset.originalPath);
}
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
await this.jobRepository.queue({
name: JobName.DELETE_FILES,
data: {
files: [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath, asset.sidecarPath, asset.originalPath],
},
});
return JobStatus.SUCCESS;
}

View File

@ -368,7 +368,6 @@ describe(LibraryService.name, () => {
type: AssetType.IMAGE,
originalFileName: 'photo.jpg',
sidecarPath: null,
isReadOnly: true,
isExternal: true,
},
],
@ -416,7 +415,6 @@ describe(LibraryService.name, () => {
type: AssetType.IMAGE,
originalFileName: 'photo.jpg',
sidecarPath: '/data/user1/photo.jpg.xmp',
isReadOnly: true,
isExternal: true,
},
],
@ -463,7 +461,6 @@ describe(LibraryService.name, () => {
type: AssetType.VIDEO,
originalFileName: 'video.mp4',
sidecarPath: null,
isReadOnly: true,
isExternal: true,
},
],
@ -1458,10 +1455,7 @@ describe(LibraryService.name, () => {
await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.ASSET_DELETION,
data: { id: assetStub.image1.id, fromExternal: true },
},
{ name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id } },
]);
});
});

View File

@ -387,7 +387,7 @@ export class LibraryService {
const assetIds = await this.repository.getAssetIds(job.id, true);
this.logger.debug(`Will delete ${assetIds.length} asset(s) in library ${job.id}`);
await this.jobRepository.queueAll(
assetIds.map((assetId) => ({ name: JobName.ASSET_DELETION, data: { id: assetId, fromExternal: true } })),
assetIds.map((assetId) => ({ name: JobName.ASSET_DELETION, data: { id: assetId } })),
);
if (assetIds.length === 0) {
@ -503,7 +503,6 @@ export class LibraryService {
type: assetType,
originalFileName,
sidecarPath,
isReadOnly: true,
isExternal: true,
});
assetId = addedAsset.id;
@ -580,7 +579,7 @@ export class LibraryService {
for await (const assets of assetPagination) {
this.logger.debug(`Removing ${assets.length} offline assets`);
await this.jobRepository.queueAll(
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id, fromExternal: true } })),
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
);
}

View File

@ -440,7 +440,6 @@ export class MetadataService {
originalPath: motionPath,
originalFileName: asset.originalFileName,
isVisible: false,
isReadOnly: false,
deviceAssetId: 'NONE',
deviceId: 'NONE',
});

View File

@ -558,26 +558,5 @@ describe(StorageTemplateService.name, () => {
);
expect(assetMock.update).not.toHaveBeenCalled();
});
it('should not move read-only asset', async () => {
assetMock.getAll.mockResolvedValue({
items: [
{
...assetStub.image,
originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
isReadOnly: true,
},
],
hasNextPage: false,
});
userMock.getList.mockResolvedValue([userStub.user1]);
await sut.handleMigration();
expect(assetMock.getAll).toHaveBeenCalled();
expect(storageMock.rename).not.toHaveBeenCalled();
expect(storageMock.copyFile).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalled();
});
});
});

View File

@ -170,7 +170,7 @@ export class StorageTemplateService {
}
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
if (asset.isReadOnly || asset.isExternal || StorageCore.isAndroidMotionPath(asset.originalPath)) {
if (asset.isExternal || StorageCore.isAndroidMotionPath(asset.originalPath)) {
// External assets are not affected by storage template
// TODO: shouldn't this only apply to external assets?
return;

View File

@ -67,7 +67,7 @@ export function searchAssetBuilder(
});
}
const status = _.pick(options, ['isExternal', 'isFavorite', 'isOffline', 'isReadOnly', 'isVisible', 'type']);
const status = _.pick(options, ['isFavorite', 'isOffline', 'isVisible', 'type']);
const {
isArchived,
isEncoded,

View File

@ -45,7 +45,6 @@ export const assetStub = {
sharedLinks: [],
faces: [],
sidecarPath: null,
isReadOnly: false,
deletedAt: null,
isOffline: false,
isExternal: false,
@ -82,7 +81,6 @@ export const assetStub = {
originalFileName: 'IMG_456.jpg',
faces: [],
sidecarPath: null,
isReadOnly: false,
isOffline: false,
isExternal: false,
libraryId: 'library-id',
@ -113,7 +111,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isOffline: false,
libraryId: 'library-id',
library: libraryStub.uploadLibrary1,
@ -150,7 +147,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
duration: null,
isVisible: true,
isExternal: false,
@ -195,7 +191,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
duration: null,
isVisible: true,
isExternal: false,
@ -235,7 +230,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: true,
duration: null,
isVisible: true,
@ -275,7 +269,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: false,
duration: null,
isVisible: true,
@ -315,7 +308,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: true,
duration: null,
isVisible: true,
@ -356,7 +348,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
@ -396,7 +387,6 @@ export const assetStub = {
isFavorite: true,
isArchived: false,
isExternal: false,
isReadOnly: false,
isOffline: false,
libraryId: 'library-id',
library: libraryStub.uploadLibrary1,
@ -436,7 +426,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: false,
isOffline: false,
libraryId: 'library-id',
@ -527,7 +516,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
isFavorite: false,
isArchived: false,
isReadOnly: false,
isExternal: false,
isOffline: false,
libraryId: 'library-id',
@ -570,7 +558,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: false,
isOffline: false,
libraryId: 'library-id',
@ -606,7 +593,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: false,
isOffline: false,
libraryId: 'library-id',
@ -643,7 +629,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: true,
isExternal: false,
isOffline: false,
libraryId: 'library-id',
@ -681,7 +666,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: false,
isOffline: false,
libraryId: 'library-id',
@ -719,7 +703,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: true,
duration: null,
isVisible: true,
@ -758,7 +741,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: true,
duration: null,
isVisible: true,
@ -797,7 +779,6 @@ export const assetStub = {
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
duration: null,
isVisible: true,
isExternal: false,

View File

@ -59,7 +59,6 @@ const assetResponse: AssetResponseDto = {
thumbhash: null,
fileModifiedAt: today,
isExternal: false,
isReadOnly: false,
isOffline: false,
fileCreatedAt: today,
localDateTime: today,
@ -210,7 +209,6 @@ export const sharedLinkStub = {
isFavorite: false,
isArchived: false,
isExternal: false,
isReadOnly: false,
isOffline: false,
libraryId: 'library-id',
library: libraryStub.uploadLibrary1,

View File

@ -193,9 +193,7 @@
{/if}
{#if isOwner}
{#if !asset.isReadOnly || !asset.isExternal}
<CircleIconButton color="opaque" icon={mdiDeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
{/if}
<CircleIconButton color="opaque" icon={mdiDeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
<div
use:clickOutside={{
onOutclick: () => (isShowAssetOptions = false),

View File

@ -308,23 +308,15 @@
{/if}
<div class="px-4 py-4">
{#if !asset.exifInfo && !asset.isExternal}
<p class="text-sm">NO EXIF INFO AVAILABLE</p>
{:else if !asset.exifInfo && asset.isExternal}
<div class="flex gap-4 py-4">
<div>
<p class="break-all">
Metadata not loaded for {asset.originalPath}
</p>
</div>
</div>
{:else}
{#if asset.exifInfo}
<div class="flex h-10 w-full items-center justify-between text-sm">
<h2>DETAILS</h2>
</div>
{:else}
<p class="text-sm">NO EXIF INFO AVAILABLE</p>
{/if}
{#if asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly}
{#if asset.exifInfo?.dateTimeOriginal}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
@ -374,7 +366,7 @@
</div>
{/if}
</button>
{:else if !asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly && isOwner}
{:else if !asset.exifInfo?.dateTimeOriginal && isOwner}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4">
<div>
@ -385,43 +377,6 @@
<Icon path={mdiPencil} size="20" />
</div>
</div>
{:else if asset.exifInfo?.dateTimeOriginal && asset.isReadOnly}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4">
<div>
<Icon path={mdiCalendar} size="24" />
</div>
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{
month: 'short',
day: 'numeric',
year: 'numeric',
},
{ locale: $locale },
)}
</p>
<div class="flex gap-2 text-sm">
<p>
{assetDateTimeOriginal.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
},
{ locale: $locale },
)}
</p>
</div>
</div>
</div>
</div>
{/if}
{#if isShowChangeDate}
@ -501,7 +456,7 @@
</div>
{/if}
{#if asset.exifInfo?.city && !asset.isReadOnly}
{#if asset.exifInfo?.city}
<button
type="button"
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
@ -534,7 +489,7 @@
</div>
{/if}
</button>
{:else if !asset.exifInfo?.city && !asset.isReadOnly && isOwner}
{:else if !asset.exifInfo?.city && isOwner}
<button
type="button"
class="flex w-full text-left justify-between place-items-start gap-4 py-4 rounded-lg hover:dark:text-immich-dark-primary hover:text-immich-primary"
@ -552,26 +507,6 @@
<Icon path={mdiPencil} size="20" />
</div>
</button>
{:else if asset.exifInfo?.city && asset.isReadOnly}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4">
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
<div>
<p>{asset.exifInfo.city}</p>
{#if asset.exifInfo?.state}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.state}</p>
</div>
{/if}
{#if asset.exifInfo?.country}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.country}</p>
</div>
{/if}
</div>
</div>
</div>
{/if}
{#if isShowChangeLocation}
<ChangeLocation

View File

@ -34,7 +34,7 @@
const handleDelete = async () => {
loading = true;
const ids = [...getOwnedAssets()].filter((a) => !a.isExternal).map((a) => a.id);
const ids = [...getOwnedAssets()].map((a) => a.id);
await deleteAssets(force, onAssetDelete, ids);
clearSelect();
isShowConfirmation = false;

View File

@ -47,7 +47,7 @@
$: timelineY = element?.scrollTop || 0;
$: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0;
$: idsSelectedAssets = [...$selectedAssets].filter((a) => !a.isExternal).map((a) => a.id);
$: idsSelectedAssets = [...$selectedAssets].map(({ id }) => id);
$: {
void assetStore.updateViewport(viewport);

View File

@ -258,9 +258,9 @@ export const getAssetType = (type: AssetTypeEnum) => {
};
export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserResponseDto | null): string[] => {
const ids = [...assets].filter((a) => !a.isExternal && user && a.ownerId === user.id).map((a) => a.id);
const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id);
const numberOfIssues = [...assets].filter((a) => a.isExternal || (user && a.ownerId !== user.id)).length;
const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length;
if (numberOfIssues > 0) {
notificationController.show({
message: `Can't change metadata of ${numberOfIssues} asset${numberOfIssues > 1 ? 's' : ''}`,

View File

@ -22,9 +22,7 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
isTrashed: Sync.each(() => faker.datatype.boolean()),
duration: '0:00:00.00000',
checksum: Sync.each(() => faker.string.alphanumeric(28)),
isExternal: Sync.each(() => faker.datatype.boolean()),
isOffline: Sync.each(() => faker.datatype.boolean()),
isReadOnly: Sync.each(() => faker.datatype.boolean()),
hasMetadata: Sync.each(() => faker.datatype.boolean()),
stackCount: null,
});