Add additional drive capacity change support (#8867)

* Add additional drive capacity change support

* Update packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>

* 🎨

* show instance default capacity in placeholder

* fix

* update api/drive

* fix

* remove :

* fix lint

Co-authored-by: Johann150 <johann@qwertqwefsday.eu>
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
CyberRex 2022-07-05 00:21:01 +09:00 committed by GitHub
parent a228d1ddaa
commit cd07eb222e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 117 additions and 6 deletions

View File

@ -203,6 +203,7 @@ done: "完了"
processing: "処理中" processing: "処理中"
preview: "プレビュー" preview: "プレビュー"
default: "デフォルト" default: "デフォルト"
defaultValueIs: "デフォルト: {value}"
noCustomEmojis: "絵文字はありません" noCustomEmojis: "絵文字はありません"
noJobs: "ジョブはありません" noJobs: "ジョブはありません"
federating: "連合中" federating: "連合中"
@ -855,6 +856,8 @@ noEmailServerWarning: "メールサーバーの設定がされていません。
thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
recommended: "推奨" recommended: "推奨"
check: "チェック" check: "チェック"
driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更"
driveCapOverrideCaption: "0以下を指定すると解除されます。"
requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。" requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。"
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
typeToConfirm: "この操作を行うには {x} と入力してください" typeToConfirm: "この操作を行うには {x} と入力してください"

View File

@ -0,0 +1,13 @@
export class driveCapacityOverrideMb1655813815729 {
name = 'driveCapacityOverrideMb1655813815729'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`);
await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
}
async down(queryRunner) {
await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`);
}
}

View File

@ -218,6 +218,12 @@ export class User {
}) })
public token: string | null; public token: string | null;
@Column('integer', {
nullable: true,
comment: 'Overrides user drive capacity limit',
})
public driveCapacityOverrideMb: number | null;
constructor(data: Partial<User>) { constructor(data: Partial<User>) {
if (data == null) return; if (data == null) return;

View File

@ -315,6 +315,7 @@ export const UserRepository = db.getRepository(User).extend({
} : undefined) : undefined, } : undefined) : undefined,
emojis: populateEmojis(user.emojis, user.host), emojis: populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user), onlineStatus: this.getOnlineStatus(user),
driveCapacityOverrideMb: user.driveCapacityOverrideMb,
...(opts.detail ? { ...(opts.detail ? {
url: profile!.url, url: profile!.url,

View File

@ -314,6 +314,7 @@ import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
const eps = [ const eps = [
['admin/meta', ep___admin_meta], ['admin/meta', ep___admin_meta],
@ -629,6 +630,7 @@ const eps = [
['users/search', ep___users_search], ['users/search', ep___users_search],
['users/show', ep___users_show], ['users/show', ep___users_show],
['users/stats', ep___users_stats], ['users/stats', ep___users_stats],
['admin/drive-capacity-override', ep___admin_driveCapOverride],
['fetch-rss', ep___fetchRss], ['fetch-rss', ep___fetchRss],
]; ];

View File

@ -0,0 +1,47 @@
import define from '../../define.js';
import { Users } from '@/models/index.js';
import { User } from '@/models/entities/user.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
overrideMb: { type: 'number', nullable: true },
},
required: ['userId', 'overrideMb'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
if (!Users.isLocalUser(user)) {
throw new Error('user is not local user');
}
/*if (user.isAdmin) {
throw new Error('cannot suspend admin');
}
if (user.isModerator) {
throw new Error('cannot suspend moderator');
}*/
await Users.update(user.id, {
driveCapacityOverrideMb: ps.overrideMb,
});
insertModerationLog(me, 'change-drive-capacity-override', {
targetId: user.id,
});
});

View File

@ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, user) => {
const usage = await DriveFiles.calcDriveUsageOf(user.id); const usage = await DriveFiles.calcDriveUsageOf(user.id);
return { return {
capacity: 1024 * 1024 * instance.localDriveCapacityMb, capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb),
usage: usage, usage: usage,
}; };
}); });

View File

