Improve drive file operation

Resolve #3789
Resolve #3790
This commit is contained in:
syuilo 2019-01-19 09:50:38 +09:00
parent 11303b5bec
commit d2a7c56149
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
5 changed files with 167 additions and 9 deletions

View file

@ -4,6 +4,7 @@ ChangeLog
unreleased unreleased
---------- ----------
* アクティブユーザー数のチャートを追加 * アクティブユーザー数のチャートを追加
* 管理画面でドライブのファイルをURLやIDから操作できるように
* ログイン時に二段階認証が分かりにくいのを改善 * ログイン時に二段階認証が分かりにくいのを改善
* 投稿のツールチップを出すのは時間の上だけに変更 * 投稿のツールチップを出すのは時間の上だけに変更
* ハッシュタグ判定の強化 * ハッシュタグ判定の強化

View file

@ -1216,6 +1216,10 @@ admin/views/charts.vue:
network-usage: "通信量" network-usage: "通信量"
admin/views/drive.vue: admin/views/drive.vue:
operation: "操作"
fileid-or-url: "ファイルIDまたはファイルURL"
file-not-found: "ファイルが見つかりません"
lookup: "照会"
sort: sort:
title: "ソート" title: "ソート"
createdAtAsc: "アップロード日時が古い順" createdAtAsc: "アップロード日時が古い順"
@ -1231,6 +1235,8 @@ admin/views/drive.vue:
deleted: "削除しました" deleted: "削除しました"
mark-as-sensitive: "閲覧注意に設定" mark-as-sensitive: "閲覧注意に設定"
unmark-as-sensitive: "閲覧注意を解除" unmark-as-sensitive: "閲覧注意を解除"
marked-as-sensitive: "閲覧注意に設定しました"
unmarked-as-sensitive: "閲覧注意を解除しました"
admin/views/users.vue: admin/views/users.vue:
operation: "操作" operation: "操作"

View file

@ -1,5 +1,21 @@
<template> <template>
<div class="pwnqwyet"> <div class="pwnqwyet">
<ui-card>
<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
<section class="fit-top">
<ui-input v-model="target" type="text">
<span>{{ $t('fileid-or-url') }}</span>
</ui-input>
<ui-horizon-group>
<ui-button @click="findAndToggleSensitive(true)"><fa :icon="faEyeSlash"/> {{ $t('mark-as-sensitive') }}</ui-button>
<ui-button @click="findAndToggleSensitive(false)"><fa :icon="faEye"/> {{ $t('unmark-as-sensitive') }}</ui-button>
</ui-horizon-group>
<ui-button @click="findAndDel()"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
<ui-button @click="show()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<ui-textarea v-if="file" :value="file | json5" readonly tall style="margin-top:16px;"></ui-textarea>
</section>
</ui-card>
<ui-card> <ui-card>
<div slot="title"><fa :icon="faCloud"/> {{ $t('@.drive') }}</div> <div slot="title"><fa :icon="faCloud"/> {{ $t('@.drive') }}</div>
<section class="fit-top"> <section class="fit-top">
@ -57,7 +73,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { faCloud } from '@fortawesome/free-solid-svg-icons'; import { faCloud, faTerminal, faSearch } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({ export default Vue.extend({
@ -65,13 +81,15 @@ export default Vue.extend({
data() { data() {
return { return {
file: null,
target: null,
sort: '+createdAt', sort: '+createdAt',
origin: 'combined', origin: 'combined',
limit: 10, limit: 10,
offset: 0, offset: 0,
files: [], files: [],
existMore: false, existMore: false,
faCloud, faTrashAlt, faEye, faEyeSlash faCloud, faTrashAlt, faEye, faEyeSlash, faTerminal, faSearch
}; };
}, },
@ -94,6 +112,24 @@ export default Vue.extend({
}, },
methods: { methods: {
async fetchFile() {
try {
return await this.$root.api('drive/files/show', this.target.startsWith('http') ? { url: this.target } : { fileId: this.target });
} catch (e) {
if (e == 'file-not-found') {
this.$root.dialog({
type: 'error',
text: this.$t('file-not-found')
});
} else {
this.$root.dialog({
type: 'error',
text: e.toString()
});
}
}
},
fetch() { fetch() {
this.$root.api('admin/drive/files', { this.$root.api('admin/drive/files', {
origin: this.origin, origin: this.origin,
@ -147,6 +183,52 @@ export default Vue.extend({
file.isSensitive = !file.isSensitive; file.isSensitive = !file.isSensitive;
}, },
async show() {
const file = await this.fetchFile();
this.$root.api('admin/drive/show-file', { fileId: file.id }).then(info => {
this.file = info;
});
},
async findAndToggleSensitive(sensitive) {
const process = async () => {
const file = await this.fetchFile();
await this.$root.api('drive/files/update', {
fileId: file.id,
isSensitive: sensitive
});
this.$root.dialog({
type: 'success',
text: sensitive ? this.$t('marked-as-sensitive') : this.$t('unmarked-as-sensitive')
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
},
async findAndDel() {
const process = async () => {
const file = await this.fetchFile();
await this.$root.api('drive/files/delete', { fileId: file.id });
this.$root.dialog({
type: 'success',
text: this.$t('deleted')
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
},
} }
}); });
</script> </script>

View file

@ -0,0 +1,28 @@
import $ from 'cafy';
import ID, { transform } from '../../../../../misc/cafy-id';
import define from '../../../define';
import DriveFile from '../../../../../models/drive-file';
export const meta = {
requireCredential: true,
requireModerator: true,
params: {
fileId: {
validator: $.type(ID),
transform: transform,
},
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const file = await DriveFile.findOne({
_id: ps.fileId
});
if (file == null) {
return rej('file not found');
}
res(file);
}));

View file

@ -1,6 +1,9 @@
import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; import $ from 'cafy';
import DriveFile, { pack } from '../../../../../models/drive-file'; import * as mongo from 'mongodb';
import ID, { transform } from '../../../../../misc/cafy-id';
import DriveFile, { pack, IDriveFile } from '../../../../../models/drive-file';
import define from '../../../define'; import define from '../../../define';
import config from '../../../../../config';
export const meta = { export const meta = {
stability: 'stable', stability: 'stable',
@ -16,24 +19,62 @@ export const meta = {
params: { params: {
fileId: { fileId: {
validator: $.type(ID), validator: $.type(ID).optional,
transform: transform, transform: transform,
desc: { desc: {
'ja-JP': '対象のファイルID', 'ja-JP': '対象のファイルID',
'en-US': 'Target file ID' 'en-US': 'Target file ID'
} }
},
url: {
validator: $.str.optional,
desc: {
'ja-JP': '対象のファイルのURL',
'en-US': 'Target file URL'
}
} }
} }
}; };
export default define(meta, (ps, user) => new Promise(async (res, rej) => { export default define(meta, (ps, user) => new Promise(async (res, rej) => {
// Fetch file let file: IDriveFile;
const file = await DriveFile
.findOne({ if (ps.fileId) {
file = await DriveFile.findOne({
_id: ps.fileId, _id: ps.fileId,
'metadata.userId': user._id,
'metadata.deletedAt': { $exists: false } 'metadata.deletedAt': { $exists: false }
}); });
} else if (ps.url) {
const isInternalStorageUrl = ps.url.startsWith(config.drive_url);
if (isInternalStorageUrl) {
// Extract file if from url
// e.g.
// http://misskey.local/files/foo?original=bar --> foo
const fileId = new mongo.ObjectID(ps.url.replace(config.drive_url, '').replace(/\?(.*)$/, '').replace(/\//g, ''));
file = await DriveFile.findOne({
_id: fileId,
'metadata.deletedAt': { $exists: false }
});
} else {
file = await DriveFile.findOne({
$or: [{
'metadata.url': ps.url
}, {
'metadata.webpublicUrl': ps.url
}, {
'metadata.thumbnailUrl': ps.url
}],
'metadata.deletedAt': { $exists: false }
});
}
} else {
return rej('fileId or url required');
}
if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) {
return rej('access denied');
}
if (file === null) { if (file === null) {
return rej('file-not-found'); return rej('file-not-found');