ソースを参照

Store picture as blob not base64

sbkwgh 7 年 前
コミット
b36ace062e

+ 24 - 0
migrations/20171208231827-create-profile-picture-table.js

@@ -0,0 +1,24 @@
+'use strict';
+
+module.exports = {
+  up: (queryInterface, Sequelize) => {
+    return queryInterface.createTable('profilepictures', {
+      id: {
+        type: Sequelize.INTEGER,
+        primaryKey: true,
+        autoIncrement: true
+      },
+      createdAt: Sequelize.DATE,
+      updatedAt: Sequelize.DATE,
+      
+      file: Sequelize.BLOB('long'),
+      UserId: Sequelize.INTEGER
+    }, {
+      charset: 'utf8mb4'
+    })
+  },
+
+  down: (queryInterface, Sequelize) => {
+    return queryInterface.dropTable('profilepictures');
+  }
+};

+ 13 - 0
migrations/20171208235706-add-profile-picture-column.js

@@ -0,0 +1,13 @@
+'use strict';
+
+module.exports = {
+  up: (queryInterface, Sequelize) => {
+    return queryInterface.addColumn('profilepictures', 'mimetype', {
+      type: Sequelize.STRING
+    })
+  },
+
+  down: (queryInterface, Sequelize) => {
+    return queryInterface.dropColumn('profilepictures', 'mimetype')
+  }
+};

+ 14 - 0
models/picture.js

@@ -0,0 +1,14 @@
+module.exports = (sequelize, DataTypes) => {
+	let ProfilePicture = sequelize.define('ProfilePicture', {
+		file: DataTypes.BLOB('long'),
+		mimetype: DataTypes.STRING
+	}, {
+		classMethods: {
+			associate (models) {
+				ProfilePicture.belongsTo(models.User)
+			}
+		}
+	})
+
+	return ProfilePicture
+}

+ 0 - 7
models/user.js

@@ -67,13 +67,6 @@ module.exports = (sequelize, DataTypes) => {
 					if(typeof val !== 'string') {
 						throw new sequelize.ValidationError('password must be a string')
 					}
-				},
-				isValidBase64OrNull (val) {
-					let base64Regexp = /^data:image\/(png|jpeg|jpg|gif);base64,[A-Za-z0-9+\/=]+$/g
-
-					if(!val.match(base64Regexp) && val !== null) {
-						throw new sequelize.ValidationError('image must be valid base64')
-					}
 				}
 			}
 		}

+ 111 - 7
package-lock.json

@@ -43,6 +43,11 @@
       "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz",
       "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8="
     },
+    "append-field": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-0.1.0.tgz",
+      "integrity": "sha1-bdxY+gg8e8VF08WZWygwzCNm1Eo="
+    },
     "archy": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
@@ -214,6 +219,38 @@
       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
       "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
     },
+    "busboy": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
+      "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
+      "requires": {
+        "dicer": "0.2.5",
+        "readable-stream": "1.1.14"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "0.0.1",
+            "string_decoder": "0.10.31"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "bytes": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@@ -401,6 +438,16 @@
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
     },
+    "concat-stream": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
+      "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
+      "requires": {
+        "inherits": "2.0.3",
+        "readable-stream": "2.3.3",
+        "typedarray": "0.0.6"
+      }
+    },
     "config-chain": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz",
@@ -550,6 +597,38 @@
         "fs-exists-sync": "0.1.0"
       }
     },
+    "dicer": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+      "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
+      "requires": {
+        "readable-stream": "1.1.14",
+        "streamsearch": "0.1.2"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "1.0.2",
+            "inherits": "2.0.3",
+            "isarray": "0.0.1",
+            "string_decoder": "0.10.31"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "diff": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
@@ -2188,6 +2267,21 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
     },
+    "multer": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.3.0.tgz",
+      "integrity": "sha1-CSsmcPaEb6SRSWXvyM+Uwg/sbNI=",
+      "requires": {
+        "append-field": "0.1.0",
+        "busboy": "0.2.14",
+        "concat-stream": "1.6.0",
+        "mkdirp": "0.5.1",
+        "object-assign": "3.0.0",
+        "on-finished": "2.3.0",
+        "type-is": "1.6.15",
+        "xtend": "4.0.1"
+      }
+    },
     "multipipe": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
@@ -3107,13 +3201,10 @@
       "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz",
       "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8="
     },
