mirror of
https://github.com/immich-app/immich.git
synced 2025-06-01 04:36:19 -04:00
feat: allow accounts with a quota of 0 GiB (#17413)
* Allow 0GiB quotas in user create/edit form, remove unused translations * Make requireQuota check for null or 0 * Add unlimited quota change to the docs * Fix user dto formatting * Fix formating edit-user-form * Regenerate open-api files * Revert unnecessary i18n file changes * Re-add newline en.json * Resolve linting issues * Fix formatting edit-user-form * Re-add manifest
This commit is contained in:
parent
be62773e31
commit
77039d87d8
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 16 KiB |
@ -31,7 +31,7 @@ Admin can send a welcome email if the Email option is set, you can learn here ho
|
|||||||
|
|
||||||
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
|
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
|
||||||
|
|
||||||
In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota using the value 0 (default).
|
In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota by leaving it empty (default).
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
The system administrator can see the usage quota percentage of all users in Server Stats page.
|
The system administrator can see the usage quota percentage of all users in Server Stats page.
|
||||||
|
@ -164,7 +164,6 @@
|
|||||||
"no_pattern_added": "No pattern added",
|
"no_pattern_added": "No pattern added",
|
||||||
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
"note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||||
"note_cannot_be_changed_later": "NOTE: This cannot be changed later!",
|
"note_cannot_be_changed_later": "NOTE: This cannot be changed later!",
|
||||||
"note_unlimited_quota": "Note: Enter 0 for unlimited quota",
|
|
||||||
"notification_email_from_address": "From address",
|
"notification_email_from_address": "From address",
|
||||||
"notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server <noreply@example.com>\"",
|
"notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server <noreply@example.com>\"",
|
||||||
"notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)",
|
"notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)",
|
||||||
@ -929,7 +928,6 @@
|
|||||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||||
"not_in_any_album": "Not in any album",
|
"not_in_any_album": "Not in any album",
|
||||||
"note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
"note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||||
"note_unlimited_quota": "Note: Enter 0 for unlimited quota",
|
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
"notification_toggle_setting_description": "Enable email notifications",
|
"notification_toggle_setting_description": "Enable email notifications",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
@ -1384,4 +1382,4 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"you_dont_have_any_shared_links": "You don't have any shared links",
|
"you_dont_have_any_shared_links": "You don't have any shared links",
|
||||||
"zoom_image": "Zoom Image"
|
"zoom_image": "Zoom Image"
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ class UserAdminCreateDto {
|
|||||||
|
|
||||||
String password;
|
String password;
|
||||||
|
|
||||||
/// Minimum value: 1
|
/// Minimum value: 0
|
||||||
int? quotaSizeInBytes;
|
int? quotaSizeInBytes;
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -45,7 +45,7 @@ class UserAdminUpdateDto {
|
|||||||
///
|
///
|
||||||
String? password;
|
String? password;
|
||||||
|
|
||||||
/// Minimum value: 1
|
/// Minimum value: 0
|
||||||
int? quotaSizeInBytes;
|
int? quotaSizeInBytes;
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -13624,7 +13624,7 @@
|
|||||||
},
|
},
|
||||||
"quotaSizeInBytes": {
|
"quotaSizeInBytes": {
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"minimum": 1,
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@ -13763,7 +13763,7 @@
|
|||||||
},
|
},
|
||||||
"quotaSizeInBytes": {
|
"quotaSizeInBytes": {
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"minimum": 1,
|
"minimum": 0,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
|
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||||
import { User, UserAdmin } from 'src/database';
|
import { User, UserAdmin } from 'src/database';
|
||||||
import { UserMetadataEntity, UserMetadataItem } from 'src/entities/user-metadata.entity';
|
import { UserMetadataEntity, UserMetadataItem } from 'src/entities/user-metadata.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
@ -77,7 +77,7 @@ export class UserAdminCreateDto {
|
|||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsPositive()
|
@Min(0)
|
||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
quotaSizeInBytes?: number | null;
|
quotaSizeInBytes?: number | null;
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ export class UserAdminUpdateDto {
|
|||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsPositive()
|
@Min(0)
|
||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
quotaSizeInBytes?: number | null;
|
quotaSizeInBytes?: number | null;
|
||||||
}
|
}
|
||||||
|
@ -435,7 +435,7 @@ export class AssetMediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private requireQuota(auth: AuthDto, size: number) {
|
private requireQuota(auth: AuthDto, size: number) {
|
||||||
if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) {
|
if (auth.user.quotaSizeInBytes !== null && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) {
|
||||||
throw new BadRequestException('Quota has been exceeded!');
|
throw new BadRequestException('Quota has been exceeded!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
let quotaSize: string | undefined = $state();
|
let quotaSize: string | undefined = $state();
|
||||||
let isCreatingUser = $state(false);
|
let isCreatingUser = $state(false);
|
||||||
|
|
||||||
let quotaSizeInBytes = $derived(quotaSize ? convertToBytes(Number(quotaSize), ByteUnit.GiB) : null);
|
let quotaSizeInBytes = $derived(quotaSize === null ? null : convertToBytes(Number(quotaSize), ByteUnit.GiB));
|
||||||
let quotaSizeWarning = $derived(
|
let quotaSizeWarning = $derived(
|
||||||
quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw,
|
quotaSizeInBytes && userInteraction.serverInfo && quotaSizeInBytes > userInteraction.serverInfo.diskSizeRaw,
|
||||||
);
|
);
|
||||||
@ -113,7 +113,7 @@
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label={$t('admin.quota_size_gib')}>
|
<Field label={$t('admin.quota_size_gib')}>
|
||||||
<Input bind:value={quotaSize} type="number" min="0" />
|
<Input bind:value={quotaSize} type="number" placeholder={$t('unlimited')} min="0" />
|
||||||
{#if quotaSizeWarning}
|
{#if quotaSizeWarning}
|
||||||
<HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
|
<HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
onEditSuccess,
|
onEditSuccess,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let quotaSize = $state(user.quotaSizeInBytes ? convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB) : null);
|
let quotaSize = $state(user.quotaSizeInBytes === null ? null : convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB));
|
||||||
|
|
||||||
const previousQutoa = user.quotaSizeInBytes;
|
const previousQutoa = user.quotaSizeInBytes;
|
||||||
|
|
||||||
@ -48,7 +48,7 @@
|
|||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
storageLabel: storageLabel || '',
|
storageLabel: storageLabel || '',
|
||||||
quotaSizeInBytes: quotaSize ? convertToBytes(Number(quotaSize), ByteUnit.GiB) : null,
|
quotaSizeInBytes: quotaSize === null ? null : convertToBytes(Number(quotaSize), ByteUnit.GiB),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,8 +126,15 @@
|
|||||||
<p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
|
<p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
|
||||||
{/if}</label
|
{/if}</label
|
||||||
>
|
>
|
||||||
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
<input
|
||||||
<p>{$t('admin.note_unlimited_quota')}</p>
|
class="immich-form-input"
|
||||||
|
id="quotaSize"
|
||||||
|
name="quotaSize"
|
||||||
|
placeholder={$t('unlimited')}
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
bind:value={quotaSize}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
|
@ -209,7 +209,7 @@
|
|||||||
<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">
|
<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.quotaSizeInBytes && immichUser.quotaSizeInBytes > 0}
|
{#if immichUser.quotaSizeInBytes !== null && immichUser.quotaSizeInBytes >= 0}
|
||||||
{getByteUnitString(immichUser.quotaSizeInBytes, $locale)}
|
{getByteUnitString(immichUser.quotaSizeInBytes, $locale)}
|
||||||
{:else}
|
{:else}
|
||||||
<Icon path={mdiInfinity} size="16" />
|
<Icon path={mdiInfinity} size="16" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user