From 3e897727ca7c8b0b5ba11c9d1866dc87ea136c22 Mon Sep 17 00:00:00 2001
From: sei0o inoue <sei0okun@gmail.com>
Date: Sun, 7 Oct 2018 16:51:46 +0900
Subject: [PATCH] Fix #2773 (#2841)

* Added an API endpoint to check the existence of the file

* fix #2773: Now we can prevent users from posting the same images

* bug fix
---
 src/client/app/common/scripts/get-md5.ts      |  8 ++
 .../app/common/views/components/uploader.vue  | 94 ++++++++++++-------
 .../endpoints/drive/files/check_existence.ts  | 38 ++++++++
 3 files changed, 106 insertions(+), 34 deletions(-)
 create mode 100644 src/client/app/common/scripts/get-md5.ts
 create mode 100644 src/server/api/endpoints/drive/files/check_existence.ts

diff --git a/src/client/app/common/scripts/get-md5.ts b/src/client/app/common/scripts/get-md5.ts
new file mode 100644
index 0000000000..24ac04c1ad
--- /dev/null
+++ b/src/client/app/common/scripts/get-md5.ts
@@ -0,0 +1,8 @@
+const crypto = require('crypto');
+
+export default (data: ArrayBuffer) => {
+  const buf = new Buffer(data);
+  const hash = crypto.createHash("md5");
+  hash.update(buf);
+  return hash.digest("hex");
+};
\ No newline at end of file
diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
index 19b0d15708..fed6477c05 100644
--- a/src/client/app/common/views/components/uploader.vue
+++ b/src/client/app/common/views/components/uploader.vue
@@ -20,6 +20,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import { apiUrl } from '../../../config';
+import getMD5 from '../../scripts/get-md5';
 
 export default Vue.extend({
 	data() {
@@ -28,53 +29,78 @@ export default Vue.extend({
 		};
 	},
 	methods: {
-		upload(file, folder) {
+		checkExistence(fileData: ArrayBuffer): Promise<any> {
+			return new Promise((resolve, reject) => {
+				const data = new FormData();
+				data.append('md5', getMD5(fileData));
+
+				(this as any).api('drive/files/check_existence', {
+					md5: getMD5(fileData)
+				}).then(resp => {
+					resolve(resp.file);
+				});
+			});
+		},
+
+		upload(file: File, folder: any) {
 			if (folder && typeof folder == 'object') folder = folder.id;
 
 			const id = Math.random();
 
-			const ctx = {
-				id: id,
-				name: file.name || 'untitled',
-				progress: undefined,
-				img: undefined
-			};
-
-			this.uploads.push(ctx);
-			this.$emit('change', this.uploads);
-
 			const reader = new FileReader();
 			reader.onload = (e: any) => {
-				ctx.img = e.target.result;
-			};
-			reader.readAsDataURL(file);
+				this.checkExistence(e.target.result).then(result => {
+					console.log(result);
+					if (result !== null) {
+						this.$emit('uploaded', result);
+						return;
+					}
 
-			const data = new FormData();
-			data.append('i', this.$store.state.i.token);
-			data.append('file', file);
+					// Upload if the file didn't exist yet
+					const buf = new Uint8Array(e.target.result);
+					let bin = "";
+					// We use for-of loop instead of apply() to avoid RangeError
+					// SEE: https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string
+					for (const byte of buf) bin += String.fromCharCode(byte);
+					const ctx = {
+						id: id,
+						name: file.name || 'untitled',
+						progress: undefined,
+						img: 'data:*/*;base64,' + btoa(bin)
+					};
 
-			if (folder) data.append('folderId', folder);
+					this.uploads.push(ctx);
+					this.$emit('change', this.uploads);
 
-			const xhr = new XMLHttpRequest();
-			xhr.open('POST', apiUrl + '/drive/files/create', true);
-			xhr.onload = (e: any) => {
-				const driveFile = JSON.parse(e.target.response);
+					const data = new FormData();
+					data.append('i', this.$store.state.i.token);
+					data.append('file', file);
 
-				this.$emit('uploaded', driveFile);
+					if (folder) data.append('folderId', folder);
 
-				this.uploads = this.uploads.filter(x => x.id != id);
-				this.$emit('change', this.uploads);
-			};
+					const xhr = new XMLHttpRequest();
+					xhr.open('POST', apiUrl + '/drive/files/create', true);
+					xhr.onload = (e: any) => {
+						const driveFile = JSON.parse(e.target.response);
 
-			xhr.upload.onprogress = e => {
-				if (e.lengthComputable) {
-					if (ctx.progress == undefined) ctx.progress = {};
-					ctx.progress.max = e.total;
-					ctx.progress.value = e.loaded;
-				}
-			};
+						this.$emit('uploaded', driveFile);
 
-			xhr.send(data);
+						this.uploads = this.uploads.filter(x => x.id != id);
+						this.$emit('change', this.uploads);
+					};
+
+					xhr.upload.onprogress = e => {
+						if (e.lengthComputable) {
+							if (ctx.progress == undefined) ctx.progress = {};
+							ctx.progress.max = e.total;
+							ctx.progress.value = e.loaded;
+						}
+					};
+
+					xhr.send(data);
+				})
+			}
+			reader.readAsArrayBuffer(file);
 		}
 	}
 });
diff --git a/src/server/api/endpoints/drive/files/check_existence.ts b/src/server/api/endpoints/drive/files/check_existence.ts
new file mode 100644
index 0000000000..73d75b7caf
--- /dev/null
+++ b/src/server/api/endpoints/drive/files/check_existence.ts
@@ -0,0 +1,38 @@
+import $ from 'cafy';
+import DriveFile, { pack } from '../../../../../models/drive-file';
+import { ILocalUser } from '../../../../../models/user';
+
+export const meta = {
+	desc: {
+		'ja-JP': '与えられたMD5ハッシュ値を持つファイルがドライブに存在するかどうかを返します。',
+		'en-US': 'Returns whether the file with the given MD5 hash exists in the user\'s drive.'
+	},
+
+	requireCredential: true,
+
+	kind: 'drive-read',
+
+	params: {
+		md5: $.str.note({
+			desc: {
+				'ja-JP': 'ファイルのMD5ハッシュ'
+			}
+		})
+	}
+};
+
+export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+	const [md5, md5Err] = $.str.get(params.md5);
+	if (md5Err) return rej('invalid md5 param');
+
+	const file = await DriveFile.findOne({
+		md5: md5,
+		'metadata.userId': user._id
+	});
+
+	if (file === null) {
+		res({ file: null });
+	} else {
+		res({ file: await pack(file) });
+	}
+});