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