UNPKG

7.98 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2015. All Rights Reserved.
2// Node module: loopback-component-storage
3// This file is licensed under the Artistic License 2.0.
4// License text available at https://opensource.org/licenses/Artistic-2.0
5'use strict';
6
7// Globalization
8var g = require('strong-globalize')();
9
10var IncomingForm = require('formidable');
11var StringDecoder = require('string_decoder').StringDecoder;
12var path = require('path');
13
14var defaultOptions = {
15 maxFileSize: 10 * 1024 * 1024, // 10 MB
16};
17
18/**
19 * Handle multipart/form-data upload to the storage service
20 * @param {Object} provider The storage service provider
21 * @param {Request} req The HTTP request
22 * @param {Response} res The HTTP response
23 * @param {Object} [options] The container name
24 * @callback {Function} cb Callback function
25 * @header storageService.upload(provider, req, res, options, cb)
26 */
27exports.upload = function(provider, req, res, options, cb) {
28 if (!cb && 'function' === typeof options) {
29 cb = options;
30 options = {};
31 }
32
33 if (!options.maxFileSize) {
34 options.maxFileSize = defaultOptions.maxFileSize;
35 }
36
37 var form = new IncomingForm(options);
38 var container = options.container || req.params.container;
39 var fields = {};
40 var files = {};
41 form.handlePart = function(part) {
42 var self = this;
43
44 if (part.filename === undefined || part.filename === '') {
45 var value = '';
46 var decoder = new StringDecoder(this.encoding);
47
48 part.on('data', function(buffer) {
49 self._fieldsSize += buffer.length;
50 if (self._fieldsSize > self.maxFieldsSize) {
51 self._error(new Error(
52 g.f('{{maxFieldsSize}} exceeded, received %s bytes of field data',
53 self._fieldsSize
54 )));
55 return;
56 }
57 value += decoder.write(buffer);
58 });
59
60 part.on('end', function() {
61 var values = fields[part.name];
62 if (values === undefined) {
63 values = [value];
64 fields[part.name] = values;
65 } else {
66 values.push(value);
67 }
68 self.emit('field', part.name, value);
69 });
70 return;
71 }
72
73 this._flushing++;
74
75 var file = {
76 container: container,
77 name: part.filename,
78 type: part.mime,
79 };
80
81 // Options for this file
82
83 // Build a filename
84 if ('function' === typeof options.getFilename) {
85 file.originalFilename = file.name;
86 file.name = options.getFilename(file, req, res);
87 }
88
89 // Get allowed mime types
90 if (options.allowedContentTypes) {
91 var allowedContentTypes;
92 if ('function' === typeof options.allowedContentTypes) {
93 allowedContentTypes = options.allowedContentTypes(file, req, res);
94 } else {
95 allowedContentTypes = options.allowedContentTypes;
96 }
97 if (Array.isArray(allowedContentTypes) && allowedContentTypes.length !== 0) {
98 if (allowedContentTypes.indexOf(file.type) === -1) {
99 self._error(new Error(
100 g.f('{{contentType}} "%s" is not allowed (Must be in [%s])',
101 file.type,
102 allowedContentTypes.join(', ')
103 )));
104 return;
105 }
106 }
107 }
108
109 // Get max file size
110 var maxFileSize;
111 if (options.maxFileSize) {
112 if ('function' === typeof options.maxFileSize) {
113 maxFileSize = options.maxFileSize(file, req, res);
114 } else {
115 maxFileSize = options.maxFileSize;
116 }
117 }
118
119 // Get access control list
120 if (options.acl) {
121 if ('function' === typeof options.acl) {
122 file.acl = options.acl(file, req, res);
123 } else {
124 file.acl = options.acl;
125 }
126 }
127
128 self.emit('fileBegin', part.name, file);
129
130 var uploadParams = {
131 container: container,
132 remote: file.name,
133 contentType: file.type,
134 };
135 if (file.acl) {
136 uploadParams.acl = file.acl;
137 }
138 var writer = provider.upload(uploadParams);
139
140 writer.on('error', function(err) {
141 self.emit('error', err);
142 });
143
144 var endFunc = function(providerFile) {
145 self._flushing--;
146
147 file.providerResponse = providerFile;
148
149 var values = files[part.name];
150 if (values === undefined) {
151 values = [file];
152 files[part.name] = values;
153 } else {
154 values.push(file);
155 }
156 self.emit('file', part.name, file);
157 self._maybeEnd();
158 };
159
160 writer.on('success', function(file) {
161 endFunc(file);
162 });
163
164 var fileSize = 0;
165 if (maxFileSize) {
166 part.on('data', function(buffer) {
167 fileSize += buffer.length;
168 file.size = fileSize;
169 if (fileSize > maxFileSize) {
170 // We are missing some way to tell the provider to cancel upload/multipart upload of the current file.
171 // - s3-upload-stream doesn't provide a way to do this in it's public interface
172 // - We could call provider.delete file but it would not delete multipart data
173 self._error(new Error(
174 g.f('{{maxFileSize}} exceeded, received %s bytes of field data (max is %s)',
175 fileSize,
176 maxFileSize
177 )));
178 return;
179 }
180 });
181 }
182
183 part.on('end', function() {
184 writer.end();
185 });
186 part.pipe(writer, {end: false});
187 };
188
189 form.parse(req, function(err, _fields, _files) {
190 cb = cb || function() {};
191
192 if (err) {
193 console.error(err);
194 return cb(err);
195 }
196
197 if (Object.keys(_files).length === 0) {
198 err = new Error('No file content uploaded');
199 err.statusCode = 400; // DO NOT MODIFY res.status directly!
200 return cb(err);
201 }
202
203 cb(null, {files: files, fields: fields});
204 });
205};
206
207/**
208 * Handle download from a container/file.
209 * @param {Object} provider The storage service provider
210 * @param {Request} req The HTTP request
211 * @param {Response} res The HTTP response
212 * @param {String} container The container name
213 * @param {String} file The file name
214 * @callback {Function} cb Callback function.
215 * @header storageService.download(provider, req, res, container, file, cb)
216 */
217exports.download = function(provider, req, res, container, file, cb) {
218 var fileName = path.basename(file);
219 var params = {
220 container: container || req && req.params.container,
221 remote: file || req && req.params.file,
222 };
223
224 var range = null;
225
226 if (!req) {
227 // TODO(rfeng/bajtos) We should let the caller now about the problem!
228 return;
229 }
230
231 if (req.headers) {
232 range = req.headers.range || '';
233 }
234
235 if (!range) {
236 return download(params);
237 }
238
239 provider.getFile(params.container, params.remote, function(err, stats) {
240 if (err) {
241 return cb(processError(err, params.remote));
242 }
243
244 setupPartialDownload(params, stats, res);
245 download(params);
246 });
247
248 function download(params) {
249 var reader = provider.download(params);
250
251 res.type(fileName);
252
253 reader.pipe(res);
254
255 reader.on('error', function onReaderError(err) {
256 cb(processError(err, params.remote));
257 cb = function() {}; // avoid double-callback
258 });
259
260 reader.on('end', function onReaderEnd() {
261 cb();
262 cb = function() {}; // avoid double-callback
263 });
264 }
265};
266
267function setupPartialDownload(params, stats, res) {
268 var total = stats.size;
269
270 var parts = range.replace(/bytes=/, '').split('-');
271 var partialstart = parts[0];
272 var partialend = parts[1];
273
274 params.start = parseInt(partialstart, 10);
275 params.end = partialend ? parseInt(partialend, 10) : total - 1;
276
277 var chunksize = (params.end - params.start) + 1;
278
279 res.status(206);
280 res.set('Content-Range', 'bytes ' + params.start + '-' + params.end + '/' + total);
281 res.set('Accept-Ranges', 'bytes');
282 res.set('Content-Length', chunksize);
283};
284
285function processError(err, fileName) {
286 if (err.code === 'ENOENT') {
287 err.statusCode = err.status = 404;
288 // Hide the original message reported e.g. by FS provider, as it may
289 // contain sensitive information.
290 err.message = 'File not found: ' + fileName;
291 }
292 return err;
293}