You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	chore(web): quota enhancement (#6371)
* chore(web): quota enhancement * show quota in user table * update quota for single user ioption * Add a note how to set unlimited storage * fixed deletion doesn't update quota * refactor relation * fixed test * re-refactor * update sql * fix e2e test * Update server/src/domain/user/user.service.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * revert e2e test --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		| @@ -845,7 +845,16 @@ describe(AssetService.name, () => { | ||||
|     it('should remove faces', async () => { | ||||
|       const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] }; | ||||
|  | ||||
|       when(assetMock.getById).calledWith(assetWithFace.id).mockResolvedValue(assetWithFace); | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetWithFace.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|           exifInfo: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetWithFace); | ||||
|  | ||||
|       await sut.handleAssetDeletion({ id: assetWithFace.id }); | ||||
|  | ||||
| @@ -870,7 +879,16 @@ describe(AssetService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should update stack parent if asset has stack children', async () => { | ||||
|       when(assetMock.getById).calledWith(assetStub.primaryImage.id).mockResolvedValue(assetStub.primaryImage); | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.primaryImage.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|           exifInfo: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.primaryImage); | ||||
|  | ||||
|       await sut.handleAssetDeletion({ id: assetStub.primaryImage.id }); | ||||
|  | ||||
| @@ -883,7 +901,16 @@ describe(AssetService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should not schedule delete-files job for readonly assets', async () => { | ||||
|       when(assetMock.getById).calledWith(assetStub.readOnly.id).mockResolvedValue(assetStub.readOnly); | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.readOnly.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|           exifInfo: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.readOnly); | ||||
|  | ||||
|       await sut.handleAssetDeletion({ id: assetStub.readOnly.id }); | ||||
|  | ||||
| @@ -903,7 +930,16 @@ describe(AssetService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should process assets from external library with fromExternal flag', async () => { | ||||
|       when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external); | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.external.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|           exifInfo: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.external); | ||||
|  | ||||
|       await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true }); | ||||
|  | ||||
| @@ -926,6 +962,27 @@ describe(AssetService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should delete a live photo', async () => { | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.livePhotoStillAsset.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|           exifInfo: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.livePhotoStillAsset); | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.livePhotoMotionAsset.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|           exifInfo: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.livePhotoMotionAsset); | ||||
|  | ||||
|       await sut.handleAssetDeletion({ id: assetStub.livePhotoStillAsset.id }); | ||||
|  | ||||
|       expect(jobMock.queue.mock.calls).toEqual([ | ||||
| @@ -950,7 +1007,16 @@ describe(AssetService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should update usage', async () => { | ||||
|       when(assetMock.getById).calledWith(assetStub.image.id).mockResolvedValue(assetStub.image); | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.image.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|           exifInfo: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.image); | ||||
|       await sut.handleAssetDeletion({ id: assetStub.image.id }); | ||||
|  | ||||
|       expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000); | ||||
| @@ -1005,7 +1071,13 @@ describe(AssetService.name, () => { | ||||
|       accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); | ||||
|  | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.image.id) | ||||
|         .calledWith(assetStub.image.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.image as AssetEntity); | ||||
|  | ||||
|       await sut.updateStackParent(authStub.user1, { | ||||
| @@ -1032,7 +1104,13 @@ describe(AssetService.name, () => { | ||||
|       accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id])); | ||||
|       accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); | ||||
|       when(assetMock.getById) | ||||
|         .calledWith(assetStub.primaryImage.id) | ||||
|         .calledWith(assetStub.primaryImage.id, { | ||||
|           faces: { | ||||
|             person: true, | ||||
|           }, | ||||
|           library: true, | ||||
|           stack: true, | ||||
|         }) | ||||
|         .mockResolvedValue(assetStub.primaryImage as AssetEntity); | ||||
|  | ||||
|       await sut.updateStackParent(authStub.user1, { | ||||
| @@ -1042,7 +1120,9 @@ describe(AssetService.name, () => { | ||||
|  | ||||
|       expect(assetMock.updateAll).toBeCalledWith( | ||||
|         [assetStub.primaryImage.id, 'stack-child-asset-1', 'stack-child-asset-2'], | ||||
|         { stackParentId: 'new' }, | ||||
|         { | ||||
|           stackParentId: 'new', | ||||
|         }, | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
| @@ -465,7 +465,15 @@ export class AssetService { | ||||
|   async handleAssetDeletion(job: IAssetDeletionJob) { | ||||
|     const { id, fromExternal } = job; | ||||
|  | ||||
|     const asset = await this.assetRepository.getById(id); | ||||
|     const asset = await this.assetRepository.getById(id, { | ||||
|       faces: { | ||||
|         person: true, | ||||
|       }, | ||||
|       library: true, | ||||
|       stack: true, | ||||
|       exifInfo: true, | ||||
|     }); | ||||
|  | ||||
|     if (!asset) { | ||||
|       return false; | ||||
|     } | ||||
| @@ -554,7 +562,13 @@ export class AssetService { | ||||
|     await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId); | ||||
|  | ||||
|     const childIds: string[] = []; | ||||
|     const oldParent = await this.assetRepository.getById(oldParentId); | ||||
|     const oldParent = await this.assetRepository.getById(oldParentId, { | ||||
|       faces: { | ||||
|         person: true, | ||||
|       }, | ||||
|       library: true, | ||||
|       stack: true, | ||||
|     }); | ||||
|     if (oldParent != null) { | ||||
|       childIds.push(oldParent.id); | ||||
|       // Get all children of old parent | ||||
|   | ||||
| @@ -34,5 +34,5 @@ export interface IUserRepository { | ||||
|   delete(user: UserEntity, hard?: boolean): Promise<UserEntity>; | ||||
|   restore(user: UserEntity): Promise<UserEntity>; | ||||
|   updateUsage(id: string, delta: number): Promise<void>; | ||||
|   syncUsage(): Promise<void>; | ||||
|   syncUsage(id?: string): Promise<void>; | ||||
| } | ||||
|   | ||||
| @@ -60,7 +60,12 @@ export class UserService { | ||||
|   } | ||||
|  | ||||
|   async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> { | ||||
|     await this.findOrFail(dto.id, {}); | ||||
|     const user = await this.findOrFail(dto.id, {}); | ||||
|  | ||||
|     if (dto.quotaSizeInBytes && user.quotaSizeInBytes !== dto.quotaSizeInBytes) { | ||||
|       await this.userRepository.syncUsage(dto.id); | ||||
|     } | ||||
|  | ||||
|     return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -363,16 +363,6 @@ export class AssetRepository implements IAssetRepository { | ||||
|  | ||||
|   @GenerateSql({ params: [DummyValue.UUID] }) | ||||
|   getById(id: string, relations: FindOptionsRelations<AssetEntity>): Promise<AssetEntity | null> { | ||||
|     if (!relations) { | ||||
|       relations = { | ||||
|         faces: { | ||||
|           person: true, | ||||
|         }, | ||||
|         library: true, | ||||
|         stack: true, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     return this.repository.findOne({ | ||||
|       where: { id }, | ||||
|       relations, | ||||
|   | ||||
| @@ -115,7 +115,8 @@ export class UserRepository implements IUserRepository { | ||||
|     await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta); | ||||
|   } | ||||
|  | ||||
|   async syncUsage() { | ||||
|   @GenerateSql({ params: [DummyValue.UUID] }) | ||||
|   async syncUsage(id?: string) { | ||||
|     const subQuery = this.assetRepository | ||||
|       .createQueryBuilder('assets') | ||||
|       .select('COALESCE(SUM(exif."fileSizeInByte"), 0)') | ||||
| @@ -123,12 +124,17 @@ export class UserRepository implements IUserRepository { | ||||
|       .where('assets.ownerId = users.id') | ||||
|       .withDeleted(); | ||||
|  | ||||
|     await this.userRepository | ||||
|     const query = this.userRepository | ||||
|       .createQueryBuilder('users') | ||||
|       .leftJoin('users.assets', 'assets') | ||||
|       .update() | ||||
|       .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` }) | ||||
|       .execute(); | ||||
|       .set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` }); | ||||
|  | ||||
|     if (id) { | ||||
|       query.where('users.id = :id', { id }); | ||||
|     } | ||||
|  | ||||
|     await query.execute(); | ||||
|   } | ||||
|  | ||||
|   private async save(user: Partial<UserEntity>) { | ||||
|   | ||||
| @@ -397,109 +397,40 @@ WHERE | ||||
|   ) | ||||
|  | ||||
| -- AssetRepository.getById | ||||
| SELECT DISTINCT | ||||
|   "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" | ||||
| SELECT | ||||
|   "AssetEntity"."id" AS "AssetEntity_id", | ||||
|   "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", | ||||
|   "AssetEntity"."ownerId" AS "AssetEntity_ownerId", | ||||
|   "AssetEntity"."libraryId" AS "AssetEntity_libraryId", | ||||
|   "AssetEntity"."deviceId" AS "AssetEntity_deviceId", | ||||
|   "AssetEntity"."type" AS "AssetEntity_type", | ||||
|   "AssetEntity"."originalPath" AS "AssetEntity_originalPath", | ||||
|   "AssetEntity"."resizePath" AS "AssetEntity_resizePath", | ||||
|   "AssetEntity"."webpPath" AS "AssetEntity_webpPath", | ||||
|   "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", | ||||
|   "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", | ||||
|   "AssetEntity"."createdAt" AS "AssetEntity_createdAt", | ||||
|   "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", | ||||
|   "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", | ||||
|   "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", | ||||
|   "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", | ||||
|   "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", | ||||
|   "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", | ||||
|   "AssetEntity"."isArchived" AS "AssetEntity_isArchived", | ||||
|   "AssetEntity"."isExternal" AS "AssetEntity_isExternal", | ||||
|   "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", | ||||
|   "AssetEntity"."isOffline" AS "AssetEntity_isOffline", | ||||
|   "AssetEntity"."checksum" AS "AssetEntity_checksum", | ||||
|   "AssetEntity"."duration" AS "AssetEntity_duration", | ||||
|   "AssetEntity"."isVisible" AS "AssetEntity_isVisible", | ||||
|   "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", | ||||
|   "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", | ||||
|   "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", | ||||
|   "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId" | ||||
| FROM | ||||
|   ( | ||||
|     SELECT | ||||
|       "AssetEntity"."id" AS "AssetEntity_id", | ||||
|       "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", | ||||
|       "AssetEntity"."ownerId" AS "AssetEntity_ownerId", | ||||
|       "AssetEntity"."libraryId" AS "AssetEntity_libraryId", | ||||
|       "AssetEntity"."deviceId" AS "AssetEntity_deviceId", | ||||
|       "AssetEntity"."type" AS "AssetEntity_type", | ||||
|       "AssetEntity"."originalPath" AS "AssetEntity_originalPath", | ||||
|       "AssetEntity"."resizePath" AS "AssetEntity_resizePath", | ||||
|       "AssetEntity"."webpPath" AS "AssetEntity_webpPath", | ||||
|       "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", | ||||
|       "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", | ||||
|       "AssetEntity"."createdAt" AS "AssetEntity_createdAt", | ||||
|       "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", | ||||
|       "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", | ||||
|       "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", | ||||
|       "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", | ||||
|       "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", | ||||
|       "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", | ||||
|       "AssetEntity"."isArchived" AS "AssetEntity_isArchived", | ||||
|       "AssetEntity"."isExternal" AS "AssetEntity_isExternal", | ||||
|       "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", | ||||
|       "AssetEntity"."isOffline" AS "AssetEntity_isOffline", | ||||
|       "AssetEntity"."checksum" AS "AssetEntity_checksum", | ||||
|       "AssetEntity"."duration" AS "AssetEntity_duration", | ||||
|       "AssetEntity"."isVisible" AS "AssetEntity_isVisible", | ||||
|       "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", | ||||
|       "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", | ||||
|       "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", | ||||
|       "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId", | ||||
|       "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", | ||||
|       "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", | ||||
|       "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", | ||||
|       "AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth", | ||||
|       "AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight", | ||||
|       "AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1", | ||||
|       "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", | ||||
|       "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", | ||||
|       "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId", | ||||
|       "8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden", | ||||
|       "AssetEntity__AssetEntity_library"."id" AS "AssetEntity__AssetEntity_library_id", | ||||
|       "AssetEntity__AssetEntity_library"."name" AS "AssetEntity__AssetEntity_library_name", | ||||
|       "AssetEntity__AssetEntity_library"."ownerId" AS "AssetEntity__AssetEntity_library_ownerId", | ||||
|       "AssetEntity__AssetEntity_library"."type" AS "AssetEntity__AssetEntity_library_type", | ||||
|       "AssetEntity__AssetEntity_library"."importPaths" AS "AssetEntity__AssetEntity_library_importPaths", | ||||
|       "AssetEntity__AssetEntity_library"."exclusionPatterns" AS "AssetEntity__AssetEntity_library_exclusionPatterns", | ||||
|       "AssetEntity__AssetEntity_library"."createdAt" AS "AssetEntity__AssetEntity_library_createdAt", | ||||
|       "AssetEntity__AssetEntity_library"."updatedAt" AS "AssetEntity__AssetEntity_library_updatedAt", | ||||
|       "AssetEntity__AssetEntity_library"."deletedAt" AS "AssetEntity__AssetEntity_library_deletedAt", | ||||
|       "AssetEntity__AssetEntity_library"."refreshedAt" AS "AssetEntity__AssetEntity_library_refreshedAt", | ||||
|       "AssetEntity__AssetEntity_library"."isVisible" AS "AssetEntity__AssetEntity_library_isVisible", | ||||
|       "AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id", | ||||
|       "AssetEntity__AssetEntity_stack"."deviceAssetId" AS "AssetEntity__AssetEntity_stack_deviceAssetId", | ||||
|       "AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId", | ||||
|       "AssetEntity__AssetEntity_stack"."libraryId" AS "AssetEntity__AssetEntity_stack_libraryId", | ||||
|       "AssetEntity__AssetEntity_stack"."deviceId" AS "AssetEntity__AssetEntity_stack_deviceId", | ||||
|       "AssetEntity__AssetEntity_stack"."type" AS "AssetEntity__AssetEntity_stack_type", | ||||
|       "AssetEntity__AssetEntity_stack"."originalPath" AS "AssetEntity__AssetEntity_stack_originalPath", | ||||
|       "AssetEntity__AssetEntity_stack"."resizePath" AS "AssetEntity__AssetEntity_stack_resizePath", | ||||
|       "AssetEntity__AssetEntity_stack"."webpPath" AS "AssetEntity__AssetEntity_stack_webpPath", | ||||
|       "AssetEntity__AssetEntity_stack"."thumbhash" AS "AssetEntity__AssetEntity_stack_thumbhash", | ||||
|       "AssetEntity__AssetEntity_stack"."encodedVideoPath" AS "AssetEntity__AssetEntity_stack_encodedVideoPath", | ||||
|       "AssetEntity__AssetEntity_stack"."createdAt" AS "AssetEntity__AssetEntity_stack_createdAt", | ||||
|       "AssetEntity__AssetEntity_stack"."updatedAt" AS "AssetEntity__AssetEntity_stack_updatedAt", | ||||
|       "AssetEntity__AssetEntity_stack"."deletedAt" AS "AssetEntity__AssetEntity_stack_deletedAt", | ||||
|       "AssetEntity__AssetEntity_stack"."fileCreatedAt" AS "AssetEntity__AssetEntity_stack_fileCreatedAt", | ||||
|       "AssetEntity__AssetEntity_stack"."localDateTime" AS "AssetEntity__AssetEntity_stack_localDateTime", | ||||
|       "AssetEntity__AssetEntity_stack"."fileModifiedAt" AS "AssetEntity__AssetEntity_stack_fileModifiedAt", | ||||
|       "AssetEntity__AssetEntity_stack"."isFavorite" AS "AssetEntity__AssetEntity_stack_isFavorite", | ||||
|       "AssetEntity__AssetEntity_stack"."isArchived" AS "AssetEntity__AssetEntity_stack_isArchived", | ||||
|       "AssetEntity__AssetEntity_stack"."isExternal" AS "AssetEntity__AssetEntity_stack_isExternal", | ||||
|       "AssetEntity__AssetEntity_stack"."isReadOnly" AS "AssetEntity__AssetEntity_stack_isReadOnly", | ||||
|       "AssetEntity__AssetEntity_stack"."isOffline" AS "AssetEntity__AssetEntity_stack_isOffline", | ||||
|       "AssetEntity__AssetEntity_stack"."checksum" AS "AssetEntity__AssetEntity_stack_checksum", | ||||
|       "AssetEntity__AssetEntity_stack"."duration" AS "AssetEntity__AssetEntity_stack_duration", | ||||
|       "AssetEntity__AssetEntity_stack"."isVisible" AS "AssetEntity__AssetEntity_stack_isVisible", | ||||
|       "AssetEntity__AssetEntity_stack"."livePhotoVideoId" AS "AssetEntity__AssetEntity_stack_livePhotoVideoId", | ||||
|       "AssetEntity__AssetEntity_stack"."originalFileName" AS "AssetEntity__AssetEntity_stack_originalFileName", | ||||
|       "AssetEntity__AssetEntity_stack"."sidecarPath" AS "AssetEntity__AssetEntity_stack_sidecarPath", | ||||
|       "AssetEntity__AssetEntity_stack"."stackParentId" AS "AssetEntity__AssetEntity_stack_stackParentId" | ||||
|     FROM | ||||
|       "assets" "AssetEntity" | ||||
|       LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id" | ||||
|       LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" | ||||
|       LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId" | ||||
|       LEFT JOIN "assets" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."stackParentId" = "AssetEntity"."id" | ||||
|     WHERE | ||||
|       ("AssetEntity"."id" = $1) | ||||
|   ) "distinctAlias" | ||||
| ORDER BY | ||||
|   "AssetEntity_id" ASC | ||||
|   "assets" "AssetEntity" | ||||
| WHERE | ||||
|   ("AssetEntity"."id" = $1) | ||||
| LIMIT | ||||
|   1 | ||||
|  | ||||
|   | ||||
| @@ -150,3 +150,19 @@ GROUP BY | ||||
|   "users"."id" | ||||
| ORDER BY | ||||
|   "users"."createdAt" ASC | ||||
|  | ||||
| -- UserRepository.syncUsage | ||||
| UPDATE "users" | ||||
| SET | ||||
|   "quotaUsageInBytes" = ( | ||||
|     SELECT | ||||
|       COALESCE(SUM(exif."fileSizeInByte"), 0) | ||||
|     FROM | ||||
|       "assets" "assets" | ||||
|       LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id" | ||||
|     WHERE | ||||
|       "assets"."ownerId" = users.id | ||||
|   ), | ||||
|   "updatedAt" = CURRENT_TIMESTAMP | ||||
| WHERE | ||||
|   users.id = $1 | ||||
|   | ||||
| @@ -104,6 +104,7 @@ | ||||
|     <div class="m-4 flex flex-col gap-2"> | ||||
|       <label class="immich-form-label" for="quotaSize">Quota Size (GB)</label> | ||||
|       <input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} /> | ||||
|       <p>Note: Enter 0 for unlimited quota</p> | ||||
|     </div> | ||||
|  | ||||
|     <div class="m-4 flex flex-col gap-2"> | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
|   import type { PageData } from './$types'; | ||||
|   import { mdiCheck, mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; | ||||
|   import { user } from '$lib/stores/user.store'; | ||||
|   import { asByteUnitString } from '$lib/utils/byte-units'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|  | ||||
| @@ -171,6 +172,7 @@ | ||||
|           <tr class="flex w-full place-items-center"> | ||||
|             <th class="w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12 text-center text-sm font-medium">Email</th> | ||||
|             <th class="hidden sm:block w-3/12 text-center text-sm font-medium">Name</th> | ||||
|             <th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">Has quota</th> | ||||
|             <th class="hidden xl:block w-3/12 2xl:w-2/12 text-center text-sm font-medium">Can import</th> | ||||
|             <th class="w-4/12 lg:w-3/12 xl:w-2/12 text-center text-sm font-medium">Action</th> | ||||
|           </tr> | ||||
| @@ -191,6 +193,15 @@ | ||||
|                   >{immichUser.email}</td | ||||
|                 > | ||||
|                 <td class="hidden sm:block w-3/12 text-ellipsis break-all px-2 text-sm">{immichUser.name}</td> | ||||
|                 <td class="hidden xl:block w-3/12 2xl:w-2/12 text-ellipsis break-all px-2 text-sm"> | ||||
|                   <div class="container mx-auto flex flex-wrap justify-center"> | ||||
|                     {#if immichUser.quotaSizeInBytes && immichUser.quotaSizeInBytes > 0} | ||||
|                       {asByteUnitString(immichUser.quotaSizeInBytes, $locale)} | ||||
|                     {:else} | ||||
|                       <Icon path={mdiClose} size="16" /> | ||||
|                     {/if} | ||||
|                   </div> | ||||
|                 </td> | ||||
|                 <td class="hidden xl:block w-3/12 2xl:w-2/12 text-ellipsis break-all px-2 text-sm"> | ||||
|                   <div class="container mx-auto flex flex-wrap justify-center"> | ||||
|                     {#if immichUser.externalPath} | ||||
| @@ -200,6 +211,7 @@ | ||||
|                     {/if} | ||||
|                   </div> | ||||
|                 </td> | ||||
|  | ||||
|                 <td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all px-4 text-sm"> | ||||
|                   {#if !isDeleted(immichUser)} | ||||
|                     <button | ||||
|   | ||||
		Reference in New Issue
	
	Block a user