@vimeo/vimeo
Version:
A Node.js library for the new Vimeo API.
295 lines (254 loc) • 7.77 kB
JavaScript
/**
* Copyright 2013 Vimeo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const urlModule = require('url')
const fsModule = require('fs')
const httpsModule = require('https')
const httpModule = require('http')
/**
* This object facilitates resumable uploading
*
* @param {string} filePath Path to the video file.
* @param {string} uploadEndpoint Upload URL provided with an upload attempt.
* @param {Function} progressCallback
*/
const FileStreamer = module.exports = function FileStreamer (filePath, uploadEndpoint, progressCallback) {
if (!filePath) {
throw new Error('You must provide a file path.')
}
if (!uploadEndpoint) {
throw new Error('You must provide an upload endpoint.')
}
this._endpoint = urlModule.parse(uploadEndpoint) // eslint-disable-line n/no-deprecated-api
this._path = filePath
this.progress_callback = progressCallback
}
FileStreamer.prototype._endpoint = null
FileStreamer.prototype._path = null
FileStreamer.prototype._fd = null
FileStreamer.prototype._file_size = 0
FileStreamer.prototype.percentage = 0
FileStreamer.prototype.sequential = true
FileStreamer.prototype.progress_callback = null
/**
* Holds the user defined ready function.
*
* Do not call this outside of the library.
*/
FileStreamer.prototype._user_ready = function () {
this.ready = function (fn) {
fn()
}
}
/**
* Called internally whenever the upload might be complete.
*
* If the upload is complete it will call `_user_ready`, if not it will attempt to upload from where it left off.
*
* Do not call this outside of the library.
*/
FileStreamer.prototype._ready = function () {
const _self = this
// If we think we are ready to complete, check with the server and see if they have the whole file
this._getNewStart(function (err, start) {
if (err) {
// If the check fails, close the file and error out immediately
_self._closeFile()
return _self._error(err)
}
if (start >= _self._file_size) {
// If the server says they have the whole file, close it and then return back to the user
_self._closeFile()
_self._user_ready()
} else {
// If the server does not have the whole file, upload from where we left off
_self._streamChunk(start)
}
})
}
/**
* Assign a callback to be called whenever the file is done uploading.
*
* @param {Function} fn The ready callback
*/
FileStreamer.prototype.ready = function (fn) {
this._user_ready = fn
}
/**
* Holds the error callback. Do not call this outside of the library
*
* @param {Error} error The error that was thrown.
*/
FileStreamer.prototype._error = function (error) {
this.error = function (fn) {
fn(error)
}
}
/**
* Assign a callback to be called whenever an error occurs.
*
* @param {Function} fn The error callback
*/
FileStreamer.prototype.error = function (fn) {
this._error = fn
}
/**
* Start uploading the file.
*
*/
FileStreamer.prototype.upload = function () {
const _self = this
fsModule.stat(_self._path, function (statErr, stats) {
if (statErr) {
return _self._error(statErr)
}
_self._file_size = stats.size
fsModule.open(_self._path, 'r', function (openErr, fd) {
if (openErr) {
return this._error(openErr)
}
_self._fd = fd
_self._streamChunk(0)
})
})
}
/**
* Send a file chunk, starting at byte [start] and ending at the end of the file.
*
* @param {number} start
*/
FileStreamer.prototype._streamChunk = function (start) {
const _self = this
_self._putFile(start, function (err, code, headers) {
// Catches a rare Vimeo server bug and exits out early.
if (err && code) {
_self._closeFile()
return _self._error(err)
}
_self._ready()
})
}
/**
* Make the HTTP put request sending a part of a file up to the upload server
*
* @param {number} start The first byte of the file
* @param {Function} callback A function which is called once the upload is complete, or has failed.
*/
FileStreamer.prototype._putFile = function (start, callback) {
const _self = this
const file = fsModule.createReadStream(_self._path, {
start
})
file.on('error', function (err) {
callback(err)
})
let uploadedSize = start || 0
file.on('data', function (chunk) {
uploadedSize += chunk.length || 0
if (_self.progress_callback) {
_self.progress_callback(uploadedSize, _self._file_size)
}
})
const headers = {
'Content-Length': _self._file_size,
'Content-Type': 'video/mp4'
}
headers['Content-Range'] = 'bytes ' + start + '-' + _self._file_size + '/' + _self._file_size
const req = _self._upload_endpoint_request({
method: 'PUT',
headers
}, callback)
file.pipe(req)
}
/**
* Close the file.
*
*/
FileStreamer.prototype._closeFile = function () {
if (this._fd) {
fsModule.close(this._fd, function (err) {
if (err) {
this._error(err)
}
})
this._fd = null
}
}
/**
* Verify the file upload and determine the last most byte the server has received.
*
* @param {Function} next A callback that will be called when the check is complete, or has errored.
*/
FileStreamer.prototype._getNewStart = function (next) {
this._upload_endpoint_request({
method: 'PUT',
headers: {
'Content-Range': 'bytes */*',
'Content-Type': 'application/octet-stream'
}
}, function (err, status, headers) {
if (err) {
return next(err)
}
if (status === 308) {
return next(null, parseInt(headers.range.split('-')[1]))
} else {
return next(new Error('Invalid http status returned from range query: [' + status + ']'))
}
}).end()
}
/**
* Makes an HTTP request to the upload server, and sends the response through the callback.
*
* @param {Object} options Request options, fed into `https.request(options)`
* @param {Function} callback Called when the upload is complete, or has failed.
*/
FileStreamer.prototype._upload_endpoint_request = function (options, callback) {
const requestOptions = {
protocol: this._endpoint.protocol,
host: this._endpoint.hostname,
port: this._endpoint.port,
query: this._endpoint.query,
headers: options.headers,
path: this._endpoint.path,
method: options.method
}
const client = requestOptions.protocol === 'https:' ? httpsModule : httpModule
const req = client.request(requestOptions)
req.on('response', function (res) {
res.setEncoding('utf8')
let buffer = ''
res.on('readable', function () {
buffer += res.read()
})
if (res.statusCode > 399) {
// failed api calls should wait for the response to end and then call the callback with an error.
res.on('end', function () {
callback(new Error('[' + buffer + ']'), res.statusCode, res.headers)
})
} else {
// successful api calls should wait for the response to end and then call the callback with the response body
res.on('end', function () {
callback(null, res.statusCode, res.headers)
})
}
})
// Notify user of any weird connection/request errors.
req.on('error', function (e) {
callback(e)
})
return req
}