feat(server, web): quotas (#4471)

* feat: quotas

* chore: open api

* chore: update status box and upload error message

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
cfitzw
2024-01-12 18:43:36 -06:00
committed by GitHub
parent f4edb6c4bd
commit deb1f970a8
63 changed files with 646 additions and 118 deletions
+22 -16
View File
@@ -13,6 +13,7 @@ import {
newPartnerRepositoryMock,
newStorageRepositoryMock,
newSystemConfigRepositoryMock,
newUserRepositoryMock,
} from '@test';
import { when } from 'jest-when';
import { Readable } from 'stream';
@@ -28,6 +29,7 @@ import {
IPartnerRepository,
IStorageRepository,
ISystemConfigRepository,
IUserRepository,
JobItem,
TimeBucketSize,
} from '../repositories';
@@ -67,6 +69,7 @@ const uploadFile = {
checksum: Buffer.from('checksum', 'utf8'),
originalPath: 'upload/admin/image.jpeg',
originalName: 'image.jpeg',
size: 1000,
},
},
filename: (fieldName: UploadFieldName, filename: string) => {
@@ -79,6 +82,7 @@ const uploadFile = {
checksum: Buffer.from('checksum', 'utf8'),
originalPath: `upload/admin/${filename}`,
originalName: filename,
size: 1000,
},
};
},
@@ -167,6 +171,7 @@ describe(AssetService.name, () => {
let cryptoMock: jest.Mocked<ICryptoRepository>;
let jobMock: jest.Mocked<IJobRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
let userMock: jest.Mocked<IUserRepository>;
let communicationMock: jest.Mocked<ICommunicationRepository>;
let configMock: jest.Mocked<ISystemConfigRepository>;
let partnerMock: jest.Mocked<IPartnerRepository>;
@@ -182,6 +187,7 @@ describe(AssetService.name, () => {
cryptoMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
configMock = newSystemConfigRepositoryMock();
partnerMock = newPartnerRepositoryMock();
@@ -192,6 +198,7 @@ describe(AssetService.name, () => {
jobMock,
configMock,
storageMock,
userMock,
communicationMock,
partnerMock,
);
@@ -836,7 +843,7 @@ describe(AssetService.name, () => {
});
it('should remove faces', async () => {
const assetWithFace = { ...(assetStub.image as AssetEntity), faces: [faceStub.face1, faceStub.mergeFace1] };
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
when(assetMock.getById).calledWith(assetWithFace.id).mockResolvedValue(assetWithFace);
@@ -863,9 +870,7 @@ describe(AssetService.name, () => {
});
it('should update stack parent if asset has stack children', async () => {
when(assetMock.getById)
.calledWith(assetStub.primaryImage.id)
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
when(assetMock.getById).calledWith(assetStub.primaryImage.id).mockResolvedValue(assetStub.primaryImage);
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
@@ -878,9 +883,7 @@ describe(AssetService.name, () => {
});
it('should not schedule delete-files job for readonly assets', async () => {
when(assetMock.getById)
.calledWith(assetStub.readOnly.id)
.mockResolvedValue(assetStub.readOnly as AssetEntity);
when(assetMock.getById).calledWith(assetStub.readOnly.id).mockResolvedValue(assetStub.readOnly);
await sut.handleAssetDeletion({ id: assetStub.readOnly.id });
@@ -890,21 +893,17 @@ describe(AssetService.name, () => {
});
it('should not process assets from external library without fromExternal flag', async () => {
when(assetMock.getById)
.calledWith(assetStub.external.id)
.mockResolvedValue(assetStub.external as AssetEntity);
when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external);
await sut.handleAssetDeletion({ id: assetStub.external.id });
expect(jobMock.queue).not.toBeCalled();
expect(jobMock.queueAll).not.toBeCalled();
expect(assetMock.remove).not.toBeCalled();
expect(jobMock.queue).not.toHaveBeenCalled();
expect(jobMock.queueAll).not.toHaveBeenCalled();
expect(assetMock.remove).not.toHaveBeenCalled();
});
it('should process assets from external library with fromExternal flag', async () => {
when(assetMock.getById)
.calledWith(assetStub.external.id)
.mockResolvedValue(assetStub.external as AssetEntity);
when(assetMock.getById).calledWith(assetStub.external.id).mockResolvedValue(assetStub.external);
await sut.handleAssetDeletion({ id: assetStub.external.id, fromExternal: true });
@@ -949,6 +948,13 @@ describe(AssetService.name, () => {
],
]);
});
it('should update usage', async () => {
when(assetMock.getById).calledWith(assetStub.image.id).mockResolvedValue(assetStub.image);
await sut.handleAssetDeletion({ id: assetStub.image.id });
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
});
});
describe('run', () => {
+4
View File
@@ -20,6 +20,7 @@ import {
IPartnerRepository,
IStorageRepository,
ISystemConfigRepository,
IUserRepository,
ImmichReadStream,
JobItem,
TimeBucketOptions,
@@ -75,6 +76,7 @@ export interface UploadFile {
checksum: Buffer;
originalPath: string;
originalName: string;
size: number;
}
export class AssetService {
@@ -89,6 +91,7 @@ export class AssetService {
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
) {
@@ -481,6 +484,7 @@ export class AssetService {
}
await this.assetRepository.remove(asset);
await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0));
this.communicationRepository.send(ClientEvent.ASSET_DELETE, asset.ownerId, id);
// TODO refactor this to use cascades