mirror of
https://github.com/immich-app/immich.git
synced 2026-02-11 11:27:56 +03:00
Merge branch 'main' of https://github.com/immich-app/immich into feat/xxhash
This commit is contained in:
@@ -15,9 +15,6 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
|
||||
export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS));
|
||||
export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO));
|
||||
|
||||
export interface MoveRequest {
|
||||
entityId: string;
|
||||
pathType: PathType;
|
||||
@@ -118,10 +115,6 @@ export class StorageCore {
|
||||
return normalizedPath.startsWith(normalizedAppMediaLocation);
|
||||
}
|
||||
|
||||
static isGeneratedAsset(path: string) {
|
||||
return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR);
|
||||
}
|
||||
|
||||
async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
|
||||
const { id: entityId, files } = asset;
|
||||
const { thumbnailFile, previewFile } = getAssetFiles(files);
|
||||
|
||||
@@ -36,7 +36,7 @@ export class AssetFileEntity {
|
||||
@Column()
|
||||
path!: string;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
@Column({ type: 'bytea' })
|
||||
@Index()
|
||||
checksum!: BigInt | null;
|
||||
checksum!: Buffer | null;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export interface UpsertFileOptions {
|
||||
assetId: string;
|
||||
type: AssetFileType;
|
||||
path: string;
|
||||
checksum?: BigInt;
|
||||
checksum?: Buffer;
|
||||
}
|
||||
|
||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
||||
@@ -173,12 +173,6 @@ export interface IAssetRepository {
|
||||
order?: FindOptionsOrder<AssetEntity>,
|
||||
): Promise<AssetEntity | null>;
|
||||
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
||||
getWith(
|
||||
pagination: PaginationOptions,
|
||||
property: WithProperty,
|
||||
libraryId?: string,
|
||||
withDeleted?: boolean,
|
||||
): Paginated<AssetEntity>;
|
||||
getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>;
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface ICryptoRepository {
|
||||
randomUUID(): string;
|
||||
hashFile(filePath: string | Buffer): Promise<Buffer>;
|
||||
hashSha256(data: string): string;
|
||||
xxHash(value: string): BigInt;
|
||||
xxHash(value: string): Buffer;
|
||||
verifySha256(data: string, encrypted: string, publicKey: string): boolean;
|
||||
hashSha1(data: string | Buffer): Buffer;
|
||||
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
|
||||
|
||||
@@ -48,7 +48,6 @@ export interface IDatabaseRepository {
|
||||
getPostgresVersion(): Promise<string>;
|
||||
getPostgresVersionRange(): string;
|
||||
createExtension(extension: DatabaseExtension): Promise<void>;
|
||||
updateExtension(extension: DatabaseExtension, version?: string): Promise<void>;
|
||||
updateVectorExtension(extension: VectorExtension, version?: string): Promise<VectorUpdateResult>;
|
||||
reindex(index: VectorIndex): Promise<void>;
|
||||
shouldReindex(name: VectorIndex): Promise<boolean>;
|
||||
|
||||
@@ -28,5 +28,4 @@ export interface IMapRepository {
|
||||
init(): Promise<void>;
|
||||
reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult>;
|
||||
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||
fetchStyle(url: string): Promise<any>;
|
||||
}
|
||||
|
||||
@@ -57,9 +57,7 @@ export interface IPersonRepository {
|
||||
|
||||
create(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
|
||||
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
||||
delete(entities: PersonEntity[]): Promise<void>;
|
||||
deleteAll(): Promise<void>;
|
||||
deleteFaces(options: DeleteFacesOptions): Promise<void>;
|
||||
refreshFaces(
|
||||
facesToAdd: Partial<AssetFaceEntity>[],
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface ImmichFile extends Express.Multer.File {
|
||||
/** sha1 hash of file */
|
||||
uuid: string;
|
||||
checksum: Buffer;
|
||||
xxhash: BigInt;
|
||||
xxhash: Buffer;
|
||||
}
|
||||
|
||||
export function mapToUploadFile(file: ImmichFile): UploadFile {
|
||||
@@ -150,7 +150,7 @@ export class FileUploadInterceptor implements NestInterceptor {
|
||||
}
|
||||
|
||||
this.logger.debug(`Handling asset upload file: ${file.originalname}`);
|
||||
const xxhash = new xxh3.Xxh3();
|
||||
const xxhash = xxh3.Xxh3.withSeed();
|
||||
const sha1hash = createHash('sha1');
|
||||
|
||||
file.stream.on('data', (chunk) => {
|
||||
@@ -164,7 +164,11 @@ export class FileUploadInterceptor implements NestInterceptor {
|
||||
xxhash.reset();
|
||||
callback(error);
|
||||
} else {
|
||||
callback(null, { ...info, checksum: sha1hash.digest(), xxhash: xxhash.digest() });
|
||||
callback(null, {
|
||||
...info,
|
||||
checksum: sha1hash.digest(),
|
||||
xxhash: Buffer.from(xxhash.digest().toString(16), 'utf8'),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ export class AssetFileChecksum1728632095015 implements MigrationInterface {
|
||||
name = 'AssetFileChecksum1728632095015';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "asset_files" ADD "checksum" bigint`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c946066edd16cfa5c25a26aa8e" ON "asset_files" ("checksum") `);
|
||||
await queryRunner.query(`ALTER TABLE "asset_files" ADD "checksum" bytea NULL`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c946066edd16cfa5c25a26aa8e" ON "asset_files" ("checksum")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
@@ -499,39 +499,6 @@ export class AssetRepository implements IAssetRepository {
|
||||
});
|
||||
}
|
||||
|
||||
getWith(
|
||||
pagination: PaginationOptions,
|
||||
property: WithProperty,
|
||||
libraryId?: string,
|
||||
withDeleted = false,
|
||||
): Paginated<AssetEntity> {
|
||||
let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
|
||||
|
||||
switch (property) {
|
||||
case WithProperty.SIDECAR: {
|
||||
where = [{ sidecarPath: Not(IsNull()), isVisible: true }];
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Invalid getWith property: ${property}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (libraryId) {
|
||||
where = [{ ...where, libraryId }];
|
||||
}
|
||||
|
||||
return paginate(this.repository, pagination, {
|
||||
where,
|
||||
withDeleted,
|
||||
order: {
|
||||
// Ensures correct order when paginating
|
||||
createdAt: 'ASC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: { albums: { id: albumId } },
|
||||
@@ -801,7 +768,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] })
|
||||
async upsertFile(file: { assetId: string; type: AssetFileType; path: string; checksum?: BigInt }): Promise<void> {
|
||||
async upsertFile(file: { assetId: string; type: AssetFileType; path: string; checksum?: Buffer }): Promise<void> {
|
||||
await this.fileRepository.upsert(file, { conflictPaths: ['assetId', 'type'] });
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export class CryptoRepository implements ICryptoRepository {
|
||||
}
|
||||
|
||||
xxHash(value: string) {
|
||||
return xxh3.Xxh3.withSeed().update(value).digest();
|
||||
return Buffer.from(xxh3.Xxh3.withSeed().update(value).digest().toString(16), 'utf8');
|
||||
}
|
||||
|
||||
verifySha256(value: string, encryptedValue: string, publicKey: string) {
|
||||
|
||||
@@ -74,10 +74,6 @@ export class DatabaseRepository implements IDatabaseRepository {
|
||||
await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`);
|
||||
}
|
||||
|
||||
async updateExtension(extension: DatabaseExtension, version?: string): Promise<void> {
|
||||
await this.dataSource.query(`ALTER EXTENSION ${extension} UPDATE${version ? ` TO '${version}'` : ''}`);
|
||||
}
|
||||
|
||||
async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise<VectorUpdateResult> {
|
||||
const { availableVersion, installedVersion } = await this.getExtensionVersion(extension);
|
||||
if (!installedVersion) {
|
||||
|
||||
@@ -113,20 +113,6 @@ export class MapRepository implements IMapRepository {
|
||||
}));
|
||||
}
|
||||
|
||||
async fetchStyle(url: string) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch data from ${url} with status ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch data from ${url}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
||||
this.logger.debug(`Request: ${point.latitude},${point.longitude}`);
|
||||
|
||||
|
||||
@@ -63,10 +63,6 @@ export class PersonRepository implements IPersonRepository {
|
||||
await this.personRepository.remove(entities);
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
await this.personRepository.clear();
|
||||
}
|
||||
|
||||
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
||||
await this.assetFaceRepository
|
||||
.createQueryBuilder('asset_faces')
|
||||
@@ -269,11 +265,6 @@ export class PersonRepository implements IPersonRepository {
|
||||
return results.map((person) => person.id);
|
||||
}
|
||||
|
||||
async createFaces(entities: AssetFaceEntity[]): Promise<string[]> {
|
||||
const res = await this.assetFaceRepository.save(entities);
|
||||
return res.map((row) => row.id);
|
||||
}
|
||||
|
||||
async refreshFaces(
|
||||
facesToAdd: Partial<AssetFaceEntity>[],
|
||||
faceIdsToRemove: string[],
|
||||
|
||||
@@ -14,12 +14,11 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ActivityEntity } from 'src/entities/activity.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class ActivityService extends BaseService {
|
||||
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
const activities = await this.activityRepository.search({
|
||||
userId: dto.userId,
|
||||
albumId: dto.albumId,
|
||||
@@ -31,12 +30,12 @@ export class ActivityService extends BaseService {
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) };
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] });
|
||||
|
||||
const common = {
|
||||
userId: auth.user.id,
|
||||
@@ -70,7 +69,7 @@ export class ActivityService extends BaseService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
|
||||
await this.activityRepository.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -82,7 +81,7 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [id] });
|
||||
await this.albumRepository.updateThumbnails();
|
||||
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
|
||||
const album = await this.findOrFail(id, { withAssets });
|
||||
@@ -106,7 +105,7 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const allowedAssetIdsSet = await checkAccess(this.accessRepository, {
|
||||
const allowedAssetIdsSet = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: dto.assetIds || [],
|
||||
@@ -130,7 +129,7 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_UPDATE, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: true });
|
||||
|
||||
@@ -153,13 +152,13 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_DELETE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_DELETE, ids: [id] });
|
||||
await this.albumRepository.delete(id);
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] });
|
||||
|
||||
const results = await addAssets(
|
||||
auth,
|
||||
@@ -182,7 +181,7 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
const results = await removeAssets(
|
||||
@@ -203,7 +202,7 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, { withAssets: false });
|
||||
|
||||
@@ -247,14 +246,14 @@ export class AlbumService extends BaseService {
|
||||
|
||||
// non-admin can remove themselves
|
||||
if (auth.user.id !== userId) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
}
|
||||
|
||||
await this.albumUserRepository.delete({ albumId: id, userId });
|
||||
}
|
||||
|
||||
async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] });
|
||||
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
||||
}
|
||||
|
||||
|
||||
@@ -539,6 +539,7 @@ describe(AssetMediaService.name, () => {
|
||||
path: '/path/to/preview',
|
||||
type: AssetFileType.THUMBNAIL,
|
||||
updatedAt: new Date(),
|
||||
checksum: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -559,6 +560,7 @@ describe(AssetMediaService.name, () => {
|
||||
path: '/path/to/preview.jpg',
|
||||
type: AssetFileType.PREVIEW,
|
||||
updatedAt: new Date(),
|
||||
checksum: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entit
|
||||
import { AssetFileType, AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
|
||||
import { JobName } from 'src/interfaces/job.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess, requireUploadAccess } from 'src/utils/access';
|
||||
import { requireUploadAccess } from 'src/utils/access';
|
||||
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
@@ -39,7 +39,7 @@ export interface UploadRequest {
|
||||
export interface UploadFile {
|
||||
uuid: string;
|
||||
checksum: Buffer;
|
||||
xxhash: BigInt;
|
||||
xxhash?: Buffer;
|
||||
originalPath: string;
|
||||
originalName: string;
|
||||
size: number;
|
||||
@@ -126,7 +126,7 @@ export class AssetMediaService extends BaseService {
|
||||
sidecarFile?: UploadFile,
|
||||
): Promise<AssetMediaResponseDto> {
|
||||
try {
|
||||
await requireAccess(this.accessRepository, {
|
||||
await this.requireAccess({
|
||||
auth,
|
||||
permission: Permission.ASSET_UPLOAD,
|
||||
// do not need an id here, but the interface requires it
|
||||
@@ -160,7 +160,7 @@ export class AssetMediaService extends BaseService {
|
||||
sidecarFile?: UploadFile,
|
||||
): Promise<AssetMediaResponseDto> {
|
||||
try {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||
const asset = (await this.assetRepository.getById(id)) as AssetEntity;
|
||||
|
||||
this.requireQuota(auth, file.size);
|
||||
@@ -183,7 +183,7 @@ export class AssetMediaService extends BaseService {
|
||||
}
|
||||
|
||||
async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
|
||||
@@ -195,7 +195,7 @@ export class AssetMediaService extends BaseService {
|
||||
}
|
||||
|
||||
async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
const size = dto.size ?? AssetMediaSize.THUMBNAIL;
|
||||
@@ -218,7 +218,7 @@ export class AssetMediaService extends BaseService {
|
||||
}
|
||||
|
||||
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] });
|
||||
|
||||
const asset = await this.findOrFail(id);
|
||||
|
||||
@@ -342,8 +342,6 @@ export class AssetMediaService extends BaseService {
|
||||
checksum: file.xxhash,
|
||||
});
|
||||
|
||||
console.log('xxhash', file.xxhash);
|
||||
|
||||
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
||||
await this.assetRepository.upsertExif({ assetId, fileSizeInByte: file.size });
|
||||
await this.jobRepository.queue({
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
JobStatus,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
||||
@@ -86,7 +85,7 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [id] });
|
||||
|
||||
const asset = await this.assetRepository.getById(
|
||||
id,
|
||||
@@ -135,7 +134,7 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||
|
||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||
@@ -178,7 +177,7 @@ export class AssetService extends BaseService {
|
||||
|
||||
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
|
||||
|
||||
for (const id of ids) {
|
||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
||||
@@ -275,7 +274,7 @@ export class AssetService extends BaseService {
|
||||
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
|
||||
const { ids, force } = dto;
|
||||
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DELETE, ids });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
|
||||
await this.assetRepository.updateAll(ids, {
|
||||
deletedAt: new Date(),
|
||||
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
|
||||
@@ -284,7 +283,7 @@ export class AssetService extends BaseService {
|
||||
}
|
||||
|
||||
async run(auth: AuthDto, dto: AssetJobsDto) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||
|
||||
const jobs: JobItem[] = [];
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from 'src/enum';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
||||
@@ -36,7 +35,7 @@ export class AuditService extends BaseService {
|
||||
|
||||
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
||||
const userId = dto.userId || auth.user.id;
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||
|
||||
const audits = await this.auditRepository.getAfter(dto.after, {
|
||||
userIds: [userId],
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { sessionStub } from 'test/fixtures/session.stub';
|
||||
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
@@ -21,6 +21,16 @@ import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
const oauthResponse = {
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
userEmail: 'immich@test.com',
|
||||
name: 'immich_name',
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
};
|
||||
|
||||
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
|
||||
|
||||
const email = 'test@immich.com';
|
||||
@@ -100,7 +110,15 @@ describe('AuthService', () => {
|
||||
it('should successfully log the user in', async () => {
|
||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
|
||||
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual({
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
userEmail: 'immich@test.com',
|
||||
name: 'immich_name',
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
});
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -469,7 +487,7 @@ describe('AuthService', () => {
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
loginResponseStub.user1oauth,
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
@@ -498,7 +516,7 @@ describe('AuthService', () => {
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
loginResponseStub.user1oauth,
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(2); // second call is for domain check before create
|
||||
@@ -546,7 +564,7 @@ describe('AuthService', () => {
|
||||
userMock.create.mockResolvedValue(userStub.user1);
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
loginResponseStub.user1oauth,
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
||||
@@ -560,7 +578,7 @@ describe('AuthService', () => {
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
loginResponseStub.user1oauth,
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
||||
@@ -574,7 +592,7 @@ describe('AuthService', () => {
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
loginResponseStub.user1oauth,
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith(oauthUserWithDefaultQuota);
|
||||
@@ -588,7 +606,7 @@ describe('AuthService', () => {
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
loginResponseStub.user1oauth,
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith({
|
||||
@@ -608,7 +626,7 @@ describe('AuthService', () => {
|
||||
oauthMock.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
|
||||
|
||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||
loginResponseStub.user1oauth,
|
||||
oauthResponse,
|
||||
);
|
||||
|
||||
expect(userMock.create).toHaveBeenCalledWith({
|
||||
|
||||
@@ -38,6 +38,7 @@ import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { getConfig, updateConfig } from 'src/utils/config';
|
||||
|
||||
export class BaseService {
|
||||
@@ -95,7 +96,7 @@ export class BaseService {
|
||||
);
|
||||
}
|
||||
|
||||
private get repos() {
|
||||
private get configRepos() {
|
||||
return {
|
||||
configRepo: this.configRepository,
|
||||
metadataRepo: this.systemMetadataRepository,
|
||||
@@ -104,10 +105,18 @@ export class BaseService {
|
||||
}
|
||||
|
||||
getConfig(options: { withCache: boolean }) {
|
||||
return getConfig(this.repos, options);
|
||||
return getConfig(this.configRepos, options);
|
||||
}
|
||||
|
||||
updateConfig(newConfig: SystemConfig) {
|
||||
return updateConfig(this.repos, newConfig);
|
||||
return updateConfig(this.configRepos, newConfig);
|
||||
}
|
||||
|
||||
requireAccess(request: AccessRequest) {
|
||||
return requireAccess(this.accessRepository, request);
|
||||
}
|
||||
|
||||
checkAccess(request: AccessRequest) {
|
||||
return checkAccess(this.accessRepository, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
@@ -8,9 +9,18 @@ describe(CliService.name, () => {
|
||||
let sut: CliService;
|
||||
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, userMock } = newTestService(CliService));
|
||||
({ sut, userMock, systemMock } = newTestService(CliService));
|
||||
});
|
||||
|
||||
describe('listUsers', () => {
|
||||
it('should list users', async () => {
|
||||
userMock.getList.mockResolvedValue([userStub.admin]);
|
||||
await expect(sut.listUsers()).resolves.toEqual([expect.objectContaining({ isAdmin: true })]);
|
||||
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAdminPassword', () => {
|
||||
@@ -51,4 +61,32 @@ describe(CliService.name, () => {
|
||||
expect(update.password).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('disablePasswordLogin', () => {
|
||||
it('should disable password login', async () => {
|
||||
await sut.disablePasswordLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', { passwordLogin: { enabled: false } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('enablePasswordLogin', () => {
|
||||
it('should enable password login', async () => {
|
||||
await sut.enablePasswordLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableOAuthLogin', () => {
|
||||
it('should disable oauth login', async () => {
|
||||
await sut.disableOAuthLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('enableOAuthLogin', () => {
|
||||
it('should enable oauth login', async () => {
|
||||
await sut.enableOAuthLogin();
|
||||
expect(systemMock.set).toHaveBeenCalledWith('system-config', { oauth: { enabled: true } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,19 +43,246 @@ describe(DatabaseService.name, () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
||||
describe('onBootstrap', () => {
|
||||
it('should throw an error if PostgreSQL version is below minimum supported version', async () => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0');
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0');
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0');
|
||||
|
||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
|
||||
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
|
||||
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
|
||||
])('should work with $extensionName', ({ extension, extensionName }) => {
|
||||
beforeEach(() => {
|
||||
describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
|
||||
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] },
|
||||
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] },
|
||||
])('should work with $extensionName', ({ extension, extensionName }) => {
|
||||
beforeEach(() => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: false,
|
||||
vectorExtension: extension,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should start up successfully with ${extension}`, async () => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValue('14.0.0');
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledWith(extension);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension is not installed`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
|
||||
const message = `The ${extensionName} extension is not available in this Postgres instance.
|
||||
If using a container image, ensure the image has the extension installed.`;
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(message);
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: versionBelowRange,
|
||||
availableVersion: versionBelowRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
|
||||
);
|
||||
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if ${extension} extension version is a nightly`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
|
||||
);
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should do in-range update for ${extension} extension`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should not upgrade ${extension} if same version`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is below range`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionBelowRange,
|
||||
installedVersion: null,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is above range`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionAboveRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if available version is below installed version', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: updateInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
|
||||
);
|
||||
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if installed version is not in version range', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: versionAboveRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is ${versionAboveRange}, but Immich only supports`,
|
||||
);
|
||||
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should raise error if ${extension} extension upgrade failed`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension');
|
||||
|
||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(
|
||||
`The ${extensionName} extension can be updated to ${updateInRange}.`,
|
||||
);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should warn if ${extension} extension update requires restart`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true });
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName);
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should reindex ${extension} indices if needed`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if reindexing fails`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(true);
|
||||
databaseMock.reindex.mockRejectedValue(new Error('Error reindexing'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toBeDefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Could not run vector reindexing checks.'),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should not reindex ${extension} indices if not needed`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(false);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(0);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
@@ -64,261 +291,112 @@ describe(DatabaseService.name, () => {
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: false,
|
||||
vectorExtension: extension,
|
||||
skipMigrations: true,
|
||||
vectorExtension: DatabaseExtension.VECTORS,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it(`should start up successfully with ${extension}`, async () => {
|
||||
databaseMock.getPostgresVersion.mockResolvedValue('14.0.0');
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.getPostgresVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledWith(extension);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension is not installed`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
|
||||
const message = `The ${extensionName} extension is not available in this Postgres instance.
|
||||
If using a container image, ensure the image has the extension installed.`;
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(message);
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: versionBelowRange,
|
||||
availableVersion: versionBelowRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
|
||||
it(`should throw error if pgvector extension could not be created`, async () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: true,
|
||||
vectorExtension: DatabaseExtension.VECTOR,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if ${extension} extension version is a nightly`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
|
||||
);
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should do in-range update for ${extension} extension`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should not upgrade ${extension} if same version`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||
|
||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||
`Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`,
|
||||
);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is below range`, async () => {
|
||||
it(`should throw error if pgvecto.rs extension could not be created`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionBelowRange,
|
||||
installedVersion: null,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is above range`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionAboveRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
expect(databaseMock.createExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if available version is below installed version', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: updateInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||
|
||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||
`Alternatively, if your Postgres instance has pgvector, you may use this instead`,
|
||||
);
|
||||
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should raise error if ${extension} extension upgrade failed`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension');
|
||||
|
||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(
|
||||
`The ${extensionName} extension can be updated to ${updateInRange}.`,
|
||||
);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should warn if ${extension} extension update requires restart`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true });
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(loggerMock.warn).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName);
|
||||
expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should reindex ${extension} indices if needed`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should not reindex ${extension} indices if not needed`, async () => {
|
||||
databaseMock.shouldReindex.mockResolvedValue(false);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2);
|
||||
expect(databaseMock.reindex).toHaveBeenCalledTimes(0);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: true,
|
||||
vectorExtension: DatabaseExtension.VECTORS,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if pgvector extension could not be created`, async () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: true,
|
||||
vectorExtension: DatabaseExtension.VECTOR,
|
||||
},
|
||||
}),
|
||||
);
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
describe('handleConnectionError', () => {
|
||||
beforeAll(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||
|
||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||
`Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`,
|
||||
);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw error if pgvecto.rs extension could not be created`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||
it('should not override interval', () => {
|
||||
sut.handleConnectionError(new Error('Error'));
|
||||
expect(loggerMock.error).toHaveBeenCalled();
|
||||
|
||||
expect(loggerMock.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.fatal.mock.calls[0][0]).toContain(
|
||||
`Alternatively, if your Postgres instance has pgvector, you may use this instead`,
|
||||
);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
sut.handleConnectionError(new Error('foo'));
|
||||
expect(loggerMock.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should reconnect when interval elapses', async () => {
|
||||
databaseMock.reconnect.mockResolvedValue(true);
|
||||
|
||||
sut.handleConnectionError(new Error('error'));
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should try again when reconnection fails', async () => {
|
||||
databaseMock.reconnect.mockResolvedValueOnce(false);
|
||||
|
||||
sut.handleConnectionError(new Error('error'));
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(1);
|
||||
expect(loggerMock.warn).toHaveBeenCalledWith(expect.stringContaining('Database connection failed'));
|
||||
|
||||
databaseMock.reconnect.mockResolvedValueOnce(true);
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
expect(databaseMock.reconnect).toHaveBeenCalledTimes(2);
|
||||
expect(loggerMock.log).toHaveBeenCalledWith('Database reconnected');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
@@ -62,7 +61,7 @@ export class DownloadService extends BaseService {
|
||||
}
|
||||
|
||||
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds });
|
||||
|
||||
const zip = this.storageRepository.createZipStream();
|
||||
const assets = await this.assetRepository.getByIds(dto.assetIds);
|
||||
@@ -105,20 +104,20 @@ export class DownloadService extends BaseService {
|
||||
|
||||
if (dto.assetIds) {
|
||||
const assetIds = dto.assetIds;
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds });
|
||||
const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
|
||||
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
|
||||
}
|
||||
|
||||
if (dto.albumId) {
|
||||
const albumId = dto.albumId;
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] });
|
||||
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
|
||||
}
|
||||
|
||||
if (dto.userId) {
|
||||
const userId = dto.userId;
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
|
||||
await this.requireAccess({ auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] });
|
||||
return usePagination(PAGINATION_SIZE, (pagination) =>
|
||||
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
|
||||
);
|
||||
|
||||
@@ -141,8 +141,6 @@ describe(LibraryService.name, () => {
|
||||
|
||||
describe('handleQueueAssetRefresh', () => {
|
||||
it('should queue refresh of a new asset', async () => {
|
||||
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
|
||||
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
|
||||
storageMock.walk.mockImplementation(mockWalk);
|
||||
|
||||
@@ -179,8 +177,6 @@ describe(LibraryService.name, () => {
|
||||
|
||||
storageMock.checkFileExists.mockResolvedValue(true);
|
||||
|
||||
assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false });
|
||||
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
|
||||
await sut.handleQueueSyncFiles({ id: libraryStub.externalLibraryWithImportPaths1.id });
|
||||
|
||||
@@ -341,7 +341,10 @@ export class LibraryService extends BaseService {
|
||||
|
||||
this.logger.debug(`Will delete all assets in library ${libraryId}`);
|
||||
for await (const assets of assetPagination) {
|
||||
assetsFound = true;
|
||||
if (assets.length > 0) {
|
||||
assetsFound = true;
|
||||
}
|
||||
|
||||
this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`);
|
||||
await this.jobRepository.queueAll(
|
||||
assets.map((asset) => ({
|
||||
@@ -544,32 +547,32 @@ export class LibraryService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (validImportPaths) {
|
||||
const assetsOnDisk = this.storageRepository.walk({
|
||||
pathsToCrawl: validImportPaths,
|
||||
includeHidden: false,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
take: JOBS_LIBRARY_PAGINATION_SIZE,
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
for await (const assetBatch of assetsOnDisk) {
|
||||
count += assetBatch.length;
|
||||
this.logger.debug(`Discovered ${count} asset(s) on disk for library ${library.id}...`);
|
||||
await this.syncFiles(library, assetBatch);
|
||||
this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`);
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
this.logger.debug(`Finished queueing scan of ${count} assets on disk for library ${library.id}`);
|
||||
} else {
|
||||
this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`);
|
||||
}
|
||||
} else {
|
||||
if (validImportPaths.length === 0) {
|
||||
this.logger.warn(`No valid import paths found for library ${library.id}`);
|
||||
}
|
||||
|
||||
const assetsOnDisk = this.storageRepository.walk({
|
||||
pathsToCrawl: validImportPaths,
|
||||
includeHidden: false,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
take: JOBS_LIBRARY_PAGINATION_SIZE,
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
for await (const assetBatch of assetsOnDisk) {
|
||||
count += assetBatch.length;
|
||||
this.logger.debug(`Discovered ${count} asset(s) on disk for library ${library.id}...`);
|
||||
await this.syncFiles(library, assetBatch);
|
||||
this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`);
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
this.logger.debug(`Finished queueing scan of ${count} assets on disk for library ${library.id}`);
|
||||
} else if (validImportPaths.length > 0) {
|
||||
this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`);
|
||||
}
|
||||
|
||||
await this.libraryRepository.update({ id: job.id, refreshedAt: new Date() });
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from '
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -16,7 +15,7 @@ export class MemoryService extends BaseService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||
const memory = await this.findOrFail(id);
|
||||
return mapMemory(memory);
|
||||
}
|
||||
@@ -25,7 +24,7 @@ export class MemoryService extends BaseService {
|
||||
// TODO validate type/data combination
|
||||
|
||||
const assetIds = dto.assetIds || [];
|
||||
const allowedAssetIds = await checkAccess(this.accessRepository, {
|
||||
const allowedAssetIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: assetIds,
|
||||
@@ -44,7 +43,7 @@ export class MemoryService extends BaseService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||
|
||||
const memory = await this.memoryRepository.update({
|
||||
id,
|
||||
@@ -57,12 +56,12 @@ export class MemoryService extends BaseService {
|
||||
}
|
||||
|
||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_DELETE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.MEMORY_DELETE, ids: [id] });
|
||||
await this.memoryRepository.delete(id);
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||
|
||||
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
||||
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
|
||||
@@ -76,7 +75,7 @@ export class MemoryService extends BaseService {
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
|
||||
|
||||
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
||||
const results = await removeAssets(auth, repos, {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class PartnerService extends BaseService {
|
||||
@@ -41,7 +40,7 @@ export class PartnerService extends BaseService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
|
||||
await this.requireAccess({ auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] });
|
||||
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
|
||||
|
||||
const entity = await this.partnerRepository.update({ ...partnerId, inTimeline: dto.inTimeline });
|
||||
|
||||
@@ -721,7 +721,6 @@ describe(PersonService.name, () => {
|
||||
'/uploads/user-id/thumbs/path.jpg',
|
||||
expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }),
|
||||
);
|
||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
|
||||
@@ -733,7 +732,6 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should create a face with no person and queue recognition job', async () => {
|
||||
personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
|
||||
machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
|
||||
@@ -761,7 +759,6 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should add new face and delete an existing face not among the new detected faces', async () => {
|
||||
personMock.createFaces.mockResolvedValue([faceStub.face1.id]);
|
||||
machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
|
||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]);
|
||||
|
||||
@@ -816,7 +813,6 @@ describe(PersonService.name, () => {
|
||||
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
expect(personMock.create).not.toHaveBeenCalled();
|
||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if face does not have asset', async () => {
|
||||
@@ -827,7 +823,6 @@ describe(PersonService.name, () => {
|
||||
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
expect(personMock.create).not.toHaveBeenCalled();
|
||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip if face already has an assigned person', async () => {
|
||||
@@ -837,7 +832,6 @@ describe(PersonService.name, () => {
|
||||
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
expect(personMock.create).not.toHaveBeenCalled();
|
||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should match existing person', async () => {
|
||||
|
||||
@@ -47,7 +47,6 @@ import { BoundingBox } from 'src/interfaces/machine-learning.interface';
|
||||
import { CropOptions, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface';
|
||||
import { UpdateFacesData } from 'src/interfaces/person.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
@@ -80,7 +79,7 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||
const person = await this.findOrFail(personId);
|
||||
const result: PersonResponseDto[] = [];
|
||||
const changeFeaturePhoto: string[] = [];
|
||||
@@ -88,7 +87,7 @@ export class PersonService extends BaseService {
|
||||
const faces = await this.personRepository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
|
||||
|
||||
for (const face of faces) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [face.id] });
|
||||
if (person.faceAssetId === null) {
|
||||
changeFeaturePhoto.push(person.id);
|
||||
}
|
||||
@@ -109,8 +108,8 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
|
||||
const face = await this.personRepository.getFaceById(dto.id);
|
||||
const person = await this.findOrFail(personId);
|
||||
|
||||
@@ -126,7 +125,7 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [dto.id] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [dto.id] });
|
||||
const faces = await this.personRepository.getFaces(dto.id);
|
||||
return faces.map((asset) => mapFaces(asset, auth));
|
||||
}
|
||||
@@ -150,17 +149,17 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
return this.personRepository.getStatistics(id);
|
||||
}
|
||||
|
||||
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
const person = await this.personRepository.getById(id);
|
||||
if (!person || !person.thumbnailPath) {
|
||||
throw new NotFoundException();
|
||||
@@ -183,13 +182,13 @@ export class PersonService extends BaseService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
if (assetId) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [assetId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [assetId] });
|
||||
const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]);
|
||||
if (!face) {
|
||||
throw new BadRequestException('Invalid assetId for feature face');
|
||||
@@ -584,13 +583,13 @@ export class PersonService extends BaseService {
|
||||
throw new BadRequestException('Cannot merge a person into themselves');
|
||||
}
|
||||
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
let primaryPerson = await this.findOrFail(id);
|
||||
const primaryName = primaryPerson.name || primaryPerson.id;
|
||||
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
|
||||
const allowedIds = await checkAccess(this.accessRepository, {
|
||||
const allowedIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.PERSON_MERGE,
|
||||
ids: mergeIds,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class SessionService extends BaseService {
|
||||
@@ -34,7 +33,7 @@ export class SessionService extends BaseService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] });
|
||||
await this.sessionRepository.delete(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { Permission, SharedLinkType } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { OpenGraphTags } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
@@ -49,7 +48,7 @@ export class SharedLinkService extends BaseService {
|
||||
if (!dto.albumId) {
|
||||
throw new BadRequestException('Invalid albumId');
|
||||
}
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] });
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -58,7 +57,7 @@ export class SharedLinkService extends BaseService {
|
||||
throw new BadRequestException('Invalid assetIds');
|
||||
}
|
||||
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds });
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -119,7 +118,7 @@ export class SharedLinkService extends BaseService {
|
||||
|
||||
const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
|
||||
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
|
||||
const allowedAssetIds = await checkAccess(this.accessRepository, {
|
||||
const allowedAssetIds = await this.checkAccess({
|
||||
auth,
|
||||
permission: Permission.ASSET_SHARE,
|
||||
ids: notPresentAssetIds,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
export class StackService extends BaseService {
|
||||
@@ -18,7 +17,7 @@ export class StackService extends BaseService {
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
|
||||
|
||||
const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds });
|
||||
|
||||
@@ -28,13 +27,13 @@ export class StackService extends BaseService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.STACK_READ, ids: [id] });
|
||||
const stack = await this.findOrFail(id);
|
||||
return mapStack(stack, { auth });
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.STACK_UPDATE, ids: [id] });
|
||||
const stack = await this.findOrFail(id);
|
||||
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
|
||||
throw new BadRequestException('Primary asset must be in the stack');
|
||||
@@ -48,13 +47,13 @@ export class StackService extends BaseService {
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_DELETE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: [id] });
|
||||
await this.stackRepository.delete(id);
|
||||
await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_DELETE, ids: dto.ids });
|
||||
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: dto.ids });
|
||||
await this.stackRepository.deleteAll(dto.ids);
|
||||
await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id });
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
|
||||
import { DatabaseAction, EntityType, Permission } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
import { setIsEqual } from 'src/utils/set';
|
||||
|
||||
@@ -15,7 +14,7 @@ export class SyncService extends BaseService {
|
||||
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
|
||||
// mobile implementation is faster if this is a single id
|
||||
const userId = dto.userId || auth.user.id;
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [userId] });
|
||||
const assets = await this.assetRepository.getAllForUserFullSync({
|
||||
ownerId: userId,
|
||||
updatedUntil: dto.updatedUntil,
|
||||
@@ -39,7 +38,7 @@ export class SyncService extends BaseService {
|
||||
return FULL_SYNC;
|
||||
}
|
||||
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
|
||||
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: dto.userIds });
|
||||
|
||||
const limit = 10_000;
|
||||
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Permission } from 'src/enum';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { AssetTagItem } from 'src/interfaces/tag.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
import { upsertTags } from 'src/utils/tag';
|
||||
|
||||
@@ -27,7 +26,7 @@ export class TagService extends BaseService {
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<TagResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [id] });
|
||||
const tag = await this.findOrFail(id);
|
||||
return mapTag(tag);
|
||||
}
|
||||
@@ -35,7 +34,7 @@ export class TagService extends BaseService {
|
||||
async create(auth: AuthDto, dto: TagCreateDto) {
|
||||
let parent: TagEntity | undefined;
|
||||
if (dto.parentId) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] });
|
||||
await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.parentId] });
|
||||
parent = (await this.tagRepository.get(dto.parentId)) || undefined;
|
||||
if (!parent) {
|
||||
throw new BadRequestException('Tag not found');
|
||||
@@ -55,7 +54,7 @@ export class TagService extends BaseService {
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_UPDATE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.TAG_UPDATE, ids: [id] });
|
||||
|
||||
const { color } = dto;
|
||||
const tag = await this.tagRepository.update({ id, color });
|
||||
@@ -68,7 +67,7 @@ export class TagService extends BaseService {
|
||||
}
|
||||
|
||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_DELETE, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.TAG_DELETE, ids: [id] });
|
||||
|
||||
// TODO sync tag changes for affected assets
|
||||
|
||||
@@ -77,8 +76,8 @@ export class TagService extends BaseService {
|
||||
|
||||
async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
|
||||
const [tagIds, assetIds] = await Promise.all([
|
||||
checkAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }),
|
||||
checkAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }),
|
||||
this.checkAccess({ auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }),
|
||||
this.checkAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }),
|
||||
]);
|
||||
|
||||
const items: AssetTagItem[] = [];
|
||||
@@ -97,7 +96,7 @@ export class TagService extends BaseService {
|
||||
}
|
||||
|
||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] });
|
||||
|
||||
const results = await addAssets(
|
||||
auth,
|
||||
@@ -115,7 +114,7 @@ export class TagService extends BaseService {
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: [id] });
|
||||
await this.requireAccess({ auth, permission: Permission.TAG_ASSET, ids: [id] });
|
||||
|
||||
const results = await removeAssets(
|
||||
auth,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dt
|
||||
import { Permission } from 'src/enum';
|
||||
import { TimeBucketOptions } from 'src/interfaces/asset.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
|
||||
export class TimelineService extends BaseService {
|
||||
@@ -48,20 +47,20 @@ export class TimelineService extends BaseService {
|
||||
|
||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||
if (dto.albumId) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
|
||||
} else {
|
||||
dto.userId = dto.userId || auth.user.id;
|
||||
}
|
||||
|
||||
if (dto.userId) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
|
||||
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
|
||||
if (dto.isArchived !== false) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
|
||||
await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.tagId) {
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [dto.tagId] });
|
||||
await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.tagId] });
|
||||
}
|
||||
|
||||
if (dto.withPartners) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { TrashResponseDto } from 'src/dtos/trash.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
import { usePagination } from 'src/utils/pagination';
|
||||
|
||||
export class TrashService extends BaseService {
|
||||
@@ -15,7 +14,7 @@ export class TrashService extends BaseService {
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DELETE, ids });
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
|
||||
await this.trashRepository.restoreAll(ids);
|
||||
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user