UNPKG

4.63 kBJavaScriptView Raw
1var assert = require('assert');
2var fs = require('fs');
3var mime = require('mime');
4var util = require('util');
5
6var MAX_FILE_SIZE_BYTES = 15 * 1024 * 1024;
7var MAX_FILE_CHUNK_BYTES = 5 * 1024 * 1024;
8
9/**
10 * FileUploader class used to upload a file to twitter via the /media/upload (chunked) API.
11 * Usage:
12 * var fu = new FileUploader({ file_path: '/foo/bar/baz.mp4' }, twit);
13 * fu.upload(function (err, bodyObj, resp) {
14 * console.log(err, bodyObj);
15 * })
16 *
17 * @param {Object} params Object of the form { file_path: String }.
18 * @param {Twit(object)} twit Twit instance.
19 */
20var FileUploader = function (params, twit) {
21 assert(params)
22 assert(params.file_path, 'Must specify `file_path` to upload a file. Got: ' + params.file_path + '.')
23 var self = this;
24 self._file_path = params.file_path;
25 self._twit = twit;
26 self._isUploading = false;
27 self._isFileStreamEnded = false;
28}
29
30/**
31 * Upload a file to Twitter via the /media/upload (chunked) API.
32 *
33 * @param {Function} cb function (err, data, resp)
34 */
35FileUploader.prototype.upload = function (cb) {
36 var self = this;
37
38 // Send INIT command with file info and get back a media_id_string we can use to APPEND chunks to it.
39 self._initMedia(function (err, bodyObj, resp) {
40 if (err) {
41 cb(err);
42 return;
43 } else {
44 var mediaTmpId = bodyObj.media_id_string;
45 var chunkNumber = 0;
46 var mediaFile = fs.createReadStream(self._file_path, { highWatermark: MAX_FILE_CHUNK_BYTES });
47
48 mediaFile.on('data', function (chunk) {
49 // Pause our file stream from emitting `data` events until the upload of this chunk completes.
50 // Any data that becomes available will remain in the internal buffer.
51 mediaFile.pause();
52 self._isUploading = true;
53
54 self._appendMedia(mediaTmpId, chunk.toString('base64'), chunkNumber, function (err, bodyObj, resp) {
55 self._isUploading = false;
56 if (err) {
57 cb(err);
58 } else {
59 if (self._isUploadComplete()) {
60 // We've hit the end of our stream; send FINALIZE command.
61 self._finalizeMedia(mediaTmpId, cb);
62 } else {
63 // Tell our file stream to start emitting `data` events again.
64 chunkNumber++;
65 mediaFile.resume();
66 }
67 }
68 });
69 });
70
71 mediaFile.on('end', function () {
72 // Mark our file streaming complete, and if done, send FINALIZE command.
73 self._isFileStreamEnded = true;
74 if (self._isUploadComplete()) {
75 self._finalizeMedia(mediaTmpId, cb);
76 }
77 });
78 }
79 })
80}
81
82FileUploader.prototype._isUploadComplete = function () {
83 return !this._isUploading && this._isFileStreamEnded;
84}
85
86 /**
87 * Send FINALIZE command for media object with id `media_id`.
88 *
89 * @param {String} media_id
90 * @param {Function} cb
91 */
92FileUploader.prototype._finalizeMedia = function(media_id, cb) {
93 var self = this;
94 self._twit.post('media/upload', {
95 command: 'FINALIZE',
96 media_id: media_id
97 }, cb);
98}
99
100 /**
101 * Send APPEND command for media object with id `media_id`.
102 * Append the chunk to the media object, then resume streaming our mediaFile.
103 *
104 * @param {String} media_id media_id_string received from Twitter after sending INIT comand.
105 * @param {String} chunk_part Base64-encoded String chunk of the media file.
106 * @param {Number} segment_index Index of the segment.
107 * @param {Function} cb
108 */
109FileUploader.prototype._appendMedia = function(media_id_string, chunk_part, segment_index, cb) {
110 var self = this;
111 self._twit.post('media/upload', {
112 command: 'APPEND',
113 media_id: media_id_string.toString(),
114 segment_index: segment_index,
115 media: chunk_part,
116 }, cb);
117}
118
119/**
120 * Send INIT command for our underlying media object.
121 *
122 * @param {Function} cb
123 */
124FileUploader.prototype._initMedia = function (cb) {
125 var self = this;
126 var mediaType = mime.lookup(self._file_path);
127 var mediaFileSizeBytes = fs.statSync(self._file_path).size;
128
129 // Check the file size - it should not go over 15MB for video.
130 // See https://dev.twitter.com/rest/reference/post/media/upload-chunked
131 if (mediaFileSizeBytes < MAX_FILE_SIZE_BYTES) {
132 self._twit.post('media/upload', {
133 'command': 'INIT',
134 'media_type': mediaType,
135 'total_bytes': mediaFileSizeBytes
136 }, cb);
137 } else {
138 var errMsg = util.format('This file is too large. Max size is %dB. Got: %dB.', MAX_FILE_SIZE_BYTES, mediaFileSizeBytes);
139 cb(new Error(errMsg));
140 }
141}
142
143module.exports = FileUploader