Bivio::Util::RealmFile
# Copyright (c) 2005-2011 bivio Software, Inc. All Rights Reserved. # $Id$ package Bivio::Util::RealmFile; use strict; use Bivio::Base 'Bivio.ShellUtil'; use File::Find (); my($_A) = b_use('IO.Alert'); my($_D) = b_use('Bivio.Die'); my($_DT) = b_use('Type.DateTime'); my($_F) = b_use('IO.File'); my($_FP) = b_use('Type.FilePath'); my($_MFN) = b_use('Type.MailFileName'); sub OPTIONS { return { %{shift->SUPER::OPTIONS(@_)}, is_public => [Boolean => 0], is_read_only => [Boolean => 0], }; } sub OPTIONS_USAGE { return shift->SUPER::OPTIONS_USAGE(@_) . <<'EOF' -is_public - operate on public files (default: 0) -is_read_only - operate on read only files (default: 0) EOF } sub USAGE { return <<'EOF'; usage: b-realm-file [options] command [args...] commands: audit_folders -- correct folder modified_date_time and user_id backup_realms dir realm... - export_tree for all realms in dir/<date-time> clear_files_and_mail create path -- creates file_path with input create_or_update path -- creates or updates file_path with input create_folder path -- creates folder and parents delete_deep path ... -- deletes files or folders specified rename old new --- moves old to new export_tree folder [noarchive] -- exports an entire tree to current directory import_tree [folder] [noarchive] -- imports files in current directory into folder [/] list_folder folder -- lists a folder purge_archive [min_file_size] -- deletes archived files read path -- returns file contents send_file_via_mail email subject path -- email a file as an attachment update path -- updates path with input EOF } sub audit_folders { my($self) = @_; $self->model('RealmFile')->do_iterate(sub { my($rf) = @_; my($max, $user_id) = _max_modified_time($self, $rf); $rf->update({ override_is_read_only => 1, modified_date_time => $max, user_id => $user_id, }) if $max; return 1; }, { is_folder => 1, }); return; } sub backup_realms { my($self, $base_dir, @realms) = @_; my($root) = $_FP->join($base_dir, $_DT->local_now_as_file_name); foreach my $r (@realms) { my($die) = b_catch(sub { $_F->do_in_dir( $_F->mkdir_p($_FP->join($root, $r)), sub { $self->req->with_realm( $r, sub {$self->export_tree('/', 1)}, ); $self->piped_exec("sh -c 'cd .. && tar czf $r.tgz $r && rm -rf $r' 2>&1"); return; }, ); }); b_warn($r, ': ', $die) if $die; } return; } sub clear_files_and_mail { my($self) = @_; $self->assert_test; $self->are_you_sure('delete realm files and mail?'); $self->model('RealmMail')->delete_all; $self->model('RealmFile')->delete_all; return; } sub create { my($self, $path) = @_; _do($self, create_with_content => $path, $self->read_input); return; } sub create_or_update { my($self, $path) = @_; _do($self, create_or_update_with_content => $path, $self->read_input); return; } sub create_folder { my($self, $path) = @_; _do($self, create_folder => $path); return; } sub delete_deep { my($self) = shift; foreach my $p (@_) { _do($self, 'unauth_delete_deep', $p); } return; } sub export_tree { my($self, $folder, $noarchive) = shift->name_args([ [qw(folder FilePath)], [qw(?noarchive Boolean)], ], \@_); $self->initialize_ui; $folder .= '/' unless length($folder) == 1; my($re) = qr{^\Q$folder\E}is; $self->model('RealmFile')->do_iterate(sub { my($it) = @_; return 1 unless (my $p = $it->get('path')) =~ $re; return 1 if $noarchive && $it->is_version; $p =~ s{^/}{}; return 1 unless $p; if ($it->get('is_folder')) { $_F->mkdir_p($p); } else { $_F->mkdir_parent_only($p); $_F->write($p, $it->get_content); $_F->chmod(0444, $p) if $it->get('is_read_only'); } $_F->set_modified_date_time($p, $it->get('modified_date_time')); return 1; }); return; } sub folder_sizes { sub FOLDER_SIZES {[[qw(folder FilePath /)]]} my($self, $bp) = shift->parameters(\@_); $self->get_request; my($res) = {TOTAL => 0}; my($pat) = qr{^\Q@{[$_FP->add_trailing_slash($bp->{folder})]}\E}i; $self->model('RealmFile')->do_iterate( sub { my($it) = @_; my($p) = $it->get('path'); return 1 unless $p =~ $pat; my($l) = $it->get_content_length; $res->{($p =~ m{(.*)/})[0] || '/'} += $l; $res->{TOTAL} += $l; return 1; }, undef, {is_folder => 0}, ); return join( '', sprintf("%6s %s\n", 'KB', 'Folder'), map( sprintf("%6d %s\n", $res->{$_}/1024, $_), sort(keys(%$res)), ), ); } sub import_tree { my($self, $folder, $noarchive) = shift->name_args([ [qw(?folder String)], [qw(?noarchive Boolean)], ], \@_); my($req) = $self->initialize_ui; $folder = $folder ? $self->convert_literal(FilePath => $folder) : '/'; my($folders) = []; my($files) = []; my($vc_re) = b_use('Util.VC')->CONTROL_DIR_RE; File::Find::find( { wanted => sub { my($name) = $_; if ($name =~ $vc_re) { $File::Find::prune = 1; return; } return if $name =~ m{(^|/)(\..*|.*~|#.*)$}; push( @{-d $name ? $folders : $files}, [$File::Find::name, (stat($name))[9]], ); return; }, }, '.', ); foreach my $x ( @$folders, sort({$a->[1] <=> $b->[1]} @$files), ) { my($name, $mtime) = @$x; my($f) = $name =~ m{^\./(.+)}; my($path) = $self->convert_literal('FilePath', "$folder/$f"); my($method) = -d $name ? 'create_folder' : 'create_with_content'; my($rf) = $self->model('RealmFile'); if ($rf->unsafe_load({path => $path})) { next if $rf->get('is_folder'); $method = 'update_with_content'; } my($modified_date_time) = $_DT->from_unix($mtime); if ($_MFN->is_absolute($path)) { $self->model('RealmMail') ->load({realm_file_id => $rf->get('realm_file_id')}) ->delete_message if $rf->is_loaded; my($in); my($die) = $_D->catch_quietly( sub { $in = $self->model('RealmMail') ->create_from_rfc822($_F->read($name)); return; }, ); if ($die) { b_info( 'mail from rfc822 failed: ', 'name: ', $name, ' err: ', ($die->unsafe_get('attrs') || {})->{message}, ); next; } my($rf) = $self->req('Model.RealmFile'); $rf->update({ override_is_read_only => 1, path => $path, modified_date_time => $in->get_date_time || $modified_date_time, }); b_die('public mismatch') unless $_MFN->is_public($path) eq $self->req(qw(Model.RealmFile is_public)); next; } $rf->$method( _fix_values($self, $path, { modified_date_time => $modified_date_time, $noarchive ? (override_versioning => 1) : (), }), $method =~ /content/ ? $_F->read($name) : (), ); next; } $self->audit_folders; $self->model('RealmMail')->audit_threads; return; } sub list_folder { my($self, $path) = @_; $self->initialize_fully; return $self->model('RealmFileList')->map_iterate( sub {shift->get('RealmFile.path')}, {path_info => $self->convert_literal('FilePath', $path)}, ); } sub purge_archive { my($self, $file_size) = @_; $file_size = defined($file_size) ? $file_size : 1; $self->are_you_sure('Delete all archived files larger than ' . $file_size . 'M in ' . $self->req(qw(auth_realm owner name)) . '?'); my($m) = 1024 * 1024; $file_size *= $m; my($c) = 0; my($commit) = sub { $self->commit_or_rollback; $_A->reset_warn_counter; return; }; $self->model('RealmFile')->do_iterate(sub { my($rf) = @_; return 1 unless $rf->is_version; return 1 if $rf->get('is_folder'); return 1 if $rf->get_content_length < $file_size; #TODO(robnagler) this does not seem useful # $self->print($rf->get('realm_file_id'), # ' ', int($rf->get_content_length / $m), # 'M ', $rf->get('path'), "\n"); # $deleted_size += $rf->get_content_length / $m; $rf->new_other('RealmFileLock')->delete_all({ realm_file_id => $rf->get('realm_file_id'), }); $rf->delete({ override_versioning => 1, override_is_read_only => 1, }); if (++$c % 100 == 0) { b_info($c); $commit->(); } return 1; }); return $c . ' archive files deleted'; } sub read { my($self, $path) = @_; return _do($self, load => $path)->get_content; } sub rename { my($self, $old, $new) = @_; _do($self, load => $old)->update({path => $new}); return; } sub send_file_via_mail { my($self, $email, $subject, $path) = @_; $self->send_mail($email, $subject || $path, _do($self, load => $path)); return; } sub update { my($self, $path) = @_; _do($self, load => $path)->update_with_content({}, $self->read_input); return; } sub _do { my($self, $method, $path, @args) = @_; $self->initialize_fully; return $self->model('RealmFile') ->$method(_fix_values($self, $path, {}, $method =~ /(delete|load)/), @args); } sub _fix_values { my($self, $path, $values, $ignore_is) = @_; return { $values ? %$values : (), path => $self->convert_literal('FilePath', $path), $ignore_is ? () : map(($_ => $self->get($_)), qw(is_public is_read_only)), $self->get('force') ? (override_is_read_only => 1) : (), }; } sub _max_modified_time { my($self, $folder) = @_; my($max) = $_DT->get_min; my($user_id); $self->model('RealmFile')->do_iterate(sub { my($rf) = @_; my($v, $u) = $rf->get('is_folder') ? _max_modified_time($self, $rf) : $rf->get(qw(modified_date_time user_id)); if ($_DT->compare($v, $max) > 0) { $max = $v; $user_id = $u; } return 1; }, { folder_id => $folder->get('realm_file_id'), }); return $max eq $_DT->get_min ? undef : ($max, $user_id); } 1;