mirror of
https://github.com/immich-app/immich.git
synced 2025-12-21 01:11:16 +03:00
refactor: library type (#9525)
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
CreateLibraryDto,
|
||||
LibraryResponseDto,
|
||||
LibraryStatsResponseDto,
|
||||
ScanLibraryDto,
|
||||
SearchLibraryDto,
|
||||
UpdateLibraryDto,
|
||||
ValidateLibraryDto,
|
||||
ValidateLibraryResponseDto,
|
||||
@@ -21,8 +20,8 @@ export class LibraryController {
|
||||
|
||||
@Get()
|
||||
@Authenticated({ admin: true })
|
||||
getAllLibraries(@Query() dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
|
||||
return this.service.getAll(dto);
|
||||
getAllLibraries(): Promise<LibraryResponseDto[]> {
|
||||
return this.service.getAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -274,7 +274,7 @@ export class AccessCore {
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
||||
}
|
||||
|
||||
case Permission.ARCHIVE_READ: {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
@@ -93,16 +92,7 @@ export class UserCore {
|
||||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||
}
|
||||
const userEntity = await this.userRepository.create(payload);
|
||||
await this.libraryRepository.create({
|
||||
owner: { id: userEntity.id } as UserEntity,
|
||||
name: 'Default Library',
|
||||
assets: [],
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
});
|
||||
|
||||
return userEntity;
|
||||
return this.userRepository.create(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||
deviceId!: string;
|
||||
ownerId!: string;
|
||||
owner?: UserResponseDto;
|
||||
libraryId!: string;
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.106.0' })
|
||||
libraryId?: string | null;
|
||||
originalPath!: string;
|
||||
originalFileName!: string;
|
||||
fileCreatedAt!: Date;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { UploadFieldName } from 'src/dtos/asset.dto';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
|
||||
|
||||
export class AssetBulkUploadCheckItem {
|
||||
@IsString()
|
||||
@@ -64,9 +64,6 @@ export class CheckExistingAssetsDto {
|
||||
}
|
||||
|
||||
export class CreateAssetDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
libraryId?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
deviceAssetId!: string;
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMaxSize, ArrayUnique, IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
||||
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class CreateLibraryDto {
|
||||
@IsEnum(LibraryType)
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
type!: LibraryType;
|
||||
|
||||
@ValidateUUID()
|
||||
ownerId!: string;
|
||||
|
||||
@@ -97,21 +93,11 @@ export class ScanLibraryDto {
|
||||
refreshAllFiles?: boolean;
|
||||
}
|
||||
|
||||
export class SearchLibraryDto {
|
||||
@IsEnum(LibraryType)
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
@Optional()
|
||||
type?: LibraryType;
|
||||
}
|
||||
|
||||
export class LibraryResponseDto {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
type!: LibraryType;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
assetCount!: number;
|
||||
|
||||
@@ -146,7 +132,6 @@ export function mapLibrary(entity: LibraryEntity): LibraryResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
type: entity.type,
|
||||
name: entity.name,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
|
||||
@@ -25,12 +25,17 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum';
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||
|
||||
@Entity('assets')
|
||||
// Checksums must be unique per user and library
|
||||
@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'library', 'checksum'], {
|
||||
@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'checksum'], {
|
||||
unique: true,
|
||||
where: '"libraryId" IS NULL',
|
||||
})
|
||||
@Index('UQ_assets_owner_library_checksum' + '', ['owner', 'library', 'checksum'], {
|
||||
unique: true,
|
||||
where: '"libraryId" IS NOT NULL',
|
||||
})
|
||||
@Index('IDX_day_of_month', { synchronize: false })
|
||||
@Index('IDX_month', { synchronize: false })
|
||||
@@ -51,11 +56,11 @@ export class AssetEntity {
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@ManyToOne(() => LibraryEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
library!: LibraryEntity;
|
||||
@ManyToOne(() => LibraryEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
library?: LibraryEntity | null;
|
||||
|
||||
@Column()
|
||||
libraryId!: string;
|
||||
@Column({ nullable: true })
|
||||
libraryId?: string | null;
|
||||
|
||||
@Column()
|
||||
deviceId!: string;
|
||||
|
||||
@@ -30,9 +30,6 @@ export class LibraryEntity {
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@Column()
|
||||
type!: LibraryType;
|
||||
|
||||
@Column('text', { array: true })
|
||||
importPaths!: string[];
|
||||
|
||||
@@ -51,8 +48,3 @@ export class LibraryEntity {
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
refreshedAt!: Date | null;
|
||||
}
|
||||
|
||||
export enum LibraryType {
|
||||
UPLOAD = 'UPLOAD',
|
||||
EXTERNAL = 'EXTERNAL',
|
||||
}
|
||||
|
||||
@@ -26,10 +26,6 @@ export interface IAccessRepository {
|
||||
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
library: {
|
||||
checkOwnerAccess(userId: string, libraryIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
timeline: {
|
||||
checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
@@ -164,7 +164,7 @@ export interface IAssetRepository {
|
||||
): Promise<AssetEntity[]>;
|
||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
||||
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
|
||||
getByChecksum(libraryId: string, checksum: Buffer): Promise<AssetEntity | null>;
|
||||
getByChecksum(libraryId: string | null, checksum: Buffer): Promise<AssetEntity | null>;
|
||||
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
|
||||
export const ILibraryRepository = 'ILibraryRepository';
|
||||
|
||||
export interface ILibraryRepository {
|
||||
getCountForUser(ownerId: string): Promise<number>;
|
||||
getAll(withDeleted?: boolean, type?: LibraryType): Promise<LibraryEntity[]>;
|
||||
getAll(withDeleted?: boolean): Promise<LibraryEntity[]>;
|
||||
getAllDeleted(): Promise<LibraryEntity[]>;
|
||||
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | null>;
|
||||
create(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
softDelete(id: string): Promise<void>;
|
||||
getDefaultUploadLibrary(ownerId: string): Promise<LibraryEntity | null>;
|
||||
getUploadLibraryCount(ownerId: string): Promise<number>;
|
||||
update(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
|
||||
getStatistics(id: string): Promise<LibraryStatsResponseDto>;
|
||||
getAssetIds(id: string, withDeleted?: boolean): Promise<string[]>;
|
||||
|
||||
29
server/src/migrations/1715804005643-RemoveLibraryType.ts
Normal file
29
server/src/migrations/1715804005643-RemoveLibraryType.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RemoveLibraryType1715804005643 implements MigrationInterface {
|
||||
name = 'RemoveLibraryType1715804005643';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."UQ_assets_owner_library_checksum"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "libraryId" DROP NOT NULL`);
|
||||
await queryRunner.query(`
|
||||
UPDATE "assets"
|
||||
SET "libraryId" = NULL
|
||||
FROM "libraries"
|
||||
WHERE "assets"."libraryId" = "libraries"."id"
|
||||
AND "libraries"."type" = 'UPLOAD'
|
||||
`);
|
||||
await queryRunner.query(`DELETE FROM "libraries" WHERE "type" = 'UPLOAD'`);
|
||||
await queryRunner.query(`ALTER TABLE "libraries" DROP COLUMN "type"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_originalPath_libraryId" ON "assets" ("originalPath", "libraryId")`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE "libraryId" IS NULL`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE "libraryId" IS NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
// not implemented
|
||||
}
|
||||
}
|
||||
@@ -191,20 +191,6 @@ WHERE
|
||||
AND ("SessionEntity"."id" IN ($2))
|
||||
)
|
||||
|
||||
-- AccessRepository.library.checkOwnerAccess
|
||||
SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
WHERE
|
||||
(
|
||||
(
|
||||
("LibraryEntity"."id" IN ($1))
|
||||
AND ("LibraryEntity"."ownerId" = $2)
|
||||
)
|
||||
)
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- AccessRepository.memory.checkOwnerAccess
|
||||
SELECT
|
||||
"MemoryEntity"."id" AS "MemoryEntity_id"
|
||||
|
||||
@@ -483,26 +483,16 @@ LIMIT
|
||||
1
|
||||
|
||||
-- AssetRepository.getUploadAssetIdByChecksum
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
(
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
|
||||
WHERE
|
||||
(
|
||||
("AssetEntity"."ownerId" = $1)
|
||||
AND ("AssetEntity"."checksum" = $2)
|
||||
AND (
|
||||
(("AssetEntity__AssetEntity_library"."type" = $3))
|
||||
)
|
||||
)
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetEntity_id" ASC
|
||||
("AssetEntity"."ownerId" = $1)
|
||||
AND ("AssetEntity"."checksum" = $2)
|
||||
AND ("AssetEntity"."libraryId" IS NULL)
|
||||
)
|
||||
LIMIT
|
||||
1
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ FROM
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||
"LibraryEntity"."name" AS "LibraryEntity_name",
|
||||
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
|
||||
"LibraryEntity"."type" AS "LibraryEntity_type",
|
||||
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
|
||||
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
|
||||
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
|
||||
@@ -77,53 +76,11 @@ WHERE
|
||||
((("LibraryEntity"."ownerId" = $1)))
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- LibraryRepository.getDefaultUploadLibrary
|
||||
SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||
"LibraryEntity"."name" AS "LibraryEntity_name",
|
||||
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
|
||||
"LibraryEntity"."type" AS "LibraryEntity_type",
|
||||
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
|
||||
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
|
||||
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
|
||||
"LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt",
|
||||
"LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt",
|
||||
"LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
WHERE
|
||||
(
|
||||
(
|
||||
("LibraryEntity"."ownerId" = $1)
|
||||
AND ("LibraryEntity"."type" = $2)
|
||||
)
|
||||
)
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"LibraryEntity"."createdAt" ASC
|
||||
LIMIT
|
||||
1
|
||||
|
||||
-- LibraryRepository.getUploadLibraryCount
|
||||
SELECT
|
||||
COUNT(1) AS "cnt"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
WHERE
|
||||
(
|
||||
(
|
||||
("LibraryEntity"."ownerId" = $1)
|
||||
AND ("LibraryEntity"."type" = $2)
|
||||
)
|
||||
)
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- LibraryRepository.getAllByUserId
|
||||
SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||
"LibraryEntity"."name" AS "LibraryEntity_name",
|
||||
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
|
||||
"LibraryEntity"."type" AS "LibraryEntity_type",
|
||||
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
|
||||
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
|
||||
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
|
||||
@@ -163,7 +120,6 @@ SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||
"LibraryEntity"."name" AS "LibraryEntity_name",
|
||||
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
|
||||
"LibraryEntity"."type" AS "LibraryEntity_type",
|
||||
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
|
||||
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
|
||||
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
|
||||
@@ -202,7 +158,6 @@ SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id",
|
||||
"LibraryEntity"."name" AS "LibraryEntity_name",
|
||||
"LibraryEntity"."ownerId" AS "LibraryEntity_ownerId",
|
||||
"LibraryEntity"."type" AS "LibraryEntity_type",
|
||||
"LibraryEntity"."importPaths" AS "LibraryEntity_importPaths",
|
||||
"LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns",
|
||||
"LibraryEntity"."createdAt" AS "LibraryEntity_createdAt",
|
||||
@@ -238,7 +193,6 @@ SELECT
|
||||
"libraries"."id" AS "libraries_id",
|
||||
"libraries"."name" AS "libraries_name",
|
||||
"libraries"."ownerId" AS "libraries_ownerId",
|
||||
"libraries"."type" AS "libraries_type",
|
||||
"libraries"."importPaths" AS "libraries_importPaths",
|
||||
"libraries"."exclusionPatterns" AS "libraries_exclusionPatterns",
|
||||
"libraries"."createdAt" AS "libraries_createdAt",
|
||||
|
||||
@@ -167,12 +167,10 @@ SET
|
||||
COALESCE(SUM(exif."fileSizeInByte"), 0)
|
||||
FROM
|
||||
"assets" "assets"
|
||||
LEFT JOIN "libraries" "library" ON "library"."id" = "assets"."libraryId"
|
||||
AND ("library"."deletedAt" IS NULL)
|
||||
LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id"
|
||||
WHERE
|
||||
"assets"."ownerId" = users.id
|
||||
AND "library"."type" = 'UPLOAD'
|
||||
AND "assets"."libraryId" IS NULL
|
||||
),
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
|
||||
@@ -20,7 +20,6 @@ type IActivityAccess = IAccessRepository['activity'];
|
||||
type IAlbumAccess = IAccessRepository['album'];
|
||||
type IAssetAccess = IAccessRepository['asset'];
|
||||
type IAuthDeviceAccess = IAccessRepository['authDevice'];
|
||||
type ILibraryAccess = IAccessRepository['library'];
|
||||
type ITimelineAccess = IAccessRepository['timeline'];
|
||||
type IMemoryAccess = IAccessRepository['memory'];
|
||||
type IPersonAccess = IAccessRepository['person'];
|
||||
@@ -313,28 +312,6 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class LibraryAccess implements ILibraryAccess {
|
||||
constructor(private libraryRepository: Repository<LibraryEntity>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, libraryIds: Set<string>): Promise<Set<string>> {
|
||||
if (libraryIds.size === 0) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return this.libraryRepository
|
||||
.find({
|
||||
select: { id: true },
|
||||
where: {
|
||||
id: In([...libraryIds]),
|
||||
ownerId: userId,
|
||||
},
|
||||
})
|
||||
.then((libraries) => new Set(libraries.map((library) => library.id)));
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineAccess implements ITimelineAccess {
|
||||
constructor(private partnerRepository: Repository<PartnerEntity>) {}
|
||||
|
||||
@@ -447,7 +424,6 @@ export class AccessRepository implements IAccessRepository {
|
||||
album: IAlbumAccess;
|
||||
asset: IAssetAccess;
|
||||
authDevice: IAuthDeviceAccess;
|
||||
library: ILibraryAccess;
|
||||
memory: IMemoryAccess;
|
||||
person: IPersonAccess;
|
||||
partner: IPartnerAccess;
|
||||
@@ -469,7 +445,6 @@ export class AccessRepository implements IAccessRepository {
|
||||
this.album = new AlbumAccess(albumRepository, sharedLinkRepository);
|
||||
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
|
||||
this.authDevice = new AuthDeviceAccess(sessionRepository);
|
||||
this.library = new LibraryAccess(libraryRepository);
|
||||
this.memory = new MemoryAccess(memoryRepository);
|
||||
this.person = new PersonAccess(assetFaceRepository, personRepository);
|
||||
this.partner = new PartnerAccess(partnerRepository);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import {
|
||||
@@ -292,8 +291,13 @@ export class AssetRepository implements IAssetRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||
getByChecksum(libraryId: string, checksum: Buffer): Promise<AssetEntity | null> {
|
||||
return this.repository.findOne({ where: { libraryId, checksum } });
|
||||
getByChecksum(libraryId: string | null, checksum: Buffer): Promise<AssetEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
libraryId: libraryId || IsNull(),
|
||||
checksum,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] })
|
||||
@@ -303,9 +307,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
where: {
|
||||
ownerId,
|
||||
checksum,
|
||||
library: {
|
||||
type: LibraryType.UPLOAD,
|
||||
},
|
||||
library: IsNull(),
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { EntityNotFoundError, IsNull, Not } from 'typeorm';
|
||||
@@ -40,34 +40,10 @@ export class LibraryRepository implements ILibraryRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getDefaultUploadLibrary(ownerId: string): Promise<LibraryEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
ownerId: ownerId,
|
||||
type: LibraryType.UPLOAD,
|
||||
},
|
||||
order: {
|
||||
createdAt: 'ASC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getUploadLibraryCount(ownerId: string): Promise<number> {
|
||||
return this.repository.count({
|
||||
where: {
|
||||
ownerId: ownerId,
|
||||
type: LibraryType.UPLOAD,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAllByUserId(ownerId: string, type?: LibraryType): Promise<LibraryEntity[]> {
|
||||
getAllByUserId(ownerId: string): Promise<LibraryEntity[]> {
|
||||
return this.repository.find({
|
||||
where: {
|
||||
ownerId,
|
||||
type,
|
||||
},
|
||||
relations: {
|
||||
owner: true,
|
||||
@@ -79,9 +55,8 @@ export class LibraryRepository implements ILibraryRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [] })
|
||||
getAll(withDeleted = false, type?: LibraryType): Promise<LibraryEntity[]> {
|
||||
getAll(withDeleted = false): Promise<LibraryEntity[]> {
|
||||
return this.repository.find({
|
||||
where: { type },
|
||||
relations: {
|
||||
owner: true,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import {
|
||||
IUserRepository,
|
||||
@@ -123,10 +122,9 @@ export class UserRepository implements IUserRepository {
|
||||
const subQuery = this.assetRepository
|
||||
.createQueryBuilder('assets')
|
||||
.select('COALESCE(SUM(exif."fileSizeInByte"), 0)')
|
||||
.leftJoin('assets.library', 'library')
|
||||
.leftJoin('assets.exifInfo', 'exif')
|
||||
.where('assets.ownerId = users.id')
|
||||
.andWhere(`library.type = '${LibraryType.UPLOAD}'`)
|
||||
.andWhere(`assets.libraryId IS NULL`)
|
||||
.withDeleted();
|
||||
|
||||
const query = this.userRepository
|
||||
|
||||
@@ -32,7 +32,6 @@ const _getCreateAssetDto = (): CreateAssetDto => {
|
||||
createAssetDto.isFavorite = false;
|
||||
createAssetDto.isArchived = false;
|
||||
createAssetDto.duration = '0:00:00.000000';
|
||||
createAssetDto.libraryId = 'libraryId';
|
||||
|
||||
return createAssetDto;
|
||||
};
|
||||
@@ -121,7 +120,6 @@ describe('AssetService', () => {
|
||||
const dto = _getCreateAssetDto();
|
||||
|
||||
assetMock.create.mockResolvedValue(assetEntity);
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||
|
||||
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
|
||||
|
||||
@@ -149,7 +147,6 @@ describe('AssetService', () => {
|
||||
|
||||
assetMock.create.mockRejectedValue(error);
|
||||
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||
|
||||
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
|
||||
|
||||
@@ -167,7 +164,6 @@ describe('AssetService', () => {
|
||||
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||
|
||||
await expect(
|
||||
sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion),
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from 'src/dtos/asset-v1.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
@@ -76,15 +75,20 @@ export class AssetServiceV1 {
|
||||
let livePhotoAsset: AssetEntity | null = null;
|
||||
|
||||
try {
|
||||
const libraryId = await this.getLibraryId(auth, dto.libraryId);
|
||||
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
|
||||
await this.access.requirePermission(
|
||||
auth,
|
||||
Permission.ASSET_UPLOAD,
|
||||
// do not need an id here, but the interface requires it
|
||||
auth.user.id,
|
||||
);
|
||||
|
||||
this.requireQuota(auth, file.size);
|
||||
if (livePhotoFile) {
|
||||
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
|
||||
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false };
|
||||
livePhotoAsset = await this.create(auth, livePhotoDto, livePhotoFile);
|
||||
}
|
||||
|
||||
const asset = await this.create(auth, { ...dto, libraryId }, file, livePhotoAsset?.id, sidecarFile?.originalPath);
|
||||
const asset = await this.create(auth, dto, file, livePhotoAsset?.id, sidecarFile?.originalPath);
|
||||
|
||||
await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size);
|
||||
|
||||
@@ -245,36 +249,16 @@ export class AssetServiceV1 {
|
||||
return asset.previewPath;
|
||||
}
|
||||
|
||||
private async getLibraryId(auth: AuthDto, libraryId?: string) {
|
||||
if (libraryId) {
|
||||
return libraryId;
|
||||
}
|
||||
|
||||
let library = await this.libraryRepository.getDefaultUploadLibrary(auth.user.id);
|
||||
if (!library) {
|
||||
library = await this.libraryRepository.create({
|
||||
ownerId: auth.user.id,
|
||||
name: 'Default Library',
|
||||
assets: [],
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
});
|
||||
}
|
||||
|
||||
return library.id;
|
||||
}
|
||||
|
||||
private async create(
|
||||
auth: AuthDto,
|
||||
dto: CreateAssetDto & { libraryId: string },
|
||||
dto: CreateAssetDto,
|
||||
file: UploadFile,
|
||||
livePhotoAssetId?: string,
|
||||
sidecarPath?: string,
|
||||
): Promise<AssetEntity> {
|
||||
const asset = await this.assetRepository.create({
|
||||
ownerId: auth.user.id,
|
||||
libraryId: dto.libraryId,
|
||||
libraryId: null,
|
||||
|
||||
checksum: file.checksum,
|
||||
originalPath: file.originalPath,
|
||||
|
||||
@@ -27,7 +27,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
@@ -424,7 +423,7 @@ export class AssetService {
|
||||
}
|
||||
|
||||
await this.assetRepository.remove(asset);
|
||||
if (asset.library.type === LibraryType.UPLOAD) {
|
||||
if (!asset.libraryId) {
|
||||
await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0));
|
||||
}
|
||||
this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, asset.ownerId, id);
|
||||
@@ -436,7 +435,7 @@ export class AssetService {
|
||||
|
||||
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
|
||||
// skip originals if the user deleted the whole library
|
||||
if (!asset.library.deletedAt) {
|
||||
if (!asset.library?.deletedAt) {
|
||||
files.push(asset.sidecarPath, asset.originalPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,6 @@ describe(DownloadService.name, () => {
|
||||
});
|
||||
|
||||
it('should return a list of archives (userId)', async () => {
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.user.id]));
|
||||
assetMock.getByUserId.mockResolvedValue({
|
||||
items: [assetStub.image, assetStub.video],
|
||||
hasNextPage: false,
|
||||
@@ -196,8 +195,6 @@ describe(DownloadService.name, () => {
|
||||
});
|
||||
|
||||
it('should split archives by size', async () => {
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.user.id]));
|
||||
|
||||
assetMock.getByUserId.mockResolvedValue({
|
||||
items: [
|
||||
{ ...assetStub.image, id: 'asset-1' },
|
||||
|
||||
@@ -4,7 +4,6 @@ import { SystemConfig } from 'src/config';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { mapLibrary } from 'src/dtos/library.dto';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
@@ -213,18 +212,6 @@ describe(LibraryService.name, () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not scan upload libraries', async () => {
|
||||
const mockLibraryJob: ILibraryRefreshJob = {
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
refreshModifiedFiles: false,
|
||||
refreshAllFiles: false,
|
||||
};
|
||||
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
|
||||
await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
|
||||
});
|
||||
|
||||
it('should ignore import paths that do not exist', async () => {
|
||||
storageMock.stat.mockImplementation((path): Promise<Stats> => {
|
||||
if (path === libraryStub.externalLibraryWithImportPaths1.importPaths[0]) {
|
||||
@@ -707,7 +694,6 @@ describe(LibraryService.name, () => {
|
||||
describe('delete', () => {
|
||||
it('should delete a library', async () => {
|
||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
libraryMock.getUploadLibraryCount.mockResolvedValue(2);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
|
||||
await sut.delete(libraryStub.externalLibrary1.id);
|
||||
@@ -720,21 +706,8 @@ describe(LibraryService.name, () => {
|
||||
expect(libraryMock.softDelete).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
||||
});
|
||||
|
||||
it('should throw error if the last upload library is deleted', async () => {
|
||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
|
||||
await expect(sut.delete(libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
expect(libraryMock.softDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow an external library to be deleted', async () => {
|
||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
|
||||
await sut.delete(libraryStub.externalLibrary1.id);
|
||||
@@ -749,7 +722,6 @@ describe(LibraryService.name, () => {
|
||||
|
||||
it('should unwatch an external library when deleted', async () => {
|
||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||
libraryMock.getUploadLibraryCount.mockResolvedValue(1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
|
||||
@@ -767,37 +739,37 @@ describe(LibraryService.name, () => {
|
||||
|
||||
describe('get', () => {
|
||||
it('should return a library', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
await expect(sut.get(libraryStub.uploadLibrary1.id)).resolves.toEqual(
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
await expect(sut.get(libraryStub.externalLibrary1.id)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: libraryStub.uploadLibrary1.id,
|
||||
name: libraryStub.uploadLibrary1.name,
|
||||
ownerId: libraryStub.uploadLibrary1.ownerId,
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
name: libraryStub.externalLibrary1.name,
|
||||
ownerId: libraryStub.externalLibrary1.ownerId,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
||||
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
||||
});
|
||||
|
||||
it('should throw an error when a library is not found', async () => {
|
||||
libraryMock.get.mockResolvedValue(null);
|
||||
await expect(sut.get(libraryStub.uploadLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
||||
await expect(sut.get(libraryStub.externalLibrary1.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(libraryMock.get).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatistics', () => {
|
||||
it('should return library statistics', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 });
|
||||
await expect(sut.getStatistics(libraryStub.uploadLibrary1.id)).resolves.toEqual({
|
||||
await expect(sut.getStatistics(libraryStub.externalLibrary1.id)).resolves.toEqual({
|
||||
photos: 10,
|
||||
videos: 0,
|
||||
total: 10,
|
||||
usage: 1337,
|
||||
});
|
||||
|
||||
expect(libraryMock.getStatistics).toHaveBeenCalledWith(libraryStub.uploadLibrary1.id);
|
||||
expect(libraryMock.getStatistics).toHaveBeenCalledWith(libraryStub.externalLibrary1.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -805,10 +777,9 @@ describe(LibraryService.name, () => {
|
||||
describe('external library', () => {
|
||||
it('should create with default settings', async () => {
|
||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
await expect(sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL })).resolves.toEqual(
|
||||
await expect(sut.create({ ownerId: authStub.admin.user.id })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
type: LibraryType.EXTERNAL,
|
||||
name: libraryStub.externalLibrary1.name,
|
||||
ownerId: libraryStub.externalLibrary1.ownerId,
|
||||
assetCount: 0,
|
||||
@@ -823,7 +794,6 @@ describe(LibraryService.name, () => {
|
||||
expect(libraryMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
@@ -832,12 +802,9 @@ describe(LibraryService.name, () => {
|
||||
|
||||
it('should create with name', async () => {
|
||||
libraryMock.create.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
await expect(
|
||||
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.EXTERNAL, name: 'My Awesome Library' }),
|
||||
).resolves.toEqual(
|
||||
await expect(sut.create({ ownerId: authStub.admin.user.id, name: 'My Awesome Library' })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
type: LibraryType.EXTERNAL,
|
||||
name: libraryStub.externalLibrary1.name,
|
||||
ownerId: libraryStub.externalLibrary1.ownerId,
|
||||
assetCount: 0,
|
||||
@@ -852,7 +819,6 @@ describe(LibraryService.name, () => {
|
||||
expect(libraryMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'My Awesome Library',
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
@@ -864,13 +830,11 @@ describe(LibraryService.name, () => {
|
||||
await expect(
|
||||
sut.create({
|
||||
ownerId: authStub.admin.user.id,
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: ['/data/images', '/data/videos'],
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
type: LibraryType.EXTERNAL,
|
||||
name: libraryStub.externalLibrary1.name,
|
||||
ownerId: libraryStub.externalLibrary1.ownerId,
|
||||
assetCount: 0,
|
||||
@@ -885,7 +849,6 @@ describe(LibraryService.name, () => {
|
||||
expect(libraryMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: ['/data/images', '/data/videos'],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
@@ -901,7 +864,6 @@ describe(LibraryService.name, () => {
|
||||
await sut.init();
|
||||
await sut.create({
|
||||
ownerId: authStub.admin.user.id,
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||
});
|
||||
});
|
||||
@@ -911,13 +873,11 @@ describe(LibraryService.name, () => {
|
||||
await expect(
|
||||
sut.create({
|
||||
ownerId: authStub.admin.user.id,
|
||||
type: LibraryType.EXTERNAL,
|
||||
exclusionPatterns: ['*.tmp', '*.bak'],
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: libraryStub.externalLibrary1.id,
|
||||
type: LibraryType.EXTERNAL,
|
||||
name: libraryStub.externalLibrary1.name,
|
||||
ownerId: libraryStub.externalLibrary1.ownerId,
|
||||
assetCount: 0,
|
||||
@@ -932,105 +892,22 @@ describe(LibraryService.name, () => {
|
||||
expect(libraryMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [],
|
||||
exclusionPatterns: ['*.tmp', '*.bak'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upload library', () => {
|
||||
it('should create with default settings', async () => {
|
||||
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
await expect(sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: libraryStub.uploadLibrary1.id,
|
||||
type: LibraryType.UPLOAD,
|
||||
name: libraryStub.uploadLibrary1.name,
|
||||
ownerId: libraryStub.uploadLibrary1.ownerId,
|
||||
assetCount: 0,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
createdAt: libraryStub.uploadLibrary1.createdAt,
|
||||
updatedAt: libraryStub.uploadLibrary1.updatedAt,
|
||||
refreshedAt: null,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(libraryMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'New Upload Library',
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create with name', async () => {
|
||||
libraryMock.create.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
await expect(
|
||||
sut.create({ ownerId: authStub.admin.user.id, type: LibraryType.UPLOAD, name: 'My Awesome Library' }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: libraryStub.uploadLibrary1.id,
|
||||
type: LibraryType.UPLOAD,
|
||||
name: libraryStub.uploadLibrary1.name,
|
||||
ownerId: libraryStub.uploadLibrary1.ownerId,
|
||||
assetCount: 0,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
createdAt: libraryStub.uploadLibrary1.createdAt,
|
||||
updatedAt: libraryStub.uploadLibrary1.updatedAt,
|
||||
refreshedAt: null,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(libraryMock.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'My Awesome Library',
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not create with import paths', async () => {
|
||||
await expect(
|
||||
sut.create({
|
||||
ownerId: authStub.admin.user.id,
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: ['/data/images', '/data/videos'],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(libraryMock.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not create with exclusion patterns', async () => {
|
||||
await expect(
|
||||
sut.create({
|
||||
ownerId: authStub.admin.user.id,
|
||||
type: LibraryType.UPLOAD,
|
||||
exclusionPatterns: ['*.tmp', '*.bak'],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(libraryMock.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueCleanup', () => {
|
||||
it('should queue cleanup jobs', async () => {
|
||||
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]);
|
||||
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.externalLibrary1, libraryStub.externalLibrary2]);
|
||||
await expect(sut.handleQueueCleanup()).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.LIBRARY_DELETE, data: { id: libraryStub.uploadLibrary1.id } },
|
||||
{ name: JobName.LIBRARY_DELETE, data: { id: libraryStub.externalLibrary1.id } },
|
||||
{ name: JobName.LIBRARY_DELETE, data: { id: libraryStub.externalLibrary2.id } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1044,9 +921,9 @@ describe(LibraryService.name, () => {
|
||||
});
|
||||
|
||||
it('should update library', async () => {
|
||||
libraryMock.update.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.uploadLibrary1));
|
||||
libraryMock.update.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.externalLibrary1));
|
||||
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||
});
|
||||
});
|
||||
@@ -1109,15 +986,6 @@ describe(LibraryService.name, () => {
|
||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when watching upload library', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.uploadLibrary1]);
|
||||
|
||||
await expect(sut.watchAll()).rejects.toThrow('Can only watch external libraries');
|
||||
|
||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a new file event', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
@@ -1253,25 +1121,25 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.getAssetIds.mockResolvedValue([]);
|
||||
libraryMock.delete.mockImplementation(async () => {});
|
||||
|
||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.FAILED);
|
||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.FAILED);
|
||||
});
|
||||
|
||||
it('should delete an empty library', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
libraryMock.getAssetIds.mockResolvedValue([]);
|
||||
libraryMock.delete.mockImplementation(async () => {});
|
||||
|
||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
});
|
||||
|
||||
it('should delete a library with assets', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
libraryMock.getAssetIds.mockResolvedValue([assetStub.image1.id]);
|
||||
libraryMock.delete.mockImplementation(async () => {});
|
||||
|
||||
assetMock.getById.mockResolvedValue(assetStub.image1);
|
||||
|
||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1295,14 +1163,6 @@ describe(LibraryService.name, () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not queue a library scan of upload library', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||
|
||||
await expect(sut.queueScan(libraryStub.uploadLibrary1.id, {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(jobMock.queue).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should queue a library scan of all modified assets', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
LibraryResponseDto,
|
||||
LibraryStatsResponseDto,
|
||||
ScanLibraryDto,
|
||||
SearchLibraryDto,
|
||||
UpdateLibraryDto,
|
||||
ValidateLibraryDto,
|
||||
ValidateLibraryImportPathResponseDto,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
mapLibrary,
|
||||
} from 'src/dtos/library.dto';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
@@ -118,10 +117,7 @@ export class LibraryService {
|
||||
}
|
||||
|
||||
const library = await this.findOrFail(id);
|
||||
|
||||
if (library.type !== LibraryType.EXTERNAL) {
|
||||
throw new BadRequestException('Can only watch external libraries');
|
||||
} else if (library.importPaths.length === 0) {
|
||||
if (library.importPaths.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -212,8 +208,7 @@ export class LibraryService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const libraries = await this.repository.getAll(false, LibraryType.EXTERNAL);
|
||||
|
||||
const libraries = await this.repository.getAll(false);
|
||||
for (const library of libraries) {
|
||||
await this.watch(library.id);
|
||||
}
|
||||
@@ -229,8 +224,8 @@ export class LibraryService {
|
||||
return mapLibrary(library);
|
||||
}
|
||||
|
||||
async getAll(dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
|
||||
const libraries = await this.repository.getAll(false, dto.type);
|
||||
async getAll(): Promise<LibraryResponseDto[]> {
|
||||
const libraries = await this.repository.getAll(false);
|
||||
return libraries.map((library) => mapLibrary(library));
|
||||
}
|
||||
|
||||
@@ -244,37 +239,12 @@ export class LibraryService {
|
||||
}
|
||||
|
||||
async create(dto: CreateLibraryDto): Promise<LibraryResponseDto> {
|
||||
switch (dto.type) {
|
||||
case LibraryType.EXTERNAL: {
|
||||
if (!dto.name) {
|
||||
dto.name = 'New External Library';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LibraryType.UPLOAD: {
|
||||
if (!dto.name) {
|
||||
dto.name = 'New Upload Library';
|
||||
}
|
||||
if (dto.importPaths && dto.importPaths.length > 0) {
|
||||
throw new BadRequestException('Upload libraries cannot have import paths');
|
||||
}
|
||||
if (dto.exclusionPatterns && dto.exclusionPatterns.length > 0) {
|
||||
throw new BadRequestException('Upload libraries cannot have exclusion patterns');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const library = await this.repository.create({
|
||||
ownerId: dto.ownerId,
|
||||
name: dto.name,
|
||||
type: dto.type,
|
||||
name: dto.name ?? 'New External Library',
|
||||
importPaths: dto.importPaths ?? [],
|
||||
exclusionPatterns: dto.exclusionPatterns ?? [],
|
||||
});
|
||||
|
||||
this.logger.log(`Creating ${dto.type} library for ${dto.ownerId}}`);
|
||||
|
||||
return mapLibrary(library);
|
||||
}
|
||||
|
||||
@@ -362,11 +332,7 @@ export class LibraryService {
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const library = await this.findOrFail(id);
|
||||
const uploadCount = await this.repository.getUploadLibraryCount(library.ownerId);
|
||||
if (library.type === LibraryType.UPLOAD && uploadCount <= 1) {
|
||||
throw new BadRequestException('Cannot delete the last upload library');
|
||||
}
|
||||
await this.findOrFail(id);
|
||||
|
||||
if (this.watchLibraries) {
|
||||
await this.unwatch(id);
|
||||
@@ -529,10 +495,7 @@ export class LibraryService {
|
||||
}
|
||||
|
||||
async queueScan(id: string, dto: ScanLibraryDto) {
|
||||
const library = await this.findOrFail(id);
|
||||
if (library.type !== LibraryType.EXTERNAL) {
|
||||
throw new BadRequestException('Can only refresh external libraries');
|
||||
}
|
||||
await this.findOrFail(id);
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.LIBRARY_SCAN,
|
||||
@@ -556,7 +519,7 @@ export class LibraryService {
|
||||
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} });
|
||||
|
||||
// Queue all library refresh
|
||||
const libraries = await this.repository.getAll(true, LibraryType.EXTERNAL);
|
||||
const libraries = await this.repository.getAll(true);
|
||||
await this.jobRepository.queueAll(
|
||||
libraries.map((library) => ({
|
||||
name: JobName.LIBRARY_SCAN,
|
||||
@@ -587,8 +550,8 @@ export class LibraryService {
|
||||
|
||||
async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise<JobStatus> {
|
||||
const library = await this.repository.get(job.id);
|
||||
if (!library || library.type !== LibraryType.EXTERNAL) {
|
||||
this.logger.warn('Can only refresh external libraries');
|
||||
if (!library) {
|
||||
this.logger.warn('Library not found');
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
|
||||
@@ -409,7 +409,7 @@ export class MetadataService {
|
||||
}
|
||||
const checksum = this.cryptoRepository.hashSha1(video);
|
||||
|
||||
let motionAsset = await this.assetRepository.getByChecksum(asset.libraryId, checksum);
|
||||
let motionAsset = await this.assetRepository.getByChecksum(asset.libraryId ?? null, checksum);
|
||||
if (motionAsset) {
|
||||
this.logger.debug(
|
||||
`Asset ${asset.id}'s motion photo video with checksum ${checksum.toString(
|
||||
|
||||
Reference in New Issue
Block a user