Skip to content

Commit b31fbc0

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 32ed20a + b4ccecf commit b31fbc0

14 files changed

Lines changed: 390 additions & 8 deletions

File tree

config.sample.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"userMaxAge": "3 months"
66
},
77
"maximumFormSize": "1 MiB",
8+
"maximumThumbnailSize": "4 MiB",
89
"password": {
910
"bcryptRounds": 12
1011
},

controllers/submissions.js

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
11
'use strict';
22

33
var Promise = require('promise');
4+
var util = require('util');
45
var querystring = require('querystring');
56
var formSession = require('../lib/form-session');
67
var Redirect = require('../lib/respond').Redirect;
78
var view = require('../lib/view').view;
9+
var unit = require('../lib/unit');
10+
var slug = require('../lib/slug');
811
var notifications = require('../models/notifications');
912
var users = require('../models/users');
1013
var media = require('../models/media');
1114
var tags = require('../models/tags');
15+
var submissions = require('../models/submissions');
16+
var config = require('../config');
17+
18+
var maximumThumbnailSize = unit.ByteSize.parse(config.maximumThumbnailSize).bytes;
19+
20+
function toInteger(obj) {
21+
return obj | 0;
22+
}
23+
24+
function validId(n) {
25+
return n > 0;
26+
}
1227

1328
var uploadForm = view('submissions/upload', [users.ensure('submit'), media.listForRequester, notifications.counts]);
14-
var createForm = view('submissions/edit', [users.ensure('submit'), tags.mostCommonTagsForRequester, notifications.counts]);
29+
var createForm_ = view('submissions/edit', [users.ensure('submit'), tags.mostCommonTagsForRequester, notifications.counts]);
30+
31+
function createForm(request) {
32+
var submit = request.query.submit;
33+
var submitMedia = Array.isArray(submit) ? submit.map(toInteger) : [submit | 0];
34+
35+
if (!submitMedia.every(validId)) {
36+
return Promise.resolve(new Redirect('/submissions/new'));
37+
}
38+
39+
return createForm_.with({ media: submitMedia })(request);
40+
}
1541

1642
function upload(request) {
1743
return request.user.ensure('submit')
@@ -66,6 +92,113 @@ function upload(request) {
6692
);
6793
}
6894

95+
function create(request) {
96+
return request.user.ensure('submit').then(
97+
function () {
98+
return formSession.formFiles(request).then(function (form) {
99+
return new Promise(function (resolve) {
100+
var thumbnail = null;
101+
102+
form.on('file', function (name, file) {
103+
var stream = file.stream;
104+
105+
if (name === 'thumbnail') {
106+
var parts = [];
107+
var totalLength = 0;
108+
109+
stream.on('data', function addPart(part) {
110+
totalLength += part.length;
111+
112+
if (totalLength >= maximumThumbnailSize) {
113+
totalLength = 0;
114+
parts = null;
115+
stream.removeListener('data', addPart);
116+
return;
117+
}
118+
119+
parts.push(part);
120+
});
121+
122+
stream.on('end', function () {
123+
if (totalLength) {
124+
var data = Buffer.concat(parts, totalLength);
125+
126+
thumbnail = media.autoThumbnail(data);
127+
}
128+
});
129+
130+
return;
131+
}
132+
133+
stream.resume();
134+
});
135+
136+
form.on('finish', function () {
137+
var submit = form.fields.media;
138+
var submitMedia = Array.isArray(submit) ? submit.map(toInteger) : [submit | 0];
139+
140+
if (!submitMedia.every(validId)) {
141+
resolve(new Redirect('/submissions/new'));
142+
return;
143+
}
144+
145+
if (thumbnail) {
146+
thumbnail = thumbnail.then(function (thumbnailData) {
147+
return media.createUploadStream().then(function (thumbnailStream) {
148+
thumbnailStream.end(thumbnailData);
149+
150+
return thumbnailStream.uploaded;
151+
});
152+
});
153+
} else {
154+
thumbnail = Promise.resolve(null);
155+
}
156+
157+
resolve(Promise.all(submitMedia.map(function (mediaId) {
158+
return media.owns(request.user, mediaId);
159+
})).then(function (ownsMedia) {
160+
if (!ownsMedia.every(Boolean)) {
161+
return Promise.reject(new Error('A specified media entry does not exist or is not available to the user.'));
162+
}
163+
164+
return thumbnail.then(function (thumbnailId) {
165+
return submissions.create({
166+
owner: request.user.id,
167+
thumbnail: thumbnailId,
168+
title: form.fields.title,
169+
description: form.fields.description,
170+
rating: form.fields.rating,
171+
tags: form.fields.tags
172+
}).then(function (submissionId) {
173+
return Promise.all(submitMedia.map(function (mediaId) {
174+
return media.associateWithSubmission(mediaId, submissionId);
175+
})).then(function () {
176+
return new Redirect(util.format('/submissions/%d/%s', submissionId, slug.slugFor(form.fields.title)));
177+
});
178+
});
179+
});
180+
}));
181+
});
182+
});
183+
});
184+
},
185+
function () {
186+
return formSession.formFiles(request).then(function (form) {
187+
return new Promise(function (resolve) {
188+
form.on('file', function (file) {
189+
file.stream.resume();
190+
});
191+
192+
form.on('finish', function () {
193+
resolve(new Redirect('/login?return_to=/submissions/new%3Fsubmit%3D' + encodeURIComponent(form.fields.user_media)));
194+
});
195+
});
196+
});
197+
}
198+
);
199+
}
200+
69201
exports.uploadForm = uploadForm;
70202
exports.createForm = createForm;
71203
exports.upload = upload;
204+
exports.create = create;

