サムネイルを予め生成するように
This commit is contained in:
		
							parent
							
								
									75764e59e1
								
							
						
					
					
						commit
						15e4cf1243
					
				|  | @ -2,8 +2,8 @@ | |||
| <div class="header" :data-is-dark-background="user.bannerUrl != null"> | ||||
| 	<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="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''"> | ||||
| 		<div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=2048)` : ''" @click="onBannerClick"></div> | ||||
| 	<div class="banner-container" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"> | ||||
| 		<div class="banner" ref="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''" @click="onBannerClick"></div> | ||||
| 		<div class="fade"></div> | ||||
| 	</div> | ||||
| 	<div class="container"> | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 		<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> | ||||
| 		<header> | ||||
| 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div> | ||||
| 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> | ||||
| 			<div class="body"> | ||||
| 				<div class="top"> | ||||
| 					<a class="avatar"> | ||||
|  |  | |||
|  | @ -0,0 +1,25 @@ | |||
| import * as stream from 'stream'; | ||||
| import * as Gm from 'gm'; | ||||
| import { IDriveFile, getDriveFileBucket } from '../models/drive-file'; | ||||
| 
 | ||||
| const gm = Gm.subClass({ | ||||
| 	imageMagick: true | ||||
| }); | ||||
| 
 | ||||
| export default async function(file: IDriveFile): Promise<stream.Readable> { | ||||
| 	if (!/^image\/.*$/.test(file.contentType)) return null; | ||||
| 
 | ||||
| 	const bucket = await getDriveFileBucket(); | ||||
| 	const readable = bucket.openDownloadStream(file._id); | ||||
| 
 | ||||
| 	const g = gm(readable); | ||||
| 
 | ||||
| 	const stream = g | ||||
| 		.resize(256, 256) | ||||
| 		.compress('jpeg') | ||||
| 		.quality(70) | ||||
| 		.interlace('line') | ||||
| 		.stream(); | ||||
| 
 | ||||
| 	return stream; | ||||
| } | ||||
|  | @ -0,0 +1,61 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| import monkDb, { nativeDbConn } from '../db/mongodb'; | ||||
| 
 | ||||
| const DriveFileThumbnail = monkDb.get<IDriveFileThumbnail>('driveFileThumbnails.files'); | ||||
| DriveFileThumbnail.createIndex('metadata.originalId', { sparse: true, unique: true }); | ||||
| export default DriveFileThumbnail; | ||||
| 
 | ||||
| export const DriveFileThumbnailChunk = monkDb.get('driveFileThumbnails.chunks'); | ||||
| 
 | ||||
| export const getDriveFileThumbnailBucket = async (): Promise<mongo.GridFSBucket> => { | ||||
| 	const db = await nativeDbConn(); | ||||
| 	const bucket = new mongo.GridFSBucket(db, { | ||||
| 		bucketName: 'driveFileThumbnails' | ||||
| 	}); | ||||
| 	return bucket; | ||||
| }; | ||||
| 
 | ||||
| export type IMetadata = { | ||||
| 	originalId: mongo.ObjectID; | ||||
| }; | ||||
| 
 | ||||
| export type IDriveFileThumbnail = { | ||||
| 	_id: mongo.ObjectID; | ||||
| 	uploadDate: Date; | ||||
| 	md5: string; | ||||
| 	filename: string; | ||||
| 	contentType: string; | ||||
| 	metadata: IMetadata; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * DriveFileThumbnailを物理削除します | ||||
|  */ | ||||
| export async function deleteDriveFileThumbnail(driveFile: string | mongo.ObjectID | IDriveFileThumbnail) { | ||||
| 	let d: IDriveFileThumbnail; | ||||
| 
 | ||||
| 	// Populate
 | ||||
| 	if (mongo.ObjectID.prototype.isPrototypeOf(driveFile)) { | ||||
| 		d = await DriveFileThumbnail.findOne({ | ||||
| 			_id: driveFile | ||||
| 		}); | ||||
| 	} else if (typeof driveFile === 'string') { | ||||
| 		d = await DriveFileThumbnail.findOne({ | ||||
| 			_id: new mongo.ObjectID(driveFile) | ||||
| 		}); | ||||
| 	} else { | ||||
| 		d = driveFile as IDriveFileThumbnail; | ||||
| 	} | ||||
| 
 | ||||
| 	if (d == null) return; | ||||
| 
 | ||||
| 	// このDriveFileThumbnailのチャンクをすべて削除
 | ||||
| 	await DriveFileThumbnailChunk.remove({ | ||||
| 		files_id: d._id | ||||
| 	}); | ||||
| 
 | ||||
| 	// このDriveFileThumbnailを削除
 | ||||
| 	await DriveFileThumbnail.remove({ | ||||
| 		_id: d._id | ||||
| 	}); | ||||
| } | ||||
|  | @ -6,6 +6,7 @@ import monkDb, { nativeDbConn } from '../db/mongodb'; | |||
| import Note, { deleteNote } from './note'; | ||||
| import MessagingMessage, { deleteMessagingMessage } from './messaging-message'; | ||||
| import User from './user'; | ||||
| import DriveFileThumbnail, { deleteDriveFileThumbnail } from './drive-file-thumbnail'; | ||||
| 
 | ||||
| const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); | ||||
| DriveFile.createIndex('metadata.uri', { sparse: true, unique: true }); | ||||
|  | @ -13,7 +14,7 @@ export default DriveFile; | |||
| 
 | ||||
| export const DriveFileChunk = monkDb.get('driveFiles.chunks'); | ||||
| 
 | ||||
| const getGridFSBucket = async (): Promise<mongo.GridFSBucket> => { | ||||
| export const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => { | ||||
| 	const db = await nativeDbConn(); | ||||
| 	const bucket = new mongo.GridFSBucket(db, { | ||||
| 		bucketName: 'driveFiles' | ||||
|  | @ -21,8 +22,6 @@ const getGridFSBucket = async (): Promise<mongo.GridFSBucket> => { | |||
| 	return bucket; | ||||
| }; | ||||
| 
 | ||||
| export { getGridFSBucket }; | ||||
| 
 | ||||
| export type IMetadata = { | ||||
| 	properties: any; | ||||
| 	userId: mongo.ObjectID; | ||||
|  | @ -93,6 +92,11 @@ export async function deleteDriveFile(driveFile: string | mongo.ObjectID | IDriv | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// このDriveFileのDriveFileThumbnailをすべて削除
 | ||||
| 	await Promise.all(( | ||||
| 		await DriveFileThumbnail.find({ 'metadata.originalId': d._id }) | ||||
| 	).map(x => deleteDriveFileThumbnail(x))); | ||||
| 
 | ||||
| 	// このDriveFileのチャンクをすべて削除
 | ||||
| 	await DriveFileChunk.remove({ | ||||
| 		files_id: d._id | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import * as fs from 'fs'; | |||
| import * as Koa from 'koa'; | ||||
| import * as cors from '@koa/cors'; | ||||
| import * as Router from 'koa-router'; | ||||
| import pour from './pour'; | ||||
| import sendDriveFile from './send-drive-file'; | ||||
| 
 | ||||
| // Init app
 | ||||
|  | @ -24,12 +23,14 @@ const router = new Router(); | |||
| 
 | ||||
| router.get('/default-avatar.jpg', ctx => { | ||||
| 	const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`); | ||||
| 	pour(file, 'image/jpeg', ctx); | ||||
| 	ctx.set('Content-Type', 'image/jpeg'); | ||||
| 	ctx.body = file; | ||||
| }); | ||||
| 
 | ||||
