mirror of
https://github.com/immich-app/immich.git
synced 2024-11-28 09:33:27 +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:
parent
2a8cb70c98
commit
d096caccac
@ -845,7 +845,16 @@ describe(AssetService.name, () => {
|
|||||||
it('should remove faces', async () => {
|
it('should remove faces', async () => {
|
||||||
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
|
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 });
|
await sut.handleAssetDeletion({ id: assetWithFace.id });
|
||||||
|
|
||||||
@ -870,7 +879,16 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update stack parent if asset has stack children', async () => {
|
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 });
|
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 () => {
|
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 });
|
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 () => {
|
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 });
|
await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true });
|
||||||
|
|
||||||
@ -926,6 +962,27 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a live photo', async () => {
|
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 });
|
await sut.handleAssetDeletion({ id: assetStub.livePhotoStillAsset.id });
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
@ -950,7 +1007,16 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update usage', async () => {
|
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 });
|
await sut.handleAssetDeletion({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
||||||
@ -1005,7 +1071,13 @@ describe(AssetService.name, () => {
|
|||||||
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
|
||||||
|
|
||||||
when(assetMock.getById)
|
when(assetMock.getById)
|
||||||
.calledWith(assetStub.image.id)
|
.calledWith(assetStub.image.id, {
|
||||||
|
faces: {
|
||||||
|
person: true,
|
||||||
|
},
|
||||||
|
library: true,
|
||||||
|
stack: true,
|
||||||
|
})
|
||||||
.mockResolvedValue(assetStub.image as AssetEntity);
|
.mockResolvedValue(assetStub.image as AssetEntity);
|
||||||
|
|
||||||
await sut.updateStackParent(authStub.user1, {
|
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([assetStub.primaryImage.id]));
|
||||||
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new']));
|
||||||
when(assetMock.getById)
|
when(assetMock.getById)
|
||||||
.calledWith(assetStub.primaryImage.id)
|
.calledWith(assetStub.primaryImage.id, {
|
||||||
|
faces: {
|
||||||
|
person: true,
|
||||||
|
},
|
||||||
|
library: true,
|
||||||
|
stack: true,
|
||||||
|
})
|
||||||
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||||
|
|
||||||
await sut.updateStackParent(authStub.user1, {
|
await sut.updateStackParent(authStub.user1, {
|
||||||
@ -1042,7 +1120,9 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.updateAll).toBeCalledWith(
|
expect(assetMock.updateAll).toBeCalledWith(
|
||||||
[assetStub.primaryImage.id, 'stack-child-asset-1', 'stack-child-asset-2'],
|
[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) {
|
async handleAssetDeletion(job: IAssetDeletionJob) {
|
||||||
const { id, fromExternal } = job;
|
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) {
|
if (!asset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -554,7 +562,13 @@ export class AssetService {
|
|||||||
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId);
|
await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId);
|
||||||
|
|
||||||
const childIds: string[] = [];
|
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) {
|
if (oldParent != null) {
|
||||||
childIds.push(oldParent.id);
|
childIds.push(oldParent.id);
|
||||||
// Get all children of old parent
|
// Get all children of old parent
|
||||||
|
@ -34,5 +34,5 @@ export interface IUserRepository {
|
|||||||
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
||||||
restore(user: UserEntity): Promise<UserEntity>;
|
restore(user: UserEntity): Promise<UserEntity>;
|
||||||
updateUsage(id: string, delta: number): Promise<void>;
|
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> {
|
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);
|
return this.userCore.updateUser(auth.user, dto.id, dto).then(mapUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,16 +363,6 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getById(id: string, relations: FindOptionsRelations<AssetEntity>): Promise<AssetEntity | null> {
|
getById(id: string, relations: FindOptionsRelations<AssetEntity>): Promise<AssetEntity | null> {
|
||||||
if (!relations) {
|
|
||||||
relations = {
|
|
||||||
faces: {
|
|
||||||
person: true,
|
|
||||||
},
|
|
||||||
library: true,
|
|
||||||
stack: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.repository.findOne({
|
return this.repository.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations,
|
relations,
|
||||||
|
@ -115,7 +115,8 @@ export class UserRepository implements IUserRepository {
|
|||||||
await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta);
|
await this.userRepository.increment({ id }, 'quotaUsageInBytes', delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncUsage() {
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
async syncUsage(id?: string) {
|
||||||
const subQuery = this.assetRepository
|
const subQuery = this.assetRepository
|
||||||
.createQueryBuilder('assets')
|
.createQueryBuilder('assets')
|
||||||
.select('COALESCE(SUM(exif."fileSizeInByte"), 0)')
|
.select('COALESCE(SUM(exif."fileSizeInByte"), 0)')
|
||||||
@ -123,12 +124,17 @@ export class UserRepository implements IUserRepository {
|
|||||||
.where('assets.ownerId = users.id')
|
.where('assets.ownerId = users.id')
|
||||||
.withDeleted();
|
.withDeleted();
|
||||||
|
|
||||||
await this.userRepository
|
const query = this.userRepository
|
||||||
.createQueryBuilder('users')
|
.createQueryBuilder('users')
|
||||||
.leftJoin('users.assets', 'assets')
|
.leftJoin('users.assets', 'assets')
|
||||||
.update()
|
.update()
|
||||||
.set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` })
|
.set({ quotaUsageInBytes: () => `(${subQuery.getQuery()})` });
|
||||||
.execute();
|
|
||||||
|
if (id) {
|
||||||
|
query.where('users.id = :id', { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
await query.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async save(user: Partial<UserEntity>) {
|
private async save(user: Partial<UserEntity>) {
|
||||||
|
@ -397,109 +397,40 @@ WHERE
|
|||||||
)
|
)
|
||||||
|
|
||||||
-- AssetRepository.getById
|
-- AssetRepository.getById
|
||||||
SELECT DISTINCT
|
SELECT
|
||||||
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
|
"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
|
FROM
|
||||||
(
|
"assets" "AssetEntity"
|
||||||
SELECT
|
WHERE
|
||||||
"AssetEntity"."id" AS "AssetEntity_id",
|
("AssetEntity"."id" = $1)
|
||||||
"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
|
|
||||||
LIMIT
|
LIMIT
|
||||||
1
|
1
|
||||||
|
|
||||||
|
@ -150,3 +150,19 @@ GROUP BY
|
|||||||
"users"."id"
|
"users"."id"
|
||||||
ORDER BY
|
ORDER BY
|
||||||
"users"."createdAt" ASC
|
"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">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="quotaSize">Quota Size (GB)</label>
|
<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} />
|
<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>
|
||||||
|
|
||||||
<div class="m-4 flex flex-col gap-2">
|
<div class="m-4 flex flex-col gap-2">
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { mdiCheck, mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
import { mdiCheck, mdiClose, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import { asByteUnitString } from '$lib/utils/byte-units';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
@ -171,6 +172,7 @@
|
|||||||
<tr class="flex w-full place-items-center">
|
<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="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 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="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>
|
<th class="w-4/12 lg:w-3/12 xl:w-2/12 text-center text-sm font-medium">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -191,6 +193,15 @@
|
|||||||
>{immichUser.email}</td
|
>{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 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">
|
<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">
|
<div class="container mx-auto flex flex-wrap justify-center">
|
||||||
{#if immichUser.externalPath}
|
{#if immichUser.externalPath}
|
||||||
@ -200,6 +211,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all px-4 text-sm">
|
<td class="w-4/12 lg:w-3/12 xl:w-2/12 text-ellipsis break-all px-4 text-sm">
|
||||||
{#if !isDeleted(immichUser)}
|
{#if !isDeleted(immichUser)}
|
||||||
<button
|
<button
|
||||||
|
Loading…
Reference in New Issue
Block a user