UNPKG

7.6 kBJavaScriptView Raw
1"use strict";
2
3/**
4 * Copyright 2013 Vimeo
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19var url_module = require('url');
20var fs_module = require('fs');
21var https_module = require('https');
22var http_module = require('http');
23
24/**
25 * This object facilitates resumable uploading
26 *
27 * @param {string} file_path Path to the video file
28 * @param {string} upload_endpoint upload URL provided with an upload ticket
29 */
30var FileStreamer = module.exports = function FileStreamer(file_path, upload_endpoint, progress_callback) {
31 if (!file_path) {
32 throw new Error('You must provide a file path');
33 }
34
35 if (!upload_endpoint) {
36 throw new Error('You must provide an upload endpoint');
37 }
38
39 this._endpoint = url_module.parse(upload_endpoint);
40 this._path = file_path;
41 this.progress_callback = progress_callback;
42}
43
44FileStreamer.prototype._endpoint = null;
45FileStreamer.prototype._path = null;
46FileStreamer.prototype._fd = null;
47FileStreamer.prototype._file_size = 0;
48FileStreamer.prototype.percentage = 0;
49FileStreamer.prototype.sequential = true;
50FileStreamer.prototype.progress_callback = null;
51
52/**
53 * Holds the user defined ready function. Do not call this outside of the library
54 */
55FileStreamer.prototype._user_ready = function () {
56 this.ready = function (fn) {
57 fn();
58 }
59};
60
61/**
62 * Called internally whenever the upload might be complete.
63 * If the upload is complete it will call _user_ready, if not it will attempt to upload from where it left off.
64 *
65 * Do not call this outside of the library.
66 */
67FileStreamer.prototype._ready = function () {
68 var _self = this;
69
70 // If we think we are ready to complete, check with the server and see if they have the whole file
71 this._getNewStart(function (err, start) {
72 if (err) {
73 // If the check fails, close the file and error out immediately
74 _self._closeFile();
75 return _self._error(err);
76 }
77
78 if (start >= _self._file_size) {
79 // If the server says they have the whole file, close it and then return back to the user
80 _self._closeFile()
81 _self._user_ready();
82 } else {
83 // If the server does not have the whole file, upload from where we left off
84 _self._streamChunk(start);
85 }
86 });
87};
88
89/**
90 * Assign a callback to be called whenever the file is done uploading.
91 *
92 * @param {Function} fn The ready callback
93 */
94FileStreamer.prototype.ready = function (fn) {
95 this._user_ready = fn;
96};
97
98/**
99 * Holds the error callback. Do not call this outside of the library
100 *
101 * @param {Error} error The error that was thrown
102 * @return {[type]} [description]
103 */
104FileStreamer.prototype._error = function (error) {
105 this.error = function (fn) {
106 fn(error);
107 }
108};
109
110/**
111 * Assign a callback to be called whenever an error occurs.
112 *
113 * @param {Function} fn The error callback
114 */
115FileStreamer.prototype.error = function (fn) {
116 this._error = fn;
117};
118
119/**
120 * Start uploading the file
121 */
122FileStreamer.prototype.upload = function () {
123 var _self = this;
124
125 fs_module.stat(_self._path, function (stat_err, stats) {
126 if (stat_err) {
127 return _self._error(stat_err);
128 }
129
130 _self._file_size = stats.size;
131
132 fs_module.open(_self._path, 'r', function(open_err, fd) {
133 if (open_err) {
134 return this._error(open_err);
135 }
136
137 _self._fd = fd;
138 _self._streamChunk(0);
139 });
140 });
141};
142
143/**
144 * Send a file chunk, starting at byte [start] and ending at the end of the file
145 *
146 * @param {Number} start
147 */
148FileStreamer.prototype._streamChunk = function (start) {
149 var _self = this;
150 _self._putFile(start, function (put_err, code, headers) {
151
152 // Catches a rare vimeo server bug and exits out early
153 if (put_err && code) {
154 _self._closeFile();
155 return _self._error(put_err);
156 }
157
158 _self._ready();
159 });
160};
161
162/**
163 * Make the HTTP put request sending a part of a file up to the upload server
164 *
165 * @param {Number} start The first byte of the file
166 * @param {Function} callback A function which is called once the upload is complete, or has failed
167 */
168FileStreamer.prototype._putFile = function (start, callback) {
169 var _self = this;
170
171 var file = fs_module.createReadStream(_self._path, {
172 start : start
173 });
174
175 file.on('error', function (err) {
176 callback(err);
177 });
178
179 var uploaded_size = start || 0;
180
181 file.on('data', function(chunk) {
182 uploaded_size += chunk.length || 0;
183 if (_self.progress_callback) {
184 _self.progress_callback(uploaded_size, _self._file_size);
185 }
186 });
187
188 var headers = {
189 'Content-Length' : _self._file_size,
190 'Content-Type' : 'video/mp4'
191 };
192
193 headers['Content-Range'] = 'bytes ' + start + '-' + _self._file_size + '/' + _self._file_size;
194
195 var req = _self._upload_endpoint_request({
196 method : 'PUT',
197 headers : headers
198 }, callback);
199
200 file.pipe(req);
201};
202
203/**
204 * Close the file
205 */
206FileStreamer.prototype._closeFile = function () {
207 if (this._fd) {
208 fs_module.close(this._fd, function (close_err) {
209 if (close_err) {
210 this._error(close_err);
211 }
212 });
213 this._fd = null;
214 }
215};
216
217/**
218 * Verify the file upload and determine the last most byte the server has received
219 * @param {Function} next A callback that will be called when the check is complete, or has errored
220 */
221FileStreamer.prototype._getNewStart = function (next) {
222 var _self = this;
223
224 this._upload_endpoint_request({
225 method : 'PUT',
226 headers : {
227 'Content-Range' : 'bytes */*',
228 'Content-Type' : 'application/octet-stream'
229 }
230 }, function (err, status, headers) {
231 if (err) {
232 return next(err);
233 }
234
235 if (status === 308) {
236 return next(null, parseInt(headers.range.split('-')[1]));
237 } else {
238 return next(new Error('Invalid http status returned from range query: [' + status + ']'));
239 }
240 }).end();
241};
242
243/**
244 * Makes an http request to the upload server, and sends the response through the callback
245 *
246 * @param {Object} options Request options, pumped into https.request(options);
247 * @param {Function} callback Called when the upload is complete, or has failed
248 */
249FileStreamer.prototype._upload_endpoint_request = function (options, callback) {
250 var request_options = {
251 protocol : this._endpoint.protocol,
252 host : this._endpoint.hostname,
253 port : this._endpoint.port,
254 query : this._endpoint.query,
255 headers : options.headers,
256 path : this._endpoint.path,
257 method : options.method
258 };
259
260 var client = request_options.protocol === 'https:' ? https_module : http_module;
261 var req = client.request(request_options);
262
263 req.on('response', function (res) {
264 res.setEncoding('utf8');
265
266 var buffer = '';
267 res.on('readable', function () {
268 buffer += res.read();
269 });
270
271 if (res.statusCode > 399) {
272 // failed api calls should wait for the response to end and then call the callback with an error.
273 res.on('end', function () {
274 callback(new Error('[' + buffer + ']'), res.statusCode, res.headers);
275 });
276 } else {
277 // successful api calls should wait for the response to end and then call the callback with the response body
278 res.on('end', function () {
279 callback(null, res.statusCode, res.headers);
280 });
281 }
282 });
283
284 // notify user of any weird connection/request errors
285 req.on('error', function(e) {
286 callback(e);
287 });
288
289 return req;
290};