Merge pull request #2251 from syuilo/provide-thumbnails
Provide drive file thumbnails
This commit is contained in:
		
						commit
						a8fb0d477f
					
				
					 8 changed files with 74 additions and 30 deletions
				
			
		| 
						 | 
				
			
			@ -16,7 +16,7 @@
 | 
			
		|||
		<p>%i18n:@banner%</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
 | 
			
		||||
		<img :src="file.url" alt="" @load="onThumbnailLoaded"/>
 | 
			
		||||
		<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<p class="name">
 | 
			
		||||
		<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ export default Vue.extend({
 | 
			
		|||
		style(): any {
 | 
			
		||||
			return {
 | 
			
		||||
				'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})`
 | 
			
		||||
				'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.thumbnailUrl})`
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ export default Vue.extend({
 | 
			
		|||
		thumbnail(): any {
 | 
			
		||||
			return {
 | 
			
		||||
				'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})`
 | 
			
		||||
				'background-image': `url(${this.file.thumbnailUrl})`
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ export default Vue.extend({
 | 
			
		|||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		style(): any {
 | 
			
		||||
			let url = `url(${this.image.url})`;
 | 
			
		||||
			let url = `url(${this.image.thumbnailUrl})`;
 | 
			
		||||
 | 
			
		||||
			if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
 | 
			
		||||
				url = null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,7 @@ export type IMetadata = {
 | 
			
		|||
	comment: string;
 | 
			
		||||
	uri?: string;
 | 
			
		||||
	url?: string;
 | 
			
		||||
	thumbnailUrl?: string;
 | 
			
		||||
	src?: string;
 | 
			
		||||
	deletedAt?: Date;
 | 
			
		||||
	withoutChunks?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -164,6 +165,7 @@ export const pack = (
 | 
			
		|||
	_target = Object.assign(_target, _file.metadata);
 | 
			
		||||
 | 
			
		||||
	_target.url = _file.metadata.url ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
 | 
			
		||||
	_target.thumbnailUrl = _file.metadata.thumbnailUrl ? _file.metadata.thumbnailUrl : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}?thumbnail`;
 | 
			
		||||
	_target.isRemote = _file.metadata.isRemote;
 | 
			
		||||
 | 
			
		||||
	if (_target.properties == null) _target.properties = {};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,3 @@
 | 
			
		|||
import * as fs from 'fs';
 | 
			
		||||
 | 
			
		||||
import * as Koa from 'koa';
 | 
			
		||||
import * as send from 'koa-send';
 | 
			
		||||
import * as mongodb from 'mongodb';
 | 
			
		||||
| 
						 | 
				
			
			@ -51,23 +49,16 @@ export default async function(ctx: Koa.Context) {
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	if ('thumbnail' in ctx.query) {
 | 
			
		||||
		// 画像以外
 | 
			
		||||
		if (!file.contentType.startsWith('image/')) {
 | 
			
		||||
			const readable = fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
 | 
			
		||||
			ctx.set('Content-Type', 'image/png');
 | 
			
		||||
			ctx.body = readable;
 | 
			
		||||
		} else if (file.contentType == 'image/gif') {
 | 
			
		||||
			// GIF
 | 
			
		||||
			await sendRaw();
 | 
			
		||||
		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 {
 | 
			
		||||
			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 {
 | 
			
		||||
				await sendRaw();
 | 
			
		||||
			}
 | 
			
		||||
			await sendRaw();
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if ('download' in ctx.query) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
import { Buffer } from 'buffer';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as stream from 'stream';
 | 
			
		||||
 | 
			
		||||
import * as mongodb from 'mongodb';
 | 
			
		||||
import * as crypto from 'crypto';
 | 
			
		||||
| 
						 | 
				
			
			@ -17,30 +16,52 @@ import { publishUserStream, publishDriveStream } from '../../stream';
 | 
			
		|||
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
 | 
			
		||||
import delFile from './delete-file';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
 | 
			
		||||
 | 
			
		||||
const log = debug('misskey:drive:add-file');
 | 
			
		||||
 | 
			
		||||
async function save(readable: stream.Readable, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
 | 
			
		||||
async function save(path: string, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
 | 
			
		||||
	let thumbnail: Buffer;
 | 
			
		||||
 | 
			
		||||
	if (['image/jpeg', 'image/png', 'image/webp'].includes(type)) {
 | 
			
		||||
		thumbnail = await sharp(path)
 | 
			
		||||
			.resize(300)
 | 
			
		||||
			.jpeg({
 | 
			
		||||
				quality: 50,
 | 
			
		||||
				progressive: true
 | 
			
		||||
			})
 | 
			
		||||
			.toBuffer();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (config.drive && config.drive.storage == 'minio') {
 | 
			
		||||
		const minio = new Minio.Client(config.drive.config);
 | 
			
		||||
		const id = uuid.v4();
 | 
			
		||||
		const obj = `${config.drive.prefix}/${id}`;
 | 
			
		||||
		const thumbnailObj = `${obj}-thumbnail`;
 | 
			
		||||
 | 
			
		||||
		const baseUrl = config.drive.baseUrl
 | 
			
		||||
			|| `${ config.drive.config.secure ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }`;
 | 
			
		||||
 | 
			
		||||
		await minio.putObject(config.drive.bucket, obj, readable, size, {
 | 
			
		||||
		await minio.putObject(config.drive.bucket, obj, fs.createReadStream(path), size, {
 | 
			
		||||
			'Content-Type': type,
 | 
			
		||||
			'Cache-Control': 'max-age=31536000, immutable'
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (thumbnail) {
 | 
			
		||||
			await minio.putObject(config.drive.bucket, thumbnailObj, fs.createReadStream(path), size, {
 | 
			
		||||
				'Content-Type': 'image/jpeg',
 | 
			
		||||
				'Cache-Control': 'max-age=31536000, immutable'
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Object.assign(metadata, {
 | 
			
		||||
			withoutChunks: true,
 | 
			
		||||
			storage: 'minio',
 | 
			
		||||
			storageProps: {
 | 
			
		||||
				id: id
 | 
			
		||||
			},
 | 
			
		||||
			url: `${ baseUrl }/${ obj }`
 | 
			
		||||
			url: `${ baseUrl }/${ obj }`,
 | 
			
		||||
			thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailObj }` : null
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const file = await DriveFile.insert({
 | 
			
		||||
| 
						 | 
				
			
			@ -57,12 +78,36 @@ async function save(readable: stream.Readable, name: string, type: string, hash:
 | 
			
		|||
		// Get MongoDB GridFS bucket
 | 
			
		||||
		const bucket = await getDriveFileBucket();
 | 
			
		||||
 | 
			
		||||
		return new Promise<IDriveFile>((resolve, reject) => {
 | 
			
		||||
			const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
 | 
			
		||||
		const file = await new Promise<IDriveFile>((resolve, reject) => {
 | 
			
		||||
			const writeStream = bucket.openUploadStream(name, {
 | 
			
		||||
				contentType: type,
 | 
			
		||||
				metadata
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			writeStream.once('finish', resolve);
 | 
			
		||||
			writeStream.on('error', reject);
 | 
			
		||||
			readable.pipe(writeStream);
 | 
			
		||||
 | 
			
		||||
			fs.createReadStream(path).pipe(writeStream);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (thumbnail) {
 | 
			
		||||
			const thumbnailBucket = await getDriveFileThumbnailBucket();
 | 
			
		||||
 | 
			
		||||
			await new Promise<IDriveFile>((resolve, reject) => {
 | 
			
		||||
				const writeStream = thumbnailBucket.openUploadStream(name, {
 | 
			
		||||
					contentType: 'image/jpeg',
 | 
			
		||||
					metadata: {
 | 
			
		||||
						originalId: file._id
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				writeStream.once('finish', resolve);
 | 
			
		||||
				writeStream.on('error', reject);
 | 
			
		||||
				writeStream.end(thumbnail);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return file;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -321,7 +366,7 @@ export default async function(
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		driveFile = await (save(fs.createReadStream(path), detectedName, mime, hash, size, metadata));
 | 
			
		||||
		driveFile = await (save(path, detectedName, mime, hash, size, metadata));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log(`drive file has been created ${driveFile._id}`);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,14 @@ import config from '../../config';
 | 
			
		|||
export default async function(file: IDriveFile, isExpired = false) {
 | 
			
		||||
	if (file.metadata.storage == 'minio') {
 | 
			
		||||
		const minio = new Minio.Client(config.drive.config);
 | 
			
		||||
 | 
			
		||||
		const obj = `${config.drive.prefix}/${file.metadata.storageProps.id}`;
 | 
			
		||||
		await minio.removeObject(config.drive.bucket, obj);
 | 
			
		||||
 | 
			
		||||
		if (file.metadata.thumbnailUrl) {
 | 
			
		||||
			const thumbnailObj = `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
 | 
			
		||||
			await minio.removeObject(config.drive.bucket, thumbnailObj);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// チャンクをすべて削除
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue