feat: アップロード可能な最大ファイルサイズをロールごとに設定可能に

This commit is contained in:
syuilo 2025-04-27 09:35:44 +09:00
parent effc84b9cc
commit 9481b5a6e8
12 changed files with 59 additions and 2 deletions

View File

@ -2,6 +2,7 @@
### General ### General
- Feat: bull-boardに代わるジョブキューの管理ツールが実装されました - Feat: bull-boardに代わるジョブキューの管理ツールが実装されました
- Feat: アップロード可能な最大ファイルサイズをロールごとに設定可能に
- Enhance: チャットの新規メッセージをプッシュ通知するように - Enhance: チャットの新規メッセージをプッシュ通知するように
### Client ### Client

4
locales/index.d.ts vendored
View File

@ -7464,6 +7464,10 @@ export interface Locale extends ILocale {
* *
*/ */
"driveCapacity": string; "driveCapacity": string;
/**
*
*/
"maxFileSize": string;
/** /**
* NSFWを常に付与 * NSFWを常に付与
*/ */

View File

@ -1934,6 +1934,7 @@ _role:
canManageCustomEmojis: "カスタム絵文字の管理" canManageCustomEmojis: "カスタム絵文字の管理"
canManageAvatarDecorations: "アバターデコレーションの管理" canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量" driveCapacity: "ドライブ容量"
maxFileSize: "アップロード可能な最大ファイルサイズ"
alwaysMarkNsfw: "ファイルにNSFWを常に付与" alwaysMarkNsfw: "ファイルにNSFWを常に付与"
canUpdateBioMedia: "アイコンとバナーの更新を許可" canUpdateBioMedia: "アイコンとバナーの更新を許可"
pinMax: "ノートのピン留めの最大数" pinMax: "ノートのピン留めの最大数"

View File

@ -522,9 +522,16 @@ export class DriveService {
const policies = await this.roleService.getUserPolicies(user.id); const policies = await this.roleService.getUserPolicies(user.id);
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb;
this.registerLogger.debug('drive capacity override applied'); this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
if (maxFileSize < info.size) {
if (isLocalUser) {
throw new IdentifiableError('f9e4e5f3-4df4-40b5-b400-f236945f7073', 'Max file size exceeded.');
}
}
// If usage limit exceeded // If usage limit exceeded
if (driveCapacity < usage + info.size) { if (driveCapacity < usage + info.size) {
if (isLocalUser) { if (isLocalUser) {

View File

@ -46,6 +46,7 @@ export type RolePolicies = {
canUseTranslator: boolean; canUseTranslator: boolean;
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
maxFileSizeMb: number;
alwaysMarkNsfw: boolean; alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean; canUpdateBioMedia: boolean;
pinLimit: number; pinLimit: number;
@ -81,6 +82,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canUseTranslator: true, canUseTranslator: true,
canHideAds: false, canHideAds: false,
driveCapacityMb: 100, driveCapacityMb: 100,
maxFileSizeMb: 10,
alwaysMarkNsfw: false, alwaysMarkNsfw: false,
canUpdateBioMedia: true, canUpdateBioMedia: true,
pinLimit: 5, pinLimit: 5,
@ -391,6 +393,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
maxFileSizeMb: calc('maxFileSizeMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)), canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)), pinLimit: calc('pinLimit', vs => Math.max(...vs)),

View File

@ -224,6 +224,10 @@ export const packedRolePoliciesSchema = {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,
}, },
maxFileSizeMb: {
type: 'integer',
optional: false, nullable: false,
},
alwaysMarkNsfw: { alwaysMarkNsfw: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -10,9 +10,9 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js';
import { MiMeta } from '@/models/_.js'; import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
tags: ['drive'], tags: ['drive'],
@ -56,6 +56,12 @@ export const meta = {
code: 'NO_FREE_SPACE', code: 'NO_FREE_SPACE',
id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064', id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064',
}, },
maxFileSizeExceeded: {
message: 'Cannot upload the file because it exceeds the maximum file size.',
code: 'MAX_FILE_SIZE_EXCEEDED',
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
},
}, },
} as const; } as const;
@ -115,6 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate); if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace); if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded);
} }
throw new ApiError(); throw new ApiError();
} finally { } finally {

View File

@ -91,6 +91,7 @@ export const ROLE_POLICIES = [
'canUseTranslator', 'canUseTranslator',
'canHideAds', 'canHideAds',
'driveCapacityMb', 'driveCapacityMb',
'maxFileSizeMb',
'alwaysMarkNsfw', 'alwaysMarkNsfw',
'canUpdateBioMedia', 'canUpdateBioMedia',
'pinLimit', 'pinLimit',

View File

@ -386,6 +386,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.maxFileSize, 'maxFileSizeMb'])">
<template #label>{{ i18n.ts._role._options.maxFileSize }}</template>
<template #suffix>
<span v-if="role.policies.maxFileSizeMb.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.maxFileSizeMb.value + 'MB' }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.maxFileSizeMb)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.maxFileSizeMb.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="role.policies.maxFileSizeMb.value" :disabled="role.policies.maxFileSizeMb.useDefault" type="number" :readonly="readonly">
<template #suffix>MB</template>
</MkInput>
<MkRange v-model="role.policies.maxFileSizeMb.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])">
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
<template #suffix> <template #suffix>

View File

@ -138,6 +138,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.maxFileSize, 'maxFileSizeMb'])">
<template #label>{{ i18n.ts._role._options.maxFileSize }}</template>
<template #suffix>{{ policies.maxFileSizeMb }}MB</template>
<MkInput v-model="policies.maxFileSizeMb" type="number">
<template #suffix>MB</template>
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])">
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
<template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template>

View File

@ -40,7 +40,7 @@ export function uploadFile(
const _folder = typeof folder === 'string' ? folder : folder?.id; const _folder = typeof folder === 'string' ? folder : folder?.id;
if (file.size > instance.maxFileSize) { if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
alert({ alert({
type: 'error', type: 'error',
title: i18n.ts.failedToUpload, title: i18n.ts.failedToUpload,

View File

@ -5216,6 +5216,7 @@ export type components = {
canUseTranslator: boolean; canUseTranslator: boolean;
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
maxFileSizeMb: number;
alwaysMarkNsfw: boolean; alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean; canUpdateBioMedia: boolean;
pinLimit: number; pinLimit: number;