directories don't have md5sum
[cloudstore.git] / lib / CloudStore / Store.pm
1 package CloudStore::Store;
2 use warnings;
3 use strict;
4
5 use autodie;
6 use JSON::XS;
7 use File::Path qw(make_path);
8 use File::Slurp qw();
9 use Digest::MD5 qw(md5_base64);
10 use Data::Dump qw(dump);
11 use Carp qw(confess);
12 use BerkeleyDB;
13
14 use WarnColor;
15
16 sub new {
17         my $class = shift;
18
19         my $self = {@_};
20         bless $self, $class;
21
22         die "no dir" unless $self->{dir};
23         $self->{md5pool} = $self->{dir} . '/md5';
24
25         warn "# new ",dump $self if $ENV{DEBUG};
26
27         return $self;
28 }
29
30 sub user_set {
31         my ( $self,$data ) = @_;
32 }
33
34 sub user_get {
35         my ( $self,$data ) = @_;
36 }
37
38 sub modify_file {
39         my ( $self,$data ) = @_;
40
41 #       if ( my $old = $self->file_get( $data ) ) {
42 #               $self->usage_decr( $data );
43 #       }
44
45         $self->new_file($data);
46 }
47
48 sub new_file {
49         my ( $self,$data ) = @_;
50 #       $self->file_set($data);
51 #       $self->usage_incr($data);
52 }
53
54 sub remove_file {
55         my ( $self, $data ) = @_;
56 #       $self->usage_decr( $data );
57
58         my $md5sum = $self->md5sum($data);
59         my $md5 = $md5sum->{ $data->{file} } || return; # directories don't have md5sums
60         my $path = $self->{md5pool} . '/' . $md5;
61         my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
62                 $atime,$mtime,$ctime,$blksize,$blocks)
63                         = stat($path);
64         if ( $nlink == 1 ) {
65                 my $id = getpwnam 'md5';
66                 chown $id,$gid, $path;
67                 warn "# chown $id $gid $path";
68         }
69 }
70
71 sub make_dir {
72         my ( $self, $data ) = @_;
73
74 }
75
76 sub new_link {
77         my ( $self, $data ) = @_;
78
79         warn "# new_link ",dump $data;
80
81         if ( $data->{file} =~ m{^(.*/?)\.send/([^/]+)/(.+)$} ) {
82                 my ( $dir, $to, $name ) = ( $1, $2, $3 );
83                 my $path = $self->blob_path($data);
84                 my $link_to = readlink $path;
85                 warn "$link_to";
86                 if ( $link_to =~ s{^\Q/rsyncd-munged/\E}{/} ) {
87
88                         my $s = $path;
89                         $s =~ s{/[^/]+$}{}; # strip filename
90                         while ( $link_to =~ s{/../}{/} ) {
91                                 $s =~ s{/[^/]+$}{} || die "can't strip $s";
92                                 warn "## simplify $s $link_to\n";
93                         }
94                         $s .= $link_to;
95
96                         my $d = $self->blob_path({
97                                 pid => $data->{pid},
98                                 file => $name
99                         });
100
101                         # $name can contain directories so we must create them
102                         my $to_dir = $d;
103                         $to_dir =~ s{/[^/]+$}{};
104                         make_path $to_dir if ! -e $to_dir;
105
106                         if ( ! -e $s ) {
107                                 warn "ERROR: can't find source $s";
108                         } else {
109
110                                 warn "link $s -> $d\n";
111                                 link $s, $d;
112
113                                 my ($l,$f) = ($1,$2) if $s =~ m{users/([^/]+)/blob/(.+)};
114
115 #                               my $origin = $self->file_get({
116 #                                       login => $l,
117 #                                       file  => $f,
118 #                               });
119 #                               $self->new_file($origin);
120                                 warn "INFO: sent file ",dump($l,$f);
121                                 my $md5sum = $self->md5sum($data);
122
123                                 my $md5 = $md5sum->{$s} || die "no md5 for $s";
124                                 $md5sum->{$d} = $md5; # FIXME broken!
125                         }
126
127
128                 } else {
129                         warn "ERROR: can't SEND To:$to Name:$name Link:$link_to";
130                 }
131         }
132 }
133
134 our $md5_login;
135 sub md5sum {
136         my ( $self, $data ) = @_;
137
138         if ( exists $md5_login->{$data->{login}} ) {
139                 return $md5_login->{$data->{login}};
140         } elsif ( my $login = $data->{login} ) {
141
142                 my $md5_path = $self->{dir} || die "no dir?";
143                 $login =~ s/^u//;
144                 $md5_path .= "/$login/.md5.db";
145
146                 my %md5;
147                 my $db = tie %md5, 'BerkeleyDB::Hash',
148                         -Filename => $md5_path,
149                         -Flags => DB_CREATE,
150                 ;
151
152                 return $md5_login->{$login} = \%md5;
153         } else {
154                 confess "can't open md5sum";
155         }
156 }
157
158 sub rsync_log {
159         my ( $self, $data ) = @_;
160         if ( $data =~ m/\[(\d+)\] rsync \w+ (\S+) \w+ (\S+)/ ) {
161                 my ( $pid, $module, $login ) = ( $1, $2, $3 );
162
163                 $login =~ s/\@.+//;
164                 my ( undef, undef, $uid, $gid, undef, undef, $email, $dir, $shell ) =
165                         getpwnam $login;
166
167                 $self->{pid}->{$pid} = {
168                         login => $login,
169                         uid => $uid,
170                         gid => $gid,
171                         email => $email,
172                         dir => $dir,
173                         shell => $shell,
174                 };
175
176                 warn "created $pid";
177
178         } elsif ( $data =~ m/\[(\d+)\] sent \S+ bytes\s+received \S+ bytes/ ) {
179                 my $pid = $1;
180                 untie $md5_login->{ $self->{$pid}->{login} } && warn "untie $pid";
181                 delete $self->{pid}->{$pid};
182                 warn "removed $pid";
183         } else {
184 #               warn "## rsync_log $data";
185         }
186 }
187
188 sub blob_path {
189         my ( $self, $data ) = @_;
190         my $blob = $self->{pid}->{ $data->{pid} }->{dir} || die "no dir for $data->{pid} in ",dump( $self->{pid} );
191         $blob .= '/' . $data->{file};
192         return $blob;
193 }
194
195
196 sub rsync_transfer {
197         my ( $self,$data ) = @_;
198
199         my $path = $self->blob_path($data);
200
201         if ( $data->{itemize} =~ m/^[c>]([fdL])/ ) { # received change/create
202                 my $type = $1;
203
204                 if ( $type eq 'f' ) {
205                         $self->modify_file( $data );
206                         $self->dedup( $data, $path );
207                 } elsif ( $type eq 'd' ) {
208                         $self->make_dir( $data );
209                 } elsif ( $type eq 'L' ) {
210                         $self->new_link( $data );
211                 } else {
212                         die "unknown type $type ", dump $data;
213                 }
214         } elsif ( $data->{itemize} =~ m/\*deleting/ ) {
215                 $self->remove_file($data);
216         }
217         return $data;
218 }
219
220 sub md5pool {
221         my ( $self, $data ) = @_;
222
223         my $pool = $self->{md5pool} || die "no md5pool in ",dump $self;
224         mkdir $pool unless -e $pool;
225
226         my $md5 = $data->{md5} || die "no md5 in ",dump $data;
227         my $path = $self->blob_path($data);
228
229         if ( -e "$pool/$md5" ) {
230                 warn "dedup hit $md5 $path\n";
231                 my $dedup = $path . '.dedup';
232                 rename $path, $dedup;
233                 link "$pool/$md5", $path;
234                 unlink $dedup;
235                 # FIXME fix perms?
236         } else {
237                 link $path, "$pool/$md5";
238         }
239
240         my $md5sum = $self->md5sum($data);
241         $md5sum->{ $data->{file} } = $md5;
242 }
243
244 my $empty_md5 = " " x 32;
245
246 sub dedup {
247         my ( $self, $data, $path ) = @_;
248
249         if ( $data->{file} =~ /^(.+\/)?md5sum$/ ) {
250                 my $dir = $1;
251                 my $imported = 0;
252                 warn "IMPORT ", $data->{file}, "\n";
253                 open(my $md5sum, '<', $path);
254                 while(<$md5sum>) {
255                         chomp;
256                         my ( $md5, $file ) = split(/\s+/,$_,2);
257                         if ( ! -e "$self->{md5path}/$md5" ) {
258                                 warn "MISSING $md5 $file\n";
259                                 next;
260                         }
261                         my $new = {
262                                 pid => $data->{pid},
263                                 file => "$dir$file",
264                                 md5 => $md5,
265                         };
266                         my $new_path = $self->blob_path($new);
267                         if ( ! -e $new_path ) {
268                                 # create path from md5sum file
269                                 my $only_dir = $1 if $new =~ m{^(.+)/[^/]+$};
270                                 make_path $only_dir unless -d $only_dir;
271                                 $imported += link "$self->{md5path}/$md5", $new_path;
272                                 $self->new_file($new);
273                                 warn "import from $path ",dump($new);
274                                 $self->md5pool( $new );
275                         } else {
276                                 $self->md5pool( $new );
277                         }
278                 }
279                 print "INFO imported $imported files from ",dump($data);
280         }
281
282         if ( $data->{md5} ne $empty_md5 ) {
283                 $self->md5pool( $data );
284         } else {
285                 warn "empty md5", dump $data;
286         }
287 }
288
289 1;