feat: ロールでアップロード可能なファイル種別を設定可能に (#16081)

* wip

* Update RoleService.ts

* wip

* Update RoleService.ts

* Update CHANGELOG.md
This commit is contained in:
syuilo
2025-05-22 23:01:31 +09:00
committed by GitHub
parent aaee0a788d
commit e750c9171e
12 changed files with 118 additions and 6 deletions

View File

@@ -515,16 +515,23 @@ export class DriveService {
this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`);
//#region Check drive usage
//#region Check drive usage and mime type
if (user && !isLink) {
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
const isLocalUser = this.userEntityService.isLocalUser(user);
const policies = await this.roleService.getUserPolicies(user.id);
const allowedMimeTypes = policies.uploadableFileTypes;
const isAllowed = allowedMimeTypes.some((mimeType) => {
if (mimeType === '*' || mimeType === '*/*') return true;
if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1));
return info.type.mime === mimeType;
});
if (!isAllowed) {
throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', 'Unallowed file type.');
}
const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb;
this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
if (maxFileSize < info.size) {
if (isLocalUser) {
@@ -532,6 +539,11 @@ export class DriveService {
}
}
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
// If usage limit exceeded
if (driveCapacity < usage + info.size) {
if (isLocalUser) {

View File

@@ -65,6 +65,7 @@ export type RolePolicies = {
canImportMuting: boolean;
canImportUserLists: boolean;
chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[];
};
export const DEFAULT_POLICIES: RolePolicies = {
@@ -101,6 +102,13 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportMuting: true,
canImportUserLists: true,
chatAvailability: 'available',
uploadableFileTypes: [
'text/plain',
'application/json',
'image/*',
'video/*',
'audio/*',
],
};
@Injectable()
@@ -412,6 +420,16 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
chatAvailability: calc('chatAvailability', aggregateChatAvailability),
uploadableFileTypes: calc('uploadableFileTypes', vs => {
const set = new Set<string>();
for (const v of vs) {
for (const type of v) {
if (type.trim() === '') continue;
set.add(type.trim());
}
}
return [...set];
}),
};
}

View File

@@ -228,6 +228,14 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
uploadableFileTypes: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
alwaysMarkNsfw: {
type: 'boolean',
optional: false, nullable: false,

View File

@@ -110,6 +110,7 @@ export const ROLE_POLICIES = [
'canImportMuting',
'canImportUserLists',
'chatAvailability',
'uploadableFileTypes',
] as const;
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];

View File

@@ -69,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
<div>{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}</div>
<div>{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}</div>
</div>
</div>
@@ -281,7 +282,7 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) {
if (item.abort != null) {
item.abort();
}
}
},
});
}

View File

@@ -406,6 +406,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])">
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
<template #suffix>
<span v-if="role.policies.uploadableFileTypes.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>...</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.uploadableFileTypes)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.uploadableFileTypes.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkTextarea :modelValue="role.policies.uploadableFileTypes.value.join('\n')" :disabled="role.policies.uploadableFileTypes.useDefault" :readonly="readonly" @update:modelValue="role.policies.uploadableFileTypes.value = $event.split('\n')">
<template #caption>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</template>
</MkTextarea>
<MkRange v-model="role.policies.uploadableFileTypes.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'])">
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
<template #suffix>

View File

@@ -146,6 +146,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])">
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
<template #suffix>...</template>
<MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')">
</MkTextarea>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])">
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
<template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template>
@@ -312,6 +319,7 @@ import { definePage } from '@/page.js';
import { instance, fetchInstance } from '@/instance.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { useRouter } from '@/router.js';
import MkTextarea from '@/components/MkTextarea.vue';
const router = useRouter();
const baseRoleQ = ref('');

View File

@@ -39,6 +39,21 @@ export function uploadFile(file: File | Blob, options: {
const filePromise = new Promise<Misskey.entities.DriveFile>((resolve, reject) => {
if ($i == null) return reject();
const allowedMimeTypes = $i.policies.uploadableFileTypes;
const isAllowedMimeType = allowedMimeTypes.some(mimeType => {
if (mimeType === '*' || mimeType === '*/*') return true;
if (mimeType.endsWith('/*')) return file.type.startsWith(mimeType.slice(0, -1));
return file.type === mimeType;
});
if (!isAllowedMimeType) {
os.alert({
type: 'error',
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseUnallowedFileType,
});
return reject();
}
if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
os.alert({
type: 'error',
@@ -75,6 +90,12 @@ export function uploadFile(file: File | Blob, options: {
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseNoFreeSpace,
});
} else if (res.error?.id === '4becd248-7f2c-48c4-a9f0-75edc4f9a1ea') {
os.alert({
type: 'error',
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseUnallowedFileType,
});
} else {
os.alert({
type: 'error',

View File

@@ -5264,6 +5264,7 @@ export type components = {
canHideAds: boolean;
driveCapacityMb: number;
maxFileSizeMb: number;
uploadableFileTypes: string[];
alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean;
pinLimit: number;