diff options
| author | MeiMei <30769358+mei23@users.noreply.github.com> | 2019-02-05 03:01:36 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2019-02-05 03:01:36 +0900 |
| commit | f014b7ae0ece886ef0cff2366b9925e23b34ba6f (patch) | |
| tree | c35760467278da408348fcd459415af7a63e08e9 /src/services/drive | |
| parent | Refactor: Better type definition (diff) | |
| download | sharkey-f014b7ae0ece886ef0cff2366b9925e23b34ba6f.tar.gz sharkey-f014b7ae0ece886ef0cff2366b9925e23b34ba6f.tar.bz2 sharkey-f014b7ae0ece886ef0cff2366b9925e23b34ba6f.zip | |
アニメーションを自動再生しないオプション (#4131)
* Refactor
* settings
* Media Proxy
* Replace API response
Diffstat (limited to 'src/services/drive')
| -rw-r--r-- | src/services/drive/add-file.ts | 277 | ||||
| -rw-r--r-- | src/services/drive/generate-video-thumbnail.ts | 15 | ||||
| -rw-r--r-- | src/services/drive/image-processor.ts | 75 |
3 files changed, 207 insertions, 160 deletions
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index b360df099b..2b3b923b93 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -23,6 +23,7 @@ import perUserDriveChart from '../../chart/per-user-drive'; import fetchMeta from '../../misc/fetch-meta'; import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; +import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; const logger = driveLogger.createSubLogger('register', 'yellow'); @@ -36,99 +37,11 @@ const logger = driveLogger.createSubLogger('register', 'yellow'); * @param metadata */ async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> { - // #region webpublic - let webpublic: Buffer; - let webpublicExt = 'jpg'; - let webpublicType = 'image/jpeg'; - - if (!metadata.uri) { // from local instance - logger.info(`creating web image of ${name}`); - - if (['image/jpeg'].includes(type)) { - webpublic = await sharp(path) - .resize(2048, 2048, { - fit: 'inside', - withoutEnlargement: true - }) - .rotate() - .jpeg({ - quality: 85, - progressive: true - }) - .toBuffer(); - } else if (['image/webp'].includes(type)) { - webpublic = await sharp(path) - .resize(2048, 2048, { - fit: 'inside', - withoutEnlargement: true - }) - .rotate() - .webp({ - quality: 85 - }) - .toBuffer(); - - webpublicExt = 'webp'; - webpublicType = 'image/webp'; - } else if (['image/png'].includes(type)) { - webpublic = await sharp(path) - .resize(2048, 2048, { - fit: 'inside', - withoutEnlargement: true - }) - .rotate() - .png() - .toBuffer(); - - webpublicExt = 'png'; - webpublicType = 'image/png'; - } else { - logger.info(`web image not created (not an image)`); - } - } else { - logger.info(`web image not created (from remote)`); - } - // #endregion webpublic - - // #region thumbnail - let thumbnail: Buffer; - let thumbnailExt = 'jpg'; - let thumbnailType = 'image/jpeg'; - - if (['image/jpeg', 'image/webp'].includes(type)) { - thumbnail = await sharp(path) - .resize(498, 280, { - fit: 'inside', - withoutEnlargement: true - }) - .rotate() - .jpeg({ - quality: 85, - progressive: true - }) - .toBuffer(); - } else if (['image/png'].includes(type)) { - thumbnail = await sharp(path) - .resize(498, 280, { - fit: 'inside', - withoutEnlargement: true - }) - .rotate() - .png() - .toBuffer(); - - thumbnailExt = 'png'; - thumbnailType = 'image/png'; - } else if (type.startsWith('video/')) { - try { - thumbnail = await GenerateVideoThumbnail(path); - } catch (e) { - logger.error(`GenerateVideoThumbnail failed: ${e}`); - } - } - // #endregion thumbnail + // thunbnail, webpublic を必要なら生成 + const alts = await generateAlts(path, type, !metadata.uri); if (config.drive && config.drive.storage == 'minio') { + //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']); if (ext === '') { @@ -137,41 +50,57 @@ async function save(path: string, name: string, type: string, hash: string, size if (type === 'image/webp') ext = '.webp'; } + const baseUrl = config.drive.baseUrl + || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; + + // for original const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; - const webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${webpublicExt}`; - const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${thumbnailExt}`; + const url = `${ baseUrl }/${ key }`; + + // for alts + let webpublicKey = null as string; + let webpublicUrl = null as string; + let thumbnailKey = null as string; + let thumbnailUrl = null as string; + //#endregion + //#region Uploads logger.info(`uploading original: ${key}`); const uploads = [ upload(key, fs.createReadStream(path), type) ]; - if (webpublic) { + if (alts.webpublic) { + webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${alts.webpublic.ext}`; + webpublicUrl = `${ baseUrl }/${ webpublicKey }`; + logger.info(`uploading webpublic: ${webpublicKey}`); - uploads.push(upload(webpublicKey, webpublic, webpublicType)); + uploads.push(upload(webpublicKey, alts.webpublic.data, alts.webpublic.type)); } - if (thumbnail) { + if (alts.thumbnail) { + thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${alts.thumbnail.ext}`; + thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; + logger.info(`uploading thumbnail: ${thumbnailKey}`); - uploads.push(upload(thumbnailKey, thumbnail, thumbnailType)); + uploads.push(upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type)); } await Promise.all(uploads); + //#endregion - const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; - + //#region DB Object.assign(metadata, { withoutChunks: true, storage: 'minio', storageProps: { - key: key, - webpublicKey: webpublic ? webpublicKey : null, - thumbnailKey: thumbnail ? thumbnailKey : null, + key, + webpublicKey, + thumbnailKey, }, - url: `${ baseUrl }/${ key }`, - webpublicUrl: webpublic ? `${ baseUrl }/${ webpublicKey }` : null, - thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null + url, + webpublicUrl, + thumbnailUrl, } as IMetadata); const file = await DriveFile.insert({ @@ -182,73 +111,91 @@ async function save(path: string, name: string, type: string, hash: string, size metadata: metadata, contentType: type }); + //#endregion return file; - } else { + } else { // use MongoDB GridFS // #region store original const originalDst = await getDriveFileBucket(); // web用(Exif削除済み)がある場合はオリジナルにアクセス制限 - if (webpublic) metadata.accessKey = uuid.v4(); + if (alts.webpublic) metadata.accessKey = uuid.v4(); - const originalFile = await new Promise<IDriveFile>((resolve, reject) => { - const writeStream = originalDst.openUploadStream(name, { - contentType: type, - metadata - }); - - writeStream.once('finish', resolve); - writeStream.on('error', reject); - fs.createReadStream(path).pipe(writeStream); - }); + const originalFile = await storeOriginal(originalDst, name, path, type, metadata); logger.info(`original stored to ${originalFile._id}`); // #endregion store original // #region store webpublic - if (webpublic) { + if (alts.webpublic) { const webDst = await getDriveFileWebpublicBucket(); - - const webFile = await new Promise<IDriveFile>((resolve, reject) => { - const writeStream = webDst.openUploadStream(name, { - contentType: webpublicType, - metadata: { - originalId: originalFile._id - } - }); - - writeStream.once('finish', resolve); - writeStream.on('error', reject); - writeStream.end(webpublic); - }); - + const webFile = await storeAlts(webDst, name, alts.webpublic.data, alts.webpublic.type, originalFile._id); logger.info(`web stored ${webFile._id}`); } // #endregion store webpublic - if (thumbnail) { - const thumbnailBucket = await getDriveFileThumbnailBucket(); + if (alts.thumbnail) { + const thumDst = await getDriveFileThumbnailBucket(); + const thumFile = await storeAlts(thumDst, name, alts.thumbnail.data, alts.thumbnail.type, originalFile._id); + logger.info(`web stored ${thumFile._id}`); + } + + return originalFile; + } +} - const tuhmFile = await new Promise<IDriveFile>((resolve, reject) => { - const writeStream = thumbnailBucket.openUploadStream(name, { - contentType: thumbnailType, - metadata: { - originalId: originalFile._id - } - }); +/** + * Generate webpublic, thumbnail, etc + * @param path Path for original + * @param type Content-Type for original + * @param generateWeb Generate webpublic or not + */ +export async function generateAlts(path: string, type: string, generateWeb: boolean) { + // #region webpublic + let webpublic: IImage; - writeStream.once('finish', resolve); - writeStream.on('error', reject); - writeStream.end(thumbnail); - }); + if (generateWeb) { + logger.info(`creating web image`); - logger.info(`thumbnail stored ${tuhmFile._id}`); + if (['image/jpeg'].includes(type)) { + webpublic = await ConvertToJpeg(path, 2048, 2048); + } else if (['image/webp'].includes(type)) { + webpublic = await ConvertToWebp(path, 2048, 2048); + } else if (['image/png'].includes(type)) { + webpublic = await ConvertToPng(path, 2048, 2048); + } else { + logger.info(`web image not created (not an image)`); } + } else { + logger.info(`web image not created (from remote)`); + } + // #endregion webpublic - return originalFile; + // #region thumbnail + let thumbnail: IImage; + + if (['image/jpeg', 'image/webp'].includes(type)) { + thumbnail = await ConvertToJpeg(path, 498, 280); + } else if (['image/png'].includes(type)) { + thumbnail = await ConvertToPng(path, 498, 280); + } else if (type.startsWith('video/')) { + try { + thumbnail = await GenerateVideoThumbnail(path); + } catch (e) { + logger.error(`GenerateVideoThumbnail failed: ${e}`); + } } + // #endregion thumbnail + + return { + webpublic, + thumbnail, + }; } +/** + * Upload to ObjectStorage + */ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string) { const minio = new Minio.Client(config.drive.config); @@ -258,6 +205,40 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string) }); } +/** + * GridFSBucketにオリジナルを格納する + */ +export async function storeOriginal(bucket: mongodb.GridFSBucket, name: string, path: string, contentType: string, metadata: any) { + return new Promise<IDriveFile>((resolve, reject) => { + const writeStream = bucket.openUploadStream(name, { + contentType, + metadata + }); + + writeStream.once('finish', resolve); + writeStream.on('error', reject); + fs.createReadStream(path).pipe(writeStream); + }); +} + +/** + * GridFSBucketにオリジナル以外を格納する + */ +export async function storeAlts(bucket: mongodb.GridFSBucket, name: string, data: Buffer, contentType: string, originalId: mongodb.ObjectID) { + return new Promise<IDriveFile>((resolve, reject) => { + const writeStream = bucket.openUploadStream(name, { + contentType, + metadata: { + originalId + } + }); + + writeStream.once('finish', resolve); + writeStream.on('error', reject); + writeStream.end(data); + }); +} + async function deleteOldFile(user: IRemoteUser) { const oldFile = await DriveFile.findOne({ _id: { diff --git a/src/services/drive/generate-video-thumbnail.ts b/src/services/drive/generate-video-thumbnail.ts index 14b3b98f97..5d7efff27b 100644 --- a/src/services/drive/generate-video-thumbnail.ts +++ b/src/services/drive/generate-video-thumbnail.ts @@ -1,9 +1,9 @@ import * as fs from 'fs'; import * as tmp from 'tmp'; -import * as sharp from 'sharp'; +import { IImage, ConvertToJpeg } from './image-processor'; const ThumbnailGenerator = require('video-thumbnail-generator').default; -export async function GenerateVideoThumbnail(path: string): Promise<Buffer> { +export async function GenerateVideoThumbnail(path: string): Promise<IImage> { const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => { tmp.dir((e, path, cleanup) => { if (e) return rej(e); @@ -23,16 +23,7 @@ export async function GenerateVideoThumbnail(path: string): Promise<Buffer> { const outPath = `${outDir}/output.png`; - const thumbnail = await sharp(outPath) - .resize(498, 280, { - fit: 'inside', - withoutEnlargement: true - }) - .jpeg({ - quality: 85, - progressive: true - }) - .toBuffer(); + const thumbnail = await ConvertToJpeg(outPath, 498, 280); // cleanup fs.unlinkSync(outPath); diff --git a/src/services/drive/image-processor.ts b/src/services/drive/image-processor.ts new file mode 100644 index 0000000000..3c538390b0 --- /dev/null +++ b/src/services/drive/image-processor.ts @@ -0,0 +1,75 @@ +import * as sharp from 'sharp'; + +export type IImage = { + data: Buffer; + ext: string; + type: string; +}; + +/** + * Convert to JPEG + * with resize, remove metadata, resolve orientation, stop animation + */ +export async function ConvertToJpeg(path: string, width: number, height: number): Promise<IImage> { + const data = await sharp(path) + .resize(width, height, { + fit: 'inside', + withoutEnlargement: true + }) + .rotate() + .jpeg({ + quality: 85, + progressive: true + }) + .toBuffer(); + + return { + data, + ext: 'jpg', + type: 'image/jpeg' + }; +} + +/** + * Convert to WebP + * with resize, remove metadata, resolve orientation, stop animation + */ +export async function ConvertToWebp(path: string, width: number, height: number): Promise<IImage> { + const data = await sharp(path) + .resize(width, height, { + fit: 'inside', + withoutEnlargement: true + }) + .rotate() + .webp({ + quality: 85 + }) + .toBuffer(); + + return { + data, + ext: 'webp', + type: 'image/webp' + }; +} + +/** + * Convert to PNG + * with resize, remove metadata, resolve orientation, stop animation + */ +export async function ConvertToPng(path: string, width: number, height: number): Promise<IImage> { + const data = await sharp(path) + .resize(width, height, { + fit: 'inside', + withoutEnlargement: true + }) + .rotate() + .png() + .toBuffer(); + + return { + data, + ext: 'png', + type: 'image/png' + }; +} |