File Coverage

blib/lib/MongoDB/GridFSBucket.pm
Criterion Covered Total %
statement 30 117 25.6
branch 0 50 0.0
condition 0 18 0.0
subroutine 10 24 41.6
pod 11 11 100.0
total 51 220 23.1


line stmt bran cond sub pod time code
1             # Copyright 2015 - present MongoDB, Inc.
2             #
3             # Licensed under the Apache License, Version 2.0 (the "License");
4             # you may not use this file except in compliance with the License.
5             # You may obtain a copy of the License at
6             #
7             # http://www.apache.org/licenses/LICENSE-2.0
8             #
9             # Unless required by applicable law or agreed to in writing, software
10             # distributed under the License is distributed on an "AS IS" BASIS,
11             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12             # See the License for the specific language governing permissions and
13             # limitations under the License.
14              
15 58     58   411 use strict;
  58         2522  
  58         1893  
16 58     58   307 use warnings;
  58         2462  
  58         4539  
17             package MongoDB::GridFSBucket;
18              
19             # ABSTRACT: A file storage abstraction
20              
21 58     58   316 use version;
  58         112  
  58         2410  
22             our $VERSION = 'v2.2.0';
23              
24 58     58   4656 use Moo;
  58         4402  
  58         4565  
25 58     58   53621 use MongoDB::GridFSBucket::DownloadStream;
  58         212  
  58         2051  
26 58     58   30890 use MongoDB::GridFSBucket::UploadStream;
  58         252  
  58         2645  
27 58         427 use MongoDB::_Types qw(
28             Boolish
29             ReadPreference
30             WriteConcern
31             ReadConcern
32             BSONCodec
33             NonNegNum
34 58     58   508 );
  58         140  
35 58     58   73969 use Scalar::Util qw/reftype/;
  58         140  
  58         3824  
36 58         335 use Types::Standard qw(
37             Str
38             InstanceOf
39 58     58   408 );
  58         149  
40 58     58   51777 use namespace::clean -except => 'meta';
  58         130  
  58         376  
41              
42             #pod =attr database
43             #pod
44             #pod The L containing the GridFS bucket collections.
45             #pod
46             #pod =cut
47              
48             has database => (
49             is => 'ro',
50             isa => InstanceOf ['MongoDB::Database'],
51             required => 1,
52             );
53              
54             #pod =attr bucket_name
55             #pod
56             #pod The name of the GridFS bucket. Defaults to 'fs'. The underlying
57             #pod collections that are used to implement a GridFS bucket get this string as a
58             #pod prefix (e.g "fs.chunks").
59             #pod
60             #pod =cut
61              
62             has bucket_name => (
63             is => 'ro',
64             isa => Str,
65             default => sub { 'fs' },
66             );
67              
68             #pod =attr chunk_size_bytes
69             #pod
70             #pod The number of bytes per chunk. Defaults to 261120 (255kb).
71             #pod
72             #pod =cut
73              
74             has chunk_size_bytes => (
75             is => 'ro',
76             isa => NonNegNum,
77             default => sub { 255 * 1024 },
78             );
79              
80             #pod =attr write_concern
81             #pod
82             #pod A L object. It may be initialized with a hash
83             #pod reference that will be coerced into a new MongoDB::WriteConcern object.
84             #pod By default it will be inherited from a L object.
85             #pod
86             #pod =cut
87              
88             has write_concern => (
89             is => 'ro',
90             isa => WriteConcern,
91             required => 1,
92             coerce => WriteConcern->coercion,
93             );
94              
95             #pod =attr read_concern
96             #pod
97             #pod A L object. May be initialized with a hash
98             #pod reference or a string that will be coerced into the level of read
99             #pod concern.
100             #pod
101             #pod By default it will be inherited from a L object.
102             #pod
103             #pod =cut
104              
105             has read_concern => (
106             is => 'ro',
107             isa => ReadConcern,
108             required => 1,
109             coerce => ReadConcern->coercion,
110             );
111              
112             #pod =attr read_preference
113             #pod
114             #pod A L object. It may be initialized with a string
115             #pod corresponding to one of the valid read preference modes or a hash reference
116             #pod that will be coerced into a new MongoDB::ReadPreference object.
117             #pod By default it will be inherited from a L object.
118             #pod
119             #pod B Because many GridFS operations require multiple independent reads from
120             #pod separate collections, use with secondaries is B because
121             #pod reads could go to different secondaries, resulting in inconsistent data
122             #pod if all file and chunk documents have not replicated to all secondaries.
123             #pod
124             #pod =cut
125              
126             has read_preference => (
127             is => 'ro',
128             isa => ReadPreference,
129             required => 1,
130             coerce => ReadPreference->coercion,
131             );
132              
133             #pod =attr bson_codec
134             #pod
135             #pod An object that provides the C and C methods, such
136             #pod as from L. It may be initialized with a hash reference that
137             #pod will be coerced into a new BSON object. By default it will be
138             #pod inherited from a L object.
139             #pod
140             #pod =cut
141              
142             has bson_codec => (
143             is => 'ro',
144             isa => BSONCodec,
145             coerce => BSONCodec->coercion,
146             required => 1,
147             );
148              
149             #pod =attr max_time_ms
150             #pod
151             #pod Specifies the maximum amount of time in milliseconds that the server should
152             #pod use for working on a query. By default it will be inherited from a
153             #pod L object.
154             #pod
155             #pod B: this will only be used for server versions 2.6 or greater, as that
156             #pod was when the C<$maxTimeMS> meta-operator was introduced.
157             #pod
158             #pod =cut
159              
160             has max_time_ms => (
161             is => 'ro',
162             isa => NonNegNum,
163             required => 1,
164             );
165              
166             #pod =attr disable_md5
167             #pod
168             #pod When true, files will not include the deprecated C field in the
169             #pod file document. Defaults to false.
170             #pod
171             #pod =cut
172              
173             has disable_md5 => (
174             is => 'ro',
175             isa => Boolish,
176             );
177              
178             # determines whether or not to attempt index creation
179             has _tried_indexing => (
180             is => 'rwp',
181             isa => Boolish,
182             );
183              
184             has _files => (
185             is => 'lazy',
186             isa => InstanceOf ['MongoDB::Collection'],
187             init_arg => undef,
188             );
189              
190             sub _build__files {
191 0     0     my $self = shift;
192 0           my $coll = $self->database->get_collection(
193             $self->bucket_name . '.files',
194             {
195             read_preference => $self->read_preference,
196             write_concern => $self->write_concern,
197             read_concern => $self->read_concern,
198             max_time_ms => $self->max_time_ms,
199             bson_codec => $self->bson_codec,
200             }
201             );
202 0           return $coll;
203             }
204              
205             has _chunks => (
206             is => 'lazy',
207             isa => InstanceOf ['MongoDB::Collection'],
208             init_arg => undef,
209             );
210              
211             sub _build__chunks {
212 0     0     my $self = shift;
213 0           my $coll = $self->database->get_collection(
214             $self->bucket_name . '.chunks',
215             {
216             read_preference => $self->read_preference,
217             write_concern => $self->write_concern,
218             read_concern => $self->read_concern,
219             max_time_ms => $self->max_time_ms,
220             # XXX: Generate a new bson codec here to
221             # prevent users from changing it?
222             bson_codec => $self->bson_codec,
223             }
224             );
225 0           return $coll;
226             }
227              
228             # index operations need primary server, regardless of bucket read prefs
229             sub _create_indexes {
230 0     0     my ($self) = @_;
231 0           $self->_set__tried_indexing(1);
232              
233 0           my $pf = $self->_files->clone( read_preference => 'primary' );
234              
235 0 0         return if $pf->count_documents({}) > 0;
236              
237 0           my $pfi = $pf->indexes;
238 0           my $pci = $self->_chunks->clone( read_preference => 'primary' )->indexes;
239              
240 0 0         if ( !grep { $_->{name} eq 'filename_1_uploadDate_1' } $pfi->list->all ) {
  0            
241 0           $pfi->create_one( [ filename => 1, uploadDate => 1 ], { unique => 1 } );
242             }
243              
244 0 0         if ( !grep { $_->{name} eq 'files_id_1_n_1' } $pci->list->all ) {
  0            
245 0           $pci->create_one( [ files_id => 1, n => 1 ], { unique => 1 } );
246             }
247              
248 0           return;
249             }
250              
251             #pod =method find
252             #pod
253             #pod $result = $bucket->find($filter);
254             #pod $result = $bucket->find($filter, $options);
255             #pod
256             #pod $file_doc = $result->next;
257             #pod
258             #pod Executes a query on the file documents collection with a
259             #pod L and
260             #pod returns a L object. It takes an optional hashref
261             #pod of options identical to L.
262             #pod
263             #pod =cut
264              
265             sub find {
266 0     0 1   my ( $self, $filter, $options ) = @_;
267 0           return $self->_files->find( $filter, $options )->result;
268             }
269              
270             #pod =method find_one
271             #pod
272             #pod $file_doc = $bucket->find_one($filter, $projection);
273             #pod $file_doc = $bucket->find_one($filter, $projection, $options);
274             #pod
275             #pod Executes a query on the file documents collection with a
276             #pod L and
277             #pod returns the first document found, or C if no document is found.
278             #pod
279             #pod See L for details about the
280             #pod C<$projection> and optional C<$options> fields.
281             #pod
282             #pod =cut
283              
284             sub find_one {
285 0     0 1   my ( $self, $filter, $projection, $options ) = @_;
286 0           return $self->_files->find_one( $filter, $projection, $options );
287             }
288              
289             #pod =method find_id
290             #pod
291             #pod $file_doc = $bucket->find_id( $id );
292             #pod $file_doc = $bucket->find_id( $id, $projection );
293             #pod $file_doc = $bucket->find_id( $id, $projection, $options );
294             #pod
295             #pod Executes a query with a L of
296             #pod C<< { _id => $id } >> and returns a single document or C if no document
297             #pod is found.
298             #pod
299             #pod See L for details about the
300             #pod C<$projection> and optional C<$options> fields.
301             #pod
302             #pod =cut
303              
304             sub find_id {
305 0     0 1   my ( $self, $id, $projection, $options ) = @_;
306 0           return $self->_files->find_id( $id, $projection, $options );
307             }
308              
309             #pod =method open_download_stream
310             #pod
311             #pod $stream = $bucket->open_download_stream($id);
312             #pod $line = $stream->readline;
313             #pod
314             #pod Returns a new L that can be used to
315             #pod download the file with the file document C<_id> matching C<$id>. This
316             #pod throws a L if no such file exists.
317             #pod
318             #pod =cut
319              
320             sub open_download_stream {
321 0     0 1   my ( $self, $id ) = @_;
322 0 0         MongoDB::UsageError->throw('No id provided to open_download_stream') unless $id;
323 0           my $file_doc = $self->_files->find_id($id);
324 0 0         MongoDB::GridFSError->throw("FileNotFound: no file found for id '$id'")
325             unless $file_doc;
326             my $result =
327 0 0         $file_doc->{'length'} > 0
328             ? $self->_chunks->find( { files_id => $id }, { sort => { n => 1 } } )->result
329             : undef;
330 0           return MongoDB::GridFSBucket::DownloadStream->new(
331             {
332             id => $id,
333             file_doc => $file_doc,
334             _result => $result,
335             }
336             );
337             }
338              
339             #pod =method open_upload_stream
340             #pod
341             #pod $stream = $bucket->open_upload_stream($filename);
342             #pod $stream = $bucket->open_upload_stream($filename, $options);
343             #pod
344             #pod $stream->print('data');
345             #pod $stream->close;
346             #pod $file_id = $stream->id
347             #pod
348             #pod Returns a new L that can be used
349             #pod to upload a new file to a GridFS bucket.
350             #pod
351             #pod This method requires a filename to store in the C field of the
352             #pod file document. B: the filename is an arbitrary string; the method
353             #pod does not read from this filename locally.
354             #pod
355             #pod You can provide an optional hash reference of options that are passed to the
356             #pod L constructor:
357             #pod
358             #pod =for :list
359             #pod * C – the number of bytes per chunk. Defaults to the
360             #pod C of the bucket object.
361             #pod * C – a hash reference for storing arbitrary metadata about the
362             #pod file.
363             #pod
364             #pod =cut
365              
366             sub open_upload_stream {
367 0     0 1   my ( $self, $filename, $options ) = @_;
368 0 0 0       MongoDB::UsageError->throw('No filename provided to open_upload_stream')
369             unless defined $filename && length $filename;
370              
371 0 0         $self->_create_indexes unless $self->_tried_indexing;
372              
373 0 0         return MongoDB::GridFSBucket::UploadStream->new(
374             {
375             chunk_size_bytes => $self->chunk_size_bytes,
376             ( $options ? %$options : () ),
377             _bucket => $self,
378             filename => "$filename", # stringify path objects
379             }
380             );
381             }
382              
383             #pod =method open_upload_stream_with_id
384             #pod
385             #pod $stream = $bucket->open_upload_stream_with_id($id, $filename);
386             #pod $stream = $bucket->open_upload_stream_with_id($id, $filename, $options);
387             #pod
388             #pod $stream->print('data');
389             #pod $stream->close;
390             #pod
391             #pod Returns a new L that can be used to
392             #pod upload a new file to a GridFS bucket.
393             #pod
394             #pod This method uses C<$id> as the _id of the file being created, which must be
395             #pod unique.
396             #pod
397             #pod This method requires a filename to store in the C field of the
398             #pod file document. B: the filename is an arbitrary string; the method
399             #pod does not read from this filename locally.
400             #pod
401             #pod You can provide an optional hash reference of options, just like
402             #pod L.
403             #pod
404             #pod =cut
405              
406             sub open_upload_stream_with_id {
407 0     0 1   my ( $self, $id, $filename, $options ) = @_;
408 0           my $id_copy = $id;
409 0 0 0       MongoDB::UsageError->throw('No id provided to open_upload_stream_with_id')
410             unless defined $id_copy && length $id_copy;
411 0 0 0       MongoDB::UsageError->throw('No filename provided to open_upload_stream_with_id')
412             unless defined $filename && length $filename;
413              
414 0 0         $self->_create_indexes unless $self->_tried_indexing;
415              
416 0 0         return MongoDB::GridFSBucket::UploadStream->new(
417             {
418             chunk_size_bytes => $self->chunk_size_bytes,
419             ( $options ? %$options : () ),
420             _bucket => $self,
421             filename => "$filename", # stringify path objects
422             id => $id,
423             }
424             );
425             }
426              
427             #pod =method download_to_stream
428             #pod
429             #pod $bucket->download_to_stream($id, $out_fh);
430             #pod
431             #pod Downloads the file matching C<$id> and writes it to the file handle C<$out_fh>.
432             #pod This throws a L if no such file exists.
433             #pod
434             #pod =cut
435              
436             sub download_to_stream {
437 0     0 1   my ( $self, $id, $target_fh ) = @_;
438 0 0         MongoDB::UsageError->throw('No id provided to download_to_stream')
439             unless defined $id;
440 0 0         MongoDB::UsageError->throw('No handle provided to download_to_stream')
441             unless defined $target_fh;
442 0 0         MongoDB::UsageError->throw(
443             'Invalid handle $target_fh provided to download_to_stream')
444             unless reftype $target_fh eq 'GLOB';
445              
446 0           my $download_stream = $self->open_download_stream($id);
447 0           my $csb = $download_stream->file_doc->{chunkSize};
448 0           my $buffer;
449 0           while ( $download_stream->read( $buffer, $csb ) ) {
450 0           print {$target_fh} $buffer;
  0            
451             }
452 0           $download_stream->close;
453 0           return;
454             }
455              
456             #pod =method upload_from_stream
457             #pod
458             #pod $file_id = $bucket->upload_from_stream($filename, $in_fh);
459             #pod $file_id = $bucket->upload_from_stream($filename, $in_fh, $options);
460             #pod
461             #pod Reads from a filehandle and uploads its contents to GridFS. It returns the
462             #pod C<_id> field stored in the file document.
463             #pod
464             #pod This method requires a filename to store in the C field of the
465             #pod file document. B: the filename is an arbitrary string; the method
466             #pod does not read from this filename locally.
467             #pod
468             #pod You can provide an optional hash reference of options, just like
469             #pod L.
470             #pod
471             #pod =cut
472              
473             sub upload_from_stream {
474 0     0 1   my ( $self, $filename, $source_fh, $options ) = @_;
475 0 0 0       MongoDB::UsageError->throw('No filename provided to upload_from_stream')
476             unless defined $filename && length $filename;
477 0 0         MongoDB::UsageError->throw('No handle provided to upload_from_stream')
478             unless defined $source_fh;
479 0 0         MongoDB::UsageError->throw(
480             'Invalid handle $source_fh provided to upload_from_stream')
481             unless reftype $source_fh eq 'GLOB';
482              
483 0           my $upload_stream = $self->open_upload_stream( $filename, $options );
484 0           my $csb = $upload_stream->chunk_size_bytes;
485 0           my $buffer;
486 0           while ( read $source_fh, $buffer, $csb ) {
487 0           $upload_stream->print($buffer);
488             }
489 0           $upload_stream->close;
490 0           return $upload_stream->id;
491             }
492              
493             #pod =method upload_from_stream_with_id
494             #pod
495             #pod $bucket->upload_from_stream_with_id($id, $filename, $in_fh);
496             #pod $bucket->upload_from_stream_with_id($id, $filename, $in_fh, $options);
497             #pod
498             #pod Reads from a filehandle and uploads its contents to GridFS.
499             #pod
500             #pod This method uses C<$id> as the _id of the file being created, which must be
501             #pod unique.
502             #pod
503             #pod This method requires a filename to store in the C field of the
504             #pod file document. B: the filename is an arbitrary string; the method
505             #pod does not read from this filename locally.
506             #pod
507             #pod You can provide an optional hash reference of options, just like
508             #pod L.
509             #pod
510             #pod Unlike L, this method returns nothing.
511             #pod
512             #pod =cut
513              
514             sub upload_from_stream_with_id {
515 0     0 1   my ( $self, $id, $filename, $source_fh, $options ) = @_;
516 0           my $id_copy = $id; # preserve number/string form
517 0 0 0       MongoDB::UsageError->throw('No id provided to upload_from_stream_with_id')
518             unless defined $id_copy && length $id_copy;
519 0 0 0       MongoDB::UsageError->throw('No filename provided to upload_from_stream_with_id')
520             unless defined $filename && length $filename;
521 0 0         MongoDB::UsageError->throw('No handle provided to upload_from_stream_with_id')
522             unless defined $source_fh;
523 0 0         MongoDB::UsageError->throw(
524             'Invalid handle $source_fh provided to upload_from_stream_with_id')
525             unless reftype $source_fh eq 'GLOB';
526              
527 0           my $upload_stream = $self->open_upload_stream_with_id( $id, $filename, $options );
528 0           my $csb = $upload_stream->chunk_size_bytes;
529 0           my $buffer;
530 0           while ( read $source_fh, $buffer, $csb ) {
531 0           $upload_stream->print($buffer);
532             }
533 0           $upload_stream->close;
534 0           return;
535             }
536              
537             #pod =method delete
538             #pod
539             #pod $bucket->delete($id);
540             #pod
541             #pod Deletes the file matching C<$id> from the bucket.
542             #pod This throws a L if no such file exists.
543             #pod
544             #pod =cut
545              
546             sub delete {
547 0     0 1   my ( $self, $id ) = @_;
548              
549 0 0         $self->_create_indexes unless $self->_tried_indexing;
550              
551 0           my $delete_result = $self->_files->delete_one( { _id => $id } );
552             # This should only ever be 0 or 1, checking for exactly 1 to be thorough
553 0 0         unless ( $delete_result->deleted_count == 1 ) {
554 0           MongoDB::GridFSError->throw("FileNotFound: no file found for id $id");
555             }
556 0           $self->_chunks->delete_many( { files_id => $id } );
557 0           return;
558             }
559              
560             #pod =method drop
561             #pod
562             #pod $bucket->drop;
563             #pod
564             #pod Drops the underlying files documents and chunks collections for this bucket.
565             #pod
566             #pod =cut
567              
568             sub drop {
569 0     0 1   my ($self) = @_;
570 0           $self->_files->drop;
571 0           $self->_chunks->drop;
572             }
573              
574             1;
575              
576             __END__