Browse Source

comment and refactor

mrkvon 3 years ago
parent
commit
6f76849e06
4 changed files with 91 additions and 49 deletions
  1. 81
    34
      controllers/avatar.js
  2. 4
    0
      controllers/deserialize.js
  3. 3
    10
      jobs/files.js
  4. 3
    5
      test/avatar.js

+ 81
- 34
controllers/avatar.js View File

@@ -1,3 +1,7 @@
1
+/**
2
+ * CRUD avatar image (middlewares)
3
+ */
4
+
1 5
 const sharp = require('sharp'),
2 6
       fs = require('fs-extra'),
3 7
       path = require('path'),
@@ -10,56 +14,44 @@ const upload = multer({ dest: 'uploads/' });
10 14
 
11 15
 const models = require(path.resolve('./models'));
12 16
 
17
+/**
18
+ * GET user's avatar.
19
+ * Will return either image uploaded by user (cropped and formatted to jpeg),
20
+ * or default svg
21
+ */
13 22
 async function get(req, res, next) {
14 23
   const { username } = req.params;
15 24
 
16 25
   const usernameExists = await models.user.exists(username);
17 26
 
27
+  // go to 404 if user was not found
28
+  // TODO perhaps return the error response here, with more specific message
18 29
   if (usernameExists !== true) {
19 30
     return next();
20 31
   }
21 32
 
22
-  const size = _.get(req, 'query.filter.size', 512);
33
+  // size of avatar (square side length in pixels)
34
+  const size = _.get(req, 'query.filter.size', 128);
23 35
 
24 36
   const avatarPath = path.resolve(`./files/avatars/${username}/${size}`);
25 37
 
38
+  // serve the avatar uploaded by user
26 39
   res.sendFile(avatarPath, { headers: { 'content-type': 'image/jpeg'} }, async (err) => {
27 40
 
28 41
     if (err) {
29
-      if (err.code === 'ENOENT') {
30 42
 
31
-        const defaultPath = path.resolve(`./files/avatars/${username}/default`);
32
-        const defaultFolderPath = path.resolve(`./files/avatars/${username}`);
43
+      // if uploaded avatar not found, serve the default avatar
44
+      if (err.code === 'ENOENT') {
33 45
 
34
-        // try to find default on disk
46
+        // if default avatar doesn't exist, save it on disk.
35 47
         try {
36
-          await fs.stat(defaultPath);
48
+          await createDefaultAvatarIfNotExist(username);
37 49
         } catch (e) {
38
-
39
-          /*
40
-           * default file not present
41
-           */
42
-          if (e.code !== 'ENOENT') {
43
-            return next(e);
44
-          }
45
-
46
-          const icon = identicon(username);
47
-
48
-          try {
49
-            // create folder if not exist
50
-            try {
51
-              await fs.mkdir(defaultFolderPath);
52
-            } catch (e) {
53
-              if (e.code !== 'EEXIST') throw e;
54
-            }
55
-
56
-            await fs.writeFile(defaultPath, icon);
57
-          } catch (e) {
58
-            return next(err);
59
-          }
50
+          return next(e);
60 51
         }
61 52
 
62 53
         // serve the default from disk
54
+        const defaultPath = getDefaultAvatarPath(username);
63 55
         return res.sendFile(defaultPath, { headers: { 'content-type': 'image/svg+xml' } }, err => { if (err) return next(err); });
64 56
       }
65 57
       return next(err);
@@ -67,6 +59,45 @@ async function get(req, res, next) {
67 59
   });
68 60
 }
69 61
 
62
+function getDefaultAvatarPath(username) {
63
+  return path.resolve(`./files/avatars/${username}/default`);
64
+}
65
+
66
+async function createDefaultAvatarIfNotExist(username) {
67
+  const defaultPath = getDefaultAvatarPath(username);
68
+  const defaultFolderPath = path.resolve(`./files/avatars/${username}`);
69
+
70
+  // try to find default on disk
71
+  try {
72
+    await fs.stat(defaultPath);
73
+  } catch (e) {
74
+
75
+    // throw unexpected error
76
+    if (e.code !== 'ENOENT') {
77
+      throw e;
78
+    }
79
+
80
+    // generate the default avatar, identicon
81
+    const icon = identicon(username);
82
+
83
+    // create folder for user's avatars if it doesn't exist already
84
+    try {
85
+      await fs.mkdir(defaultFolderPath);
86
+    } catch (e) {
87
+      if (e.code !== 'EEXIST') throw e;
88
+    }
89
+
90
+    // save the default user's avatar on disk.
91
+    await fs.writeFile(defaultPath, icon);
92
+  }
93
+}
94
+
95
+/**
96
+ * Generate identicon for a given username
97
+ *
98
+ * @param {string} username - who is the avatar for
99
+ * @returns {string} - a generated svg image
100
+ */
70 101
 function identicon(username) {
71 102
   const hash = crypto.createHash('sha256').update(username).digest('hex');
72 103
 
@@ -83,31 +114,40 @@ function identicon(username) {
83 114
   }
84 115
 }
85 116
 
117
+/**
118
+ * PATCH - update the avatar of a user
119
+ */
86 120
 async function patch(req, res, next) {
87 121
   const temporaryFilePath = path.resolve(`./${req.file.path}`);
88 122
   const { username } = req.auth;
123
+
89 124
   // sizes of the profile pictures
90 125
   const sizes = [512, 256, 128, 64, 32, 16];
91 126
 
92 127
   try {
93
-    // read the uploaded file
128
+    // read the uploaded file from a temporary folder
94 129
     const buffer = await fs.readFile(temporaryFilePath);
95 130
 
131
+    // resize the uploaded image to expected sizes and save
96 132
     const imgPromises = sizes.map(size => resizeSaveAvatar(buffer, size, username));
97 133
     await Promise.all(imgPromises);
98 134
 
99
-    // remove the temporary file
100
-    await fs.unlink(temporaryFilePath);
101
-
102 135
     return res.status(204).end();
103 136
   } catch (e) {
137
+    return next(e);
138
+  } finally {
104 139
     // remove the temporary file
105 140
     await fs.unlink(temporaryFilePath);
106
-
107
-    return next(e);
108 141
   }
109 142
 }
110 143
 
144
+/**
145
+ * Resize provided image and save it.
146
+ * @param {Buffer} buffer - the uploaded image
147
+ * @param {number} size - the final size of the image (square side) in pixels
148
+ * @param {string} username - user to save the avatar to
149
+ * @returns {Promise<info>} info returned by sharp library
150
+ */
111 151
 async function resizeSaveAvatar(buffer, size, username) {
112 152
   // create the user directory if not exist
113 153
   try {
@@ -126,8 +166,15 @@ async function resizeSaveAvatar(buffer, size, username) {
126 166
   return info;
127 167
 }
128 168
 
169
+/**
170
+ * Parse the uploaded multipart/form-data image (multer library)
171
+ */
129 172
 const parseAvatar = upload.single('avatar');
130 173
 
174
+/**
175
+ * If any error is encountered during validation etc., remove the temporary file
176
+ * from the temporary folder
177
+ */
131 178
 async function removeTemporaryFileOnError(err, req, res, next) {
132 179
   const temporaryFilePath = path.resolve(`./${req.file.path}`);
133 180
   await fs.unlink(temporaryFilePath);

+ 4
- 0
controllers/deserialize.js View File

@@ -9,6 +9,10 @@ const router = express.Router();
9 9
 
10 10
 const serializers = require(path.resolve('./serializers'));
11 11
 
12
+/**
13
+ * Make sure we don't try to deserialize multipart/form-data requests
14
+ * Especially useful for avatar and other file uploads
15
+ */
12 16
 function notMultipart(req, res, next) {
13 17
   // if the request is multipart/form-data, don't deserialize.
14 18
   const isMultipart = /^multipart\/form-data/.test(req.headers['content-type']);

+ 3
- 10
jobs/files.js View File

@@ -1,14 +1,7 @@
1 1
 'use strict';
2 2
 
3 3
 const path = require('path'),
4
-      { promisify } = require('util'),
5
-      rimraf = promisify(require('rimraf')),
6
-      fs = require('fs');
7
-
8
-// fs promisified
9
-const fsp = {
10
-  mkdir: promisify(fs.mkdir)
11
-};
4
+      fs = require('fs-extra');
12 5
 
13 6
 /**
14 7
  * clear the ./uploads folder (temporary files for uploading avatar)
@@ -19,9 +12,9 @@ async function clearTemporary() {
19 12
   const temp = path.resolve('./uploads');
20 13
 
21 14
   // rm -rf the temp folder
22
-  await rimraf(temp);
15
+  await fs.remove(temp);
23 16
   // recreate the temp folder
24
-  await fsp.mkdir(temp);
17
+  await fs.mkdir(temp);
25 18
 }
26 19
 
27 20
 module.exports = { clearTemporary };

+ 3
- 5
test/avatar.js View File

@@ -58,10 +58,6 @@ describe('/users/:username/avatar', function () {
58 58
     [loggedUser, otherUser] = dbData.users;
59 59
   });
60 60
 
61
-  describe('create default image identicon on email confirmation', () => {
62
-    it('create the svg image');
63
-  });
64
-
65 61
   describe('GET', function () {
66 62
 
67 63
     context('logged', function () {
@@ -302,7 +298,7 @@ describe('/users/:username/avatar', function () {
302 298
 
303 299
     });
304 300
 
305
-    describe('job: regularly clear all temp files older than 1 minute', () => {
301
+    describe('job: regularly clear stale temporary upload files', () => {
306 302
       it('should leave the /uploads folder empty', async () => {
307 303
         const uploads = path.resolve('./uploads');
308 304
 
@@ -320,6 +316,8 @@ describe('/users/:username/avatar', function () {
320 316
         const filesAfter = await fs.readdir(uploads);
321 317
         should(filesAfter).Array().length(0);
322 318
       });
319
+
320
+      it('should leave the files less than 1 minute old');
323 321
     });
324 322
 
325 323
   });