1 /**
  2  * @fileOverview Base Blob.
  3  */
  4 
  5 (function () {
  6   var fs = require('fs'),
  7     DummyRequest = require("../../request").DummyRequest,
  8     CloudError = require("../../errors").CloudError,
  9     Blob;
 10 
 11   /**
 12    * Blob class.
 13    *
 14    * @param {base.Container} cont       Container object.
 15    * @param {Object}      attrs         Attributes.
 16    * @config {string}     name          Name.
 17    * @config {string}     created       Creation date.
 18    * @config {string}     lastModified  Last modified date.
 19    * @config {number}     size          Byte size of object.
 20    * @config {string}     etag          ETag.
 21    * @exports Blob as base.blob.Blob
 22    * @constructor
 23    */
 24   Blob = function (cont, attrs) {
 25     // Validation.
 26     if (!cont) { throw new Error("No container object."); }
 27     if (!attrs || !attrs.name) { throw new Error("No blob name."); }
 28 
 29     this._cont = cont;
 30     this._name = attrs.name;
 31     this._created = attrs.created || null;
 32     this._lastModified = attrs.lastModified || null;
 33     this._size = (typeof attrs.size === 'string')
 34       ? parseInt(attrs.size, 10)
 35       : null;
 36     this._etag = attrs.etag || null;
 37   };
 38 
 39   Object.defineProperties(Blob.prototype, {
 40     /**
 41      * Container object.
 42      *
 43      * @name Blob#container
 44      * @type base.blob.Container
 45      */
 46     container: {
 47       get: function () {
 48         return this._cont;
 49       }
 50     },
 51 
 52     /**
 53      * Blob name.
 54      *
 55      * @name Blob#name
 56      * @type string
 57      */
 58     name: {
 59       get: function () {
 60         return this._name;
 61       }
 62     }
 63   });
 64 
 65   /**
 66    * Data event ('``data``').
 67    *
 68    * @name base.blob.Blob#get_data
 69    * @event
 70    * @param  {Buffer|string}       chunk  Data chunk.
 71    * @param  {Object}               meta  Headers, meta object.
 72    * @config {Object}          [headers]  HTTP headers.
 73    * @config {Object}     [cloudHeaders]  Cloud provider headers.
 74    * @config {Object}         [metadata]  Cloud metadata.
 75    */
 76   /**
 77    * Completion event ('``end``').
 78    *
 79    * @name base.blob.Blob#get_end
 80    * @event
 81    * @param  {Object}            results  Results object.
 82    * @config {base.blob.Blob}       blob  Blob object.
 83    * @param  {Object}               meta  Headers, meta object.
 84    * @config {Object}          [headers]  HTTP headers.
 85    * @config {Object}     [cloudHeaders]  Cloud provider headers.
 86    * @config {Object}         [metadata]  Cloud metadata.
 87    */
 88   /**
 89    * Error event ('``error``').
 90    *
 91    * @name base.blob.Blob#get_error
 92    * @event
 93    * @param   {Error|errors.CloudError} err Error object.
 94    */
 95   /**
 96    * Get blob data (and metadata).
 97    *
 98    * ## Events
 99    *  - [``data(chunk)``](#get_data)
100    *  - [``end(results, meta)``](#get_end)
101    *  - [``error(err)``](#get_error)
102    *
103    * @param   {Object}  [options]         Options object.
104    * @config  {string}  [encoding]        Encoding to use (if set, a string
105    *                                      will be passed to 'data' or 'end'
106    *                                      instead of array of Buffer objects).
107    * @config  {bool}    [validate=false]  Validate?
108    * @config  {Object}  [headers]         Raw headers to add.
109    * @config  {Object}  [cloudHeaders]    Cloud provider headers to add.
110    * @config  {Object}  [metadata]        Cloud metadata to add.
111    * @returns {stream.ReadStream}         Readable cloud stream object.
112    */
113   Blob.prototype.get = function (options) {
114     throw new Error("Not implemented.");
115   };
116 
117   /**
118    * Get blob data to file.
119    *
120    * ## Note
121    * Just a wrapper around a writable file stream and a GET.
122    * Must still call ``end()`` to invoke.
123    *
124    * ## Events
125    *  - [``end(results, meta)``](#get_end)
126    *  - [``error(err)``](#get_error)
127    *
128    * @param   {string}  filePath          Path to file.
129    * @param   {Object}  [options]         Options object.
130    * @config  {string}  [encoding]        Encoding to use.
131    * @config  {Object}  [headers]         Raw headers to add.
132    * @config  {Object}  [cloudHeaders]    Cloud provider headers to add.
133    * @config  {Object}  [metadata]        Cloud metadata to add.
134    * @returns {request.DummyRequest}      Request object.
135    */
136   // TOOD: Blob.getToFile - consider fs.writeFile() implementation instead.
137   Blob.prototype.getToFile = function (filePath, options) {
138     options = options || {};
139     var self = this,
140       encoding = options.encoding || null,
141       getResults = {},
142       getMeta = {},
143       getFinished = false,
144       writeFinished = false,
145       endEmitted = false,
146       errorEmitted = false,
147       request,
148       getStream,
149       writeStream,
150       errHandler,
151       endHandler;
152 
153     // Update encoding.
154     options.encoding = encoding;
155 
156     // Set up streams.
157     getStream = self.get({ encoding: encoding });
158     writeStream = fs.createWriteStream(filePath, options);
159 
160     // Request setup.
161     // Request.end() starts the pipe, WriteStream:end completes request.
162     request = new DummyRequest({
163       endFn: function () {
164         // Start the pipe and call 'end()'.
165         getStream.pipe(writeStream);
166         getStream.end();
167       }
168     });
169 
170     // Need to handle errors in **both** streams.
171     /** @private */
172     errHandler = function (err) {
173       if (!errorEmitted) {
174         errorEmitted = true;
175         request.emit('error', err);
176       }
177       getStream.destroy();
178       writeStream.destroy();
179     };
180 
181     // End when we have both (1) cloud results, and (2) closed file stream.
182     /** @private */
183     endHandler = function () {
184       if (getFinished && writeFinished && !endEmitted) {
185         endEmitted = true;
186         request.emit('end', getResults, getMeta);
187       }
188     };
189 
190     // Stream handlers.
191     getStream.on('error', errHandler);
192     getStream.on('end', function (results, meta) {
193       // GET blob finishes, and we store values.
194       getFinished = true;
195       getResults = results;
196       getMeta = meta;
197       endHandler();
198     });
199     writeStream.on('error', errHandler);
200     writeStream.on('close', function () {
201       // File stream is closed.
202       writeFinished = true;
203       endHandler();
204     });
205 
206     return request;
207   };
208 
209   /**
210    * Completion event ('``end``').
211    *
212    * @name base.blob.Blob#head_end
213    * @event
214    * @param  {Object}            results  Results object.
215    * @config {base.blob.Blob}       blob  Blob object.
216    * @param  {Object}               meta  Headers, meta object.
217    * @config {Object}          [headers]  HTTP headers.
218    * @config {Object}     [cloudHeaders]  Cloud provider headers.
219    * @config {Object}         [metadata]  Cloud metadata.
220    */
221   /**
222    * Error event ('``error``').
223    *
224    * @name base.blob.Blob#head_error
225    * @event
226    * @param   {Error|errors.CloudError} err Error object.
227    */
228   /**
229    * HEAD blob (check blob exists and return metadata).
230    *
231    * ## Events
232    *  - [``end(results, meta)``](#head_end)
233    *  - [``error(err)``](#head_error)
234    *
235    * @param   {Object}  [options]         Options object.
236    * @config  {Object}  [headers]         Raw headers to add.
237    * @config  {Object}  [cloudHeaders]    Cloud provider headers to add.
238    * @config  {Object}  [metadata]        Cloud metadata to add.
239    * @returns {request.AuthenticatedRequest} Request object.
240    */
241   Blob.prototype.head = function (options) {
242     throw new Error("Not implemented.");
243   };
244 
245   /**
246    * Completion event ('``end``').
247    *
248    * @name base.blob.Blob#put_end
249    * @event
250    * @param  {Object}            results  Results object.
251    * @config {base.blob.Blob}       blob  Blob object.
252    * @param  {Object}               meta  Headers, meta object.
253    * @config {Object}          [headers]  HTTP headers.
254    * @config {Object}     [cloudHeaders]  Cloud provider headers.
255    * @config {Object}         [metadata]  Cloud metadata.
256    */
257   /**
258    * Error event ('``error``').
259    *
260    * @name base.blob.Blob#put_error
261    * @event
262    * @param   {Error|errors.CloudError} err Error object.
263    */
264   /**
265    * Put blob data (and metadata).
266    *
267    * ## Events
268    *  - [``end(results, meta)``](#get_end)
269    *  - [``error(err)``](#get_error)
270    *
271    * @param   {Object}  [options]         Options object.
272    * @config  {string}  [encoding]        Encoding to use.
273    * @config  {Object}  [headers]         Raw headers to add.
274    * @config  {Object}  [cloudHeaders]    Cloud provider headers to add.
275    * @config  {Object}  [metadata]        Cloud metadata to add.
276    * @returns {stream.WriteStream}        Writable cloud stream object.
277    */
278   Blob.prototype.put = function (options) {
279     throw new Error("Not implemented.");
280   };
281 
282   /**
283    * Put blob data from file.
284    *
285    * ## Note
286    * Just a wrapper around a readable file stream and a PUT.
287    * Must still call ``end()`` to invoke.
288    *
289    * ## Events
290    *  - [``end(results, meta)``](#get_end)
291    *  - [``error(err)``](#get_error)
292    *
293    * @param   {string}  filePath          Path to file.
294    * @param   {Object}  [options]         Options object.
295    * @config  {string}  [encoding]        Encoding to use.
296    * @config  {Object}  [headers]         Raw headers to add.
297    * @config  {Object}  [cloudHeaders]    Cloud provider headers to add.
298    * @config  {Object}  [metadata]        Cloud metadata to add.
299    * @returns {request.DummyRequest}      Request object.
300    */
301   // TOOD: Blob.putFromFile - consider fs.readFile() implementation instead.
302   Blob.prototype.putFromFile = function (filePath, options) {
303     options = options || {};
304     var self = this,
305       encoding = options.encoding || null,
306       errorEmitted = false,
307       request,
308       readStream,
309       putStream,
310       errHandler;
311 
312     // Update encoding.
313     options.encoding = encoding;
314 
315     // Set up streams.
316     readStream = fs.createReadStream(filePath, { encoding: encoding });
317     putStream = self.put(options);
318 
319     // Request setup.
320     // Request.end() starts the pipe, PutStream:end completes request.
321     request = new DummyRequest({
322       endFn: function () {
323         // Start the pipe.
324         readStream.pipe(putStream);
325       }
326     });
327 
328     // Need to handle errors in **both** streams.
329     /** @private */
330     errHandler = function (err) {
331       if (!errorEmitted) {
332         errorEmitted = true;
333         request.emit('error', err);
334       }
335       readStream.destroy();
336       putStream.destroy();
337     };
338 
339     // Stream handlers.
340     readStream.on('error', errHandler);
341     putStream.on('error', errHandler);
342     putStream.on('end', function (results, meta) {
343       // PUT finishes, and we signal request.
344       request.emit('end', results, meta);
345     });
346 
347     return request;
348   };
349 
350   /**
351    * Completion event ('``end``').
352    *
353    * **Note**: Callback indicates the object no longer exists.
354    *
355    * @name base.blob.Blob#del_end
356    * @event
357    * @param  {Object}            results  Results object.
358    * @config {base.blob.Blob}       blob  Blob object.
359    * @config {boolean}          notFound  True if object was not found.
360    * @param  {Object}               meta  Headers, meta object.
361    * @config {Object}          [headers]  HTTP headers.
362    * @config {Object}     [cloudHeaders]  Cloud provider headers.
363    * @config {Object}         [metadata]  Cloud metadata.
364    */
365   /**
366    * Error event ('``error``').
367    *
368    * @name base.blob.Blob#del_error
369    * @event
370    * @param   {Error|errors.CloudError} err Error object.
371    */
372   /**
373    * Delete a blob.
374    *
375    * ## Events
376    *  - [``end(results, meta)``](#del_end)
377    *  - [``error(err)``](#del_error)
378    *
379    * ## Not Found Blobs
380    * This method emits '``end``' and **not** '``error``' for a not found
381    * blob, reasoning that it becomes easier to have multiple deletes at
382    * the same time. Moreover, AWS S3 doesn't return a 404, so we can't really
383    * even detect this (although Google Storage does).
384    *
385    * On ``end``, ``result.notFound`` is returned that at least for Google
386    * Storage indicates if the blob didn't exist.
387    *
388    * @param   {Object}  [options]         Options object.
389    * @config  {Object}  [headers]         Raw headers to add.
390    * @config  {Object}  [cloudHeaders]    Cloud provider headers to add.
391    * @config  {Object}  [metadata]        Cloud metadata to add.
392    * @returns {request.AuthenticatedRequest} Request object.
393    */
394   Blob.prototype.del = function (options) {
395     throw new Error("Not implemented.");
396   };
397 
398   module.exports.Blob = Blob;
399 }());
400