Merge pull request #1968 from syuilo/object-storage
Object storage support
This commit is contained in:
		
						commit
						7432de3d33
					
				
					 45 changed files with 174 additions and 159 deletions
				
			
		| 
						 | 
					@ -53,6 +53,22 @@ remoteDriveCapacityMb: 8
 | 
				
			||||||
#  Users cannot see remote images when they turn off "Show media from a remote server" setting.
 | 
					#  Users cannot see remote images when they turn off "Show media from a remote server" setting.
 | 
				
			||||||
preventCache: false
 | 
					preventCache: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					drive:
 | 
				
			||||||
 | 
					  storage: 'db'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # OR
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # storage: 'object-storage'
 | 
				
			||||||
 | 
					  # service: 'minio'
 | 
				
			||||||
 | 
					  # bucket:
 | 
				
			||||||
 | 
					  # prefix:
 | 
				
			||||||
 | 
					  # config:
 | 
				
			||||||
 | 
					  #   endPoint:
 | 
				
			||||||
 | 
					  #   port:
 | 
				
			||||||
 | 
					  #   secure:
 | 
				
			||||||
 | 
					  #   accessKey:
 | 
				
			||||||
 | 
					  #   secretKey:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Below settings are optional
 | 
					# Below settings are optional
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ const q = {
 | 
				
			||||||
	'metadata._user.host': {
 | 
						'metadata._user.host': {
 | 
				
			||||||
		$ne: null
 | 
							$ne: null
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	'metadata.isMetaOnly': false
 | 
						'metadata.withoutChunks': false
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ async function main() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					DriveFile.update({ _id: file._id }, {
 | 
										DriveFile.update({ _id: file._id }, {
 | 
				
			||||||
						$set: {
 | 
											$set: {
 | 
				
			||||||
							'metadata.isMetaOnly': true
 | 
												'metadata.withoutChunks': true
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					})
 | 
										})
 | 
				
			||||||
				]).then(async () => {
 | 
									]).then(async () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,8 @@
 | 
				
			||||||
const chalk = require('chalk');
 | 
					const chalk = require('chalk');
 | 
				
			||||||
const sequential = require('promise-sequential');
 | 
					const sequential = require('promise-sequential');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { default: User } = require('../built/models/user');
 | 
					const { default: User } = require('../../built/models/user');
 | 
				
			||||||
const { default: DriveFile } = require('../built/models/drive-file');
 | 
					const { default: DriveFile } = require('../../built/models/drive-file');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	const promiseGens = [];
 | 
						const promiseGens = [];
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,8 @@
 | 
				
			||||||
const chalk = require('chalk');
 | 
					const chalk = require('chalk');
 | 
				
			||||||
const sequential = require('promise-sequential');
 | 
					const sequential = require('promise-sequential');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { default: User } = require('../built/models/user');
 | 
					const { default: User } = require('../../built/models/user');
 | 
				
			||||||
const { default: DriveFile } = require('../built/models/drive-file');
 | 
					const { default: DriveFile } = require('../../built/models/drive-file');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function main() {
 | 
					async function main() {
 | 
				
			||||||
	const promiseGens = [];
 | 
						const promiseGens = [];
 | 
				
			||||||
							
								
								
									
										10
									
								
								cli/migration/5.0.0.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								cli/migration/5.0.0.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					const { default: DriveFile } = require('../../built/models/drive-file');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DriveFile.update({}, {
 | 
				
			||||||
 | 
						$rename: {
 | 
				
			||||||
 | 
							'metadata.url': 'metadata.src',
 | 
				
			||||||
 | 
							'metadata.isMetaOnly': 'metadata.withoutChunks',
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}, {
 | 
				
			||||||
 | 
						multi: true
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,11 +0,0 @@
 | 
				
			||||||
Misskeyの破壊的変更に対応するいくつかのスニペットがあります。
 | 
					 | 
				
			||||||
MongoDBシェルで実行する必要のあるものとnodeで直接実行する必要のあるものがあります。
 | 
					 | 
				
			||||||
ファイル名が `shell.` から始まるものは前者、 `node.` から始まるものは後者です。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
MongoDBシェルで実行する場合、`use`でデータベースを選択しておく必要があります。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
nodeで実行するいくつかのスニペットは、並列処理させる数を引数で設定できるものがあります。
 | 
					 | 
				
			||||||
処理中にエラーで落ちる場合は、メモリが足りていない可能性があるので、少ない数に設定してみてください。
 | 
					 | 
				
			||||||
※デフォルトは`5`です。
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
ファイルを作成する際は `../init-migration-file.sh -t _type_ -n _name_` を実行すると _type_._unixtime_._name_.js が生成されます
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,37 +0,0 @@
 | 
				
			||||||
#!/bin/bash
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
usage() {
 | 
					 | 
				
			||||||
		echo "$0 [-t type] [-n name]"
 | 
					 | 
				
			||||||
		echo "  type: [node | shell]"
 | 
					 | 
				
			||||||
		echo "  name: if no present, set untitled"
 | 
					 | 
				
			||||||
		exit 0
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
while getopts :t:n:h OPT
 | 
					 | 
				
			||||||
do
 | 
					 | 
				
			||||||
	case $OPT in
 | 
					 | 
				
			||||||
		t)	type=$OPTARG
 | 
					 | 
				
			||||||
				;;
 | 
					 | 
				
			||||||
		n)	name=$OPTARG
 | 
					 | 
				
			||||||
				;;
 | 
					 | 
				
			||||||
		h)	usage
 | 
					 | 
				
			||||||
				;;
 | 
					 | 
				
			||||||
		\?) usage
 | 
					 | 
				
			||||||
				;;
 | 
					 | 
				
			||||||
		:)	usage
 | 
					 | 
				
			||||||
				;;
 | 
					 | 
				
			||||||
	esac
 | 
					 | 
				
			||||||
done
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if [ "$type" = "" ]
 | 
					 | 
				
			||||||
then
 | 
					 | 
				
			||||||
	echo "no type present!!!"
 | 
					 | 
				
			||||||
	usage
 | 
					 | 
				
			||||||
fi
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if [ "$name" = "" ]
 | 
					 | 
				
			||||||
then
 | 
					 | 
				
			||||||
	name="untitled"
 | 
					 | 
				
			||||||
fi
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
touch "$(realpath $(dirname $BASH_SOURCE))/migration/$type.$(date +%s).$name.js"
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	"name": "misskey",
 | 
						"name": "misskey",
 | 
				
			||||||
	"author": "syuilo <i@syuilo.com>",
 | 
						"author": "syuilo <i@syuilo.com>",
 | 
				
			||||||
	"version": "4.27.0",
 | 
						"version": "5.0.0",
 | 
				
			||||||
	"clientVersion": "1.0.7487",
 | 
						"clientVersion": "1.0.7487",
 | 
				
			||||||
	"codename": "nighthike",
 | 
						"codename": "nighthike",
 | 
				
			||||||
	"main": "./built/index.js",
 | 
						"main": "./built/index.js",
 | 
				
			||||||
| 
						 | 
					@ -57,6 +57,7 @@
 | 
				
			||||||
		"@types/koa-views": "2.0.3",
 | 
							"@types/koa-views": "2.0.3",
 | 
				
			||||||
		"@types/koa__cors": "2.2.2",
 | 
							"@types/koa__cors": "2.2.2",
 | 
				
			||||||
		"@types/kue": "0.11.9",
 | 
							"@types/kue": "0.11.9",
 | 
				
			||||||
 | 
							"@types/minio": "6.0.2",
 | 
				
			||||||
		"@types/mkdirp": "0.5.2",
 | 
							"@types/mkdirp": "0.5.2",
 | 
				
			||||||
		"@types/mocha": "5.2.3",
 | 
							"@types/mocha": "5.2.3",
 | 
				
			||||||
		"@types/mongodb": "3.1.2",
 | 
							"@types/mongodb": "3.1.2",
 | 
				
			||||||
| 
						 | 
					@ -147,6 +148,7 @@
 | 
				
			||||||
		"kue": "0.11.6",
 | 
							"kue": "0.11.6",
 | 
				
			||||||
		"loader-utils": "1.1.0",
 | 
							"loader-utils": "1.1.0",
 | 
				
			||||||
		"mecab-async": "0.1.2",
 | 
							"mecab-async": "0.1.2",
 | 
				
			||||||
 | 
							"minio": "6.0.0",
 | 
				
			||||||
		"mkdirp": "0.5.1",
 | 
							"mkdirp": "0.5.1",
 | 
				
			||||||
		"mocha": "5.2.0",
 | 
							"mocha": "5.2.0",
 | 
				
			||||||
		"moji": "0.5.1",
 | 
							"moji": "0.5.1",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<div class="form">
 | 
					<div class="form">
 | 
				
			||||||
	<header>
 | 
						<header>
 | 
				
			||||||
		<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1>
 | 
							<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1>
 | 
				
			||||||
		<img :src="`${app.iconUrl}?thumbnail&size=64`"/>
 | 
							<img :src="app.iconUrl"/>
 | 
				
			||||||
	</header>
 | 
						</header>
 | 
				
			||||||
	<div class="app">
 | 
						<div class="app">
 | 
				
			||||||
		<section>
 | 
							<section>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,21 +17,21 @@ export default function(type, data): Notification {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				title: 'ファイルがアップロードされました',
 | 
									title: 'ファイルがアップロードされました',
 | 
				
			||||||
				body: data.name,
 | 
									body: data.name,
 | 
				
			||||||
				icon: data.url + '?thumbnail&size=64'
 | 
									icon: data.url
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		case 'unread_messaging_message':
 | 
							case 'unread_messaging_message':
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				title: `${getUserName(data.user)}さんからメッセージ:`,
 | 
									title: `${getUserName(data.user)}さんからメッセージ:`,
 | 
				
			||||||
				body: data.text, // TODO: getMessagingMessageSummary(data),
 | 
									body: data.text, // TODO: getMessagingMessageSummary(data),
 | 
				
			||||||
				icon: data.user.avatarUrl + '?thumbnail&size=64'
 | 
									icon: data.user.avatarUrl
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		case 'reversi_invited':
 | 
							case 'reversi_invited':
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				title: '対局への招待があります',
 | 
									title: '対局への招待があります',
 | 
				
			||||||
				body: `${getUserName(data.parent)}さんから`,
 | 
									body: `${getUserName(data.parent)}さんから`,
 | 
				
			||||||
				icon: data.parent.avatarUrl + '?thumbnail&size=64'
 | 
									icon: data.parent.avatarUrl
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		case 'notification':
 | 
							case 'notification':
 | 
				
			||||||
| 
						 | 
					@ -40,28 +40,28 @@ export default function(type, data): Notification {
 | 
				
			||||||
					return {
 | 
										return {
 | 
				
			||||||
						title: `${getUserName(data.user)}さんから:`,
 | 
											title: `${getUserName(data.user)}さんから:`,
 | 
				
			||||||
						body: getNoteSummary(data),
 | 
											body: getNoteSummary(data),
 | 
				
			||||||
						icon: data.user.avatarUrl + '?thumbnail&size=64'
 | 
											icon: data.user.avatarUrl
 | 
				
			||||||
					};
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case 'reply':
 | 
									case 'reply':
 | 
				
			||||||
					return {
 | 
										return {
 | 
				
			||||||
						title: `${getUserName(data.user)}さんから返信:`,
 | 
											title: `${getUserName(data.user)}さんから返信:`,
 | 
				
			||||||
						body: getNoteSummary(data),
 | 
											body: getNoteSummary(data),
 | 
				
			||||||
						icon: data.user.avatarUrl + '?thumbnail&size=64'
 | 
											icon: data.user.avatarUrl
 | 
				
			||||||
					};
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case 'quote':
 | 
									case 'quote':
 | 
				
			||||||
					return {
 | 
										return {
 | 
				
			||||||
						title: `${getUserName(data.user)}さんが引用:`,
 | 
											title: `${getUserName(data.user)}さんが引用:`,
 | 
				
			||||||
						body: getNoteSummary(data),
 | 
											body: getNoteSummary(data),
 | 
				
			||||||
						icon: data.user.avatarUrl + '?thumbnail&size=64'
 | 
											icon: data.user.avatarUrl
 | 
				
			||||||
					};
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				case 'reaction':
 | 
									case 'reaction':
 | 
				
			||||||
					return {
 | 
										return {
 | 
				
			||||||
						title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
 | 
											title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`,
 | 
				
			||||||
						body: getNoteSummary(data.note),
 | 
											body: getNoteSummary(data.note),
 | 
				
			||||||
						icon: data.user.avatarUrl + '?thumbnail&size=64'
 | 
											icon: data.user.avatarUrl
 | 
				
			||||||
					};
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				default:
 | 
									default:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<div class="mk-autocomplete" @contextmenu.prevent="() => {}">
 | 
					<div class="mk-autocomplete" @contextmenu.prevent="() => {}">
 | 
				
			||||||
	<ol class="users" ref="suggests" v-if="users.length > 0">
 | 
						<ol class="users" ref="suggests" v-if="users.length > 0">
 | 
				
			||||||
		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
 | 
							<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
 | 
				
			||||||
			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
 | 
								<img class="avatar" :src="user.avatarUrl" alt=""/>
 | 
				
			||||||
			<span class="name">{{ user | userName }}</span>
 | 
								<span class="name">{{ user | userName }}</span>
 | 
				
			||||||
			<span class="username">@{{ user | acct }}</span>
 | 
								<span class="username">@{{ user | acct }}</span>
 | 
				
			||||||
		</li>
 | 
							</li>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@ export default Vue.extend({
 | 
				
			||||||
					: this.user.avatarColor && this.user.avatarColor.length == 3
 | 
										: this.user.avatarColor && this.user.avatarColor.length == 3
 | 
				
			||||||
						? `rgb(${ this.user.avatarColor.join(',') })`
 | 
											? `rgb(${ this.user.avatarColor.join(',') })`
 | 
				
			||||||
						: null,
 | 
											: null,
 | 
				
			||||||
				backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl }?thumbnail)`,
 | 
									backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl })`,
 | 
				
			||||||
				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
 | 
									borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,8 @@
 | 
				
			||||||
						:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
 | 
											:class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn, can: turnUser ? o.canPut(turnUser.id == blackUser.id, i) : null, prev: o.prevPos == i }"
 | 
				
			||||||
						@click="set(i)"
 | 
											@click="set(i)"
 | 
				
			||||||
						:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`">
 | 
											:title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`">
 | 
				
			||||||
					<img v-if="stone === true" :src="`${blackUser.avatarUrl}?thumbnail&size=128`" alt="">
 | 
										<img v-if="stone === true" :src="blackUser.avatarUrl" alt="">
 | 
				
			||||||
					<img v-if="stone === false" :src="`${whiteUser.avatarUrl}?thumbnail&size=128`" alt="">
 | 
										<img v-if="stone === false" :src="whiteUser.avatarUrl" alt="">
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels">
 | 
								<div class="labels-y" v-if="this.$store.state.settings.reversiBoardLabels">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		<p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 | 
							<p :class="$style.fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 | 
				
			||||||
		<div :class="$style.stream" v-if="!fetching && images.length > 0">
 | 
							<div :class="$style.stream" v-if="!fetching && images.length > 0">
 | 
				
			||||||
			<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url}?thumbnail&size=256)`"></div>
 | 
								<div v-for="image in images" :class="$style.img" :style="`background-image: url(${image.url})`"></div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
 | 
							<p :class="$style.empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
 | 
				
			||||||
	</mk-widget-container>
 | 
						</mk-widget-container>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -72,7 +72,7 @@ export default define({
 | 
				
			||||||
			if (this.images.length == 0) return;
 | 
								if (this.images.length == 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const index = Math.floor(Math.random() * this.images.length);
 | 
								const index = Math.floor(Math.random() * this.images.length);
 | 
				
			||||||
			const img = `url(${ this.images[index].url }?thumbnail&size=1024)`;
 | 
								const img = `url(${ this.images[index].url })`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			(this.$refs.slideB as any).style.backgroundImage = img;
 | 
								(this.$refs.slideB as any).style.backgroundImage = img;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@
 | 
				
			||||||
		<p>%i18n:@banner%</p>
 | 
							<p>%i18n:@banner%</p>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
 | 
						<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
 | 
				
			||||||
		<img :src="`${file.url}?thumbnail&size=128`" alt="" @load="onThumbnailLoaded"/>
 | 
							<img :src="file.url" alt="" @load="onThumbnailLoaded"/>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<p class="name">
 | 
						<p class="name">
 | 
				
			||||||
		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 | 
							<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-window width="400px" height="550px" @closed="$destroy">
 | 
					<mk-window width="400px" height="550px" @closed="$destroy">
 | 
				
			||||||
	<span slot="header" :class="$style.header">
 | 
						<span slot="header" :class="$style.header">
 | 
				
			||||||
		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
 | 
							<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
 | 
				
			||||||
	</span>
 | 
						</span>
 | 
				
			||||||
	<mk-followers :user="user"/>
 | 
						<mk-followers :user="user"/>
 | 
				
			||||||
</mk-window>
 | 
					</mk-window>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-window width="400px" height="550px" @closed="$destroy">
 | 
					<mk-window width="400px" height="550px" @closed="$destroy">
 | 
				
			||||||
	<span slot="header" :class="$style.header">
 | 
						<span slot="header" :class="$style.header">
 | 
				
			||||||
		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
 | 
							<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
 | 
				
			||||||
	</span>
 | 
						</span>
 | 
				
			||||||
	<mk-following :user="user"/>
 | 
						<mk-following :user="user"/>
 | 
				
			||||||
</mk-window>
 | 
					</mk-window>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ export default Vue.extend({
 | 
				
			||||||
		style(): any {
 | 
							style(): any {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 | 
									'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
 | 
				
			||||||
				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url}?thumbnail&size=512)`
 | 
									'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url})`
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@ export default Vue.extend({
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
		imageStyle(): any {
 | 
							imageStyle(): any {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				'background-image': `url(${this.video.url}?thumbnail&size=512)`
 | 
									'background-image': `url(${this.video.url})`
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@
 | 
				
			||||||
		<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 | 
							<div class="medias" :class="{ with: poll }" v-show="files.length != 0">
 | 
				
			||||||
			<x-draggable :list="files" :options="{ animation: 150 }">
 | 
								<x-draggable :list="files" :options="{ animation: 150 }">
 | 
				
			||||||
				<div v-for="file in files" :key="file.id">
 | 
									<div v-for="file in files" :key="file.id">
 | 
				
			||||||
					<div class="img" :style="{ backgroundImage: `url(${file.url}?thumbnail&size=64)` }" :title="file.name"></div>
 | 
										<div class="img" :style="{ backgroundImage: `url(${file.url})` }" :title="file.name"></div>
 | 
				
			||||||
					<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:@attach-cancel%" alt=""/>
 | 
										<img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" title="%i18n:@attach-cancel%" alt=""/>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</x-draggable>
 | 
								</x-draggable>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<div class="profile">
 | 
					<div class="profile">
 | 
				
			||||||
	<label class="avatar ui from group">
 | 
						<label class="avatar ui from group">
 | 
				
			||||||
		<p>%i18n:@avatar%</p>
 | 
							<p>%i18n:@avatar%</p>
 | 
				
			||||||
		<img class="avatar" :src="`${$store.state.i.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 | 
							<img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/>
 | 
				
			||||||
		<button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button>
 | 
							<button class="ui" @click="updateAvatar">%i18n:@choice-avatar%</button>
 | 
				
			||||||
	</label>
 | 
						</label>
 | 
				
			||||||
	<label class="ui from group">
 | 
						<label class="ui from group">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="mk-user-preview">
 | 
					<div class="mk-user-preview">
 | 
				
			||||||
	<template v-if="u != null">
 | 
						<template v-if="u != null">
 | 
				
			||||||
		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
 | 
							<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl})` : ''"></div>
 | 
				
			||||||
		<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
 | 
							<mk-avatar class="avatar" :user="u" :disable-preview="true"/>
 | 
				
			||||||
		<div class="title">
 | 
							<div class="title">
 | 
				
			||||||
			<router-link class="name" :to="u | userPage">{{ u | userName }}</router-link>
 | 
								<router-link class="name" :to="u | userPage">{{ u | userName }}</router-link>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 | 
						<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 | 
				
			||||||
	<div v-if="!fetching && users.length > 0">
 | 
						<div v-if="!fetching && users.length > 0">
 | 
				
			||||||
	<router-link v-for="user in users" :to="user | userPage" :key="user.id">
 | 
						<router-link v-for="user in users" :to="user | userPage" :key="user.id">
 | 
				
			||||||
		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName" v-user-preview="user.id"/>
 | 
							<img :src="user.avatarUrl" :alt="user | userName" v-user-preview="user.id"/>
 | 
				
			||||||
	</router-link>
 | 
						</router-link>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
 | 
						<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 | 
						<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 | 
				
			||||||
	<div class="stream" v-if="!fetching && images.length > 0">
 | 
						<div class="stream" v-if="!fetching && images.length > 0">
 | 
				
			||||||
		<div v-for="image in images" class="img"
 | 
							<div v-for="image in images" class="img"
 | 
				
			||||||
			:style="`background-image: url(${image.url}?thumbnail&size=256)`"
 | 
								:style="`background-image: url(${image.url})`"
 | 
				
			||||||
		></div>
 | 
							></div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
 | 
						<p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
	:data-melt="props.design == 2"
 | 
						:data-melt="props.design == 2"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<div class="banner"
 | 
						<div class="banner"
 | 
				
			||||||
		:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl}?thumbnail&size=256)` : ''"
 | 
							:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
 | 
				
			||||||
		title="%i18n:@update-banner%"
 | 
							title="%i18n:@update-banner%"
 | 
				
			||||||
		@click="os.apis.updateBanner"
 | 
							@click="os.apis.updateBanner"
 | 
				
			||||||
	></div>
 | 
						></div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ export default Vue.extend({
 | 
				
			||||||
		thumbnail(): any {
 | 
							thumbnail(): any {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 | 
									'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
 | 
				
			||||||
				'background-image': `url(${this.file.url}?thumbnail&size=128)`
 | 
									'background-image': `url(${this.file.url})`
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ export default Vue.extend({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
		style(): any {
 | 
							style(): any {
 | 
				
			||||||
			let url = `url(${this.image.url}?thumbnail)`;
 | 
								let url = `url(${this.image.url})`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
 | 
								if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
 | 
				
			||||||
				url = null;
 | 
									url = null;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ export default Vue.extend({
 | 
				
			||||||
	computed: {
 | 
						computed: {
 | 
				
			||||||
		imageStyle(): any {
 | 
							imageStyle(): any {
 | 
				
			||||||
			return {
 | 
								return {
 | 
				
			||||||
				'background-image': `url(${this.video.url}?thumbnail&size=512)`
 | 
									'background-image': `url(${this.video.url})`
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},})
 | 
						},})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<div class="mk-note-card">
 | 
					<div class="mk-note-card">
 | 
				
			||||||
	<a :href="note | notePage">
 | 
						<a :href="note | notePage">
 | 
				
			||||||
		<header>
 | 
							<header>
 | 
				
			||||||
			<img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3>
 | 
								<img :src="note.user.avatarUrl" alt="avatar"/><h3>{{ note.user | userName }}</h3>
 | 
				
			||||||
		</header>
 | 
							</header>
 | 
				
			||||||
		<div>
 | 
							<div>
 | 
				
			||||||
			{{ text }}
 | 
								{{ text }}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@
 | 
				
			||||||
			<div class="attaches" v-show="files.length != 0">
 | 
								<div class="attaches" v-show="files.length != 0">
 | 
				
			||||||
				<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 | 
									<x-draggable class="files" :list="files" :options="{ animation: 150 }">
 | 
				
			||||||
					<div class="file" v-for="file in files" :key="file.id">
 | 
										<div class="file" v-for="file in files" :key="file.id">
 | 
				
			||||||
						<div class="img" :style="`background-image: url(${file.url}?thumbnail&size=128)`" @click="detachMedia(file)"></div>
 | 
											<div class="img" :style="`background-image: url(${file.url})`" @click="detachMedia(file)"></div>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</x-draggable>
 | 
									</x-draggable>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@
 | 
				
			||||||
	<transition name="nav">
 | 
						<transition name="nav">
 | 
				
			||||||
		<div class="body" v-if="isOpen">
 | 
							<div class="body" v-if="isOpen">
 | 
				
			||||||
			<router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`">
 | 
								<router-link class="me" v-if="$store.getters.isSignedIn" :to="`/@${$store.state.i.username}`">
 | 
				
			||||||
				<img class="avatar" :src="`${$store.state.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
 | 
									<img class="avatar" :src="$store.state.i.avatarUrl" alt="avatar"/>
 | 
				
			||||||
				<p class="name">{{ $store.state.i | userName }}</p>
 | 
									<p class="name">{{ $store.state.i | userName }}</p>
 | 
				
			||||||
			</router-link>
 | 
								</router-link>
 | 
				
			||||||
			<div class="links">
 | 
								<div class="links">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="mk-user-card">
 | 
					<div class="mk-user-card">
 | 
				
			||||||
	<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
 | 
						<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
 | 
				
			||||||
		<mk-avatar class="avatar" :user="user"/>
 | 
							<mk-avatar class="avatar" :user="user"/>
 | 
				
			||||||
	</header>
 | 
						</header>
 | 
				
			||||||
	<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
 | 
						<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<template slot="header" v-if="!fetching">
 | 
						<template slot="header" v-if="!fetching">
 | 
				
			||||||
		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
 | 
							<img :src="user.avatarUrl" alt="">
 | 
				
			||||||
		{{ '%i18n:@followers-of%'.replace('{}', name) }}
 | 
							{{ '%i18n:@followers-of%'.replace('{}', name) }}
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
	<mk-users-list
 | 
						<mk-users-list
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<template slot="header" v-if="!fetching">
 | 
						<template slot="header" v-if="!fetching">
 | 
				
			||||||
		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">
 | 
							<img :src="user.avatarUrl" alt="">
 | 
				
			||||||
		{{ '%i18n:@following-of%'.replace('{}', name) }}
 | 
							{{ '%i18n:@following-of%'.replace('{}', name) }}
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
	<mk-users-list
 | 
						<mk-users-list
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mk-ui>
 | 
					<mk-ui>
 | 
				
			||||||
	<template slot="header" v-if="!fetching"><img :src="`${user.avatarUrl}?thumbnail&size=64`" alt="">{{ user | userName }}</template>
 | 
						<template slot="header" v-if="!fetching"><img :src="user.avatarUrl" alt="">{{ user | userName }}</template>
 | 
				
			||||||
	<main v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
 | 
						<main v-if="!fetching" :data-darkmode="$store.state.device.darkmode">
 | 
				
			||||||
		<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
 | 
							<div class="is-suspended" v-if="user.isSuspended"><p>%fa:exclamation-triangle% %i18n:@is-suspended%</p></div>
 | 
				
			||||||
		<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
 | 
							<div class="is-remote" v-if="user.host != null"><p>%fa:exclamation-triangle% %i18n:@is-remote%<a :href="user.url || user.uri" target="_blank">%i18n:@view-remote%</a></p></div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 | 
						<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
 | 
				
			||||||
	<div v-if="!fetching && users.length > 0">
 | 
						<div v-if="!fetching && users.length > 0">
 | 
				
			||||||
		<a v-for="user in users" :key="user.id" :href="user | userPage">
 | 
							<a v-for="user in users" :key="user.id" :href="user | userPage">
 | 
				
			||||||
			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName"/>
 | 
								<img :src="user.avatarUrl" :alt="user | userName"/>
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
 | 
						<p class="empty" v-if="!fetching && users.length == 0">%i18n:@no-users%</p>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
	<div class="stream" v-if="!fetching && images.length > 0">
 | 
						<div class="stream" v-if="!fetching && images.length > 0">
 | 
				
			||||||
		<a v-for="image in images"
 | 
							<a v-for="image in images"
 | 
				
			||||||
			class="img"
 | 
								class="img"
 | 
				
			||||||
			:style="`background-image: url(${image.media.url}?thumbnail&size=256)`"
 | 
								:style="`background-image: url(${image.media.url})`"
 | 
				
			||||||
			:href="image.note | notePage"
 | 
								:href="image.note | notePage"
 | 
				
			||||||
		></a>
 | 
							></a>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,10 @@
 | 
				
			||||||
<div class="mkw-profile">
 | 
					<div class="mkw-profile">
 | 
				
			||||||
	<mk-widget-container>
 | 
						<mk-widget-container>
 | 
				
			||||||
		<div :class="$style.banner"
 | 
							<div :class="$style.banner"
 | 
				
			||||||
			:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl}?thumbnail&size=256)` : ''"
 | 
								:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
 | 
				
			||||||
		></div>
 | 
							></div>
 | 
				
			||||||
		<img :class="$style.avatar"
 | 
							<img :class="$style.avatar"
 | 
				
			||||||
			:src="`${$store.state.i.avatarUrl}?thumbnail&size=96`"
 | 
								:src="$store.state.i.avatarUrl"
 | 
				
			||||||
			alt="avatar"
 | 
								alt="avatar"
 | 
				
			||||||
		/>
 | 
							/>
 | 
				
			||||||
		<router-link :class="$style.name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
 | 
							<router-link :class="$style.name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,6 +49,14 @@ export type Source = {
 | 
				
			||||||
	remoteDriveCapacityMb: number;
 | 
						remoteDriveCapacityMb: number;
 | 
				
			||||||
	preventCacheRemoteFiles: boolean;
 | 
						preventCacheRemoteFiles: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						drive?: {
 | 
				
			||||||
 | 
							storage: string;
 | 
				
			||||||
 | 
							bucket: string;
 | 
				
			||||||
 | 
							prefix: string;
 | 
				
			||||||
 | 
							service?: string;
 | 
				
			||||||
 | 
							config?: any;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * ゴーストアカウントのID
 | 
						 * ゴーストアカウントのID
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,8 +31,11 @@ export type IMetadata = {
 | 
				
			||||||
	comment: string;
 | 
						comment: string;
 | 
				
			||||||
	uri?: string;
 | 
						uri?: string;
 | 
				
			||||||
	url?: string;
 | 
						url?: string;
 | 
				
			||||||
 | 
						src?: string;
 | 
				
			||||||
	deletedAt?: Date;
 | 
						deletedAt?: Date;
 | 
				
			||||||
	isMetaOnly?: boolean;
 | 
						withoutChunks?: boolean;
 | 
				
			||||||
 | 
						storage?: string;
 | 
				
			||||||
 | 
						storageProps?: any;
 | 
				
			||||||
	isSensitive?: boolean;
 | 
						isSensitive?: boolean;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -155,9 +158,9 @@ export const pack = (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	_target = Object.assign(_target, _file.metadata);
 | 
						_target = Object.assign(_target, _file.metadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_target.url = _file.metadata.url ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 | 
				
			||||||
	_target.src = _file.metadata.url;
 | 
						_target.src = _file.metadata.url;
 | 
				
			||||||
	_target.url = _file.metadata.isMetaOnly ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 | 
						_target.isRemote = _file.metadata.withoutChunks;
 | 
				
			||||||
	_target.isRemote = _file.metadata.isMetaOnly;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (_target.properties == null) _target.properties = {};
 | 
						if (_target.properties == null) _target.properties = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -152,8 +152,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const avatarId = avatar ? avatar._id : null;
 | 
						const avatarId = avatar ? avatar._id : null;
 | 
				
			||||||
	const bannerId = banner ? banner._id : null;
 | 
						const bannerId = banner ? banner._id : null;
 | 
				
			||||||
	const avatarUrl = avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null;
 | 
						const avatarUrl = avatar && avatar.metadata.url ? avatar.metadata.url : null;
 | 
				
			||||||
	const bannerUrl = banner && banner.metadata.isMetaOnly ? banner.metadata.url : null;
 | 
						const bannerUrl = banner && banner.metadata.url ? banner.metadata.url : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await User.update({ _id: user._id }, {
 | 
						await User.update({ _id: user._id }, {
 | 
				
			||||||
		$set: {
 | 
							$set: {
 | 
				
			||||||
| 
						 | 
					@ -243,8 +243,8 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
 | 
				
			||||||
			sharedInbox: person.sharedInbox,
 | 
								sharedInbox: person.sharedInbox,
 | 
				
			||||||
			avatarId: avatar ? avatar._id : null,
 | 
								avatarId: avatar ? avatar._id : null,
 | 
				
			||||||
			bannerId: banner ? banner._id : null,
 | 
								bannerId: banner ? banner._id : null,
 | 
				
			||||||
			avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null,
 | 
								avatarUrl: avatar && avatar.metadata.url ? avatar.metadata.url : null,
 | 
				
			||||||
			bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null,
 | 
								bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null,
 | 
				
			||||||
			description: htmlToMFM(person.summary),
 | 
								description: htmlToMFM(person.summary),
 | 
				
			||||||
			followersCount,
 | 
								followersCount,
 | 
				
			||||||
			followingCount,
 | 
								followingCount,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ export default async function(ctx: Koa.Context) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (file.metadata.isMetaOnly) {
 | 
						if (file.metadata.withoutChunks) {
 | 
				
			||||||
		ctx.status = 204;
 | 
							ctx.status = 204;
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,14 +8,14 @@ import * as _gm from 'gm';
 | 
				
			||||||
import * as debug from 'debug';
 | 
					import * as debug from 'debug';
 | 
				
			||||||
import fileType = require('file-type');
 | 
					import fileType = require('file-type');
 | 
				
			||||||
const prominence = require('prominence');
 | 
					const prominence = require('prominence');
 | 
				
			||||||
 | 
					import * as Minio from 'minio';
 | 
				
			||||||
 | 
					import * as uuid from 'uuid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file';
 | 
					import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file';
 | 
				
			||||||
import DriveFolder from '../../models/drive-folder';
 | 
					import DriveFolder from '../../models/drive-folder';
 | 
				
			||||||
import { pack } from '../../models/drive-file';
 | 
					import { pack } from '../../models/drive-file';
 | 
				
			||||||
import event, { publishDriveStream } from '../../stream';
 | 
					import event, { publishDriveStream } from '../../stream';
 | 
				
			||||||
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
 | 
					import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
 | 
				
			||||||
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
 | 
					 | 
				
			||||||
import genThumbnail from '../../drive/gen-thumbnail';
 | 
					 | 
				
			||||||
import delFile from './delete-file';
 | 
					import delFile from './delete-file';
 | 
				
			||||||
import config from '../../config';
 | 
					import config from '../../config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,28 +25,47 @@ const gm = _gm.subClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = debug('misskey:drive:add-file');
 | 
					const log = debug('misskey:drive:add-file');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const writeChunks = (name: string, readable: stream.Readable, type: string, metadata: any) =>
 | 
					async function save(readable: stream.Readable, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
 | 
				
			||||||
	getDriveFileBucket()
 | 
						if (config.drive && config.drive.storage == 'object-storage') {
 | 
				
			||||||
		.then(bucket => new Promise((resolve, reject) => {
 | 
							if (config.drive.service == 'minio') {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const minio = new Minio.Client(config.drive.config);
 | 
				
			||||||
 | 
								const id = uuid.v4();
 | 
				
			||||||
 | 
								const obj = `${config.drive.prefix}/${id}`;
 | 
				
			||||||
 | 
								await minio.putObject(config.drive.bucket, obj, readable);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								Object.assign(metadata, {
 | 
				
			||||||
 | 
									withoutChunks: true,
 | 
				
			||||||
 | 
									storage: 'object-storage',
 | 
				
			||||||
 | 
									storageProps: {
 | 
				
			||||||
 | 
										id: id
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									url: `${ config.drive.config.secure ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }/${ obj }`
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const file = await DriveFile.insert({
 | 
				
			||||||
 | 
									length: size,
 | 
				
			||||||
 | 
									uploadDate: new Date(),
 | 
				
			||||||
 | 
									md5: hash,
 | 
				
			||||||
 | 
									filename: name,
 | 
				
			||||||
 | 
									metadata: metadata,
 | 
				
			||||||
 | 
									contentType: type
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return file;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Get MongoDB GridFS bucket
 | 
				
			||||||
 | 
							const bucket = await getDriveFileBucket();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return new Promise<IDriveFile>((resolve, reject) => {
 | 
				
			||||||
			const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
 | 
								const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
 | 
				
			||||||
			writeStream.once('finish', resolve);
 | 
								writeStream.once('finish', resolve);
 | 
				
			||||||
			writeStream.on('error', reject);
 | 
								writeStream.on('error', reject);
 | 
				
			||||||
			readable.pipe(writeStream);
 | 
								readable.pipe(writeStream);
 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const writeThumbnailChunks = (name: string, readable: stream.Readable, originalId: mongodb.ObjectID) =>
 | 
					 | 
				
			||||||
	getDriveFileThumbnailBucket()
 | 
					 | 
				
			||||||
		.then(bucket => new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
			const writeStream = bucket.openUploadStream(name, {
 | 
					 | 
				
			||||||
				contentType: 'image/jpeg',
 | 
					 | 
				
			||||||
				metadata: {
 | 
					 | 
				
			||||||
					originalId
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
			writeStream.once('finish', resolve);
 | 
						}
 | 
				
			||||||
			writeStream.on('error', reject);
 | 
					}
 | 
				
			||||||
			readable.pipe(writeStream);
 | 
					 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function deleteOldFile(user: IRemoteUser) {
 | 
					async function deleteOldFile(user: IRemoteUser) {
 | 
				
			||||||
	const oldFile = await DriveFile.findOne({
 | 
						const oldFile = await DriveFile.findOne({
 | 
				
			||||||
| 
						 | 
					@ -82,7 +101,7 @@ export default async function(
 | 
				
			||||||
	comment: string = null,
 | 
						comment: string = null,
 | 
				
			||||||
	folderId: mongodb.ObjectID = null,
 | 
						folderId: mongodb.ObjectID = null,
 | 
				
			||||||
	force: boolean = false,
 | 
						force: boolean = false,
 | 
				
			||||||
	metaOnly: boolean = false,
 | 
						isLink: boolean = false,
 | 
				
			||||||
	url: string = null,
 | 
						url: string = null,
 | 
				
			||||||
	uri: string = null,
 | 
						uri: string = null,
 | 
				
			||||||
	sensitive = false
 | 
						sensitive = false
 | 
				
			||||||
| 
						 | 
					@ -150,7 +169,7 @@ export default async function(
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//#region Check drive usage
 | 
						//#region Check drive usage
 | 
				
			||||||
	if (!metaOnly) {
 | 
						if (!isLink) {
 | 
				
			||||||
		const usage = await DriveFile
 | 
							const usage = await DriveFile
 | 
				
			||||||
			.aggregate([{
 | 
								.aggregate([{
 | 
				
			||||||
				$match: {
 | 
									$match: {
 | 
				
			||||||
| 
						 | 
					@ -262,19 +281,23 @@ export default async function(
 | 
				
			||||||
		folderId: folder !== null ? folder._id : null,
 | 
							folderId: folder !== null ? folder._id : null,
 | 
				
			||||||
		comment: comment,
 | 
							comment: comment,
 | 
				
			||||||
		properties: properties,
 | 
							properties: properties,
 | 
				
			||||||
		isMetaOnly: metaOnly,
 | 
							withoutChunks: isLink,
 | 
				
			||||||
		isSensitive: sensitive
 | 
							isSensitive: sensitive
 | 
				
			||||||
	} as IMetadata;
 | 
						} as IMetadata;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (url !== null) {
 | 
						if (url !== null) {
 | 
				
			||||||
 | 
							metadata.src = url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (isLink) {
 | 
				
			||||||
			metadata.url = url;
 | 
								metadata.url = url;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (uri !== null) {
 | 
						if (uri !== null) {
 | 
				
			||||||
		metadata.uri = uri;
 | 
							metadata.uri = uri;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const driveFile = metaOnly
 | 
						const driveFile = isLink
 | 
				
			||||||
		? await DriveFile.insert({
 | 
							? await DriveFile.insert({
 | 
				
			||||||
			length: 0,
 | 
								length: 0,
 | 
				
			||||||
			uploadDate: new Date(),
 | 
								uploadDate: new Date(),
 | 
				
			||||||
| 
						 | 
					@ -283,7 +306,7 @@ export default async function(
 | 
				
			||||||
			metadata: metadata,
 | 
								metadata: metadata,
 | 
				
			||||||
			contentType: mime
 | 
								contentType: mime
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
		: await (writeChunks(detectedName, fs.createReadStream(path), mime, metadata) as Promise<IDriveFile>);
 | 
							: await (save(fs.createReadStream(path), detectedName, mime, hash, size, metadata));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log(`drive file has been created ${driveFile._id}`);
 | 
						log(`drive file has been created ${driveFile._id}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -293,16 +316,7 @@ export default async function(
 | 
				
			||||||
		publishDriveStream(user._id, 'file_created', packedFile);
 | 
							publishDriveStream(user._id, 'file_created', packedFile);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!metaOnly) {
 | 
						// TODO: サムネイル生成
 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			const thumb = await genThumbnail(driveFile);
 | 
					 | 
				
			||||||
			if (thumb) {
 | 
					 | 
				
			||||||
				await writeThumbnailChunks(detectedName, thumb, driveFile._id);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			// noop
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return driveFile;
 | 
						return driveFile;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,16 @@
 | 
				
			||||||
 | 
					import * as Minio from 'minio';
 | 
				
			||||||
import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
 | 
					import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
 | 
				
			||||||
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
 | 
					import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
 | 
				
			||||||
 | 
					import config from '../../config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async function(file: IDriveFile, isExpired = false) {
 | 
					export default async function(file: IDriveFile, isExpired = false) {
 | 
				
			||||||
 | 
						if (file.metadata.withoutChunks) {
 | 
				
			||||||
 | 
							if (file.metadata.storage == 'object-storage') {
 | 
				
			||||||
 | 
								const minio = new Minio.Client(config.drive.config);
 | 
				
			||||||
 | 
								const obj = `${config.drive.prefix}/${file.metadata.storageProps.id}`;
 | 
				
			||||||
 | 
								await minio.removeObject(config.drive.bucket, obj);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
		// チャンクをすべて削除
 | 
							// チャンクをすべて削除
 | 
				
			||||||
		await DriveFileChunk.remove({
 | 
							await DriveFileChunk.remove({
 | 
				
			||||||
			files_id: file._id
 | 
								files_id: file._id
 | 
				
			||||||
| 
						 | 
					@ -27,4 +36,5 @@ export default async function(file: IDriveFile, isExpired = false) {
 | 
				
			||||||
			await DriveFileThumbnail.remove({ _id: thumbnail._id });
 | 
								await DriveFileThumbnail.remove({ _id: thumbnail._id });
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		//#endregion
 | 
							//#endregion
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue