From 9703ba53405b2f355c6e0317f714d82ff3d4dee3 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sun, 12 Jan 2020 16:40:58 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=A8?=
 =?UTF-8?q?=E7=94=BB=E5=83=8F=E8=AA=8D=E8=AD=98=E5=87=A6=E7=90=86=E3=81=AE?=
 =?UTF-8?q?=E6=94=B9=E5=96=84=20(#5690)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 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>
---
 .imgbotconfig                                 |   5 +
 package.json                                  |   1 +
 src/@types/probe-image-size.d.ts              |  27 +++
 src/misc/check-svg.ts                         |  12 --
 src/misc/detect-mine.ts                       |  31 ---
 ...{detect-url-mine.ts => detect-url-mime.ts} |   8 +-
 src/misc/get-file-info.ts                     | 201 ++++++++++++++++++
 src/server/api/endpoints/admin/emoji/add.ts   |   4 +-
 .../api/endpoints/admin/emoji/update.ts       |   4 +-
 src/server/file/send-drive-file.ts            |  12 +-
 src/server/proxy/proxy-media.ts               |  12 +-
 src/services/drive/add-file.ts                | 103 ++-------
 test/get-file-info.ts                         | 152 +++++++++++++
 test/resources/25000x25000.png                | Bin 0 -> 75933 bytes
 test/resources/anime.gif                      | Bin 0 -> 2248 bytes
 test/resources/anime.png                      | Bin 0 -> 1868 bytes
 test/resources/emptyfile                      |   0
 test/resources/with-alpha.png                 | Bin 0 -> 3772 bytes
 test/resources/with-xml-def.svg               | Bin 0 -> 544 bytes
 yarn.lock                                     |  36 +++-
 20 files changed, 454 insertions(+), 154 deletions(-)
 create mode 100644 .imgbotconfig
 create mode 100644 src/@types/probe-image-size.d.ts
 delete mode 100644 src/misc/check-svg.ts
 delete mode 100644 src/misc/detect-mine.ts
 rename src/misc/{detect-url-mine.ts => detect-url-mime.ts} (57%)
 create mode 100644 src/misc/get-file-info.ts
 create mode 100644 test/get-file-info.ts
 create mode 100644 test/resources/25000x25000.png
 create mode 100644 test/resources/anime.gif
 create mode 100644 test/resources/anime.png
 create mode 100644 test/resources/emptyfile
 create mode 100644 test/resources/with-alpha.png
 create mode 100644 test/resources/with-xml-def.svg

diff --git a/.imgbotconfig b/.imgbotconfig
new file mode 100644
index 0000000000..6a1dfe1ed3
--- /dev/null
+++ b/.imgbotconfig
@@ -0,0 +1,5 @@
+{
+	"ignoredFiles": [
+		"test/resources/*"
+	]
+}
diff --git a/package.json b/package.json
index 0f0dda57ad..d5b85fc8ca 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/@types/probe-image-size.d.ts b/src/@types/probe-image-size.d.ts
new file mode 100644
index 0000000000..665edcf2e7
--- /dev/null
+++ b/src/@types/probe-image-size.d.ts
@@ -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;
+}
diff --git a/src/misc/check-svg.ts b/src/misc/check-svg.ts
deleted file mode 100644
index 8ddeefede9..0000000000
--- a/src/misc/check-svg.ts
+++ /dev/null
@@ -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;
-	}
-}
diff --git a/src/misc/detect-mine.ts b/src/misc/detect-mine.ts
deleted file mode 100644
index f47f127353..0000000000
--- a/src/misc/detect-mine.ts
+++ /dev/null
@@ -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]);
-			});
-	});
-}
diff --git a/src/misc/detect-url-mine.ts b/src/misc/detect-url-mime.ts
similarity index 57%
rename from src/misc/detect-url-mine.ts
rename to src/misc/detect-url-mime.ts
index eef64cfc56..8d71cd0137 100644
--- a/src/misc/detect-url-mine.ts
+++ b/src/misc/detect-url-mime.ts
@@ -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();
 	}
diff --git a/src/misc/get-file-info.ts b/src/misc/get-file-info.ts
new file mode 100644
index 0000000000..5ccb280260
--- /dev/null
+++ b/src/misc/get-file-info.ts
@@ -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];
+	}
+}
diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts
index 5345876da8..3a17760e53 100644
--- a/src/server/api/endpoints/admin/emoji/add.ts
+++ b/src/server/api/endpoints/admin/emoji/add.ts
@@ -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,
diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts
index f4a01a3976..0651b8d283 100644
--- a/src/server/api/endpoints/admin/emoji/update.ts
+++ b/src/server/api/endpoints/admin/emoji/update.ts
@@ -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(),
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index 2283435794..0b14378589 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -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,
 					};
 				};
 
diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts
index 232b7a09cd..6b90e99921 100644
--- a/src/server/proxy/proxy-media.ts
+++ b/src/server/proxy/proxy-media.ts
@@ -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,
 			};
 		}
 
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 350e4dfe19..7cc710c8b6 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -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} = {};
-
-	let propPromises: Promise<void>[] = [];
-
-	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...');
+	const properties: {
+		width?: number;
+		height?: number;
+		avgColor?: string;
+	} = {};
 
-			// 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) { }
-		};
+	if (info.width) {
+		properties['width'] = info.width;
+		properties['height'] = info.height;
+	}
 
-		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}`);
diff --git a/test/get-file-info.ts b/test/get-file-info.ts
new file mode 100644
index 0000000000..920df07382
--- /dev/null
+++ b/test/get-file-info.ts
@@ -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
+		});
+	}));
+});
diff --git a/test/resources/25000x25000.png b/test/resources/25000x25000.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ed4666925f715eca6fdd9831206ec478b304047
GIT binary patch
literal 75933
zcmeI&y-HhQ0LS6?cp^w3Q3pW{loTAi19gy^5<dbWp(JZ#F2HLr7#s>6yo);7$+;8+
za|eD*p>uJ_kV$%)H+S*EbKrb%&T{_G{QK5EJFb*B%OQkH>!kT5g#K+jvwZeu@?r1m
z?3TArJBLM4{CKF1Lbly)H5;AvjlaLMZ1!<*^Y7=|(!%V?L4W`O0t5&UAV7cs0RjXF
z5FkK+009C72oNAZfB*pk1PCN8FnAf}F$M_r;_0Z}br2vxfB*pk1PBlyK!5-N0t5&U
zAV7cs0RjXF5FkK+009E23-p((rPu-t-g~u1>SO#r0RjXF5FkK+009C72oNAZfB*pk
z1PBlyK!5-N0t5&UAke+u-(86<K$u)!lz0#zK!5-N0t5&UAV7cs0RjXF5FkK+009C7
z2oNAZfB*pk$qTGa#|QHkU{u_%c9NfC009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs
z0RjXF5Fqe8%(EB+)QhL1HVy&=2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkJx
zd4c|NwG>-`!F#XPNPdk01PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfWY<s
z?n-O{!sPOzgo6M90t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBmFUSMrHKA5)v
zqvC$Gll&Y52oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D;$Gz7S)8k40~1
lYhDGo5FkK+009DN3w&SvbrQ12Pmh1%j@IE>^X~Kc)qni7KGgsK

literal 0
HcmV?d00001

diff --git a/test/resources/anime.gif b/test/resources/anime.gif
new file mode 100644
index 0000000000000000000000000000000000000000..256ba495ce11b5f1ec7649c739feb750844eb4df
GIT binary patch
literal 2248
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
W2~9|w)0}XH4ipRvSICP30028Jj!=I9

literal 0
HcmV?d00001

diff --git a/test/resources/anime.png b/test/resources/anime.png
new file mode 100644
index 0000000000000000000000000000000000000000..f13600f7a49951d0bd67cd0c8c34c06379b6a4cf
GIT binary patch
literal 1868
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
MpCIp+qtr{k0MoP{oB#j-

literal 0
HcmV?d00001

diff --git a/test/resources/emptyfile b/test/resources/emptyfile
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/resources/with-alpha.png b/test/resources/with-alpha.png
new file mode 100644
index 0000000000000000000000000000000000000000..adc8d01805dbcf1bf38f506096b99bc157f25de5
GIT binary patch
literal 3772
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
Gh5idy2NOjA

literal 0
HcmV?d00001

diff --git a/test/resources/with-xml-def.svg b/test/resources/with-xml-def.svg
new file mode 100644
index 0000000000000000000000000000000000000000..90971215a64a6ad9e0e48aed2d72c98a881dcf70
GIT binary patch
literal 544
zcmcJM!ES>v42JK03d=hOoYYI*iBPT8xa}b(N+}Y9sxau|&!Ic@vR!tFtvLSe&rY_x
zeYP-F*P-dVfHon7sw?|r)71gL#*-1;<*RI%YS7GX2zVaH%S)Qh^PJ4<q`%gwBcfC@
z5T>S@_x&v(0fBq=@nD`^KE^Ygnzjx2R1{3FjBpJ2Zk>QoX{-k}P8Ew~nXE4f#0Xfg
z@nj7GtaeF>Uav9gcyMGK>w{61)+tFv5(`F|k%RRN!eE(U<Kf;A(?_JeLj53Z-G3vw
zICW%}1!-ych{PmsMR7-p6Z~yS2d!*34p^NKsL*cw?<*qRb?gywmS&MAerUe>1{+L#
AmH+?%

literal 0
HcmV?d00001

diff --git a/yarn.lock b/yarn.lock
index d6208cc76b..764d48cbec 100644
--- a/yarn.lock
+++ b/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"
-- 
GitLab