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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 646 additions and 118 deletions

View file

@ -250,7 +250,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(user1Albums[0]);
expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
});
it('should return album info for shared album', async () => {
@ -259,7 +259,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(user2Albums[0]);
expect(body).toEqual({ ...user2Albums[0], assets: [expect.objectContaining(user2Albums[0].assets[0])] });
});
it('should return album info with assets when withoutAssets is undefined', async () => {
@ -268,7 +268,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(user1Albums[0]);
expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
});
it('should return album info without assets when withoutAssets is true', async () => {

View file

@ -43,6 +43,7 @@ describe(`${AssetController.name} (e2e)`, () => {
let assetRepository: IAssetRepository;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let userWithQuota: LoginResponseDto;
let libraries: LibraryResponseDto[];
let asset1: AssetResponseDto;
let asset2: AssetResponseDto;
@ -75,11 +76,13 @@ describe(`${AssetController.name} (e2e)`, () => {
await Promise.all([
api.userApi.create(server, admin.accessToken, userDto.user1),
api.userApi.create(server, admin.accessToken, userDto.user2),
api.userApi.create(server, admin.accessToken, userDto.userWithQuota),
]);
[user1, user2] = await Promise.all([
[user1, user2, userWithQuota] = await Promise.all([
api.authApi.login(server, userDto.user1),
api.authApi.login(server, userDto.user2),
api.authApi.login(server, userDto.userWithQuota),
]);
const [user1Libraries, user2Libraries] = await Promise.all([
@ -634,6 +637,46 @@ describe(`${AssetController.name} (e2e)`, () => {
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('Not found or no asset.upload access'));
});
it('should update the used quota', async () => {
const content = randomBytes(32);
const { body, status } = await request(server)
.post('/asset/upload')
.set('Authorization', `Bearer ${userWithQuota.accessToken}`)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'TEST')
.field('fileCreatedAt', new Date().toISOString())
.field('fileModifiedAt', new Date().toISOString())
.field('isFavorite', 'true')
.field('duration', '0:00:00.000000')
.attach('assetData', content, 'example.jpg');
expect(status).toBe(201);
expect(body).toEqual({ id: expect.any(String), duplicate: false });
const { body: user } = await request(server)
.get('/user/me')
.set('Authorization', `Bearer ${userWithQuota.accessToken}`);
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 32 }));
});
it('should not upload an asset if it would exceed the quota', async () => {
const content = randomBytes(420);
const { body, status } = await request(server)
.post('/asset/upload')
.set('Authorization', `Bearer ${userWithQuota.accessToken}`)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'TEST')
.field('fileCreatedAt', new Date().toISOString())
.field('fileModifiedAt', new Date().toISOString())
.field('isFavorite', 'true')
.field('duration', '0:00:00.000000')
.attach('assetData', content, 'example.jpg');
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('Quota has been exceeded!'));
});
});
describe('PUT /asset/:id', () => {

View file

@ -32,6 +32,8 @@ const adminSignupResponse = {
deletedAt: null,
oauthId: '',
memoriesEnabled: true,
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
};
describe(`${AuthController.name} (e2e)`, () => {

View file

@ -128,6 +128,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
usage: 0,
usageByUser: [
{
quotaSizeInBytes: null,
photos: 0,
usage: 0,
userName: 'Immich Admin',
@ -135,6 +136,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
videos: 0,
},
{
quotaSizeInBytes: null,
photos: 0,
usage: 0,
userName: 'User 1',