summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2019-05-16 01:18:06 +0900
committersyuilo <syuilotan@yahoo.co.jp>2019-05-16 01:18:06 +0900
commit7d70126072437369a813850db956f4e5e0104483 (patch)
treec7c7805ea223ba1c2161c5c256d0c2648731ccfa /src
parentMerge branch 'develop' (diff)
parent11.14.0 (diff)
downloadmisskey-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.vue41
-rw-r--r--src/client/app/admin/views/index.vue4
-rw-r--r--src/client/app/admin/views/instance.vue287
-rw-r--r--src/client/app/admin/views/users.vue28
-rw-r--r--src/config/types.ts7
-rw-r--r--src/models/entities/meta.ts57
-rw-r--r--src/ormconfig.ts18
-rw-r--r--src/server/api/endpoints/admin/delete-all-files-of-a-user.ts32
-rw-r--r--src/server/api/endpoints/admin/logs.ts22
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts82
-rw-r--r--src/server/api/endpoints/meta.ts12
-rw-r--r--src/server/proxy/proxy-media.ts6
-rw-r--r--src/services/drive/add-file.ts45
-rw-r--r--src/services/drive/delete-file.ts18
-rw-r--r--src/services/drive/generate-video-thumbnail.ts4
-rw-r--r--src/services/drive/image-processor.ts33
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'
+ };
+}