File Coverage

blib/lib/Catmandu/DirectoryIndex/Map.pm
Criterion Covered Total %
statement 86 86 100.0
branch 18 30 60.0
condition 4 9 44.4
subroutine 23 23 100.0
pod 0 5 0.0
total 131 153 85.6


line stmt bran cond sub pod time code
1             package Catmandu::DirectoryIndex::Map;
2              
3             our $VERSION = '1.14';
4              
5 1     1   105760 use Catmandu::Sane;
  1         186183  
  1         9  
6 1     1   347 use Catmandu::Util qw(:is :check);
  1         3  
  1         415  
7 1     1   652 use Catmandu;
  1         96498  
  1         5  
8 1     1   241 use Cwd;
  1         2  
  1         72  
9 1     1   6 use File::Spec;
  1         2  
  1         22  
10 1     1   5 use Catmandu::BadArg;
  1         3  
  1         31  
11 1     1   5 use Catmandu::Error;
  1         2  
  1         20  
12 1     1   5 use POSIX qw();
  1         1  
  1         15  
13 1     1   4 use Data::Dumper;
  1         5  
  1         57  
14 1     1   8 use Moo;
  1         2  
  1         7  
15 1     1   583 use Path::Tiny qw(path);
  1         2  
  1         90  
16 1     1   461 use URI::Escape qw();
  1         1538  
  1         27  
17 1     1   7 use namespace::clean;
  1         2  
  1         7  