lib/db.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,33 @@ function query() {
3333
});
3434
}
3535

36+
function useClient() {
37+
return new Promise(function (resolve, reject) {
38+
pg.connect(connectionString, function (error, client, done) {
39+
if (error) {
40+
reject(error);
41+
return;
42+
}
43+
44+
resolve({
45+
query: function query(queryString, parameters) {
46+
return new Promise(function (resolve, reject) {
47+
client.query(queryString, parameters, function (error, result) {
48+
if (error) {
49+
reject(error);
50+
return;
51+
}
52+
53+
resolve(result);
54+
});
55+
});
56+
},
57+
done: done
58+
});
59+
});
60+
});
61+
}
62+
3663
exports.query = query;
64+
exports.useClient = useClient;
3765
exports.pg = pg;

lib/slug.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
3+
var unorm = require('unorm');
4+
5+
var AMPERSAND = /&/g;
6+
var SLUG_PART = /[a-z0-9]+/g;
7+
8+
function slugFor(text) {
9+
return unorm.nfd(text.toLowerCase().replace(AMPERSAND, ' and ')).match(SLUG_PART).join('-');
10+
}
11+
12+
exports.slugFor = slugFor;

migrations/current

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
portfolios
1+
submission-thumbnails

migrations/submission-media.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
var Promise = require('promise');
4+
5+
exports.base = 'portfolios';
6+
7+
exports.up = function (query) {
8+
return Promise.resolve()
9+
.then(query(
10+
"CREATE TABLE submission_media (\
11+
submission INTEGER NOT NULL REFERENCES submissions (id),\
12+
media INTEGER NOT NULL REFERENCES media (id)\
13+
)"
14+
));
15+
};
16+
17+
exports.down = function (query) {
18+
return Promise.resolve()
19+
.then(query("DROP TABLE submission_media"));
20+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
3+
var Promise = require('promise');
4+
5+
exports.base = 'submission-media';
6+
7+
exports.up = function (query) {
8+
return Promise.resolve()
9+
.then(query(
10+
"ALTER TABLE submissions ADD thumbnail INTEGER REFERENCES media (id)"
11+
));
12+
};
13+
14+
exports.down = function (query) {
15+
return Promise.resolve()
16+
.then(query("ALTER TABLE submissions DROP thumbnail"));
17+
};

models/media.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var crypto = require('crypto');
55
var stream = require('stream');
66
var fs = require('fs');
77
var path = require('path');
8+
var sharp = require('sharp');
89
var db = require('../lib/db');
910
var config = require('../config');
1011

@@ -17,6 +18,14 @@ function const_(obj) {
1718
};
1819
}
1920

21+
function autoThumbnail(imageBuffer) {
22+
return sharp(imageBuffer).resize(300, 300).max().png();
23+
}
24+
25+
function associateWithSubmission(mediaId, submissionId) {
26+
return db.query('INSERT INTO submission_media (media, submission) VALUES ($1, $2)', [mediaId, submissionId]);
27+
}
28+
2029
function createUploadStream() {
2130
return new Promise(function (resolve, reject) {
2231
crypto.randomBytes(15, function (error, bytes) {
@@ -89,6 +98,15 @@ function associate(user, mediaId, filename) {
8998
.then(mediaId_, mediaId_);
9099
}
91100

101+
function owns(user, mediaId) {
102+
return db.query(
103+
'SELECT COUNT(*)::int AS count FROM user_media WHERE user_media.owner = $1 AND user_media.media = $2',
104+
[user.id, mediaId]
105+
).then(function (result) {
106+
return result.rows[0].count === 1;
107+
});
108+
}
109+
92110
function listFor(user) {
93111
return db.query(
94112
'SELECT media.id, media.hash, media.type, media.file_size, media.width, media.height, user_media.media, user_media.name FROM media INNER JOIN user_media ON media.id = user_media.media WHERE user_media.owner = $1',
@@ -108,7 +126,10 @@ function listForRequester(request) {
108126
});
109127
}
110128

129+
exports.autoThumbnail = autoThumbnail;
111130
exports.createUploadStream = createUploadStream;
112131
exports.associate = associate;
132+
exports.associateWithSubmission = associateWithSubmission;
133+
exports.owns = owns;
113134
exports.listFor = listFor;
114135
exports.listForRequester = listForRequester;

0 commit comments

Comments
 (0)