File Coverage

blib/lib/MogileFS/Client/Fuse/FilePaths.pm
Criterion Covered Total %
statement 18 18 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 24 24 100.0


line stmt bran cond sub pod time code
1             package MogileFS::Client::Fuse::FilePaths;
2              
3 1     1   1126 use strict;
  1         2  
  1         131  
4 1     1   7 use warnings;
  1         3  
  1         38  
5 1     1   1047 use MRO::Compat;
  1         3781  
  1         30  
6 1     1   7 use mro 'c3';
  1         1  
  1         6  
7 1     1   1401 use threads::shared;
  1         1728  
  1         10  
8 1     1   389 use base qw{MogileFS::Client::Fuse};
  1         2  
  1         1565  
9              
10             Class::C3::initialize();
11              
12             our $VERSION = '0.05';
13              
14             use Errno qw{EACCES EEXIST EIO ENOENT};
15             use MogileFS::Client::FilePaths;
16             use MogileFS::Client::Fuse::Constants qw{:LEVELS};
17             use Params::Validate qw{validate_with BOOLEAN SCALAR};
18              
19             ##Instance Methods
20              
21             #method that will initialize the MogileFS::Client::Fuse::FilePaths object
22             sub _init {
23             my $self = shift;
24             my %opt = validate_with(
25             'allow_extra' => 1,
26             'params' => \@_,
27             'spec' => {
28             'filepaths.dircache' => {'type' => BOOLEAN, 'default' => 1},
29             'filepaths.dircache.duration' => {'type' => SCALAR, 'default' => 2},
30             },
31             );
32              
33             #initialize any ancestor classes
34             $self = $self->next::method(%opt);
35              
36             #initialize this object
37             $self->{'dirs'} = shared_clone({});
38              
39             #return the initialized object
40             return $self;
41             }
42              
43             # method that returns a directory listing for the current directory as a HASHREF
44             sub _listDir {
45             my $self = shift;
46             my ($path) = @_;
47             $path .= '/' if($path !~ m!/$!so);
48             my $config = $self->_config;
49              
50             #short-circuit if the dir cache is disabled
51             return {map {($_->{'name'} => $_)} $self->MogileFS->list($path)} if(!$config->{'filepaths.dircache'});
52              
53             #check to see if the specified path is cached
54             my $cache = $self->{'dirs'};
55             my $dir = $cache->{$path};
56              
57             #load the directory listing if the current cached listing is stale
58             if(!defined($dir) || $dir->{'expires'} <= time) {
59             #fetch and store the files in the dir cache
60             $dir = {
61             'expires' => time + $config->{'filepaths.dircache.duration'},
62             'files' => {
63             map {($_->{'name'} => $_)} $self->MogileFS->list($path),
64             },
65             };
66             $cache->{$path} = shared_clone($dir);
67             }
68              
69             #return the files for the current directory
70             return $dir->{'files'};
71             }
72              
73             #method that flushes the specified dir from the dir cache
74             sub _flushDir {
75             my $self = shift;
76             my ($path, $flushParent) = @_;
77             $path .= '/' if($path !~ m!/$!so);
78             delete $self->{'dirs'}->{$path};
79              
80             #flush the parent directory from the cache as well
81             $self->_flushDir($1, 1) if($flushParent && $path =~ m!^(.*/)[^/]*/$!so);
82              
83             return;
84             }
85              
86             sub _generateAttrs {
87             my $self = shift;
88             my ($finfo) = @_;
89              
90             if(ref($finfo) eq 'HASH') {
91             # Cook some permissions since we don't store this information in mogile
92             #TODO: how should we set file/dir permissions?
93             my $modes = 0444; # read bit
94             $modes |= 0222 if(!$self->_config->{'readonly'}); # write bit
95             $modes |= 0111 if($finfo->{'is_directory'}); # execute bit
96             $modes |= (($finfo->{'is_directory'} ? 0040 : 0100) << 9); # entry type bits
97              
98             my $size = $finfo->{'size'} || 0;
99              
100             # set some generic attributes
101             my $blksize = 1024;
102             my $blocks = (($size - 1) / $blksize) + 1;
103             my ($atime, $ctime, $mtime);
104             $ctime = $mtime = $finfo->{'modified'} || time;
105             $atime = time;
106              
107             # generate and return the entry attributes
108             #TODO: set more sane values for file attributes
109             return [
110             0, # device
111             0, # inode
112             $modes, # mode
113             1, # hard links
114             0, # user id
115             0, # group id
116             0, # device identifier (special files)
117             $size, # size (in bytes)
118             $atime, # last access time
119             $mtime, # last modified time
120             $ctime, # inode change time
121             $blksize, # block size
122             $blocks, # number of blocks
123             ];
124             }
125              
126             return [];
127             }
128              
129             #fetch meta-data about the specified file
130             sub get_file_info($) {
131             my $self = shift;
132             my ($path) = @_;
133              
134             #short-circuit if this is the root directory
135             return {
136             'name' => '/',
137             'is_directory' => 1,
138             } if($path eq '/');
139              
140             #split the path into the directory and the file
141             $path =~ m!^(.*/)([^/]+)$!so;
142             my ($dir, $file) = ($1, $2);
143              
144             #look up meta-data in the directory containing the specified file
145             my $finfo = eval {
146             my $files = $self->_listDir($dir);
147             return undef if(!(ref($files) eq 'HASH' && exists $files->{$file}));
148             return $files->{$file};
149             };
150              
151             #return the found file info
152             return $finfo;
153             }
154              
155             #method that will return a MogileFS object
156             sub MogileFS {
157             my $client = $_[0]->_localElem('MogileFS');
158              
159             #create and store a new client if one doesn't exist already
160             if(!defined $client) {
161             my $config = $_[0]->_config;
162             $client = MogileFS::Client::FilePaths->new(
163             'hosts' => [@{$config->{'trackers'}}],
164             'domain' => $config->{'domain'},
165             );
166             $_[0]->_localElem('MogileFS', $client);
167             }
168              
169             #return the MogileFS client
170             return $client;
171             }
172              
173             ##Fuse callbacks
174              
175             sub fuse_flush {
176             my $self = shift;
177             my ($path, $file) = @_;
178              
179             #does the directory cache need a flush after this file is flushed
180             my $needsFlush = eval{$file->writable && $file->dirty};
181              
182             #issue actual flush
183             my $resp = $self->next::method(@_);
184              
185             #flush the directory cache if necessary
186             eval {$self->_flushDir($file->path, 1)} if($needsFlush);
187              
188             #return the response for the flush
189             return $resp;
190             }
191              
192             sub fuse_getattr {
193             my $self = shift;
194             my ($path) = @_;
195             $path = $self->sanitize_path($path);
196              
197             # short-circuit if the file doesn't exist
198             my $finfo = $self->get_file_info($path);
199             return -ENOENT() if(!defined $finfo);
200              
201             # generate and return the file attributes
202             return @{$self->_generateAttrs($finfo)};
203             }
204              
205             sub fuse_getdir {
206             my $self = shift;
207             my ($path) = @_;
208             $path = $self->sanitize_path($path);
209              
210             #fetch all the files in the specified directory
211             my @names = eval {keys %{$self->_listDir($path)}};
212             return -EIO() if($@);
213              
214             #return this directory listing
215             return ('.', '..', @names), 0;
216             }
217              
218             sub fuse_mkdir {
219             my $self = shift;
220             my ($path, $mode) = @_;
221             $path = $self->sanitize_path($path);
222              
223             # throw an error if read-only is enabled
224             return -EACCES() if($self->_config->{'readonly'});
225              
226             #create and delete a file to force path vivification
227             eval{
228             my $file = $path . '/.mkdir_tmp_' . join('', map {chr(int(rand(26)) + 97)} (0..9));
229             my $mogc = $self->MogileFS();
230             die unless($mogc->new_file($file, $self->_config->{'class'})->close);
231             $mogc->delete($file);
232              
233             #flush the directory cache
234             $self->_flushDir($path, 1);
235             };
236             if($@) {
237             $self->log(ERROR, 'Error creating new directory: ' . $path);
238             return -EIO();
239             }
240              
241             return 0;
242             }
243              
244             sub fuse_mknod {
245             my $self = shift;
246             my ($path) = @_;
247             $path = $self->sanitize_path($path);
248              
249             # throw an error if read-only is enabled
250             return -EACCES() if($self->_config->{'readonly'});
251              
252             #issue actual mknod callback
253             my $resp = $self->next::method(@_);
254              
255             #flush affected entries from the dir cache
256             eval {$self->_flushDir($path, 1)};
257              
258             #return the actual response
259             return $resp;
260             }
261              
262             sub fuse_release {
263             my $self = shift;
264             my ($path, $flags, $file) = @_;
265              
266             #does the directory cache need a flush after this file is released
267             my $needsFlush = eval{$file->writable && $file->dirty};
268              
269             #issue actual release
270             my $resp = $self->next::method(@_);
271              
272             #flush the directory cache if necessary
273             eval {$self->_flushDir($file->path, 1)} if($needsFlush);
274              
275             #return the response for the release
276             return $resp;
277             }
278              
279             sub fuse_rename {
280             my $self = shift;
281             my ($old, $new) = @_;
282             $old = $self->sanitize_path($old);
283             $new = $self->sanitize_path($new);
284              
285             # throw an error if read-only is enabled
286             return -EACCES() if($self->_config->{'readonly'});
287              
288             #throw an error if the new file already exists
289             return -EEXIST() if(defined $self->get_file_info($new));
290              
291             #attempt renaming the specified file
292             my $mogc = $self->MogileFS();
293             my $response = eval {
294             my $resp = $mogc->rename($old, $new);
295             if($resp) {
296             $self->_flushDir($old, 1);
297             $self->_flushDir($new, 1);
298             }
299             return $resp;
300             };
301             if($@ || !$response) {
302             ($?, $!) = (-1, '');
303             #set the error code and string if we have a MogileFS::Client object
304             if($mogc) {
305             $? = $mogc->errcode || -1;
306             $! = $mogc->errstr || '';
307             }
308             $self->log(ERROR, "Error renaming file: $?: $!");
309             return -EIO();
310             }
311              
312             #return success
313             return 0;
314             }
315              
316             sub fuse_truncate {
317             my $self = shift;
318             my ($path, $size) = @_;
319             $path = $self->sanitize_path($path);
320              
321             # throw an error if read-only is enabled
322             return -EACCES() if($self->_config->{'readonly'});
323              
324             #issue actual truncate callback
325             my $resp = $self->next::method(@_);
326              
327             #flush affected entries from the dir cache
328             eval {$self->_flushDir($path, 1)};
329              
330             #return the actual response
331             return $resp;
332             }
333              
334             sub fuse_unlink {
335             my $self = shift;
336             my ($path) = @_;
337             $path = $self->sanitize_path($path);
338              
339             # throw an error if read-only is enabled
340             return -EACCES() if($self->_config->{'readonly'});
341              
342             #issue actual unlink callback
343             my $resp = $self->next::method(@_);
344              
345             #flush affected entries from the dir cache
346             eval {$self->_flushDir($path, 1)};
347              
348             #return the actual response
349             return $resp;
350             }
351              
352             1;