| router.get('/app-default.jpg', ctx => { | ||||
| 	const file = fs.createReadStream(`${__dirname}/assets/dummy.png`); | ||||
| 	pour(file, 'image/png', ctx); | ||||
| 	ctx.set('Content-Type', 'image/jpeg'); | ||||
| 	ctx.body = file; | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id', sendDriveFile); | ||||
|  |  | |||
|  | @ -1,88 +0,0 @@ | |||
| import * as fs from 'fs'; | ||||
| import * as stream from 'stream'; | ||||
| import * as Koa from 'koa'; | ||||
| import * as Gm from 'gm'; | ||||
| 
 | ||||
| const gm = Gm.subClass({ | ||||
| 	imageMagick: true | ||||
| }); | ||||
| 
 | ||||
| interface ISend { | ||||
| 	contentType: string; | ||||
| 	stream: stream.Readable; | ||||
| } | ||||
| 
 | ||||
| function thumbnail(data: stream.Readable, type: string, resize: number): ISend { | ||||
| 	const readable: stream.Readable = (() => { | ||||
| 		// 動画であれば
 | ||||
| 		if (/^video\/.*$/.test(type)) { | ||||
| 			// TODO
 | ||||
| 			// 使わないことになったストリームはしっかり取り壊す
 | ||||
| 			data.destroy(); | ||||
| 			return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); | ||||
| 		// 画像であれば
 | ||||
| 		// Note: SVGはapplication/xml
 | ||||
| 		} else if (/^image\/.*$/.test(type) || type == 'application/xml') { | ||||
| 			// 0フレーム目を送る
 | ||||
| 			try { | ||||
| 				return gm(data).selectFrame(0).stream(); | ||||
| 			// だめだったら
 | ||||
| 			} catch (e) { | ||||
| 				// 使わないことになったストリームはしっかり取り壊す
 | ||||
| 				data.destroy(); | ||||
| 				return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); | ||||
| 			} | ||||
| 		// 動画か画像以外
 | ||||
| 		} else { | ||||
| 			data.destroy(); | ||||
| 			return fs.createReadStream(`${__dirname}/assets/not-an-image.png`); | ||||
| 		} | ||||
| 	})(); | ||||
| 
 | ||||
| 	let g = gm(readable); | ||||
| 
 | ||||
| 	if (resize) { | ||||
| 		g = g.resize(resize, resize); | ||||
| 	} | ||||
| 
 | ||||
| 	const stream = g | ||||
| 		.compress('jpeg') | ||||
| 		.quality(80) | ||||
| 		.interlace('line') | ||||
| 		.stream(); | ||||
| 
 | ||||
| 	return { | ||||
| 		contentType: 'image/jpeg', | ||||
| 		stream | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => { | ||||
| 	console.error(e); | ||||
| 	ctx.status = 500; | ||||
| }; | ||||
| 
 | ||||
| export default function(readable: stream.Readable, type: string, ctx: Koa.Context): void { | ||||
| 	readable.on('error', commonReadableHandlerGenerator(ctx)); | ||||
| 
 | ||||
| 	const data = ((): ISend => { | ||||
| 		if (ctx.query.thumbnail !== undefined) { | ||||
| 			return thumbnail(readable, type, ctx.query.size); | ||||
| 		} | ||||
| 		return { | ||||
| 			contentType: type, | ||||
| 			stream: readable | ||||
| 		}; | ||||
| 	})(); | ||||
| 
 | ||||
| 	if (readable !== data.stream) { | ||||
| 		data.stream.on('error', commonReadableHandlerGenerator(ctx)); | ||||
| 	} | ||||
| 
 | ||||
| 	if (ctx.query.download !== undefined) { | ||||
| 		ctx.set('Content-Disposition', 'attachment'); | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.set('Content-Type', data.contentType); | ||||
| 	ctx.body = data.stream; | ||||
| } | ||||
|  | @ -1,8 +1,15 @@ | |||
| import * as fs from 'fs'; | ||||
| 
 | ||||
| import * as Koa from 'koa'; | ||||
| import * as send from 'koa-send'; | ||||
| import * as mongodb from 'mongodb'; | ||||
| import DriveFile, { getGridFSBucket } from '../../models/drive-file'; | ||||
| import pour from './pour'; | ||||
| import DriveFile, { getDriveFileBucket } from '../../models/drive-file'; | ||||
| import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; | ||||
| 
 | ||||
| const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => { | ||||
| 	console.error(e); | ||||
| 	ctx.status = 500; | ||||
| }; | ||||
| 
 | ||||
| export default async function(ctx: Koa.Context) { | ||||
| 	// Validate id
 | ||||
|  | @ -28,9 +35,33 @@ export default async function(ctx: Koa.Context) { | |||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const bucket = await getGridFSBucket(); | ||||
| 	if ('thumbnail' in ctx.query) { | ||||
| 		// 動画か画像以外
 | ||||
| 		if (!/^image\/.*$/.test(file.contentType) && !/^video\/.*$/.test(file.contentType)) { | ||||
| 			const readable = fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); | ||||
| 			ctx.set('Content-Type', 'image/png'); | ||||
| 			ctx.body = readable; | ||||
| 		} else { | ||||
| 			const thumb = await DriveFileThumbnail.findOne({ 'metadata.originalId': fileId }); | ||||
| 			if (thumb != null) { | ||||
| 				ctx.set('Content-Type', 'image/jpeg'); | ||||
| 				const bucket = await getDriveFileThumbnailBucket(); | ||||
| 				ctx.body = bucket.openDownloadStream(thumb._id); | ||||
| 			} else { | ||||
| 				ctx.set('Content-Type', file.contentType); | ||||
| 				const bucket = await getDriveFileBucket(); | ||||
| 				ctx.body = bucket.openDownloadStream(fileId); | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		if ('download' in ctx.query) { | ||||
| 			ctx.set('Content-Disposition', 'attachment'); | ||||
| 		} | ||||
| 
 | ||||
| 	const readable = bucket.openDownloadStream(fileId); | ||||
| 
 | ||||
| 	pour(readable, file.contentType, ctx); | ||||
| 		const bucket = await getDriveFileBucket(); | ||||
| 		const readable = bucket.openDownloadStream(fileId); | ||||
| 		readable.on('error', commonReadableHandlerGenerator(ctx)); | ||||
| 		ctx.set('Content-Type', file.contentType); | ||||
| 		ctx.body = readable; | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -10,12 +10,14 @@ import * as debug from 'debug'; | |||
| import fileType = require('file-type'); | ||||
| import prominence = require('prominence'); | ||||
| 
 | ||||
| import DriveFile, { IMetadata, getGridFSBucket, IDriveFile, DriveFileChunk } from '../../models/drive-file'; | ||||
| import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile, DriveFileChunk } from '../../models/drive-file'; | ||||
| import DriveFolder from '../../models/drive-folder'; | ||||
| import { pack } from '../../models/drive-file'; | ||||
| import event, { publishDriveStream } from '../../publishers/stream'; | ||||
| import getAcct from '../../acct/render'; | ||||
| import { IUser, isLocalUser } from '../../models/user'; | ||||
| import DriveFileThumbnail, { getDriveFileThumbnailBucket, DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; | ||||
| import genThumbnail from '../../drive/gen-thumbnail'; | ||||
| 
 | ||||
| const gm = _gm.subClass({ | ||||
| 	imageMagick: true | ||||
|  | @ -30,8 +32,8 @@ const tmpFile = (): Promise<[string, any]> => new Promise((resolve, reject) => { | |||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| const addToGridFS = (name: string, readable: stream.Readable, type: string, metadata: any): Promise<any> => | ||||
| 	getGridFSBucket() | ||||
| const writeChunks = (name: string, readable: stream.Readable, type: string, metadata: any) => | ||||
| 	getDriveFileBucket() | ||||
| 		.then(bucket => new Promise((resolve, reject) => { | ||||
| 			const writeStream = bucket.openUploadStream(name, { contentType: type, metadata }); | ||||
| 			writeStream.once('finish', resolve); | ||||
|  | @ -39,6 +41,20 @@ const addToGridFS = (name: string, readable: stream.Readable, type: string, meta | |||
| 			readable.pipe(writeStream); | ||||
| 		})); | ||||
| 
 | ||||
| const writeThumbnailChunks = (name: string, readable: stream.Readable, originalId) => | ||||
| 	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); | ||||
| 		})); | ||||
| 
 | ||||
| const addFile = async ( | ||||
| 	user: IUser, | ||||
| 	path: string, | ||||
|  | @ -232,6 +248,20 @@ const addFile = async ( | |||
| 								'metadata.deletedAt': new Date() | ||||
| 							} | ||||
| 						}); | ||||
| 
 | ||||
| 						//#region サムネイルもあれば削除
 | ||||
| 						const thumbnail = await DriveFileThumbnail.findOne({ | ||||
| 							'metadata.originalId': oldFile._id | ||||
| 						}); | ||||
| 
 | ||||
| 						if (thumbnail) { | ||||
| 							DriveFileThumbnailChunk.remove({ | ||||
| 								files_id: thumbnail._id | ||||
| 							}); | ||||
| 
 | ||||
| 							DriveFileThumbnail.remove({ _id: thumbnail._id }); | ||||
| 						} | ||||
| 						//#endregion
 | ||||
| 					} | ||||
| 					//#endregion
 | ||||
| 				} | ||||
|  | @ -263,7 +293,18 @@ const addFile = async ( | |||
| 		metadata.uri = uri; | ||||
| 	} | ||||
| 
 | ||||
| 	return addToGridFS(detectedName, readable, mime, metadata); | ||||
| 	const file = await (writeChunks(detectedName, readable, mime, metadata) as Promise<IDriveFile>); | ||||
| 
 | ||||
| 	try { | ||||
| 		const thumb = await genThumbnail(file); | ||||
| 		if (thumb) { | ||||
| 			await writeThumbnailChunks(detectedName, thumb, file._id); | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		// noop
 | ||||
| 	} | ||||
| 
 | ||||
| 	return file; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue