diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2019-05-16 01:18:06 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2019-05-16 01:18:06 +0900 |
| commit | 7d70126072437369a813850db956f4e5e0104483 (patch) | |
| tree | c7c7805ea223ba1c2161c5c256d0c2648731ccfa /src | |
| parent | Merge branch 'develop' (diff) | |
| parent | 11.14.0 (diff) | |
| download | misskey-7d70126072437369a813850db956f4e5e0104483.tar.gz misskey-7d70126072437369a813850db956f4e5e0104483.tar.bz2 misskey-7d70126072437369a813850db956f4e5e0104483.zip | |
Merge branch 'develop'
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/app/admin/views/hashtags.vue | 41 | ||||
| -rw-r--r-- | src/client/app/admin/views/index.vue | 4 | ||||
| -rw-r--r-- | src/client/app/admin/views/instance.vue | 287 | ||||
| -rw-r--r-- | src/client/app/admin/views/users.vue | 28 | ||||
| -rw-r--r-- | src/config/types.ts | 7 | ||||
| -rw-r--r-- | src/models/entities/meta.ts | 57 | ||||
| -rw-r--r-- | src/ormconfig.ts | 18 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/delete-all-files-of-a-user.ts | 32 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/logs.ts | 22 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/update-meta.ts | 82 | ||||
| -rw-r--r-- | src/server/api/endpoints/meta.ts | 12 | ||||
| -rw-r--r-- | src/server/proxy/proxy-media.ts | 6 | ||||
| -rw-r--r-- | src/services/drive/add-file.ts | 45 | ||||
| -rw-r--r-- | src/services/drive/delete-file.ts | 18 | ||||
| -rw-r--r-- | src/services/drive/generate-video-thumbnail.ts | 4 | ||||
| -rw-r--r-- | src/services/drive/image-processor.ts | 33 |
16 files changed, 520 insertions, 176 deletions
diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue deleted file mode 100644 index e1cc4b494d..0000000000 --- a/src/client/app/admin/views/hashtags.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<div> - <ui-card> - <template #title>{{ $t('hided-tags') }}</template> - <section> - <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea> - <ui-button @click="save">{{ $t('save') }}</ui-button> - </section> - </ui-card> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; -import i18n from '../../i18n'; - -export default Vue.extend({ - i18n: i18n('admin/views/hashtags.vue'), - data() { - return { - hiddenTags: '', - }; - }, - created() { - this.$root.getMeta().then(meta => { - this.hiddenTags = meta.hiddenTags.join('\n'); - }); - }, - methods: { - save() { - this.$root.api('admin/update-meta', { - hiddenTags: this.hiddenTags.split('\n') - }).then(() => { - //this.$root.os.apis.dialog({ text: `Saved` }); - }).catch(e => { - //this.$root.os.apis.dialog({ text: `Failed ${e}` }); - }); - } - } -}); -</script> diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue index 4bce197edb..43e47038f3 100644 --- a/src/client/app/admin/views/index.vue +++ b/src/client/app/admin/views/index.vue @@ -28,7 +28,6 @@ <li @click="nav('federation')" :class="{ active: page == 'federation' }"><fa :icon="faGlobe" 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('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li> </ul> <div class="back-to-misskey"> @@ -48,7 +47,6 @@ <div v-if="page == 'users'"><x-users/></div> <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'"><x-drive/></div> <div v-if="page == 'federation'"><x-federation/></div> <div v-if="page == 'abuse'"><x-abuse/></div> @@ -68,7 +66,6 @@ import XLogs from "./logs.vue"; import XModerators from "./moderators.vue"; 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 XAbuse from "./abuse.vue"; @@ -91,7 +88,6 @@ export default Vue.extend({ XModerators, XEmoji, XAnnouncements, - XHashtags, XUsers, XDrive, XAbuse, diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 5cdd22296f..3ac4d6d721 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -2,7 +2,7 @@ <div> <ui-card> <template #title><fa icon="cog"/> {{ $t('instance') }}</template> - <section class="fit-top fit-bottom"> + <section class="fit-top"> <ui-input :value="host" readonly>{{ $t('host') }}</ui-input> <ui-input v-model="name">{{ $t('instance-name') }}</ui-input> <ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea> @@ -11,77 +11,83 @@ <ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input> <ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input> <ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input> - <ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input> - <ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input> <ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input> + <details> + <summary>{{ $t('advanced-config') }}</summary> + <ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input> + <ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input> + </details> </section> <section class="fit-bottom"> <header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header> <ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input> <ui-input v-model="maintainerEmail" type="email"><template #icon><fa :icon="farEnvelope"/></template>{{ $t('maintainer-email') }}</ui-input> </section> + <section> + <ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch> + <ui-button v-if="disableRegistration" @click="invite">{{ $t('invite') }}</ui-button> + </section> + <section> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> + </section> + </ui-card> + + <ui-card> + <template #title><fa :icon="faPencilAlt"/> {{ $t('note-and-tl') }}</template> <section class="fit-top fit-bottom"> <ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input> </section> <section> - <ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch> <ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch> <ui-switch v-model="disableGlobalTimeline">{{ $t('disable-global-timeline') }}</ui-switch> <ui-info>{{ $t('disabling-timelines-info') }}</ui-info> + </section> + <section> <ui-switch v-model="enableEmojiReaction">{{ $t('enable-emoji-reaction') }}</ui-switch> <ui-switch v-model="useStarForReactionFallback">{{ $t('use-star-for-reaction-fallback') }}</ui-switch> </section> - <section class="fit-bottom"> - <header><fa icon="cloud"/> {{ $t('drive-config') }}</header> - <ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch> - <ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> - <ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> - </section> - <section class="fit-bottom"> - <header><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</header> - <ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch> - <ui-info>{{ $t('recaptcha-info') }}</ui-info> - <ui-horizon-group inputs> - <ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input> - <ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input> - </ui-horizon-group> - </section> <section> - <header><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</header> - <ui-info>{{ $t('proxy-account-info') }}</ui-info> - <ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input> - <ui-info warn>{{ $t('proxy-account-warn') }}</ui-info> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> </section> + </ui-card> + + <ui-card> + <template #title><fa icon="cloud"/> {{ $t('drive-config') }}</template> <section> - <header><fa :icon="farEnvelope"/> {{ $t('email-config') }}</header> - <ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch> - <ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input> - <ui-horizon-group inputs> - <ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input> - <ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch> - <ui-horizon-group inputs> - <ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input> - <ui-input v-model="smtpPass" type="password" :withPasswordToggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input> - </ui-horizon-group> - <ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch> + <ui-switch v-model="useObjectStorage">{{ $t('use-object-storage') }}</ui-switch> + <template v-if="useObjectStorage"> + <ui-info> + <i18n path="object-storage-s3-info"> + <a href="https://docs.aws.amazon.com/general/latest/gr/rande.html" target="_blank">{{ $t('object-storage-s3-info-here') }}</a> + </i18n> + </ui-info> + <ui-info>{{ $t('object-storage-gcs-info') }}</ui-info> + <ui-input v-model="objectStorageBaseUrl" :disabled="!useObjectStorage">{{ $t('object-storage-base-url') }}</ui-input> + <ui-horizon-group inputs> + <ui-input v-model="objectStorageBucket" :disabled="!useObjectStorage">{{ $t('object-storage-bucket') }}</ui-input> + <ui-input v-model="objectStoragePrefix" :disabled="!useObjectStorage">{{ $t('object-storage-prefix') }}</ui-input> + </ui-horizon-group> + <ui-input v-model="objectStorageEndpoint" :disabled="!useObjectStorage">{{ $t('object-storage-endpoint') }}</ui-input> + <ui-horizon-group inputs> + <ui-input v-model="objectStorageRegion" :disabled="!useObjectStorage">{{ $t('object-storage-region') }}</ui-input> + <ui-input v-model="objectStoragePort" type="number" :disabled="!useObjectStorage">{{ $t('object-storage-port') }}</ui-input> + </ui-horizon-group> + <ui-horizon-group inputs> + <ui-input v-model="objectStorageAccessKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-access-key') }}</ui-input> + <ui-input v-model="objectStorageSecretKey" :disabled="!useObjectStorage"><template #icon><fa icon="key"/></template>{{ $t('object-storage-secret-key') }}</ui-input> + </ui-horizon-group> + <ui-switch v-model="objectStorageUseSSL" :disabled="!useObjectStorage">{{ $t('object-storage-use-ssl') }}</ui-switch> + </template> </section> <section> - <header><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</header> - <ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch> - <ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info> - <ui-horizon-group inputs class="fit-bottom"> - <ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input> - <ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input> - </ui-horizon-group> + <ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<template #desc>{{ $t('cache-remote-files-desc') }}</template></ui-switch> </section> - <section> - <header>summaly Proxy</header> - <ui-input v-model="summalyProxy">URL</ui-input> + <section class="fit-top fit-bottom"> + <ui-input v-model="localDriveCapacityMb" type="number">{{ $t('local-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> + <ui-input v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">{{ $t('remote-drive-capacity-mb') }}<template #suffix>MB</template><template #desc>{{ $t('mb') }}</template></ui-input> </section> <section> - <ui-button @click="updateMeta">{{ $t('save') }}</ui-button> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> </section> </ui-card> @@ -91,56 +97,142 @@ <ui-textarea v-model="pinnedUsers"> <template #desc>{{ $t('pinned-users-info') }}</template> </ui-textarea> - <ui-button @click="updateMeta">{{ $t('save') }}</ui-button> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> </section> </ui-card> <ui-card> - <template #title>{{ $t('invite') }}</template> + <template #title><fa :icon="faGhost"/> {{ $t('proxy-account-config') }}</template> <section> - <ui-button @click="invite">{{ $t('invite') }}</ui-button> - <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> + <ui-info>{{ $t('proxy-account-info') }}</ui-info> + <ui-input v-model="proxyAccount"><template #prefix>@</template>{{ $t('proxy-account-username') }}<template #desc>{{ $t('proxy-account-username-desc') }}</template></ui-input> + <ui-info warn>{{ $t('proxy-account-warn') }}</ui-info> + </section> + <section> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> </section> </ui-card> <ui-card> - <template #title><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</template> + <template #title><fa :icon="farEnvelope"/> {{ $t('email-config') }}</template> <section> - <ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch> - <ui-horizon-group> - <ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input> - <ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info> - <ui-button @click="updateMeta">{{ $t('save') }}</ui-button> + <ui-switch v-model="enableEmail">{{ $t('enable-email') }}<template #desc>{{ $t('email-config-info') }}</template></ui-switch> + <template v-if="enableEmail"> + <ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input> + <ui-horizon-group inputs> + <ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input> + <ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input> + </ui-horizon-group> + <ui-switch v-model="smtpAuth">{{ $t('smtp-auth') }}</ui-switch> + <ui-horizon-group inputs> + <ui-input v-model="smtpUser" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-user') }}</ui-input> + <ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input> + </ui-horizon-group> + <ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch> + </template> + </section> + <section> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> </section> </ui-card> <ui-card> - <template #title><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</template> + <template #title><fa :icon="faBolt"/> {{ $t('serviceworker-config') }}</template> <section> - <ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch> - <ui-horizon-group> - <ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input> - <ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info> - <ui-button @click="updateMeta">{{ $t('save') }}</ui-button> + <ui-switch v-model="enableServiceWorker">{{ $t('enable-serviceworker') }}<template #desc>{{ $t('serviceworker-info') }}</template></ui-switch> + <template v-if="enableServiceWorker"> + <ui-info>{{ $t('vapid-info') }}<br><code>npm i web-push -g<br>web-push generate-vapid-keys</code></ui-info> + <ui-horizon-group inputs class="fit-bottom"> + <ui-input v-model="swPublicKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-publickey') }}</ui-input> + <ui-input v-model="swPrivateKey" :disabled="!enableServiceWorker"><template #icon><fa icon="key"/></template>{{ $t('vapid-privatekey') }}</ui-input> + </ui-horizon-group> + </template> + </section> + <section> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> </section> </ui-card> <ui-card> - <template #title><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</template> + <template #title><fa :icon="faShieldAlt"/> {{ $t('recaptcha-config') }}</template> + <section :class="enableRecaptcha ? 'fit-bottom' : ''"> + <ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch> + <template v-if="enableRecaptcha"> + <ui-info>{{ $t('recaptcha-info') }}</ui-info> + <ui-horizon-group inputs> + <ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input> + <ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input> + </ui-horizon-group> + </template> + </section> <section> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> + </section> + </ui-card> + + <ui-card> + <template #title><fa :icon="faShieldAlt"/> {{ $t('external-service-integration-config') }}</template> + <section> + <header><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</header> + <ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch> + <template v-if="enableTwitterIntegration"> + <ui-horizon-group> + <ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-key') }}</ui-input> + <ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa icon="key"/></template>{{ $t('twitter-integration-consumer-secret') }}</ui-input> + </ui-horizon-group> + <ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info> + </template> + </section> + <section> + <header><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</header> + <ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch> + <template v-if="enableGithubIntegration"> + <ui-horizon-group> + <ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-id') }}</ui-input> + <ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa icon="key"/></template>{{ $t('github-integration-client-secret') }}</ui-input> + </ui-horizon-group> + <ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info> + </template> + </section> + <section> + <header><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</header> <ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch> - <ui-horizon-group> - <ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input> - <ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input> - </ui-horizon-group> - <ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info> - <ui-button @click="updateMeta">{{ $t('save') }}</ui-button> + <template v-if="enableDiscordIntegration"> + <ui-horizon-group> + <ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-id') }}</ui-input> + <ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa icon="key"/></template>{{ $t('discord-integration-client-secret') }}</ui-input> + </ui-horizon-group> + <ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info> + </template> + </section> + <section> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> </section> </ui-card> + + <details> + <summary style="color:var(--text);">{{ $t('advanced-config') }}</summary> + + <ui-card> + <template #title><fa :icon="faHashtag"/> {{ $t('hidden-tags') }}</template> + <section class="fit-top"> + <ui-textarea v-model="hiddenTags"> + <template #desc>{{ $t('hidden-tags-info') }}</template> + </ui-textarea> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> + </section> + </ui-card> + + <ui-card> + <template #title>summaly Proxy</template> + <section class="fit-top fit-bottom"> + <ui-input v-model="summalyProxy">URL</ui-input> + </section> + <section> + <ui-button @click="updateMeta"><fa :icon="faSave"/> {{ $t('save') }}</ui-button> + </section> + </ui-card> + </details> </div> </template> @@ -149,8 +241,8 @@ import Vue from 'vue'; import i18n from '../../i18n'; import { url, host } from '../../config'; import { toUnicode } from 'punycode'; -import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack } from '@fortawesome/free-solid-svg-icons'; -import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons'; +import { faHeadset, faShieldAlt, faGhost, faUserPlus, faBolt, faThumbtack, faPencilAlt, faHashtag } from '@fortawesome/free-solid-svg-icons'; +import { faEnvelope as farEnvelope, faSave } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ i18n: i18n('admin/views/instance.vue'), @@ -193,7 +285,6 @@ export default Vue.extend({ discordClientId: null, discordClientSecret: null, proxyAccount: null, - inviteCode: null, summalyProxy: null, enableEmail: false, email: null, @@ -207,7 +298,18 @@ export default Vue.extend({ swPublicKey: null, swPrivateKey: null, pinnedUsers: '', - faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack + hiddenTags: '', + useObjectStorage: false, + objectStorageBaseUrl: null, + objectStorageBucket: null, + objectStoragePrefix: null, + objectStorageEndpoint: null, + objectStorageRegion: null, + objectStoragePort: null, + objectStorageAccessKey: null, + objectStorageSecretKey: null, + objectStorageUseSSL: false, + faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope, faBolt, faThumbtack, faPencilAlt, faSave, faHashtag }; }, @@ -260,13 +362,27 @@ export default Vue.extend({ this.swPublicKey = meta.swPublickey; this.swPrivateKey = meta.swPrivateKey; this.pinnedUsers = meta.pinnedUsers.join('\n'); + this.hiddenTags = meta.hiddenTags.join('\n'); + this.useObjectStorage = meta.useObjectStorage; + this.objectStorageBaseUrl = meta.objectStorageBaseUrl; + this.objectStorageBucket = meta.objectStorageBucket; + this.objectStoragePrefix = meta.objectStoragePrefix; + this.objectStorageEndpoint = meta.objectStorageEndpoint; + this.objectStorageRegion = meta.objectStorageRegion; + this.objectStoragePort = meta.objectStoragePort; + this.objectStorageAccessKey = meta.objectStorageAccessKey; + this.objectStorageSecretKey = meta.objectStorageSecretKey; + this.objectStorageUseSSL = meta.objectStorageUseSSL; }); }, methods: { invite() { this.$root.api('admin/invite').then(x => { - this.inviteCode = x.code; + this.$root.dialog({ + type: 'info', + text: x.code + }); }).catch(e => { this.$root.dialog({ type: 'error', @@ -322,7 +438,18 @@ export default Vue.extend({ enableServiceWorker: this.enableServiceWorker, swPublicKey: this.swPublicKey, swPrivateKey: this.swPrivateKey, - pinnedUsers: this.pinnedUsers.split('\n') + pinnedUsers: this.pinnedUsers.split('\n'), + hiddenTags: this.hiddenTags.split('\n'), + useObjectStorage: this.useObjectStorage, + objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null, + objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null, + objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null, + objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null, + objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null, + objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null, + objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null, + objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null, + objectStorageUseSSL: this.objectStorageUseSSL, }).then(() => { this.$root.dialog({ type: 'success', diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue index cc38108532..fd9f0dd8b2 100644 --- a/src/client/app/admin/views/users.vue +++ b/src/client/app/admin/views/users.vue @@ -9,8 +9,9 @@ <ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> <div class="user" v-if="user"> - <x-user :user='user'/> + <x-user :user="user"/> <div class="actions"> + <ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button> <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> <ui-horizon-group> <ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button> @@ -20,7 +21,7 @@ <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> </ui-horizon-group> - <ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button> + <ui-button @click="deleteAllFiles"><fa :icon="faTrashAlt"/> {{ $t('delete-all-files') }}</ui-button> <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> </div> </div> @@ -67,7 +68,7 @@ import Vue from 'vue'; import i18n from '../../i18n'; import parseAcct from "../../../../misc/acct/parse"; import { faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'; -import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; +import { faSnowflake, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; import XUser from './users.user.vue'; export default Vue.extend({ @@ -88,7 +89,7 @@ export default Vue.extend({ offset: 0, users: [], existMore: false, - faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash + faTerminal, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash, faTrashAlt }; }, @@ -277,6 +278,25 @@ export default Vue.extend({ this.refreshUser(); }, + async deleteAllFiles() { + if (!await this.getConfirmed(this.$t('delete-all-files-confirm'))) return; + + const process = async () => { + await this.$root.api('admin/delete-all-files-of-a-user', { userId: this.user.id }); + this.$root.dialog({ + type: 'success', + splash: true + }); + }; + + await process().catch(e => { + this.$root.dialog({ + type: 'error', + text: e.toString() + }); + }); + }, + async getConfirmed(text: string): Promise<Boolean> { const confirm = await this.$root.dialog({ type: 'warning', diff --git a/src/config/types.ts b/src/config/types.ts index d312a5a181..7da9820f22 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -27,13 +27,6 @@ export type Source = { port: number; pass: string; }; - drive?: { - storage: string; - bucket?: string; - prefix?: string; - baseUrl?: string; - config?: any; - }; autoAdmin?: boolean; diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts index c3797a9ed6..fdd2818238 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -288,4 +288,61 @@ export class Meta { nullable: true }) public feedbackUrl: string | null; + + @Column('boolean', { + default: false, + }) + public useObjectStorage: boolean; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageBucket: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStoragePrefix: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageBaseUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageEndpoint: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageRegion: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageAccessKey: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public objectStorageSecretKey: string | null; + + @Column('integer', { + nullable: true + }) + public objectStoragePort: number | null; + + @Column('boolean', { + default: true, + }) + public objectStorageUseSSL: boolean; } diff --git a/src/ormconfig.ts b/src/ormconfig.ts new file mode 100644 index 0000000000..91f33181f4 --- /dev/null +++ b/src/ormconfig.ts @@ -0,0 +1,18 @@ +import * as fs from 'fs'; +import config from './config'; + +const json = { + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + entities: ['src/models/entities/*.ts'], + migrations: ['migration/*.ts'], + cli: { + migrationsDir: 'migration' + } +}; + +fs.writeFileSync('ormconfig.json', JSON.stringify(json)); diff --git a/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts new file mode 100644 index 0000000000..84e9c363e1 --- /dev/null +++ b/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -0,0 +1,32 @@ +import $ from 'cafy'; +import define from '../../define'; +import del from '../../../../services/drive/delete-file'; +import { DriveFiles } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + + params: { + userId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to suspend' + } + }, + } +}; + +export default define(meta, async (ps, me) => { + const files = await DriveFiles.find({ + userId: ps.userId + }); + + for (const file of files) { + del(file); + } +}); diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts index 86e99730c5..060df09adf 100644 --- a/src/server/api/endpoints/admin/logs.ts +++ b/src/server/api/endpoints/admin/logs.ts @@ -53,16 +53,18 @@ export default define(meta, async (ps) => { if (blackDomains.length > 0) { query.andWhere(new Brackets(qb => { for (const blackDomain of blackDomains) { - const subDomains = blackDomain.split('.'); - let i = 0; - for (const subDomain of subDomains) { - const p = `blackSubDomain_${subDomain}_${i}`; - // 全体で否定できないのでド・モルガンの法則で - // !(P && Q) を !P || !Q で表す - // SQL is 1 based, so we need '+ 1' - qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain }); - i++; - } + qb.andWhere(new Brackets(qb => { + const subDomains = blackDomain.split('.'); + let i = 0; + for (const subDomain of subDomains) { + const p = `blackSubDomain_${subDomain}_${i}`; + // 全体で否定できないのでド・モルガンの法則で + // !(P && Q) を !P || !Q で表す + // SQL is 1 based, so we need '+ 1' + qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain }); + i++; + } + })); } })); } diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index e4f2e86aaa..8e98d203ff 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -357,7 +357,47 @@ export const meta = { desc: { 'ja-JP': 'フィードバックのURL' } - } + }, + + useObjectStorage: { + validator: $.optional.bool + }, + + objectStorageBaseUrl: { + validator: $.optional.nullable.str + }, + + objectStorageBucket: { + validator: $.optional.nullable.str + }, + + objectStoragePrefix: { + validator: $.optional.nullable.str + }, + + objectStorageEndpoint: { + validator: $.optional.nullable.str + }, + + objectStorageRegion: { + validator: $.optional.nullable.str + }, + + objectStoragePort: { + validator: $.optional.nullable.num + }, + + objectStorageAccessKey: { + validator: $.optional.nullable.str + }, + + objectStorageSecretKey: { + validator: $.optional.nullable.str + }, + + objectStorageUseSSL: { + validator: $.optional.bool + }, } }; @@ -560,6 +600,46 @@ export default define(meta, async (ps) => { set.feedbackUrl = ps.feedbackUrl; } + if (ps.useObjectStorage !== undefined) { + set.useObjectStorage = ps.useObjectStorage; + } + + if (ps.objectStorageBaseUrl !== undefined) { + set.objectStorageBaseUrl = ps.objectStorageBaseUrl; + } + + if (ps.objectStorageBucket !== undefined) { + set.objectStorageBucket = ps.objectStorageBucket; + } + + if (ps.objectStoragePrefix !== undefined) { + set.objectStoragePrefix = ps.objectStoragePrefix; + } + + if (ps.objectStorageEndpoint !== undefined) { + set.objectStorageEndpoint = ps.objectStorageEndpoint; + } + + if (ps.objectStorageRegion !== undefined) { + set.objectStorageRegion = ps.objectStorageRegion; + } + + if (ps.objectStoragePort !== undefined) { + set.objectStoragePort = ps.objectStoragePort; + } + + if (ps.objectStorageAccessKey !== undefined) { + set.objectStorageAccessKey = ps.objectStorageAccessKey; + } + + if (ps.objectStorageSecretKey !== undefined) { + set.objectStorageSecretKey = ps.objectStorageSecretKey; + } + + if (ps.objectStorageUseSSL !== undefined) { + set.objectStorageUseSSL = ps.objectStorageUseSSL; + } + await getConnection().transaction(async transactionalEntityManager => { const meta = await transactionalEntityManager.findOne(Meta, { order: { diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 1bd88a1e6d..4f418c63c1 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -153,7 +153,7 @@ export default define(meta, async (ps, me) => { globalTimeLine: !instance.disableGlobalTimeline, elasticsearch: config.elasticsearch ? true : false, recaptcha: instance.enableRecaptcha, - objectStorage: config.drive && config.drive.storage === 'minio', + objectStorage: instance.useObjectStorage, twitter: instance.enableTwitterIntegration, github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, @@ -182,6 +182,16 @@ export default define(meta, async (ps, me) => { response.smtpUser = instance.smtpUser; response.smtpPass = instance.smtpPass; response.swPrivateKey = instance.swPrivateKey; + response.useObjectStorage = instance.useObjectStorage; + response.objectStorageBaseUrl = instance.objectStorageBaseUrl; + response.objectStorageBucket = instance.objectStorageBucket; + response.objectStoragePrefix = instance.objectStoragePrefix; + response.objectStorageEndpoint = instance.objectStorageEndpoint; + response.objectStorageRegion = instance.objectStorageRegion; + response.objectStoragePort = instance.objectStoragePort; + response.objectStorageAccessKey = instance.objectStorageAccessKey; + response.objectStorageSecretKey = instance.objectStorageSecretKey; + response.objectStorageUseSSL = instance.objectStorageUseSSL; } return response; diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts index 357715bb92..e16665f6cd 100644 --- a/src/server/proxy/proxy-media.ts +++ b/src/server/proxy/proxy-media.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as Koa from 'koa'; import { serverLogger } from '..'; -import { IImage, ConvertToPng, ConvertToJpeg } from '../../services/drive/image-processor'; +import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor'; import { createTemp } from '../../misc/create-temp'; import { downloadUrl } from '../../misc/donwload-url'; import { detectMine } from '../../misc/detect-mine'; @@ -20,9 +20,9 @@ export async function proxyMedia(ctx: Koa.BaseContext) { let image: IImage; if ('static' in ctx.query && ['image/png', 'image/gif'].includes(type)) { - image = await ConvertToPng(path, 498, 280); + image = await convertToPng(path, 498, 280); } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif'].includes(type)) { - image = await ConvertToJpeg(path, 200, 200); + image = await convertToJpeg(path, 200, 200); } else { image = { data: fs.readFileSync(path), diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index c67ee475a8..701878b282 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -8,11 +8,10 @@ import * as sharp from 'sharp'; import { publishMainStream, publishDriveStream } from '../stream'; import delFile from './delete-file'; -import config from '../../config'; import { fetchMeta } from '../../misc/fetch-meta'; import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; -import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; +import { IImage, convertToJpeg, convertToWebp, convertToPng, convertToGif, convertToApng } from './image-processor'; import { contentDisposition } from '../../misc/content-disposition'; import { detectMine } from '../../misc/detect-mine'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; @@ -37,7 +36,9 @@ async function save(file: DriveFile, path: string, name: string, type: string, h // thunbnail, webpublic を必要なら生成 const alts = await generateAlts(path, type, !file.uri); - if (config.drive && config.drive.storage == 'minio') { + const meta = await fetchMeta(); + + if (meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']); @@ -47,11 +48,11 @@ async function save(file: DriveFile, path: string, name: string, type: string, h if (type === 'image/webp') ext = '.webp'; } - const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; + const baseUrl = meta.objectStorageBaseUrl + || `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; // for original - const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; + const key = `${meta.objectStoragePrefix}/${uuid.v4()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -68,7 +69,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h ]; if (alts.webpublic) { - webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${alts.webpublic.ext}`; + webpublicKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; logger.info(`uploading webpublic: ${webpublicKey}`); @@ -76,7 +77,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h } if (alts.thumbnail) { - thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${alts.thumbnail.ext}`; + thumbnailKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; logger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -149,11 +150,15 @@ export async function generateAlts(path: string, type: string, generateWeb: bool logger.info(`creating web image`); if (['image/jpeg'].includes(type)) { - webpublic = await ConvertToJpeg(path, 2048, 2048); + webpublic = await convertToJpeg(path, 2048, 2048); } else if (['image/webp'].includes(type)) { - webpublic = await ConvertToWebp(path, 2048, 2048); + webpublic = await convertToWebp(path, 2048, 2048); } else if (['image/png'].includes(type)) { - webpublic = await ConvertToPng(path, 2048, 2048); + webpublic = await convertToPng(path, 2048, 2048); + } else if (['image/apng', 'image/vnd.mozilla.apng'].includes(type)) { + webpublic = await convertToApng(path); + } else if (['image/gif'].includes(type)) { + webpublic = await convertToGif(path); } else { logger.info(`web image not created (not an image)`); } @@ -166,9 +171,11 @@ export async function generateAlts(path: string, type: string, generateWeb: bool let thumbnail: IImage | null = null; if (['image/jpeg', 'image/webp'].includes(type)) { - thumbnail = await ConvertToJpeg(path, 498, 280); + thumbnail = await convertToJpeg(path, 498, 280); } else if (['image/png'].includes(type)) { - thumbnail = await ConvertToPng(path, 498, 280); + thumbnail = await convertToPng(path, 498, 280); + } else if (['image/gif'].includes(type)) { + thumbnail = await convertToGif(path); } else if (type.startsWith('video/')) { try { thumbnail = await GenerateVideoThumbnail(path); @@ -188,7 +195,15 @@ export async function generateAlts(path: string, type: string, generateWeb: bool * Upload to ObjectStorage */ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { - const minio = new Minio.Client(config.drive!.config); + const meta = await fetchMeta(); + + const minio = new Minio.Client({ + endPoint: meta.objectStorageEndpoint!, + port: meta.objectStoragePort ? meta.objectStoragePort : undefined, + useSSL: meta.objectStorageUseSSL, + accessKey: meta.objectStorageAccessKey!, + secretKey: meta.objectStorageSecretKey!, + }); const metadata = { 'Content-Type': type, @@ -197,7 +212,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, if (filename) metadata['Content-Disposition'] = contentDisposition('inline', filename); - await minio.putObject(config.drive!.bucket!, key, stream, undefined, metadata); + await minio.putObject(meta.objectStorageBucket!, key, stream, undefined, metadata); } async function deleteOldFile(user: IRemoteUser) { diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index f1280822a4..ba0482dbe2 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -1,9 +1,9 @@ import * as Minio from 'minio'; -import config from '../../config'; import { DriveFile } from '../../models/entities/drive-file'; import { InternalStorage } from './internal-storage'; import { DriveFiles, Instances, Notes } from '../../models'; import { driveChart, perUserDriveChart, instanceChart } from '../chart'; +import { fetchMeta } from '../../misc/fetch-meta'; export default async function(file: DriveFile, isExpired = false) { if (file.storedInternal) { @@ -17,16 +17,24 @@ export default async function(file: DriveFile, isExpired = false) { InternalStorage.del(file.webpublicAccessKey!); } } else if (!file.isLink) { - const minio = new Minio.Client(config.drive!.config); + const meta = await fetchMeta(); - await minio.removeObject(config.drive!.bucket!, file.accessKey!); + const minio = new Minio.Client({ + endPoint: meta.objectStorageEndpoint!, + port: meta.objectStoragePort ? meta.objectStoragePort : undefined, + useSSL: meta.objectStorageUseSSL, + accessKey: meta.objectStorageAccessKey!, + secretKey: meta.objectStorageSecretKey!, + }); + + await minio.removeObject(meta.objectStorageBucket!, file.accessKey!); if (file.thumbnailUrl) { - await minio.removeObject(config.drive!.bucket!, file.thumbnailAccessKey!); + await minio.removeObject(meta.objectStorageBucket!, file.thumbnailAccessKey!); } if (file.webpublicUrl) { - await minio.removeObject(config.drive!.bucket!, file.webpublicAccessKey!); + await minio.removeObject(meta.objectStorageBucket!, file.webpublicAccessKey!); } } diff --git a/src/services/drive/generate-video-thumbnail.ts b/src/services/drive/generate-video-thumbnail.ts index 5d7efff27b..c2646182db 100644 --- a/src/services/drive/generate-video-thumbnail.ts +++ b/src/services/drive/generate-video-thumbnail.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as tmp from 'tmp'; -import { IImage, ConvertToJpeg } from './image-processor'; +import { IImage, convertToJpeg } from './image-processor'; const ThumbnailGenerator = require('video-thumbnail-generator').default; export async function GenerateVideoThumbnail(path: string): Promise<IImage> { @@ -23,7 +23,7 @@ export async function GenerateVideoThumbnail(path: string): Promise<IImage> { const outPath = `${outDir}/output.png`; - const thumbnail = await ConvertToJpeg(outPath, 498, 280); + const thumbnail = await convertToJpeg(outPath, 498, 280); // cleanup fs.unlinkSync(outPath); diff --git a/src/services/drive/image-processor.ts b/src/services/drive/image-processor.ts index 89ac331ca1..4b8db0e0c8 100644 --- a/src/services/drive/image-processor.ts +++ b/src/services/drive/image-processor.ts @@ -1,4 +1,5 @@ import * as sharp from 'sharp'; +import * as fs from 'fs'; export type IImage = { data: Buffer; @@ -10,7 +11,7 @@ export type IImage = { * Convert to JPEG * with resize, remove metadata, resolve orientation, stop animation */ -export async function ConvertToJpeg(path: string, width: number, height: number): Promise<IImage> { +export async function convertToJpeg(path: string, width: number, height: number): Promise<IImage> { const data = await sharp(path) .resize(width, height, { fit: 'inside', @@ -34,7 +35,7 @@ export async function ConvertToJpeg(path: string, width: number, height: number) * Convert to WebP * with resize, remove metadata, resolve orientation, stop animation */ -export async function ConvertToWebp(path: string, width: number, height: number): Promise<IImage> { +export async function convertToWebp(path: string, width: number, height: number): Promise<IImage> { const data = await sharp(path) .resize(width, height, { fit: 'inside', @@ -57,7 +58,7 @@ export async function ConvertToWebp(path: string, width: number, height: number) * Convert to PNG * with resize, remove metadata, resolve orientation, stop animation */ -export async function ConvertToPng(path: string, width: number, height: number): Promise<IImage> { +export async function convertToPng(path: string, width: number, height: number): Promise<IImage> { const data = await sharp(path) .resize(width, height, { fit: 'inside', @@ -73,3 +74,29 @@ export async function ConvertToPng(path: string, width: number, height: number): type: 'image/png' }; } + +/** + * Convert to GIF (Actually just NOP) + */ +export async function convertToGif(path: string): Promise<IImage> { + const data = await fs.promises.readFile(path); + + return { + data, + ext: 'gif', + type: 'image/gif' + }; +} + +/** + * Convert to APNG (Actually just NOP) + */ +export async function convertToApng(path: string): Promise<IImage> { + const data = await fs.promises.readFile(path); + + return { + data, + ext: 'apng', + type: 'image/apng' + }; +} |