twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

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