better list_files from cache message
[cloudstore.git] / lib / CloudStore / API.pm
1 package CloudStore::API;
2 use warnings;
3 use strict;
4 use autodie;
5
6 use lib 'lib';
7 use base qw(CloudStore::Gearman CloudStore::MD5sum);
8
9 use File::Path qw(make_path remove_tree);
10 use File::Find;
11 use Data::Dump qw(dump);
12 use Carp qw(confess cluck);
13
14 sub new {
15         my ($class,$slice) = @_;
16
17         cluck "DEPRICIATED $slice specified" if $slice;
18
19         my $self = {
20                 passwd => '/var/lib/extrausers/passwd',
21         };
22         bless $self, $class;
23
24         $self->{md5} = $self->user_info("md5") || die "can't find user md5";
25 =for removed
26         $self->{md5}->{dir} = "$dir/md5";
27         if ( ! -e $self->{md5}->{dir} ) {
28                 make_path $self->{md5}->{dir}, { uid => $self->{md5}->{uid}, gid => $self->{md5}->{gid} };
29                 warn "## CREATED md5pool $self->{md5}->{dir}\n";
30         }
31 =cut
32
33         return $self;
34 }
35
36 sub slice_dir_port {
37         my ($self,$slice) = @_;
38         my ( undef, $dir, $port, undef ) = getgrnam($slice);
39         die "getgrnam $slice: $!" if $!;
40         warn "# slice_dir_port $slice = $dir $port\n";
41         return ( $dir, $port );
42 }
43
44 sub dir2gearman {
45         my $self = shift;
46         my $dir  = shift;
47         $dir =~ s/\W+/_/g;
48         $dir =~ s/^_+//;
49         $dir =~ s{_\d+$}{};
50         return join('_', $dir, @_);
51 }
52
53 sub user_info {
54         my ($self,$login) = @_;
55
56         confess "need login" unless $login;
57
58         my @n = qw/ login passwd uid gid quota comment gecos dir shell expire /;
59         my @p = $login =~ m/^\d+$/ ? getpwuid $login : getpwnam $login;
60         die "user_info $login: $@" if $@;
61         my $user;
62         $user->{$_} = shift @p foreach @n;
63         return $user;
64 }
65
66 sub create_user {
67         my ( $self, $new_email, $new_passwd, $new_quota ) = @_;
68
69         my $max_uid = 0;
70         my $found = 0;
71
72         open(my $fh, '<', $self->{passwd});
73         while(<$fh>) {
74                 my ( $login, $passwd, $uid, $gid, $email, $dir, $shell ) = split(/:/,$_);
75                 $max_uid = $uid if $uid > $max_uid;
76                 $found = $login if $email eq $new_email;
77         }
78         close($fh);
79
80         my $slice = $ENV{SLICE} || 's1';
81         $slice =~ s{/.+/(\w+)$}{$1};
82         my ( $dir, $port ) = $self->slice_dir_port( $slice );
83
84         $dir ||= $ENV{SLICE};
85         $port ||= 6501;
86
87         if ( ! $found ) {
88                 $max_uid++;
89                 $dir .= "/$max_uid";
90                 warn "# create_user $slice $new_email $new_quota = $max_uid $dir";
91                 open(my $fh, '>>', $self->{passwd});
92                 print $fh "u$max_uid:$new_passwd:$max_uid:$port:$new_email:$dir:/bin/true\n";
93                 close($fh);
94                 $found = "u$max_uid";
95
96                 mkdir $dir;
97                 chown $max_uid, $port, $dir;
98
99                 my $path = "$dir/.meta/secrets";
100                 $self->mkbasepath($path);
101                 open($fh, '>', $path);
102                 print $fh "u$max_uid:$new_passwd\n";
103                 close $fh;
104         }
105
106         # FIXME update quota only on create?
107         $self->gearman_do( $self->dir2gearman( $dir, 'quota', 'set' ) => "$found $new_quota" );
108
109         return $found;
110 }
111
112 sub mkbasepath {
113         my ($self,$path,$opts) = @_;
114         $path =~ s{/[^/]+$}{};
115         make_path $path unless -d $path;
116 }
117
118 sub user_dir {
119         my ( $self, $user, $dir ) = @_;
120         $user = $self->user_info($user) unless ref $user eq 'HASH';
121         my $path;
122         if ( exists $user->{dir} ) {
123                 $path = $user->{dir} . '/.meta/' . $dir;
124         } else {
125                 die "no dir in ", dump $user;
126         }
127         $path =~ s{//+}{/}g;
128
129         if ( ! -e $path ) {
130                 $self->mkbasepath( $path, { uid => $user->{uid} } );
131                 open(my $fh, '>', $path);
132                 close $fh;
133                 chown $user->{uid}, $user->{gid}, $path;
134                 warn "# user_dir created $path\n";
135         }
136
137         #warn "### user_dir $path";
138         return $path;
139 }
140
141 sub append {
142         my $self = shift @_;
143         $self->append_meta( 'usage', @_ );
144 }
145
146 sub append_meta {
147         my $self = shift @_;
148         my $log  = shift @_;
149         my $user = shift @_;
150         my $path = $self->user_dir( $user => $log );
151         my $delimiter = '#';
152            $delimiter = '  ' if $log =~ m/md5sum$/;
153         my $line = join($delimiter,@_);
154         open(my $fh, '>>', $path);
155         print $fh "$line\n";
156         close $fh;
157         warn "## $path $line\n";
158 }
159
160 sub usage {
161         my ( $self, $user ) = @_;
162         $user = $self->user_info($user) unless ref $user eq 'HASH';
163
164         my $usage_path = $user->{dir} . '/.meta/files.usage';
165         $self->mkbasepath( $usage_path, { uid => $user->{uid} } );
166         if ( ! -e $usage_path ) {
167                 warn "# usage $usage_path missing";
168                 $self->list_files($user);
169         }
170
171         open(my $fh, '<', $usage_path);
172         my $size = <$fh>;
173         chomp $size;
174
175         warn "# usage $user->{login} $size bytes\n";
176         return $size;
177
178 =for slow and broken
179
180         my $path = $self->user_dir( $user => 'usage');
181         my $sum;
182         open(my $fh, '<', $path);
183         while(<$fh>) {
184                 chomp;
185                 my @v = split(/#/,$_);
186                 $sum->{ $v[0] } += $v[1];
187                 $sum->{_usage}  += $v[1];
188         }
189         my ( $usage, $quota ) = split(/ /,
190                 $self->gearman_do( $self->dir2gearman( $user->{dir}, 'quota', 'get' ) => $user->{uid} )
191         );
192         $sum->{_usage} += $usage;
193         $sum->{_quota} = $quota;
194         warn "## usage ",dump($user, $sum), $/;
195         return $sum;
196
197 =cut
198
199 }
200
201 sub send_file {
202         my ( $self, $f_uid,$f_path, $t_uid,$t_path ) = @_;
203
204         my $f = $self->user_info($f_uid);
205         die "FIXME not on current slice" if $f->{dir} !~ m/^$ENV{SLICE}/;
206         my $t = $self->user_info($t_uid);
207
208         my $f_full = "$f->{dir}/$f_path";
209         my $t_full = "$t->{dir}/$t_path";
210
211         $self->mkbasepath( $t_full, { uid => $t->{uid} } );
212
213         my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat $f_full;
214         if ( $uid == $f->{uid} ) {
215                 warn "# send_file - move $f_uid $f_path to pool\n";
216                 chown $self->{md5}->{uid}, $self->{md5}->{gid}, $f_full;
217                 chmod oct("0444"), $f_full;
218                 $self->append( $f, 'sent', -s $f_full, $t->{uid}, $f_path );
219         } elsif ( $uid == $self->{md5}->{uid} ) {
220                 warn "# send_file - shared $f_full\n";
221         }
222
223         $self->delete( $t, $t_path ) if -e $t_full;
224
225         my $size = -s $f_full;
226         my $md5;
227
228         my $ok;
229         {
230                 no autodie qw(link);
231                 $ok = link $f_full, $t_full
232         };
233         if ( ! $ok || $! =~ m/cross-link/ ) {
234                 $ok = symlink $f_full, $t_full;
235         } else {
236                 $size = -s $t_full;
237
238                 if ( $f->{uid} == $self->{md5}->{uid} ) {
239                         $md5 = $f_path; # we don't have local md5sum db for md5 user!
240                 } else {
241                         $md5 = $self->md5_get($f_full);
242                 }
243
244         }
245
246         if ( $ok ) {
247                 $self->append( $t, 'recv', $size, $f->{uid}, $t_path );
248                 $self->append_meta('md5sum', $t, $md5 => $t_path ) if $md5; # md5sum for received files! FIXME -- cross-slice md5
249                 $self->refresh_file_list( $t );
250         } else {
251                 warn "ERROR: send_file $f_full -> $t_full: $!";
252         }
253
254         return $size;
255 }
256
257 sub rename_file {
258         my ( $self, $user, $from, $to ) = @_;
259         $user = $self->user_info($user) unless ref $user eq 'HASH';
260
261         my $f_full = "$user->{dir}/$from";
262         my $t_full = "$user->{dir}/$to";
263
264         $self->mkbasepath( $t_full, { uid => $user->{uid}, gid => $user->{gid} } );
265         my $ok = rename $f_full, $t_full;
266
267         $self->refresh_file_list( $user );
268
269         my $md5 = $self->md5_get($t_full);
270         if ( ! $md5 ) {
271                 warn "ERROR: no md5sum for $from";
272                 return $ok; # XXX our internal error
273         }
274
275         $self->append_meta('md5sum', $user, 'rename' => $from );
276         $self->append_meta('md5sum', $user, $md5 => $from );
277
278         return $ok;
279 }
280
281
282 sub delete {
283         my ( $self, $user, $path ) = @_;
284         $user = $self->user_info($user) unless ref $user eq 'HASH';
285
286         my $deleted_size = 0;
287         my $full_path = "$user->{dir}/$path";
288
289         if ( -d $full_path ) {
290
291                 find({ 
292                 no_chdir => 1,
293                 wanted => sub {
294                         return if -d $_;
295                         my ($uid,$size) = (stat($_))[4,7];
296                         warn "## find $uid $size $_\n";
297                         if ( $uid == $self->{md5}->{uid} ) {
298                                 $deleted_size += $size;
299                         }
300                 }}, $full_path);
301
302                 remove_tree $full_path;
303         } else {
304                 $deleted_size += -s $full_path;
305                 unlink $full_path;
306         }
307
308         warn "delete $deleted_size bytes shared\n";
309
310         $self->append( $user, 'delete', -$deleted_size, $user->{uid}, $path );
311         $self->append_meta('md5sum', $user, 'delete', $path );
312
313         $self->refresh_file_list( $user );
314
315         return $full_path;
316 }
317
318 sub file_size {
319         my ( $self, $user, $path ) = @_;
320         $user = $self->user_info($user) unless ref $user eq 'HASH';
321
322         my $full_path = "$user->{dir}/$path";
323         my $size = -s $full_path;
324         warn "# file_size $full_path = $size bytes\n";
325         return $size;
326 }
327
328 sub list_files {
329         my ( $self, $user, $path ) = @_;
330
331         $user =~ s{/+$}{} && warn "cleanup list_files arg [$user]";
332
333         $user = $self->user_info($user) unless ref $user eq 'HASH';
334
335         die "no dir for ",dump($user) unless exists $user->{dir};
336
337         my $files = $user->{dir} . '/.meta/files';
338         $self->mkbasepath( $files, { uid => $user->{uid} } );
339         if ( -e $files && -s $files > 0 && -e "$files.usage") {
340                 local $/ = undef;
341                 open(my $fh, '<', $files);
342                 my $list = <$fh>;
343                 close($fh);
344                 warn "# list_files $user->{login} from cache ", length($list), " bytes\n";
345                 return $list;
346         }
347
348         my $dir = $user->{dir};
349         open(my $pipe, '-|', qq|find -L $dir -printf "%y %s %p\n"|);
350         open(my $fh, '>', "$files.new");
351         my $total_usage = 0;
352         my $list_txt;
353         while(<$pipe>) {
354                 chomp;
355                 my ( $type, $size, $name ) = split(/\s/, $_, 3);
356                 $name =~ s{$dir}{./};
357                 $name =~ s{//+}{/}g;
358                 my $line = "$type $size $name\n";
359                 print $fh $line;
360                 $list_txt .= $line;
361                 $total_usage += $size;
362         }
363         close($pipe);
364         close($fh);
365         rename "$files.new", $files;
366
367         open(my $usage, '>', "$files.usage.new");
368         print $usage $total_usage;
369         close($usage);
370         rename "$files.usage.new", "$files.usage";
371
372         warn "# list_files $dir usage: $total_usage\n";
373
374         return $list_txt;
375 }
376
377 sub refresh_file_list {
378         my ( $self, $user ) = @_;
379         $user = $self->user_info($user) unless ref $user eq 'HASH';
380         my $full_path = "$user->{dir}/.meta/files";
381         if ( -e $full_path ) {
382                 warn "## refresh_file_list $full_path";
383                 unlink $full_path || warn "unlink $full_path: $!";
384         } else {
385                 warn "## refresh_file_list $full_path missing";
386         }
387
388         unlink "$full_path.usage" if -e "$full_path.usage";
389 }
390
391 1;