@ -307,7 +307,7 @@ async function deleteOldFile(user: IRemoteUser) {
type AddFileArgs = { type AddFileArgs = {
/** User who wish to add file */ /** User who wish to add file */
user: { id: User['id']; host: User['host'] } | null; user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null;
/** File path */ /** File path */
path: string; path: string;
/** Name */ /** Name */
@ -371,9 +371,16 @@ export async function addFile({
//#region Check drive usage //#region Check drive usage
if (user && !isLink) { if (user && !isLink) {
const usage = await DriveFiles.calcDriveUsageOf(user); const usage = await DriveFiles.calcDriveUsageOf(user);
const u = await Users.findOneBy({ id: user.id });
const instance = await fetchMeta(); const instance = await fetchMeta();
const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); let driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) {
driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb;
logger.debug('drive capacity override applied');
logger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
}
logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);

View File

@ -85,6 +85,17 @@
</FormSection> </FormSection>
</div> </div>
<div v-else-if="tab === 'moderation'" class="_formRoot"> <div v-else-if="tab === 'moderation'" class="_formRoot">
<FormSection>
<template #label>Drive Capacity Override</template>
<FormInput v-if="user.host == null" v-model="driveCapacityOverrideMb" inline :manual-save="true" type="number" :placeholder="i18n.t('defaultValueIs', { value: instance.driveCapacityPerLocalUserMb })" @update:model-value="applyDriveCapacityOverride">
<template #label>{{ i18n.ts.driveCapOverrideLabel }}</template>
<template #suffix>MB</template>
<template #caption>
{{ i18n.ts.driveCapOverrideCaption }}
</template>
</FormInput>
</FormSection>
<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> <FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch> <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
@ -141,7 +152,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, defineComponent, watch } from 'vue'; import { computed, watch } from 'vue';
import * as misskey from 'misskey-js'; import * as misskey from 'misskey-js';
import MkChart from '@/components/chart.vue'; import MkChart from '@/components/chart.vue';
import MkObjectView from '@/components/object-view.vue'; import MkObjectView from '@/components/object-view.vue';
@ -150,6 +161,8 @@ import FormSwitch from '@/components/form/switch.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormButton from '@/components/ui/button.vue'; import FormButton from '@/components/ui/button.vue';
import FormInput from '@/components/form/input.vue';
import FormSplit from '@/components/form/split.vue';
import FormFolder from '@/components/form/folder.vue'; import FormFolder from '@/components/form/folder.vue';
import MkKeyValue from '@/components/key-value.vue'; import MkKeyValue from '@/components/key-value.vue';
import MkSelect from '@/components/form/select.vue'; import MkSelect from '@/components/form/select.vue';
@ -164,6 +177,7 @@ import { userPage, acct } from '@/filters/user';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { iAmAdmin, iAmModerator } from '@/account'; import { iAmAdmin, iAmModerator } from '@/account';
import { instance } from '@/instance';
const props = defineProps<{ const props = defineProps<{
userId: string; userId: string;
@ -172,13 +186,14 @@ const props = defineProps<{
let tab = $ref('overview'); let tab = $ref('overview');
let chartSrc = $ref('per-user-notes'); let chartSrc = $ref('per-user-notes');
let user = $ref<null | misskey.entities.UserDetailed>(); let user = $ref<null | misskey.entities.UserDetailed>();
let init = $ref(); let init = $ref<ReturnType<typeof createFetcher>>();
let info = $ref(); let info = $ref();
let ips = $ref(null); let ips = $ref(null);
let ap = $ref(null); let ap = $ref(null);
let moderator = $ref(false); let moderator = $ref(false);
let silenced = $ref(false); let silenced = $ref(false);
let suspended = $ref(false); let suspended = $ref(false);
let driveCapacityOverrideMb: number | null = $ref(0);
let moderationNote = $ref(''); let moderationNote = $ref('');
const filesPagination = { const filesPagination = {
endpoint: 'admin/drive/files' as const, endpoint: 'admin/drive/files' as const,
@ -203,6 +218,7 @@ function createFetcher() {
moderator = info.isModerator; moderator = info.isModerator;
silenced = info.isSilenced; silenced = info.isSilenced;
suspended = info.isSuspended; suspended = info.isSuspended;
driveCapacityOverrideMb = user.driveCapacityOverrideMb;
moderationNote = info.moderationNote; moderationNote = info.moderationNote;
watch($$(moderationNote), async () => { watch($$(moderationNote), async () => {
@ -289,6 +305,22 @@ async function deleteAllFiles() {
await refreshUser(); await refreshUser();
} }
async function applyDriveCapacityOverride() {
let driveCapOrMb = driveCapacityOverrideMb;
if (driveCapacityOverrideMb && driveCapacityOverrideMb < 0) {
driveCapOrMb = null;
}
try {
await os.apiWithDialog('admin/drive-capacity-override', { userId: user.id, overrideMb: driveCapOrMb });
await refreshUser();
} catch (e) {
os.alert({
type: 'error',
text: e.toString(),
});
}
}
async function deleteAccount() { async function deleteAccount() {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'warning', type: 'warning',
@ -319,7 +351,7 @@ watch(() => props.userId, () => {
immediate: true, immediate: true,
}); });
watch(() => user, () => { watch($$(user), () => {
os.api('ap/get', { os.api('ap/get', {
uri: user.uri ?? `${url}/users/${user.id}`, uri: user.uri ?? `${url}/users/${user.id}`,
}).then(res => { }).then(res => {