Merge pull request #2251 from syuilo/provide-thumbnails

Provide drive file thumbnails
This commit is contained in:
syuilo 2018-08-16 07:59:16 +09:00 committed by GitHub
commit a8fb0d477f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 74 additions and 30 deletions

View file

@ -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" alt="" @load="onThumbnailLoaded"/> <img :src="file.thumbnailUrl" 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>

View file

@ -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})` 'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.thumbnailUrl})`
}; };
} }
}, },

View file

@ -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})` 'background-image': `url(${this.file.thumbnailUrl})`
}; };
} }
}, },

View file

@ -27,7 +27,7 @@ export default Vue.extend({
}, },
computed: { computed: {
style(): any { 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) { if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
url = null; url = null;

View file

@ -31,6 +31,7 @@ export type IMetadata = {
comment: string; comment: string;
uri?: string; uri?: string;
url?: string; url?: string;
thumbnailUrl?: string;
src?: string; src?: string;
deletedAt?: Date; deletedAt?: Date;
withoutChunks?: boolean; withoutChunks?: boolean;
@ -164,6 +165,7 @@ 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.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; _target.isRemote = _file.metadata.isRemote;
if (_target.properties == null) _target.properties = {}; if (_target.properties == null) _target.properties = {};

View file

@ -1,5 +1,3 @@
import * as fs from 'fs';
import * as Koa from 'koa'; import * as Koa from 'koa';
import * as send from 'koa-send'; import * as send from 'koa-send';
import * as mongodb from 'mongodb'; import * as mongodb from 'mongodb';
@ -51,23 +49,16 @@ export default async function(ctx: Koa.Context) {
}; };
if ('thumbnail' in ctx.query) { if ('thumbnail' in ctx.query) {
// 画像以外 const thumb = await DriveFileThumbnail.findOne({
if (!file.contentType.startsWith('image/')) { 'metadata.originalId': fileId
const readable = fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); });
ctx.set('Content-Type', 'image/png');
ctx.body = readable; if (thumb != null) {
} else if (file.contentType == 'image/gif') { ctx.set('Content-Type', 'image/jpeg');
// GIF const bucket = await getDriveFileThumbnailBucket();
await sendRaw(); ctx.body = bucket.openDownloadStream(thumb._id);
} else { } else {
const thumb = await DriveFileThumbnail.findOne({ 'metadata.originalId': fileId }); await sendRaw();
if (thumb != null) {
ctx.set('Content-Type', 'image/jpeg');
const bucket = await getDriveFileThumbnailBucket();
ctx.body = bucket.openDownloadStream(thumb._id);
} else {
await sendRaw();
}
} }
} else { } else {
if ('download' in ctx.query) { if ('download' in ctx.query) {

View file

@ -1,6 +1,5 @@
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import * as fs from 'fs'; import * as fs from 'fs';
import * as stream from 'stream';
import * as mongodb from 'mongodb'; import * as mongodb from 'mongodb';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
@ -17,30 +16,52 @@ import { publishUserStream, publishDriveStream } from '../../stream';
import { isLocalUser, IUser, IRemoteUser } from '../../models/user'; import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import delFile from './delete-file'; import delFile from './delete-file';
import config from '../../config'; import config from '../../config';
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
const log = debug('misskey:drive:add-file'); 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') { if (config.drive && config.drive.storage == 'minio') {
const minio = new Minio.Client(config.drive.config); const minio = new Minio.Client(config.drive.config);
const id = uuid.v4(); const id = uuid.v4();
const obj = `${config.drive.prefix}/${id}`; const obj = `${config.drive.prefix}/${id}`;
const thumbnailObj = `${obj}-thumbnail`;
const baseUrl = config.drive.baseUrl 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 }`; || `${ 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, 'Content-Type': type,
'Cache-Control': 'max-age=31536000, immutable' '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, { Object.assign(metadata, {
withoutChunks: true, withoutChunks: true,
storage: 'minio', storage: 'minio',
storageProps: { storageProps: {
id: id id: id
}, },
url: `${ baseUrl }/${ obj }` url: `${ baseUrl }/${ obj }`,
thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailObj }` : null
}); });
const file = await DriveFile.insert({ const file = await DriveFile.insert({
@ -57,12 +78,36 @@ async function save(readable: stream.Readable, name: string, type: string, hash:
// Get MongoDB GridFS bucket // Get MongoDB GridFS bucket
const bucket = await getDriveFileBucket(); const bucket = await getDriveFileBucket();
return new Promise<IDriveFile>((resolve, reject) => { const file = await 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);
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 { } 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}`); log(`drive file has been created ${driveFile._id}`);

View file

@ -6,8 +6,14 @@ import config from '../../config';
export default async function(file: IDriveFile, isExpired = false) { export default async function(file: IDriveFile, isExpired = false) {
if (file.metadata.storage == 'minio') { if (file.metadata.storage == 'minio') {
const minio = new Minio.Client(config.drive.config); const minio = new Minio.Client(config.drive.config);
const obj = `${config.drive.prefix}/${file.metadata.storageProps.id}`; const obj = `${config.drive.prefix}/${file.metadata.storageProps.id}`;
await minio.removeObject(config.drive.bucket, obj); 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);
}
} }
// チャンクをすべて削除 // チャンクをすべて削除