18              
19             with "Catmandu::DirectoryIndex";
20              
21             has store_name => (is => "ro");
22              
23             has bag_name => (is => "ro");
24              
25             has bag => (
26             is => "ro",
27             isa => sub {
28             my $l = $_[0];
29              
30             #check_instance( $l, "Catmandu::Bag" ) returns false ..
31             check_instance($l);
32             $l->does("Catmandu::Bag")
33             or die("lookup should be Catmandu::Bag implementation");
34             },
35             lazy => 1,
36             builder => "_build_bag"
37             );
38              
39             sub _build_bag {
40 2     2   472 Catmandu->store($_[0]->store_name)->bag($_[0]->bag_name);
41             }
42              
43             #checks whether mapping record is syntactically correct
44             sub _is_valid_mapping {
45 8     8   13 my $map = $_[0];
46              
47 8 100       34 return unless is_hash_ref($map);
48              
49 6 50       42 is_string($map->{_id}) && is_string($map->{_path});
50             }
51              
52             #creates new directory: returns path if all is ok, throws an error on failure
53             sub _new_path {
54 2     2   5 my ($self, $id) = @_;
55              
56 2 50       12 Catmandu::BadArg->throw("need id") unless is_string($id);
57              
58 2         8 my $escaped_id = URI::Escape::uri_escape_utf8($id);
59              
60 2         229 my $t = POSIX::strftime("%Y/%m/%d/%H/%M/%S", gmtime(time));
61 2         40 my $path = File::Spec->catdir($self->base_dir(), split("/", $t), $escaped_id);
62              
63 2         6 my $err;
64 2         10 path($path)->mkpath({error => \$err});
65              
66 2 50 50     1095 Catmandu::Error->throw(
67             "unable to create directory $path: " . Dumper($err))
68             if defined($err) && scalar(@$err);
69              
70 2         55 $self->bag()->add({_id => $id, _path => $path});
71              
72 2         935 $path;
73             }
74              
75             #translates id to path: return either valid path or undef.
76             sub _to_path {
77 5     5   13 my ($self, $id) = @_;
78              
79 5 50       21 Catmandu::BadArg->throw("need id") unless is_string($id);
80              
81 5         122 my $mapping = $self->bag()->get($id);
82              
83             #no mapping, no path
84 5 100       1009 return unless _is_valid_mapping($mapping);
85              
86             #inconsistent behaviour: mapping exists, but directory is gone
87             Catmandu::Error->throw("mapping $id contains non existant directory")
88 3 50       63 unless -d $mapping->{_path};
89              
90 3         17 $mapping->{_path};
91             }
92              
93             sub get {
94 2     2 0 922 my ($self, $id) = @_;
95              
96 2         6 my $path = $self->_to_path($id);
97              
98 2 50       22 is_string($path) ? {_id => $id, _path => $path} : undef;
99             }
100              
101             sub add {
102 2     2 0 753 my ($self, $id) = @_;
103              
104 2   33     9 my $path = $self->_to_path($id) || $self->_new_path($id);
105              
106 2         15 {_id => $id, _path => $path};
107             }
108              
109             sub delete {
110 1     1 0 961 my ($self, $id) = @_;
111              
112 1         5 my $path = $self->_to_path($id);
113              
114 1 50       7 if (is_string($path)) {
115              
116 1         3 my $err;
117 1         5 path($path)->remove_tree({error => \$err});
118              
119 1 50 50     488 Catmandu::Error->throw(
120             "unable to remove directory $path: " . Dumper($err))
121             if defined($err) && scalar(@$err);
122              
123             }
124              
125 1         28 $self->bag()->delete($id);
126             }
127              
128             sub delete_all {
129 1     1 0 807 my $self = $_[0];
130              
131 1 50       22 if (-d $self->base_dir) {
132              
133 1         4 my $err;
134 1         8 path($self->base_dir)->remove_tree({keep_root => 1, error => \$err});
135              
136 1 50 50     1442 Catmandu::Error->throw("unable to remove entries from base directory "
137             . $self->base_dir . " : "
138             . Dumper($err))
139             if defined($err) && scalar(@$err);
140              
141             }
142              
143 1         30 $self->bag->delete_all;
144             }
145              
146             sub generator {
147 3     3 0 959 my $self = $_[0];
148              
149             return sub {
150 6     6   100 state $gen = $self->bag()->generator();
151              
152 6         55 my $mapping = $gen->();
153              
154 6 100       58 return unless defined $mapping;
155              
156 3 50       9 Catmandu::Error->throw(
157             "invalid mapping detected: " . Dumper($mapping))
158             unless _is_valid_mapping($mapping);
159              
160             Catmandu::Error->throw(
161             "mapping $mapping->{_id} contains non existant directory")
162 3 50       54 unless -d $mapping->{_path};
163              
164 3         19 +{_id => $mapping->{_id}, _path => $mapping->{_path}};
165 3         21 };
166             }
167              
168             1;
169              
170             __END__
171              
172             =pod
173              
174             =head1 NAME
175              
176             Catmandu::DirectoryIndex::Map - translates between id and path using a Catmandu::Bag as lookup
177              
178             =head1 SYNOPSIS
179              
180             use Catmandu::DirectoryIndex::Map;
181             use Catmandu::Store::DBI;
182              
183             # Bag to store/retrieve all path -> directory mapping
184             my $bag = Catmandu::Store::DBI->new(
185             data_source => "dbi:sqlite:dbname=/data/index.db"
186             )->bag("paths");
187              
188             my $p = Catmandu::DirectoryIndex::Map->new(
189             base_dir => "/data",
190             bag => $bag
191             );
192              
193             # Tries to find a mapping for id "a".
194             # return: mapping or undef
195             my $mapping = $p->get("a");
196              
197             # Returns a mapping like { _id => "a", _path => "/data/2018/01/01/16/00/00/0cc175b9c0f1b6a831c399e269772661" }
198             my $mapping = $p->add("a");
199              
200             # Catmandu::DirectoryIndex::Map is a Catmandu::Iterable
201             # Returns list of records: [{ _id => "a", _path => "/data/2018/01/01/16/00/00/0cc175b9c0f1b6a831c399e269772661" }]
202             my $mappings = $p->to_array();
203              
204             =head1 DESCRIPTION
205              
206             This package uses a Catmandu::Bag backend to translate between ids and paths.
207              
208             Each record looks like this:
209              
210             { _id => "a", _path => "/data/2018/01/01/16/00/00/0cc175b9c0f1b6a831c399e269772661" }
211              
212             If the mapping for the id does not exist yet, this package calculates it by concatenating
213             into a path:
214              
215             * $base_dir which is configurable
216             * YYYY: current year
217             * MM: current month
218             * DD: current day of month
219             * HH: current hour
220             * MM: current minute
221             * SS: current second
222             * TEXT: the md5 of the _id
223              
224             Every call to C<add> will generate a directory entry in the backend database,
225             if it didn't already exist.
226              
227             =head1 METHODS
228              
229             =head2 new( OPTIONS )
230              
231             Create a new Catmandu::DirectoryIndex::Map with the following configuration
232             parameters:
233              
234             =over
235              
236             =item base_dir
237              
238             See L<Catmandu::DirectoryIndex>
239              
240             =item store_name
241              
242             Name of the store in the Catmandu configuration.
243              
244             Ignored when bag instance is given.
245              
246             =item bag_name
247              
248             Name of the bag in the Catmandu configuration.
249              
250             Ignored when bag instance is given
251              
252             =item bag
253              
254             Instance of L<Catmandu::Bag> where all mappings between _id and _path are stored.
255              
256             =back
257              
258             =head1 INHERITED METHODS
259              
260             This Catmandu::DirectoryIndex::Map implements:
261              
262             =over 3
263              
264             =item L<Catmandu::DirectoryIndex>
265              
266             =back
267              
268             =head1 SEE ALSO
269              
270             L<Catmandu::DirectoryIndex>
271              
272             =cut