UNPKG

7.88 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) {
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}
42
43FileStreamer.prototype._endpoint = null;
44FileStreamer.prototype._path = null;
45FileStreamer.prototype._fd = null;
46FileStreamer.prototype._file_size = 0;
47FileStreamer.prototype.sequential = true;
48
49/**
50 * Holds the user defined ready function. Do not call this outside of the library
51 */
52FileStreamer.prototype._user_ready = function () {
53 this.ready = function (fn) {
54 fn();
55 }
56};
57
58/**
59 * Called internally whenever the upload might be complete.
60 * If the upload is complete it will call _user_ready, if not it will attempt to upload from where it left off.
61 *
62 * Do not call this outside of the library.
63 */
64FileStreamer.prototype._ready = function () {
65 var _self = this;
66
67 // If we think we are ready to complete, check with the server and see if they have the whole file
68 this._getNewStart(function (err, start) {
69 if (err) {
70 // If the check fails, close the file and error out immediately
71 _self._closeFile();
72 return _self._error(err);
73 }
74
75 if (start >= _self._file_size) {
76 // If the server says they have the whole file, close it and then return back to the user
77 _self._closeFile()
78 _self._user_ready();
79 } else {
80 // If the server does not have the whole file, upload from where we left off
81 _self._streamChunk(start);
82 }
83 });
84};
85
86/**
87 * Assign a callback to be called whenever the file is done uploading.
88 *
89 * @param {Function} fn The ready callback
90 */
91FileStreamer.prototype.ready = function (fn) {
92 this._user_ready = fn;
93};
94
95/**
96 * Holds the error callback. Do not call this outside of the library
97 *
98 * @param {Error} error The error that was thrown
99 * @return {[type]} [description]
100 */
101FileStreamer.prototype._error = function (error) {
102 this.error = function (fn) {
103 fn(error);
104 }
105};
106
107/**
108 * Assign a callback to be called whenever an error occurs.
109 *
110 * @param {Function} fn The error callback
111 */
112FileStreamer.prototype.error = function (fn) {
113 this._error = fn;
114};
115
116/**
117 * Start uploading the file
118 */
119FileStreamer.prototype.upload = function () {
120 var _self = this;
121
122 fs_module.stat(_self._path, function (stat_err, stats) {
123 if (stat_err) {
124 return _self._error(stat_err);
125 }
126
127 _self._file_size = stats.size;
128
129 fs_module.open(_self._path, 'r', function(open_err, fd) {
130 if (open_err) {
131 return this._error(open_err);
132 }
133
134 _self._fd = fd;
135 _self._streamChunk(0);
136 });
137 });
138};
139
140/**
141 * Send a file chunk, starting at byte [start] and ending at the end of the file
142 *
143 * @param {Number} start
144 */
145FileStreamer.prototype._streamChunk = function (start) {
146 var _self = this;
147 _self._putFile(start, function (put_err, code, headers) {
148
149 // Catches a rare vimeo server bug and exits out early
150 if (put_err && code) {
151 _self._closeFile();
152 return _self._error(put_err);
153 }
154
155 _self._ready();
156 });
157}
158
159/**
160 * Make the HTTP put request sending a part of a file up to the upload server
161 *
162 * @param {Number} start The first byte of the file
163 * @param {Function} callback A function which is called once the upload is complete, or has failed
164 */
165FileStreamer.prototype._putFile = function (start, callback) {
166 var _self = this;
167
168 var file = fs_module.createReadStream(_self._path, {
169 start : start
170 });
171
172 file.on('error', function (err) {
173 callback(err);
174 });
175
176 var headers = {
177 'Content-Length' : _self._file_size,
178 'Content-Type' : 'video/mp4'
179 };
180
181 headers['Content-Range'] = 'bytes ' + start + '-' + _self._file_size + '/' + _self._file_size;
182
183 var req = _self._upload_endpoint_request({
184 method : 'PUT',
185 headers : headers
186 }, callback);
187
188 file.pipe(req);
189}
190
191/**
192 * Close the file
193 */
194FileStreamer.prototype._closeFile = function () {
195 if (this._fd) {
196 fs_module.close(this._fd);
197 this._fd = null;
198 }
199};
200
201/**
202 * Verify the file upload and determine the last most byte the server has received
203 * @param {Function} next A callback that will be called when the check is complete, or has errored
204 */
205FileStreamer.prototype._getNewStart = function (next) {
206 var _self = this;
207
208 this._upload_endpoint_request({
209 method : 'PUT',
210 headers : {
211 'Content-Range' : 'bytes */*',
212 'Content-Type' : 'application/octet-stream'
213 }
214 }, function (err, status, headers) {
215 if (err) {
216 return next(err);
217 }
218
219 if (status === 308) {
220 return next(null, parseInt(headers.range.split('-')[1]));
221 } else {
222 return next(new Error('Invalid http status returned from range query: [' + status + ']'));
223 }
224 }).end();
225};
226
227/**
228 * Makes an http request to the upload server, and sends the response through the callback
229 *
230 * @param {Object} options Request options, pumped into https.request(options);
231 * @param {Function} callback Called when the upload is complete, or has failed
232 */
233FileStreamer.prototype._upload_endpoint_request = function (options, callback) {
234 var request_options = {
235 protocol : this._endpoint.protocol,
236 host : this._endpoint.hostname,
237 port : this._endpoint.port,
238 query : this._endpoint.query,
239 headers : options.headers,
240 path : this._endpoint.path,
241 method : options.method
242 };
243
244 var client = request_options.protocol === 'https:' ? https_module : http_module;
245 var req = client.request(request_options);
246
247 req.on('response', function (res) {
248 res.setEncoding('utf8');
249
250 var buffer = '';
251 res.on('readable', function () {
252 buffer += res.read();
253 });
254
255 if (res.statusCode > 399) {
256 // failed api calls should wait for the response to end and then call the callback with an error.
257 res.on('end', function () {
258 callback(new Error('[' + buffer + ']'), res.statusCode, res.headers);
259 });
260 } else {
261 // successful api calls should wait for the response to end and then call the callback with the response body
262 res.on('end', function () {
263 callback(null, res.statusCode, res.headers);
264 });
265 }
266 });
267
268 // notify user of any weird connection/request errors
269 req.on('error', function(e) {
270 callback(e);
271 });
272
273 return req;
274};
\No newline at end of file