-    "string_decoder": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-      "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
-      "requires": {
-        "safe-buffer": "5.1.1"
-      }
+    "streamsearch": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+      "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
     },
     "string-width": {
       "version": "2.1.1",
@@ -3144,6 +3235,14 @@
         }
       }
     },
+    "string_decoder": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+      "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
     "strip-ansi": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@@ -3269,6 +3368,11 @@
         "mime-types": "2.1.17"
       }
     },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
     "uid-safe": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
     "express-session": "^1.15.1",
     "highlight.js": "^9.10.0",
     "marked": "^0.3.6",
+    "multer": "^1.3.0",
     "mysql": "^2.13.0",
     "mysql2": "^1.4.2",
     "randomcolor": "^0.4.4",

+ 95 - 3
routes/user.js

@@ -1,9 +1,12 @@
 let bcrypt = require('bcryptjs')
+let multer = require('multer')
 let express = require('express')
 let router = express.Router()
 
 const Errors = require('../lib/errors.js')
-let { User, Post, AdminToken, Thread, Category, Sequelize, Ip, Ban } = require('../models')
+let {
+	User, Post, ProfilePicture, AdminToken, Thread, Category, Sequelize, Ip, Ban
+} = require('../models')
 let pagination = require('../lib/pagination.js')
 
 function setUserSession(req, res, username, UserId, admin) {
@@ -172,6 +175,41 @@ router.post('/:username/logout', async (req, res) => {
 	})
 })
 
+router.get('/:username/picture', async (req, res) => {
+	try {
+		let user = await User.findOne({
+			where: {
+				username: req.params.username
+			}
+		})
+		if(!user) throw Errors.accountDoesNotExist
+
+		let picture = await ProfilePicture.findOne({
+			where: {
+				UserId: user.id
+			}
+		})
+
+		res.writeHead(200, {
+			'Content-Type': picture.mimetype,
+			'Content-disposition': 'attachment;filename=profile',
+			'Content-Length': picture.file.length
+		});
+		res.end(new Buffer(picture.file, 'binary'));
+	} catch (e) {
+		if(err === Errors.accountDoesNotExist) {
+			res.status(400)
+			res.json({ errors: [err] })
+		} else {
+			console.log(err)
+			res.status(500)
+			res.json({
+				errors: [Errors.unknown]
+			})
+		}
+	}
+})
+
 router.all('*', (req, res, next) => {
 	if(req.session.username) {
 		next()
@@ -183,13 +221,32 @@ router.all('*', (req, res, next) => {
 	}
 })
 
-router.post('/:username/picture', async (req, res) => {
+let upload = multer({ storage: multer.memoryStorage() })
+router.post('/:username/picture', upload.single('picture'), async (req, res) => {
 	try {
 		if(req.session.username !== req.params.username) {
 			throw Errors.requestNotAuthorized
 		} else {
 			let user = await User.findById(req.session.UserId)
-			await user.update({ picture: req.body.picture })
+			let picture = await ProfilePicture.findOne({
+				where: { UserId: user.id}
+			})
+
+			let pictureObj = {
+				file: req.file.buffer,
+				mimetype: req.file.mimetype
+			}
+			
+			//No picture set yet
+			if(!picture) {
+				picture = await ProfilePicture.create(pictureObj)
+				await picture.setUser(user)
+				await user.update({
+					picture: '/api/v1/user/' + req.session.username + '/picture'
+				})
+			} else {
+				await ProfilePicture.update(pictureObj)
+			}
 
 			res.json(user.toJSON())
 		}
@@ -213,6 +270,41 @@ router.post('/:username/picture', async (req, res) => {
 	}
 })
 
+router.delete('/:username/picture', async (req, res) => {
+	try {
+		if(req.session.username !== req.params.username) {
+			throw Errors.requestNotAuthorized
+		} else {
+			let user = await User.findById(req.session.UserId)
+			let picture = await ProfilePicture.findOne({
+				where: { UserId: user.id}
+			})
+
+			await user.update({
+				picture: null
+			})
+			await picture.destroy()
+
+			res.json(user.toJSON())
+		}
+	} catch (e) {
+		if(e === Errors.requestNotAuthorized) {
+			res.status(401)
+			res.json({
+				errors: [e]
+			})
+		} else {
+			console.log(e)
+
+			res.status(500)
+			res.json({
+				errors: [Errors.unknown]
+			})
+		}
+	}
+})
+
+
 router.put('/:username', async (req, res) => {
 	try {
 		if(req.session.username !== req.params.username) {