ファイルと画像認識処理の改善 (#5690)
* dimensions制限とリファクタ * comment * 不要な変更削除 * use fromFile など * Add probe-image-size.d.ts * えーCRLFで作るなよ… * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * fix d.ts * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * Update src/@types/probe-image-size.d.ts Co-Authored-By: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * fix Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
This commit is contained in:
		
							parent
							
								
									d09d06e4cb
								
							
						
					
					
						commit
						9703ba5340
					
				
					 20 changed files with 456 additions and 154 deletions
				
			
		
							
								
								
									
										5
									
								
								.imgbotconfig
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.imgbotconfig
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | { | ||||||
|  | 	"ignoredFiles": [ | ||||||
|  | 		"test/resources/*" | ||||||
|  | 	] | ||||||
|  | } | ||||||
|  | @ -180,6 +180,7 @@ | ||||||
| 		"portscanner": "2.2.0", | 		"portscanner": "2.2.0", | ||||||
| 		"postcss-loader": "3.0.0", | 		"postcss-loader": "3.0.0", | ||||||
| 		"prismjs": "1.18.0", | 		"prismjs": "1.18.0", | ||||||
|  | 		"probe-image-size": "5.0.0", | ||||||
| 		"progress-bar-webpack-plugin": "1.12.1", | 		"progress-bar-webpack-plugin": "1.12.1", | ||||||
| 		"promise-limit": "2.7.0", | 		"promise-limit": "2.7.0", | ||||||
| 		"promise-sequential": "1.1.1", | 		"promise-sequential": "1.1.1", | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								src/@types/probe-image-size.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/@types/probe-image-size.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | declare module 'probe-image-size' { | ||||||
|  | 	import { ReadStream } from 'fs'; | ||||||
|  | 
 | ||||||
|  | 	type ProbeOptions = { | ||||||
|  | 		retries: 1; | ||||||
|  | 		timeout: 30000; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	type ProbeResult = { | ||||||
|  | 		width: number; | ||||||
|  | 		height: number; | ||||||
|  | 		length?: number; | ||||||
|  | 		type: string; | ||||||
|  | 		mime: string; | ||||||
|  | 		wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; | ||||||
|  | 		hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; | ||||||
|  | 		url?: string; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise<ProbeResult>; | ||||||
|  | 	function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void; | ||||||
|  | 	function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void; | ||||||
|  | 
 | ||||||
|  | 	namespace probeImageSize {} // Hack
 | ||||||
|  | 
 | ||||||
|  | 	export = probeImageSize; | ||||||
|  | } | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| import * as fs from 'fs'; |  | ||||||
| import isSvg from 'is-svg'; |  | ||||||
| 
 |  | ||||||
| export default function(path: string) { |  | ||||||
| 	try { |  | ||||||
| 		const size = fs.statSync(path).size; |  | ||||||
| 		if (size > 1 * 1024 * 1024) return false; |  | ||||||
| 		return isSvg(fs.readFileSync(path)); |  | ||||||
| 	} catch { |  | ||||||
| 		return false; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | @ -1,31 +0,0 @@ | ||||||
| import * as fs from 'fs'; |  | ||||||
| import checkSvg from '../misc/check-svg'; |  | ||||||
| const FileType = require('file-type'); |  | ||||||
| 
 |  | ||||||
| export async function detectMine(path: string) { |  | ||||||
| 	return new Promise<[string, string | null]>((res, rej) => { |  | ||||||
| 		const readable = fs.createReadStream(path); |  | ||||||
| 		readable |  | ||||||
| 			.on('error', rej) |  | ||||||
| 			.once('data', async (buffer: Buffer) => { |  | ||||||
| 				readable.destroy(); |  | ||||||
| 				const type = await FileType.fromBuffer(buffer); |  | ||||||
| 				if (type) { |  | ||||||
| 					if (type.mime == 'application/xml' && checkSvg(path)) { |  | ||||||
| 						res(['image/svg+xml', 'svg']); |  | ||||||
| 					} else { |  | ||||||
| 						res([type.mime, type.ext]); |  | ||||||
| 					} |  | ||||||
| 				} else if (checkSvg(path)) { |  | ||||||
| 					res(['image/svg+xml', 'svg']); |  | ||||||
| 				} else { |  | ||||||
| 					// 種類が同定できなかったら application/octet-stream にする
 |  | ||||||
| 					res(['application/octet-stream', null]); |  | ||||||
| 				} |  | ||||||
| 			}) |  | ||||||
| 			.on('end', () => { |  | ||||||
| 				// maybe 0 bytes
 |  | ||||||
| 				res(['application/octet-stream', null]); |  | ||||||
| 			}); |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
|  | @ -1,14 +1,14 @@ | ||||||
| import { createTemp } from './create-temp'; | import { createTemp } from './create-temp'; | ||||||
| import { downloadUrl } from './donwload-url'; | import { downloadUrl } from './donwload-url'; | ||||||
| import { detectMine } from './detect-mine'; | import { detectType } from './get-file-info'; | ||||||
| 
 | 
 | ||||||
| export async function detectUrlMine(url: string) { | export async function detectUrlMime(url: string) { | ||||||
| 	const [path, cleanup] = await createTemp(); | 	const [path, cleanup] = await createTemp(); | ||||||
| 
 | 
 | ||||||
| 	try { | 	try { | ||||||
| 		await downloadUrl(url, path); | 		await downloadUrl(url, path); | ||||||
| 		const [type] = await detectMine(path); | 		const { mime } = await detectType(path); | ||||||
| 		return type; | 		return mime; | ||||||
| 	} finally { | 	} finally { | ||||||
| 		cleanup(); | 		cleanup(); | ||||||
| 	} | 	} | ||||||
							
								
								
									
										201
									
								
								src/misc/get-file-info.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/misc/get-file-info.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | ||||||
|  | import * as fs from 'fs'; | ||||||
|  | import * as crypto from 'crypto'; | ||||||
|  | import * as fileType from 'file-type'; | ||||||
|  | import isSvg from 'is-svg'; | ||||||
|  | import * as probeImageSize from 'probe-image-size'; | ||||||
|  | import * as sharp from 'sharp'; | ||||||
|  | 
 | ||||||
|  | export type FileInfo = { | ||||||
|  | 	size: number; | ||||||
|  | 	md5: string; | ||||||
|  | 	type: { | ||||||
|  | 		mime: string; | ||||||
|  | 		ext: string | null; | ||||||
|  | 	}; | ||||||
|  | 	width?: number; | ||||||
|  | 	height?: number; | ||||||
|  | 	avgColor?: number[]; | ||||||
|  | 	warnings: string[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const TYPE_OCTET_STREAM = { | ||||||
|  | 	mime: 'application/octet-stream', | ||||||
|  | 	ext: null | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const TYPE_SVG = { | ||||||
|  | 	mime: 'image/svg+xml', | ||||||
|  | 	ext: 'svg' | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get file information | ||||||
|  |  */ | ||||||
|  | export async function getFileInfo(path: string): Promise<FileInfo> { | ||||||
|  | 	const warnings = [] as string[]; | ||||||
|  | 
 | ||||||
|  | 	const size = await getFileSize(path); | ||||||
|  | 	const md5 = await calcHash(path); | ||||||
|  | 
 | ||||||
|  | 	let type = await detectType(path); | ||||||
|  | 
 | ||||||
|  | 	// image dimensions
 | ||||||
|  | 	let width: number | undefined; | ||||||
|  | 	let height: number | undefined; | ||||||
|  | 
 | ||||||
|  | 	if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) { | ||||||
|  | 		const imageSize = await detectImageSize(path).catch(e => { | ||||||
|  | 			warnings.push(`detectImageSize failed: ${e}`); | ||||||
|  | 			return undefined; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		// うまく判定できない画像は octet-stream にする
 | ||||||
|  | 		if (!imageSize) { | ||||||
|  | 			warnings.push(`cannot detect image dimensions`); | ||||||
|  | 			type = TYPE_OCTET_STREAM; | ||||||
|  | 		} else if (imageSize.wUnits === 'px') { | ||||||
|  | 			width = imageSize.width; | ||||||
|  | 			height = imageSize.height; | ||||||
|  | 
 | ||||||
|  | 			// 制限を超えている画像は octet-stream にする
 | ||||||
|  | 			if (imageSize.width > 16383 || imageSize.height > 16383) { | ||||||
|  | 				warnings.push(`image dimensions exceeds limits`); | ||||||
|  | 				type = TYPE_OCTET_STREAM; | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			warnings.push(`unsupported unit type: ${imageSize.wUnits}`); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// average color
 | ||||||
|  | 	let avgColor: number[] | undefined; | ||||||
|  | 
 | ||||||
|  | 	if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) { | ||||||
|  | 		avgColor = await calcAvgColor(path).catch(e => { | ||||||
|  | 			warnings.push(`calcAvgColor failed: ${e}`); | ||||||
|  | 			return undefined; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return { | ||||||
|  | 		size, | ||||||
|  | 		md5, | ||||||
|  | 		type, | ||||||
|  | 		width, | ||||||
|  | 		height, | ||||||
|  | 		avgColor, | ||||||
|  | 		warnings, | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Detect MIME Type and extension | ||||||
|  |  */ | ||||||
|  | export async function detectType(path: string) { | ||||||
|  | 	// Check 0 byte
 | ||||||
|  | 	const fileSize = await getFileSize(path); | ||||||
|  | 	if (fileSize === 0) { | ||||||
|  | 		return TYPE_OCTET_STREAM; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const type = await fileType.fromFile(path); | ||||||
|  | 
 | ||||||
|  | 	if (type) { | ||||||
|  | 		// XMLはSVGかもしれない
 | ||||||
|  | 		if (type.mime === 'application/xml' && await checkSvg(path)) { | ||||||
|  | 			return TYPE_SVG; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			mime: type.mime, | ||||||
|  | 			ext: type.ext | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 種類が不明でもSVGかもしれない
 | ||||||
|  | 	if (await checkSvg(path)) { | ||||||
|  | 		return TYPE_SVG; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// それでも種類が不明なら application/octet-stream にする
 | ||||||
|  | 	return TYPE_OCTET_STREAM; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Check the file is SVG or not | ||||||
|  |  */ | ||||||
|  | export async function checkSvg(path: string) { | ||||||
|  | 	try { | ||||||
|  | 		const size = await getFileSize(path); | ||||||
|  | 		if (size > 1 * 1024 * 1024) return false; | ||||||
|  | 		return isSvg(fs.readFileSync(path)); | ||||||
|  | 	} catch { | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Get file size | ||||||
|  |  */ | ||||||
|  | export async function getFileSize(path: string): Promise<number> { | ||||||
|  | 	return new Promise<number>((res, rej) => { | ||||||
|  | 		fs.stat(path, (err, stats) => { | ||||||
|  | 			if (err) return rej(err); | ||||||
|  | 			res(stats.size); | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Calculate MD5 hash | ||||||
|  |  */ | ||||||
|  | async function calcHash(path: string): Promise<string> { | ||||||
|  | 	return new Promise<string>((res, rej) => { | ||||||
|  | 		const readable = fs.createReadStream(path); | ||||||
|  | 		const hash = crypto.createHash('md5'); | ||||||
|  | 		const chunks: Buffer[] = []; | ||||||
|  | 		readable | ||||||
|  | 			.on('error', rej) | ||||||
|  | 			.pipe(hash) | ||||||
|  | 			.on('error', rej) | ||||||
|  | 			.on('data', chunk => chunks.push(chunk)) | ||||||
|  | 			.on('end', () => { | ||||||
|  | 				const buffer = Buffer.concat(chunks); | ||||||
|  | 				res(buffer.toString('hex')); | ||||||
|  | 			}); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Detect dimensions of image | ||||||
|  |  */ | ||||||
|  | async function detectImageSize(path: string): Promise<{ | ||||||
|  | 	width: number; | ||||||
|  | 	height: number; | ||||||
|  | 	wUnits: string; | ||||||
|  | 	hUnits: string; | ||||||
|  | }> { | ||||||
|  | 	const readable = fs.createReadStream(path); | ||||||
|  | 	const imageSize = await probeImageSize(readable); | ||||||
|  | 	readable.destroy(); | ||||||
|  | 	return imageSize; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Calculate average color of image | ||||||
|  |  */ | ||||||
|  | async function calcAvgColor(path: string): Promise<number[]> { | ||||||
|  | 	const img = sharp(path); | ||||||
|  | 
 | ||||||
|  | 	const info = await (img as any).stats(); | ||||||
|  | 
 | ||||||
|  | 	if (info.isOpaque) { | ||||||
|  | 		const r = Math.round(info.channels[0].mean); | ||||||
|  | 		const g = Math.round(info.channels[1].mean); | ||||||
|  | 		const b = Math.round(info.channels[2].mean); | ||||||
|  | 
 | ||||||
|  | 		return [r, g, b]; | ||||||
|  | 	} else { | ||||||
|  | 		return [255, 255, 255]; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import define from '../../../define'; | import define from '../../../define'; | ||||||
| import { detectUrlMine } from '../../../../../misc/detect-url-mine'; | import { detectUrlMime } from '../../../../../misc/detect-url-mime'; | ||||||
| import { Emojis } from '../../../../../models'; | import { Emojis } from '../../../../../models'; | ||||||
| import { genId } from '../../../../../misc/gen-id'; | import { genId } from '../../../../../misc/gen-id'; | ||||||
| import { getConnection } from 'typeorm'; | import { getConnection } from 'typeorm'; | ||||||
|  | @ -46,7 +46,7 @@ export const meta = { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, me) => { | export default define(meta, async (ps, me) => { | ||||||
| 	const type = await detectUrlMine(ps.url); | 	const type = await detectUrlMime(ps.url); | ||||||
| 
 | 
 | ||||||
| 	const exists = await Emojis.findOne({ | 	const exists = await Emojis.findOne({ | ||||||
| 		name: ps.name, | 		name: ps.name, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import define from '../../../define'; | import define from '../../../define'; | ||||||
| import { detectUrlMine } from '../../../../../misc/detect-url-mine'; | import { detectUrlMime } from '../../../../../misc/detect-url-mime'; | ||||||
| import { ID } from '../../../../../misc/cafy-id'; | import { ID } from '../../../../../misc/cafy-id'; | ||||||
| import { Emojis } from '../../../../../models'; | import { Emojis } from '../../../../../models'; | ||||||
| import { getConnection } from 'typeorm'; | import { getConnection } from 'typeorm'; | ||||||
|  | @ -52,7 +52,7 @@ export default define(meta, async (ps) => { | ||||||
| 
 | 
 | ||||||
| 	if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); | 	if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); | ||||||
| 
 | 
 | ||||||
| 	const type = await detectUrlMine(ps.url); | 	const type = await detectUrlMime(ps.url); | ||||||
| 
 | 
 | ||||||
| 	await Emojis.update(emoji.id, { | 	await Emojis.update(emoji.id, { | ||||||
| 		updatedAt: new Date(), | 		updatedAt: new Date(), | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import { contentDisposition } from '../../misc/content-disposition'; | ||||||
| import { DriveFiles } from '../../models'; | import { DriveFiles } from '../../models'; | ||||||
| import { InternalStorage } from '../../services/drive/internal-storage'; | import { InternalStorage } from '../../services/drive/internal-storage'; | ||||||
| import { downloadUrl } from '../../misc/donwload-url'; | import { downloadUrl } from '../../misc/donwload-url'; | ||||||
| import { detectMine } from '../../misc/detect-mine'; | import { detectType } from '../../misc/get-file-info'; | ||||||
| import { convertToJpeg, convertToPng } from '../../services/drive/image-processor'; | import { convertToJpeg, convertToPng } from '../../services/drive/image-processor'; | ||||||
| import { GenerateVideoThumbnail } from '../../services/drive/generate-video-thumbnail'; | import { GenerateVideoThumbnail } from '../../services/drive/generate-video-thumbnail'; | ||||||
| 
 | 
 | ||||||
|  | @ -52,15 +52,15 @@ export default async function(ctx: Koa.Context) { | ||||||
| 			try { | 			try { | ||||||
| 				await downloadUrl(file.uri, path); | 				await downloadUrl(file.uri, path); | ||||||
| 
 | 
 | ||||||
| 				const [type, ext] = await detectMine(path); | 				const { mime, ext } = await detectType(path); | ||||||
| 
 | 
 | ||||||
| 				const convertFile = async () => { | 				const convertFile = async () => { | ||||||
| 					if (isThumbnail) { | 					if (isThumbnail) { | ||||||
| 						if (['image/jpeg', 'image/webp'].includes(type)) { | 						if (['image/jpeg', 'image/webp'].includes(mime)) { | ||||||
| 							return await convertToJpeg(path, 498, 280); | 							return await convertToJpeg(path, 498, 280); | ||||||
| 						} else if (['image/png'].includes(type)) { | 						} else if (['image/png'].includes(mime)) { | ||||||
| 							return await convertToPng(path, 498, 280); | 							return await convertToPng(path, 498, 280); | ||||||
| 						} else if (type.startsWith('video/')) { | 						} else if (mime.startsWith('video/')) { | ||||||
| 							return await GenerateVideoThumbnail(path); | 							return await GenerateVideoThumbnail(path); | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
|  | @ -68,7 +68,7 @@ export default async function(ctx: Koa.Context) { | ||||||
| 					return { | 					return { | ||||||
| 						data: fs.readFileSync(path), | 						data: fs.readFileSync(path), | ||||||
| 						ext, | 						ext, | ||||||
| 						type, | 						type: mime, | ||||||
| 					}; | 					}; | ||||||
| 				}; | 				}; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import { serverLogger } from '..'; | ||||||
| import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor'; | import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor'; | ||||||
| import { createTemp } from '../../misc/create-temp'; | import { createTemp } from '../../misc/create-temp'; | ||||||
| import { downloadUrl } from '../../misc/donwload-url'; | import { downloadUrl } from '../../misc/donwload-url'; | ||||||
| import { detectMine } from '../../misc/detect-mine'; | import { detectType } from '../../misc/get-file-info'; | ||||||
| 
 | 
 | ||||||
| export async function proxyMedia(ctx: Koa.Context) { | export async function proxyMedia(ctx: Koa.Context) { | ||||||
| 	const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; | 	const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; | ||||||
|  | @ -15,21 +15,21 @@ export async function proxyMedia(ctx: Koa.Context) { | ||||||
| 	try { | 	try { | ||||||
| 		await downloadUrl(url, path); | 		await downloadUrl(url, path); | ||||||
| 
 | 
 | ||||||
| 		const [type, ext] = await detectMine(path); | 		const { mime, ext } = await detectType(path); | ||||||
| 
 | 
 | ||||||
| 		if (!type.startsWith('image/')) throw 403; | 		if (!mime.startsWith('image/')) throw 403; | ||||||
| 
 | 
 | ||||||
| 		let image: IImage; | 		let image: IImage; | ||||||
| 
 | 
 | ||||||
| 		if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(type)) { | 		if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) { | ||||||
| 			image = await convertToPng(path, 498, 280); | 			image = await convertToPng(path, 498, 280); | ||||||
| 		} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(type)) { | 		} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) { | ||||||
| 			image = await convertToJpeg(path, 200, 200); | 			image = await convertToJpeg(path, 200, 200); | ||||||
| 		} else { | 		} else { | ||||||
| 			image = { | 			image = { | ||||||
| 				data: fs.readFileSync(path), | 				data: fs.readFileSync(path), | ||||||
| 				ext, | 				ext, | ||||||
| 				type, | 				type: mime, | ||||||
| 			}; | 			}; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,9 +1,6 @@ | ||||||
| import { Buffer } from 'buffer'; |  | ||||||
| import * as fs from 'fs'; | import * as fs from 'fs'; | ||||||
| 
 | 
 | ||||||
| import * as crypto from 'crypto'; |  | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| import * as sharp from 'sharp'; |  | ||||||
| 
 | 
 | ||||||
| import { publishMainStream, publishDriveStream } from '../stream'; | import { publishMainStream, publishDriveStream } from '../stream'; | ||||||
| import { deleteFile } from './delete-file'; | import { deleteFile } from './delete-file'; | ||||||
|  | @ -12,7 +9,7 @@ import { GenerateVideoThumbnail } from './generate-video-thumbnail'; | ||||||
| import { driveLogger } from './logger'; | import { driveLogger } from './logger'; | ||||||
| import { IImage, convertToJpeg, convertToWebp, convertToPng } from './image-processor'; | import { IImage, convertToJpeg, convertToWebp, convertToPng } from './image-processor'; | ||||||
| import { contentDisposition } from '../../misc/content-disposition'; | import { contentDisposition } from '../../misc/content-disposition'; | ||||||
| import { detectMine } from '../../misc/detect-mine'; | import { getFileInfo } from '../../misc/get-file-info'; | ||||||
| import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; | import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; | ||||||
| import { InternalStorage } from './internal-storage'; | import { InternalStorage } from './internal-storage'; | ||||||
| import { DriveFile } from '../../models/entities/drive-file'; | import { DriveFile } from '../../models/entities/drive-file'; | ||||||
|  | @ -271,41 +268,16 @@ export default async function( | ||||||
| 	uri: string | null = null, | 	uri: string | null = null, | ||||||
| 	sensitive: boolean | null = null | 	sensitive: boolean | null = null | ||||||
| ): Promise<DriveFile> { | ): Promise<DriveFile> { | ||||||
| 	// Calc md5 hash
 | 	const info = await getFileInfo(path); | ||||||
| 	const calcHash = new Promise<string>((res, rej) => { | 	logger.info(`${JSON.stringify(info)}`); | ||||||
| 		const readable = fs.createReadStream(path); |  | ||||||
| 		const hash = crypto.createHash('md5'); |  | ||||||
| 		const chunks: Buffer[] = []; |  | ||||||
| 		readable |  | ||||||
| 			.on('error', rej) |  | ||||||
| 			.pipe(hash) |  | ||||||
| 			.on('error', rej) |  | ||||||
| 			.on('data', chunk => chunks.push(chunk)) |  | ||||||
| 			.on('end', () => { |  | ||||||
| 				const buffer = Buffer.concat(chunks); |  | ||||||
| 				res(buffer.toString('hex')); |  | ||||||
| 			}); |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	// Get file size
 |  | ||||||
| 	const getFileSize = new Promise<number>((res, rej) => { |  | ||||||
| 		fs.stat(path, (err, stats) => { |  | ||||||
| 			if (err) return rej(err); |  | ||||||
| 			res(stats.size); |  | ||||||
| 		}); |  | ||||||
| 	}); |  | ||||||
| 
 |  | ||||||
| 	const [hash, [mime, ext], size] = await Promise.all([calcHash, detectMine(path), getFileSize]); |  | ||||||
| 
 |  | ||||||
| 	logger.info(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); |  | ||||||
| 
 | 
 | ||||||
| 	// detect name
 | 	// detect name
 | ||||||
| 	const detectedName = name || (ext ? `untitled.${ext}` : 'untitled'); | 	const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); | ||||||
| 
 | 
 | ||||||
| 	if (!force) { | 	if (!force) { | ||||||
| 		// Check if there is a file with the same hash
 | 		// Check if there is a file with the same hash
 | ||||||
| 		const much = await DriveFiles.findOne({ | 		const much = await DriveFiles.findOne({ | ||||||
| 			md5: hash, | 			md5: info.md5, | ||||||
| 			userId: user.id, | 			userId: user.id, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  | @ -325,7 +297,7 @@ export default async function( | ||||||
| 		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); | 		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); | ||||||
| 
 | 
 | ||||||
| 		// If usage limit exceeded
 | 		// If usage limit exceeded
 | ||||||
| 		if (usage + size > driveCapacity) { | 		if (usage + info.size > driveCapacity) { | ||||||
| 			if (Users.isLocalUser(user)) { | 			if (Users.isLocalUser(user)) { | ||||||
| 				throw new Error('no-free-space'); | 				throw new Error('no-free-space'); | ||||||
| 			} else { | 			} else { | ||||||
|  | @ -351,57 +323,24 @@ export default async function( | ||||||
| 		return driveFolder; | 		return driveFolder; | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const properties: {[key: string]: any} = {}; | 	const properties: { | ||||||
|  | 		width?: number; | ||||||
|  | 		height?: number; | ||||||
|  | 		avgColor?: string; | ||||||
|  | 	} = {}; | ||||||
| 
 | 
 | ||||||
| 	let propPromises: Promise<void>[] = []; | 	if (info.width) { | ||||||
|  | 		properties['width'] = info.width; | ||||||
|  | 		properties['height'] = info.height; | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	const isImage = ['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime); | 	if (info.avgColor) { | ||||||
| 
 | 		properties['avgColor'] = `rgb(${info.avgColor.join(',')}`; | ||||||
| 	if (isImage) { |  | ||||||
| 		const img = sharp(path); |  | ||||||
| 
 |  | ||||||
| 		// Calc width and height
 |  | ||||||
| 		const calcWh = async () => { |  | ||||||
| 			logger.debug('calculating image width and height...'); |  | ||||||
| 
 |  | ||||||
| 			// Calculate width and height
 |  | ||||||
| 			const meta = await img.metadata(); |  | ||||||
| 
 |  | ||||||
| 			logger.debug(`image width and height is calculated: ${meta.width}, ${meta.height}`); |  | ||||||
| 
 |  | ||||||
| 			properties['width'] = meta.width; |  | ||||||
| 			properties['height'] = meta.height; |  | ||||||
| 		}; |  | ||||||
| 
 |  | ||||||
| 		// Calc average color
 |  | ||||||
| 		const calcAvg = async () => { |  | ||||||
| 			logger.debug('calculating average color...'); |  | ||||||
| 
 |  | ||||||
| 			try { |  | ||||||
| 				const info = await img.stats(); |  | ||||||
| 
 |  | ||||||
| 				if (info.isOpaque) { |  | ||||||
| 					const r = Math.round(info.channels[0].mean); |  | ||||||
| 					const g = Math.round(info.channels[1].mean); |  | ||||||
| 					const b = Math.round(info.channels[2].mean); |  | ||||||
| 
 |  | ||||||
| 					logger.debug(`average color is calculated: ${r}, ${g}, ${b}`); |  | ||||||
| 
 |  | ||||||
| 					properties['avgColor'] = `rgb(${r},${g},${b})`; |  | ||||||
| 				} else { |  | ||||||
| 					logger.debug(`this image is not opaque so average color is 255, 255, 255`); |  | ||||||
| 
 |  | ||||||
| 					properties['avgColor'] = `rgb(255,255,255)`; |  | ||||||
| 				} |  | ||||||
| 			} catch (e) { } |  | ||||||
| 		}; |  | ||||||
| 
 |  | ||||||
| 		propPromises = [calcWh(), calcAvg()]; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const profile = await UserProfiles.findOne(user.id); | 	const profile = await UserProfiles.findOne(user.id); | ||||||
| 
 | 
 | ||||||
| 	const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); | 	const folder = await fetchFolder(); | ||||||
| 
 | 
 | ||||||
| 	let file = new DriveFile(); | 	let file = new DriveFile(); | ||||||
| 	file.id = genId(); | 	file.id = genId(); | ||||||
|  | @ -436,9 +375,9 @@ export default async function( | ||||||
| 	if (isLink) { | 	if (isLink) { | ||||||
| 		try { | 		try { | ||||||
| 			file.size = 0; | 			file.size = 0; | ||||||
| 			file.md5 = hash; | 			file.md5 = info.md5; | ||||||
| 			file.name = detectedName; | 			file.name = detectedName; | ||||||
| 			file.type = mime; | 			file.type = info.type.mime; | ||||||
| 			file.storedInternal = false; | 			file.storedInternal = false; | ||||||
| 
 | 
 | ||||||
| 			file = await DriveFiles.save(file); | 			file = await DriveFiles.save(file); | ||||||
|  | @ -457,7 +396,7 @@ export default async function( | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		file = await (save(file, path, detectedName, mime, hash, size)); | 		file = await (save(file, path, detectedName, info.type.mime, info.md5, info.size)); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	logger.succ(`drive file has been created ${file.id}`); | 	logger.succ(`drive file has been created ${file.id}`); | ||||||
|  |  | ||||||
							
								
								
									
										152
									
								
								test/get-file-info.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								test/get-file-info.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,152 @@ | ||||||
|  | /* | ||||||
|  |  * Tests for detection of file information | ||||||
|  |  * | ||||||
|  |  * How to run the tests: | ||||||
|  |  * > TS_NODE_FILES=true npx mocha test/get-file-info.ts --require ts-node/register | ||||||
|  |  * | ||||||
|  |  * To specify test: | ||||||
|  |  * > TS_NODE_FILES=true npx mocha test/get-file-info.ts --require ts-node/register -g 'test name' | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | import * as assert from 'assert'; | ||||||
|  | import { async } from './utils'; | ||||||
|  | import { getFileInfo } from '../src/misc/get-file-info'; | ||||||
|  | 
 | ||||||
|  | describe('Get file info', () => { | ||||||
|  | 	it('Empty file', async (async () => { | ||||||
|  | 		const path = `${__dirname}/resources/emptyfile`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 0, | ||||||
|  | 			md5: 'd41d8cd98f00b204e9800998ecf8427e', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'application/octet-stream', | ||||||
|  | 				ext: null | ||||||
|  | 			}, | ||||||
|  | 			width: undefined, | ||||||
|  | 			height: undefined, | ||||||
|  | 			avgColor: undefined | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('Generic JPEG', async (async () => { | ||||||
|  | 		const path = `${__dirname}/resources/Lenna.jpg`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 25360, | ||||||
|  | 			md5: '091b3f259662aa31e2ffef4519951168', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'image/jpeg', | ||||||
|  | 				ext: 'jpg' | ||||||
|  | 			}, | ||||||
|  | 			width: 512, | ||||||
|  | 			height: 512, | ||||||
|  | 			avgColor: [ 181, 99, 106 ] | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('Generic APNG', async (async () => { | ||||||
|  | 		const path = `${__dirname}/resources/anime.png`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 1868, | ||||||
|  | 			md5: '08189c607bea3b952704676bb3c979e0', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'image/apng', | ||||||
|  | 				ext: 'apng' | ||||||
|  | 			}, | ||||||
|  | 			width: 256, | ||||||
|  | 			height: 256, | ||||||
|  | 			avgColor: [ 249, 253, 250 ] | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('Generic AGIF', async (async () => { | ||||||
|  | 		const path = `${__dirname}/resources/anime.gif`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 2248, | ||||||
|  | 			md5: '32c47a11555675d9267aee1a86571e7e', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'image/gif', | ||||||
|  | 				ext: 'gif' | ||||||
|  | 			}, | ||||||
|  | 			width: 256, | ||||||
|  | 			height: 256, | ||||||
|  | 			avgColor: [ 249, 253, 250 ] | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('PNG with alpha', async (async () => { | ||||||
|  | 		const path = `${__dirname}/resources/with-alpha.png`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 3772, | ||||||
|  | 			md5: 'f73535c3e1e27508885b69b10cf6e991', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'image/png', | ||||||
|  | 				ext: 'png' | ||||||
|  | 			}, | ||||||
|  | 			width: 256, | ||||||
|  | 			height: 256, | ||||||
|  | 			avgColor: [ 255, 255, 255 ] | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('Generic SVG', async (async () => { | ||||||
|  | 		const path = `${__dirname}/resources/image.svg`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 505, | ||||||
|  | 			md5: 'b6f52b4b021e7b92cdd04509c7267965', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'image/svg+xml', | ||||||
|  | 				ext: 'svg' | ||||||
|  | 			}, | ||||||
|  | 			width: 256, | ||||||
|  | 			height: 256, | ||||||
|  | 			avgColor: [ 255, 255, 255 ] | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('SVG with XML definition', async (async () => { | ||||||
|  | 		// https://github.com/syuilo/misskey/issues/4413
 | ||||||
|  | 		const path = `${__dirname}/resources/with-xml-def.svg`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 544, | ||||||
|  | 			md5: '4b7a346cde9ccbeb267e812567e33397', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'image/svg+xml', | ||||||
|  | 				ext: 'svg' | ||||||
|  | 			}, | ||||||
|  | 			width: 256, | ||||||
|  | 			height: 256, | ||||||
|  | 			avgColor: [ 255, 255, 255 ] | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | 
 | ||||||
|  | 	it('Dimension limit', async (async () => { | ||||||
|  | 		const path = `${__dirname}/resources/25000x25000.png`; | ||||||
|  | 		const info = await getFileInfo(path); | ||||||
|  | 		delete info.warnings; | ||||||
|  | 		assert.deepStrictEqual(info, { | ||||||
|  | 			size: 75933, | ||||||
|  | 			md5: '268c5dde99e17cf8fe09f1ab3f97df56', | ||||||
|  | 			type: { | ||||||
|  | 				mime: 'application/octet-stream',	// do not treat as image
 | ||||||
|  | 				ext: null | ||||||
|  | 			}, | ||||||
|  | 			width: 25000, | ||||||
|  | 			height: 25000, | ||||||
|  | 			avgColor: undefined | ||||||
|  | 		}); | ||||||
|  | 	})); | ||||||
|  | }); | ||||||
							
								
								
									
										
											BIN
										
									
								
								test/resources/25000x25000.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								test/resources/25000x25000.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 74 KiB | 
							
								
								
									
										
											BIN
										
									
								
								test/resources/anime.gif
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								test/resources/anime.gif
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								test/resources/anime.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								test/resources/anime.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										0
									
								
								test/resources/emptyfile
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								test/resources/emptyfile
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										
											BIN
										
									
								
								test/resources/with-alpha.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								test/resources/with-alpha.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.7 KiB | 
							
								
								
									
										2
									
								
								test/resources/with-xml-def.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								test/resources/with-xml-def.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#FF40A4" d="M128 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C164 84 144 76 128 80"/><path fill="#FFBF40" d="M192 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C228 84 208 76 192 80"/><path fill="#408EFF" d="M64 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8C28 172 48 180 64 176s20-24 20-48v-16c0-8 8-16 20.3-8C100 84 80 76 64 80"/></svg> | ||||||
| After Width: | Height: | Size: 544 B | 
							
								
								
									
										36
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										36
									
								
								yarn.lock
									
										
									
									
									
								
							|  | @ -3038,6 +3038,13 @@ debug-fabulous@1.X: | ||||||
|     memoizee "0.4.X" |     memoizee "0.4.X" | ||||||
|     object-assign "4.X" |     object-assign "4.X" | ||||||
| 
 | 
 | ||||||
|  | debug@2, debug@^2.2.0, debug@^2.3.3, debug@^2.5.2: | ||||||
|  |   version "2.6.9" | ||||||
|  |   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | ||||||
|  |   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== | ||||||
|  |   dependencies: | ||||||
|  |     ms "2.0.0" | ||||||
|  | 
 | ||||||
| debug@3.1.0, debug@~3.1.0: | debug@3.1.0, debug@~3.1.0: | ||||||
|   version "3.1.0" |   version "3.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" |   resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" | ||||||
|  | @ -3059,13 +3066,6 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: | ||||||
|   dependencies: |   dependencies: | ||||||
|     ms "^2.1.1" |     ms "^2.1.1" | ||||||
| 
 | 
 | ||||||
| debug@^2.2.0, debug@^2.3.3, debug@^2.5.2: |  | ||||||
|   version "2.6.9" |  | ||||||
|   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" |  | ||||||
|   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== |  | ||||||
|   dependencies: |  | ||||||
|     ms "2.0.0" |  | ||||||
| 
 |  | ||||||
| debuglog@^1.0.0: | debuglog@^1.0.0: | ||||||
|   version "1.0.1" |   version "1.0.1" | ||||||
|   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" |   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" | ||||||
|  | @ -3110,7 +3110,7 @@ deep-is@~0.1.3: | ||||||
|   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" |   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" | ||||||
|   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= |   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= | ||||||
| 
 | 
 | ||||||
| deepmerge@^4.2.2: | deepmerge@^4.0.0, deepmerge@^4.2.2: | ||||||
|   version "4.2.2" |   version "4.2.2" | ||||||
|   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" |   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" | ||||||
|   integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== |   integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== | ||||||
|  | @ -8040,6 +8040,17 @@ prismjs@1.18.0: | ||||||
|   optionalDependencies: |   optionalDependencies: | ||||||
|     clipboard "^2.0.0" |     clipboard "^2.0.0" | ||||||
| 
 | 
 | ||||||
|  | probe-image-size@5.0.0: | ||||||
|  |   version "5.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-5.0.0.tgz#1b87d20340ab8fcdb4324ec77fbc8a5f53419878" | ||||||
|  |   integrity sha512-V6uBYw5eBc5UVIE7MUZD6Nxg0RYuGDWLDenEn0B1WC6PcTvn1xdQ6HLDDuznefsiExC6rNrCz7mFRBo0f3Xekg== | ||||||
|  |   dependencies: | ||||||
|  |     deepmerge "^4.0.0" | ||||||
|  |     inherits "^2.0.3" | ||||||
|  |     next-tick "^1.0.0" | ||||||
|  |     request "^2.83.0" | ||||||
|  |     stream-parser "~0.3.1" | ||||||
|  | 
 | ||||||
| process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: | process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: | ||||||
|   version "2.0.1" |   version "2.0.1" | ||||||
|   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" |   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" | ||||||
|  | @ -8652,7 +8663,7 @@ request-stats@3.0.0: | ||||||
|     http-headers "^3.0.1" |     http-headers "^3.0.1" | ||||||
|     once "^1.4.0" |     once "^1.4.0" | ||||||
| 
 | 
 | ||||||
| request@2.88.0, request@^2.73.0, request@^2.88.0: | request@2.88.0, request@^2.73.0, request@^2.83.0, request@^2.88.0: | ||||||
|   version "2.88.0" |   version "2.88.0" | ||||||
|   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" |   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" | ||||||
|   integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== |   integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== | ||||||
|  | @ -9356,6 +9367,13 @@ stream-http@^2.7.2: | ||||||
|     to-arraybuffer "^1.0.0" |     to-arraybuffer "^1.0.0" | ||||||
|     xtend "^4.0.0" |     xtend "^4.0.0" | ||||||
| 
 | 
 | ||||||
|  | stream-parser@~0.3.1: | ||||||
|  |   version "0.3.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773" | ||||||
|  |   integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M= | ||||||
|  |   dependencies: | ||||||
|  |     debug "2" | ||||||
|  | 
 | ||||||
| stream-shift@^1.0.0: | stream-shift@^1.0.0: | ||||||
|   version "1.0.0" |   version "1.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" |   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue