diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue new file mode 100644 index 0000000000..d543d0a077 --- /dev/null +++ b/src/client/app/admin/views/drive.vue @@ -0,0 +1,139 @@ +<template> +<div class="pwnqwyet"> + <ui-card> + <div slot="title"><fa :icon="faCloud"/> {{ $t('@.drive') }}</div> + <section class="fit-top"> + <ui-horizon-group inputs> + <ui-select v-model="sort"> + <span slot="label">{{ $t('sort.title') }}</span> + <option value="-createdAt">{{ $t('sort.createdAtAsc') }}</option> + <option value="+createdAt">{{ $t('sort.createdAtDesc') }}</option> + <option value="-size">{{ $t('sort.sizeAsc') }}</option> + <option value="+size">{{ $t('sort.sizeDesc') }}</option> + </ui-select> + <ui-select v-model="origin"> + <span slot="label">{{ $t('origin.title') }}</span> + <option value="combined">{{ $t('origin.combined') }}</option> + <option value="local">{{ $t('origin.local') }}</option> + <option value="remote">{{ $t('origin.remote') }}</option> + </ui-select> + </ui-horizon-group> + <div class="kidvdlkg" v-for="file in files"> + <div> + <div class="thumbnail" :style="thumbnail(file)"></div> + </div> + <div> + <header> + <b>{{ file.name }}</b> + <span class="username">@{{ file.user | acct }}</span> + </header> + <div> + <div> + <span style="margin-right:16px;">{{ file.type }}</span> + <span>{{ file.datasize | bytes }}</span> + </div> + <div><mk-time :time="file.createdAt" mode="detail"/></div> + </div> + </div> + </div> + <ui-button v-if="existMore" @click="fetch">{{ $t('@.load-more') }}</ui-button> + </section> + </ui-card> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../i18n'; +import { faCloud } from '@fortawesome/free-solid-svg-icons'; + +export default Vue.extend({ + i18n: i18n('admin/views/drive.vue'), + + data() { + return { + sort: '+createdAt', + origin: 'combined', + limit: 10, + offset: 0, + files: [], + existMore: false, + faCloud + }; + }, + + watch: { + sort() { + this.files = []; + this.offset = 0; + this.fetch(); + }, + + origin() { + this.files = []; + this.offset = 0; + this.fetch(); + } + }, + + mounted() { + this.fetch(); + }, + + methods: { + fetch() { + this.$root.api('admin/drive/files', { + origin: this.origin, + sort: this.sort, + offset: this.offset, + limit: this.limit + 1 + }).then(files => { + if (files.length == this.limit + 1) { + files.pop(); + this.existMore = true; + } else { + this.existMore = false; + } + this.files = this.files.concat(files); + this.offset += this.limit; + }); + }, + + thumbnail(file: any): any { + return { + 'background-color': file.properties.avgColor && file.properties.avgColor.length == 3 ? `rgb(${file.properties.avgColor.join(',')})` : 'transparent', + 'background-image': `url(${file.thumbnailUrl})` + }; + } + } +}); +</script> + +<style lang="stylus" scoped> +.pwnqwyet + @media (min-width 500px) + padding 16px + + .kidvdlkg + display flex + padding 16px 0 + border-top solid 1px var(--faceDivider) + + > div:first-child + > .thumbnail + display block + width 64px + height 64px + background-size cover + background-position center center + + > div:last-child + flex 1 + padding-left 16px + + > header + > .username + margin-left 8px + opacity 0.7 + +</style> diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue index 3da13ad214..9524a98542 100644 --- a/src/client/app/admin/views/index.vue +++ b/src/client/app/admin/views/index.vue @@ -22,12 +22,11 @@ <li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li> <li @click="nav('moderators')" :class="{ active: page == 'moderators' }"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</li> <li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>{{ $t('users') }}</li> + <li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li> <!-- <li @click="nav('federation')" :class="{ active: page == 'federation' }"><fa :icon="faShareAlt" fixed-width/>{{ $t('federation') }}</li> --> <li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li> <li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li> <li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li> - - <!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li> --> </ul> <div class="back-to-misskey"> <a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a> @@ -45,7 +44,7 @@ <div v-if="page == 'emoji'"><x-emoji/></div> <div v-if="page == 'announcements'"><x-announcements/></div> <div v-if="page == 'hashtags'"><x-hashtags/></div> - <div v-if="page == 'drive'"></div> + <div v-if="page == 'drive'"><x-drive/></div> <div v-if="page == 'update'"></div> </div> </main> @@ -63,6 +62,7 @@ import XEmoji from "./emoji.vue"; import XAnnouncements from "./announcements.vue"; import XHashtags from "./hashtags.vue"; import XUsers from "./users.vue"; +import XDrive from "./drive.vue"; import { faHeadset, faArrowLeft, faShareAlt } from '@fortawesome/free-solid-svg-icons'; import { faGrin } from '@fortawesome/free-regular-svg-icons'; @@ -79,7 +79,8 @@ export default Vue.extend({ XEmoji, XAnnouncements, XHashtags, - XUsers + XUsers, + XDrive, }, provide: { isMobile diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index e4c1598049..24c7ac75c0 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -1,6 +1,7 @@ import * as mongo from 'mongodb'; const deepcopy = require('deepcopy'); import { pack as packFolder } from './drive-folder'; +import { pack as packUser } from './user'; import monkDb, { nativeDbConn } from '../db/mongodb'; import isObjectId from '../misc/is-objectid'; import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url'; @@ -131,6 +132,7 @@ export const packMany = ( options?: { detail?: boolean self?: boolean, + withUser?: boolean, } ) => { return Promise.all(files.map(f => pack(f, options))); @@ -144,6 +146,7 @@ export const pack = ( options?: { detail?: boolean, self?: boolean, + withUser?: boolean, } ) => new Promise<any>(async (resolve, reject) => { const opts = Object.assign({ @@ -208,6 +211,11 @@ export const pack = ( */ } + if (opts.withUser) { + // Populate user + _target.user = await packUser(_file.metadata.userId); + } + delete _target.withoutChunks; delete _target.storage; delete _target.storageProps; diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts new file mode 100644 index 0000000000..2e54270a0f --- /dev/null +++ b/src/server/api/endpoints/admin/drive/files.ts @@ -0,0 +1,79 @@ +import $ from 'cafy'; +import File, { packMany } from '../../../../../models/drive-file'; +import define from '../../../define'; + +export const meta = { + requireCredential: false, + requireModerator: true, + + params: { + limit: { + validator: $.num.optional.range(1, 100), + default: 10 + }, + + offset: { + validator: $.num.optional.min(0), + default: 0 + }, + + sort: { + validator: $.str.optional.or([ + '+createdAt', + '-createdAt', + '+size', + '-size', + ]), + }, + + origin: { + validator: $.str.optional.or([ + 'combined', + 'local', + 'remote', + ]), + default: 'local' + } + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + let _sort; + if (ps.sort) { + if (ps.sort == '+createdAt') { + _sort = { + uploadDate: -1 + }; + } else if (ps.sort == '-createdAt') { + _sort = { + uploadDate: 1 + }; + } else if (ps.sort == '+size') { + _sort = { + length: -1 + }; + } else if (ps.sort == '+size') { + _sort = { + length: -1 + }; + } + } else { + _sort = { + _id: -1 + }; + } + + const q = + ps.origin == 'local' ? { host: null } : + ps.origin == 'remote' ? { host: { $ne: null } } : + {}; + + const files = await File + .find(q, { + limit: ps.limit, + sort: _sort, + skip: ps.offset + }); + + res(await packMany(files, { detail: true, withUser: true })); +}));