ファイルと画像認識処理の改善 (#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", | ||||
| 		"postcss-loader": "3.0.0", | ||||
| 		"prismjs": "1.18.0", | ||||
| 		"probe-image-size": "5.0.0", | ||||
| 		"progress-bar-webpack-plugin": "1.12.1", | ||||
| 		"promise-limit": "2.7.0", | ||||
| 		"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 { 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(); | ||||
| 
 | ||||
| 	try { | ||||
| 		await downloadUrl(url, path); | ||||
| 		const [type] = await detectMine(path); | ||||
| 		return type; | ||||
| 		const { mime } = await detectType(path); | ||||
| 		return mime; | ||||
| 	} finally { | ||||
| 		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 define from '../../../define'; | ||||
| import { detectUrlMine } from '../../../../../misc/detect-url-mine'; | ||||
| import { detectUrlMime } from '../../../../../misc/detect-url-mime'; | ||||
| import { Emojis } from '../../../../../models'; | ||||
| import { genId } from '../../../../../misc/gen-id'; | ||||
| import { getConnection } from 'typeorm'; | ||||
|  | @ -46,7 +46,7 @@ export const meta = { | |||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps, me) => { | ||||
| 	const type = await detectUrlMine(ps.url); | ||||
| 	const type = await detectUrlMime(ps.url); | ||||
| 
 | ||||
| 	const exists = await Emojis.findOne({ | ||||
| 		name: ps.name, | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import $ from 'cafy'; | ||||
| 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 { Emojis } from '../../../../../models'; | ||||
| import { getConnection } from 'typeorm'; | ||||
|  | @ -52,7 +52,7 @@ export default define(meta, async (ps) => { | |||
| 
 | ||||
| 	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, { | ||||
| 		updatedAt: new Date(), | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import { contentDisposition } from '../../misc/content-disposition'; | |||
| import { DriveFiles } from '../../models'; | ||||
| import { InternalStorage } from '../../services/drive/internal-storage'; | ||||
| 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 { GenerateVideoThumbnail } from '../../services/drive/generate-video-thumbnail'; | ||||
| 
 | ||||
|  | @ -52,15 +52,15 @@ export default async function(ctx: Koa.Context) { | |||
| 			try { | ||||
| 				await downloadUrl(file.uri, path); | ||||
| 
 | ||||
| 				const [type, ext] = await detectMine(path); | ||||
| 				const { mime, ext } = await detectType(path); | ||||
| 
 | ||||
| 				const convertFile = async () => { | ||||
| 					if (isThumbnail) { | ||||
| 						if (['image/jpeg', 'image/webp'].includes(type)) { | ||||
| 						if (['image/jpeg', 'image/webp'].includes(mime)) { | ||||
| 							return await convertToJpeg(path, 498, 280); | ||||
| 						} else if (['image/png'].includes(type)) { | ||||
| 						} else if (['image/png'].includes(mime)) { | ||||
| 							return await convertToPng(path, 498, 280); | ||||
| 						} else if (type.startsWith('video/')) { | ||||
| 						} else if (mime.startsWith('video/')) { | ||||
| 							return await GenerateVideoThumbnail(path); | ||||
| 						} | ||||
| 					} | ||||
|  | @ -68,7 +68,7 @@ export default async function(ctx: Koa.Context) { | |||
| 					return { | ||||
| 						data: fs.readFileSync(path), | ||||
| 						ext, | ||||
| 						type, | ||||
| 						type: mime, | ||||
| 					}; | ||||
| 				}; | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import { serverLogger } from '..'; | |||
| import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor'; | ||||
| import { createTemp } from '../../misc/create-temp'; | ||||
| 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) { | ||||
| 	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 { | ||||
| 		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; | ||||
| 
 | ||||
| 		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); | ||||
| 		} 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); | ||||
| 		} else { | ||||
| 			image = { | ||||
| 				data: fs.readFileSync(path), | ||||
| 				ext, | ||||
| 				type, | ||||
| 				type: mime, | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,9 +1,6 @@ | |||
| import { Buffer } from 'buffer'; | ||||
| import * as fs from 'fs'; | ||||
| 
 | ||||
| import * as crypto from 'crypto'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import * as sharp from 'sharp'; | ||||
| 
 | ||||
| import { publishMainStream, publishDriveStream } from '../stream'; | ||||
| import { deleteFile } from './delete-file'; | ||||
|  | @ -12,7 +9,7 @@ import { GenerateVideoThumbnail } from './generate-video-thumbnail'; | |||
| import { driveLogger } from './logger'; | ||||
| import { IImage, convertToJpeg, convertToWebp, convertToPng } from './image-processor'; | ||||
| 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 { InternalStorage } from './internal-storage'; | ||||
| import { DriveFile } from '../../models/entities/drive-file'; | ||||
|  | @ -271,41 +268,16 @@ export default async function( | |||
| 	uri: string | null = null, | ||||
| 	sensitive: boolean | null = null | ||||
| ): Promise<DriveFile> { | ||||
| 	// Calc md5 hash
 | ||||
| 	const calcHash = 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')); | ||||
| 			}); | ||||
| 	}); | ||||
| 
 | ||||
| 	// 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}`); | ||||
| 	const info = await getFileInfo(path); | ||||
| 	logger.info(`${JSON.stringify(info)}`); | ||||
| 
 | ||||
| 	// detect name
 | ||||
| 	const detectedName = name || (ext ? `untitled.${ext}` : 'untitled'); | ||||
| 	const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); | ||||
| 
 | ||||
| 	if (!force) { | ||||
| 		// Check if there is a file with the same hash
 | ||||
| 		const much = await DriveFiles.findOne({ | ||||
| 			md5: hash, | ||||
| 			md5: info.md5, | ||||
| 			userId: user.id, | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -325,7 +297,7 @@ export default async function( | |||
| 		logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); | ||||
| 
 | ||||
| 		// If usage limit exceeded
 | ||||
| 		if (usage + size > driveCapacity) { | ||||
| 		if (usage + info.size > driveCapacity) { | ||||
| 			if (Users.isLocalUser(user)) { | ||||
| 				throw new Error('no-free-space'); | ||||
| 			} else { | ||||
|  | @ -351,57 +323,24 @@ export default async function( | |||
| 		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 (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()]; | ||||
| 	if (info.avgColor) { | ||||
| 		properties['avgColor'] = `rgb(${info.avgColor.join(',')}`; | ||||
| 	} | ||||
| 
 | ||||
| 	const profile = await UserProfiles.findOne(user.id); | ||||
| 
 | ||||
| 	const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); | ||||
| 	const folder = await fetchFolder(); | ||||
| 
 | ||||
| 	let file = new DriveFile(); | ||||
| 	file.id = genId(); | ||||
|  | @ -436,9 +375,9 @@ export default async function( | |||
| 	if (isLink) { | ||||
| 		try { | ||||
| 			file.size = 0; | ||||
| 			file.md5 = hash; | ||||
| 			file.md5 = info.md5; | ||||
| 			file.name = detectedName; | ||||
| 			file.type = mime; | ||||
| 			file.type = info.type.mime; | ||||
| 			file.storedInternal = false; | ||||
| 
 | ||||
| 			file = await DriveFiles.save(file); | ||||
|  | @ -457,7 +396,7 @@ export default async function( | |||
| 			} | ||||
| 		} | ||||
| 	} 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}`); | ||||
|  |  | |||
							
								
								
									
										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" | ||||
|     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: | ||||
|   version "3.1.0" | ||||
|   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: | ||||
|     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: | ||||
|   version "1.0.1" | ||||
|   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" | ||||
|   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= | ||||
| 
 | ||||
| deepmerge@^4.2.2: | ||||
| deepmerge@^4.0.0, deepmerge@^4.2.2: | ||||
|   version "4.2.2" | ||||
|   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" | ||||
|   integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== | ||||
|  | @ -8040,6 +8040,17 @@ prismjs@1.18.0: | |||
|   optionalDependencies: | ||||
|     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: | ||||
|   version "2.0.1" | ||||
|   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" | ||||
|     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" | ||||
|   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" | ||||
|   integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== | ||||
|  | @ -9356,6 +9367,13 @@ stream-http@^2.7.2: | |||
|     to-arraybuffer "^1.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: | ||||
|   version "1.0.0" | ||||
|   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