parent
							
								
									11303b5bec
								
							
						
					
					
						commit
						d2a7c56149
					
				|  | @ -4,6 +4,7 @@ ChangeLog | |||
| unreleased | ||||
| ---------- | ||||
| * アクティブユーザー数のチャートを追加 | ||||
| * 管理画面でドライブのファイルをURLやIDから操作できるように | ||||
| * ログイン時に二段階認証が分かりにくいのを改善 | ||||
| * 投稿のツールチップを出すのは時間の上だけに変更 | ||||
| * ハッシュタグ判定の強化 | ||||
|  |  | |||
|  | @ -1216,6 +1216,10 @@ admin/views/charts.vue: | |||
|     network-usage: "通信量" | ||||
| 
 | ||||
| admin/views/drive.vue: | ||||
|   operation: "操作" | ||||
|   fileid-or-url: "ファイルIDまたはファイルURL" | ||||
|   file-not-found: "ファイルが見つかりません" | ||||
|   lookup: "照会" | ||||
|   sort: | ||||
|     title: "ソート" | ||||
|     createdAtAsc: "アップロード日時が古い順" | ||||
|  | @ -1231,6 +1235,8 @@ admin/views/drive.vue: | |||
|   deleted: "削除しました" | ||||
|   mark-as-sensitive: "閲覧注意に設定" | ||||
|   unmark-as-sensitive: "閲覧注意を解除" | ||||
|   marked-as-sensitive: "閲覧注意に設定しました" | ||||
|   unmarked-as-sensitive: "閲覧注意を解除しました" | ||||
| 
 | ||||
| admin/views/users.vue: | ||||
|   operation: "操作" | ||||
|  |  | |||
|  | @ -1,5 +1,21 @@ | |||
| <template> | ||||
| <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> | ||||
| 		<div slot="title"><fa :icon="faCloud"/> {{ $t('@.drive') }}</div> | ||||
| 		<section class="fit-top"> | ||||
|  | @ -57,7 +73,7 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 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'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
|  | @ -65,13 +81,15 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			file: null, | ||||
| 			target: null, | ||||
| 			sort: '+createdAt', | ||||
| 			origin: 'combined', | ||||
| 			limit: 10, | ||||
| 			offset: 0, | ||||
| 			files: [], | ||||
| 			existMore: false, | ||||
| 			faCloud, faTrashAlt, faEye, faEyeSlash | ||||
| 			faCloud, faTrashAlt, faEye, faEyeSlash, faTerminal, faSearch | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -94,6 +112,24 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 
 | ||||
| 	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() { | ||||
| 			this.$root.api('admin/drive/files', { | ||||
| 				origin: this.origin, | ||||
|  | @ -147,6 +183,52 @@ export default Vue.extend({ | |||
| 
 | ||||
| 			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> | ||||
|  |  | |||
|  | @ -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); | ||||
| })); | ||||
|  | @ -1,6 +1,9 @@ | |||
| import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; | ||||
| import DriveFile, { pack } from '../../../../../models/drive-file'; | ||||
| import $ from 'cafy'; | ||||
| 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 config from '../../../../../config'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	stability: 'stable', | ||||
|  | @ -16,24 +19,62 @@ export const meta = { | |||
| 
 | ||||
| 	params: { | ||||
| 		fileId: { | ||||
| 			validator: $.type(ID), | ||||
| 			validator: $.type(ID).optional, | ||||
| 			transform: transform, | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のファイル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) => { | ||||
| 	// Fetch file
 | ||||
| 	const file = await DriveFile | ||||
| 		.findOne({ | ||||
| 	let file: IDriveFile; | ||||
| 
 | ||||
| 	if (ps.fileId) { | ||||
| 		file = await DriveFile.findOne({ | ||||
| 			_id: ps.fileId, | ||||
| 			'metadata.userId': user._id, | ||||
| 			'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) { | ||||
| 		return rej('file-not-found'); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue