| File: | blib/lib/App/FileSync/Store.pm |
| Coverage: | 91.1% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::FileSync::Store; | |||||
| 2 | ||||||
| 3 | # The server's view of one repo: the master tree, its index (master.json), the | |||||
| 4 | # per-client bases, the trash, the journal and the session lock, all under | |||||
| 5 | # ROOT/master/REPO and ROOT/.filesync/repos/REPO. | |||||
| 6 | ||||||
| 7 | 314 314 314 | 829 292 4586 | use App::FileSync::Platform; | |||
| 8 | ||||||
| 9 | 314 314 314 | 6052 2758 4454 | use strict; | |||
| 10 | 314 314 314 | 1028 768 7113 | use warnings; | |||
| 11 | 314 314 314 | 698 248 1075 | use autodie qw(:all); | |||
| 12 | ||||||
| 13 | 314 314 314 | 686276 317 7887 | use File::Basename qw(dirname); | |||
| 14 | 314 314 314 | 651 270 4330 | use File::Spec; | |||
| 15 | 314 314 314 | 507 256 1189 | use Sub::Private { mode => 'enforce' }; | |||
| 16 | ||||||
| 17 | 314 314 314 | 36631 279 1041 | use parent -norequire, 'App::FileSync::Base'; | |||
| 18 | 314 314 314 | 6732 248 4534 | use App::FileSync::Base; | |||
| 19 | ||||||
| 20 | 314 314 314 | 555 243 9819 | use App::FileSync qw(json_encode json_decode read_text write_atomic validate_relpath validate_client_id validate_repo_name); | |||
| 21 | 314 314 314 | 559 265 4850 | use App::FileSync::Args qw(named); | |||
| 22 | 314 314 314 | 719 340 3900 | use App::FileSync::Config; | |||
| 23 | 314 314 314 | 558 505 15637 | use App::FileSync::Constants qw($STALE_TEMP_SECONDS $TRASH_DAYS); | |||
| 24 | 314 314 314 | 703 275 6773 | use App::FileSync::I18N qw(strip_location throw); | |||
| 25 | 314 314 314 | 549 4661 10109 | use App::FileSync::IO qw(append_line ensure_dir install_file); | |||
| 26 | 314 314 314 | 1733 756 3086 | use App::FileSync::Ignore; | |||
| 27 | 314 314 314 | 44829 392 4514 | use App::FileSync::Lock; | |||
| 28 | 314 314 314 | 49767 424 4978 | use App::FileSync::Scan; | |||
| 29 | 314 314 314 | 51852 429 4987 | use App::FileSync::Trash; | |||
| 30 | ||||||
| 31 | 314 314 314 | 665 238 563 | use version; our $VERSION = version->declare('0.001.1'); | |||
| 32 | ||||||
| 33 | =encoding utf8 | |||||
| 34 | ||||||
| 35 - 95 | =head1 NAME
App::FileSync::Store - the server-side files of one repo
=head1 SYNOPSIS
my $store = App::FileSync::Store->new(root => '/srv/filesync', repo => 'docs');
my $lock = $store->acquire_lock() or die 'busy';
my $index = $store->load_index();
my $scan = $store->walk_master($index);
$store->save_index($scan->{manifest});
=head1 METHODS
=head2 new
Purpose: open a repo on the server.
Args: C<root>, C<repo>, optional C<config> (the repo config, read from disk
when omitted) and C<language>.
Returns: the store.
Side Effects: reads the repo config.
Usage:
my $store = App::FileSync::Store->new(root => $root, repo => $repo);
=head3 EXAMPLE
my $store = App::FileSync::Store->new(root => '/srv/filesync', repo => 'docs');
print $store->master_dir, "\n";
=head3 API SPECIFICATION
=head4 INPUT
{
root => { type => 'string', min => 1 },
repo => { type => 'string', min => 1 },
config => { type => 'hashref', optional => 1 },
language => { type => 'string', optional => 1 },
}
=head4 OUTPUT
{ type => 'object', isa => 'App::FileSync::Store' }
=head3 MESSAGES
Key Meaning Resolution
args.invalid root or repo missing pass both
store.bad_repo the repo name is not an id use letters, digits, '.', '_', '-'
config.no_repo the repo does not exist filesync-admin repo create
=head3 FORMAL SPECIFICATION
new(r, n, c) = â¨root ⦠r, repo ⦠n, config ⦠c or load_repo(r, n), ignore ⦠Ignore(config.ignore)â©
=cut | |||||
| 96 | ||||||
| 97 | sub new { | |||||
| 98 | 384 | 2891 | my $class = shift; | |||
| 99 | 384 | 3385 | my $args = named(\@_, { | |||
| 100 | root => { type => 'string', min => 1 }, | |||||
| 101 | repo => { type => 'string', min => 1 }, | |||||
| 102 | config => { type => 'hashref', optional => 1 }, | |||||
| 103 | language => { type => 'string', optional => 1 }, | |||||
| 104 | }, 'Store->new'); | |||||
| 105 | ||||||
| 106 | # The name becomes a directory under the root, so it must be a plain id | |||||
| 107 | # before any path is built from it. | |||||
| 108 | 384 | 1231 | my ($ok, $reason) = validate_repo_name($args->{repo}); | |||
| 109 | 384 | 678 | throw('store.bad_repo', { reason => $reason }) unless $ok; | |||
| 110 | ||||||
| 111 | 383 | 1672 | my $config = $args->{config} || App::FileSync::Config::load_repo($args->{root}, $args->{repo}); | |||
| 112 | ||||||
| 113 | return bless { | |||||
| 114 | root => $args->{root}, | |||||
| 115 | repo => $args->{repo}, | |||||
| 116 | language => $args->{language}, | |||||
| 117 | config => $config, | |||||
| 118 | 383 | 2695 | ignore => App::FileSync::Ignore->new(patterns => $config->{ignore}), | |||
| 119 | }, $class; | |||||
| 120 | } | |||||
| 121 | ||||||
| 122 - 196 | =head2 root
Documented with L</journal_path> below.
=head2 repo
Documented with L</journal_path> below.
=head2 config
Documented with L</journal_path> below.
=head2 master_dir
Documented with L</journal_path> below.
=head2 meta_dir
Documented with L</journal_path> below.
=head2 index_path
Documented with L</journal_path> below.
=head2 trash_dir
Documented with L</journal_path> below.
=head2 lock_path
Documented with L</journal_path> below.
=head2 journal_path
Purpose: where the repo's pieces live.
Args: none.
Returns: the server root, the repo name, the config hashref, and the paths of
the master tree (F<ROOT/master/REPO>), the metadata directory, the index
(F<master.json>), the trash, the lock file and this month's journal
(F<journal/YYYY-MM.jsonl>, UTC).
Side Effects: none.
Usage:
my $master = $store->master_dir;
=head3 EXAMPLE
print join("\n", $store->master_dir, $store->index_path, $store->journal_path), "\n";
=head3 API SPECIFICATION
=head4 INPUT
[ { name => 'self', type => 'object' } ]
=head4 OUTPUT
{ type => 'string' }
=head3 MESSAGES
None.
=head3 FORMAL SPECIFICATION
master_dir = root / "master" / repo
meta_dir = root / ".filesync" / "repos" / repo
index_path = meta_dir / "master.json"
journal_path = meta_dir / "journal" / yyyy-mm(now) â ".jsonl"
=cut | |||||
| 197 | ||||||
| 198 | sub root { | |||||
| 199 | 0 | 0 | my ($self) = @_; | |||
| 200 | ||||||
| 201 | 0 | 0 | return $self->{root}; | |||
| 202 | } | |||||
| 203 | ||||||
| 204 | sub repo { | |||||
| 205 | 0 | 0 | my ($self) = @_; | |||
| 206 | ||||||
| 207 | 0 | 0 | return $self->{repo}; | |||
| 208 | } | |||||
| 209 | ||||||
| 210 | sub config { | |||||
| 211 | 520 | 755 | my ($self) = @_; | |||
| 212 | ||||||
| 213 | 520 | 1939 | return $self->{config}; | |||
| 214 | } | |||||
| 215 | ||||||
| 216 | sub master_dir { | |||||
| 217 | 1701 | 1397 | my ($self) = @_; | |||
| 218 | ||||||
| 219 | 1701 | 13398 | return File::Spec->catdir($self->{root}, 'master', $self->{repo}); | |||
| 220 | } | |||||
| 221 | ||||||
| 222 | sub meta_dir { | |||||
| 223 | 1736 | 1587 | my ($self) = @_; | |||
| 224 | ||||||
| 225 | 1736 | 3169 | return App::FileSync::Config::repo_meta_dir($self->{root}, $self->{repo}); | |||
| 226 | } | |||||
| 227 | ||||||
| 228 | sub index_path { | |||||
| 229 | 420 | 603 | my ($self) = @_; | |||
| 230 | ||||||
| 231 | 420 | 728 | return File::Spec->catfile($self->meta_dir(), 'master.json'); | |||
| 232 | } | |||||
| 233 | ||||||
| 234 | sub trash_dir { | |||||
| 235 | 192 | 518 | my ($self) = @_; | |||
| 236 | ||||||
| 237 | 192 | 310 | return File::Spec->catdir($self->meta_dir(), 'trash'); | |||
| 238 | } | |||||
| 239 | ||||||
| 240 | sub lock_path { | |||||
| 241 | 181 | 280 | my ($self) = @_; | |||
| 242 | ||||||
| 243 | 181 | 494 | return File::Spec->catfile($self->meta_dir(), 'locks', 'repo.lock'); | |||
| 244 | } | |||||
| 245 | ||||||
| 246 | sub journal_path { | |||||
| 247 | 542 | 559 | my ($self) = @_; | |||
| 248 | ||||||
| 249 | 542 | 1285 | my @t = gmtime(); | |||
| 250 | 542 | 762 | return File::Spec->catfile($self->meta_dir(), 'journal', | |||
| 251 | sprintf('%04d-%02d.jsonl', $t[5] + 1900, $t[4] + 1)); | |||||
| 252 | } | |||||
| 253 | ||||||
| 254 - 295 | =head2 base_path
Purpose: where a client's base (its state at its last sync) is stored.
Args: C<$client>, a client id.
Returns: F<META/clients/CLIENT.json>.
Side Effects: none.
Usage:
my $path = $store->base_path('laptop-1');
=head3 EXAMPLE
unlink $store->base_path($client) if $forget;
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'client', type => 'string', min => 1 },
]
=head4 OUTPUT
{ type => 'string', min => 1 }
=head3 MESSAGES
Key Meaning Resolution
store.bad_client the id is not a valid client id use letters, digits, . _ -
=head3 FORMAL SPECIFICATION
pre client â Id
base_path(client) = meta_dir / "clients" / client â ".json"
=cut | |||||
| 296 | ||||||
| 297 | sub base_path { | |||||
| 298 | 314 | 504 | my ($self, $client) = @_; | |||
| 299 | ||||||
| 300 | # The id becomes a file name, so it must not be able to climb out of the | |||||
| 301 | # clients directory. | |||||
| 302 | 314 | 693 | my ($ok, $reason) = validate_client_id($client); | |||
| 303 | 314 | 541 | $self->_throw('store.bad_client', { reason => $reason }) unless $ok; | |||
| 304 | ||||||
| 305 | 313 | 594 | return File::Spec->catfile($self->meta_dir(), 'clients', "$client.json"); | |||
| 306 | } | |||||
| 307 | ||||||
| 308 - 345 | =head2 acquire_lock
Purpose: take the repo's session lock without waiting.
Args: none.
Returns: the lock handle, or C<undef> when another session holds it.
Side Effects: creates the lock file.
Usage:
my $lock = $store->acquire_lock() or return refuse('locked');
=head3 EXAMPLE
if (my $lock = $store->acquire_lock) { ... }
=head3 API SPECIFICATION
=head4 INPUT
[ { name => 'self', type => 'object' } ]
=head4 OUTPUT
{ type => 'handle', optional => 1 }
=head3 MESSAGES
Key Meaning Resolution
io.append the lock file cannot be made check permissions
=head3 FORMAL SPECIFICATION
acquire_lock = Lock::acquire(lock_path, nonblock)
=cut | |||||
| 346 | ||||||
| 347 | sub acquire_lock { | |||||
| 348 | 177 | 244 | my ($self) = @_; | |||
| 349 | ||||||
| 350 | 177 | 527 | return App::FileSync::Lock::acquire(path => $self->lock_path(), nonblock => 1); | |||
| 351 | } | |||||
| 352 | ||||||
| 353 - 396 | =head2 abs_path
Purpose: the absolute path in the master for a relative path.
Args: C<$rel>.
Returns: the path under L</master_dir>.
Side Effects: none. Raises if C<$rel> is unsafe or if a directory on the way
is a symlink, so nothing can be written outside the master through one.
Usage:
my $abs = $store->abs_path($path);
=head3 EXAMPLE
my $abs = $store->abs_path('notes/plan.txt');
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'rel', type => 'string' },
]
=head4 OUTPUT
{ type => 'string', min => 1 }
=head3 MESSAGES
Key Meaning Resolution
path.unsafe the path fails validate_relpath rename the file
store.symlink a parent directory is a symlink remove the symlink
=head3 FORMAL SPECIFICATION
pre rel â SafePath â§ â a : ancestors(master / rel) â© under(master) ⢠kind a â symlink
abs_path(rel) = master / rel
=cut | |||||
| 397 | ||||||
| 398 | sub abs_path { | |||||
| 399 | 661 | 591 | my ($self, $rel) = @_; | |||
| 400 | ||||||
| 401 | 661 | 833 | my ($ok, $reason) = validate_relpath($rel); | |||
| 402 | 661 | 670 | $self->_throw('path.unsafe', { path => $rel, reason => $reason }) unless $ok; | |||
| 403 | ||||||
| 404 | 661 | 1100 | $self->_assert_no_symlinked_parent($rel); | |||
| 405 | ||||||
| 406 | 661 | 668 | return File::Spec->catfile($self->master_dir(), split m{/}, $rel); | |||
| 407 | } | |||||
| 408 | ||||||
| 409 - 468 | =head2 load_index
Documented with L</save_base> below.
=head2 save_index
Documented with L</save_base> below.
=head2 load_base
Documented with L</save_base> below.
=head2 save_base
Purpose: read and write the master's index and a client's base.
Args: C<save_index(\%entries)>; C<load_base($client)>;
C<save_base($client, \%entries)>.
Returns: the entries hashref (empty when the file does not exist); 1 for the
save methods.
Side Effects: the save methods replace the file atomically.
Usage:
my $index = $store->load_index();
$store->save_base($client, $base_after);
=head3 EXAMPLE
my $base = $store->load_base('laptop-1');
print scalar(keys %{$base}), " paths known to laptop-1\n";
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'client', type => 'string', optional => 1 },
{ name => 'entries', type => 'hashref', optional => 1 },
]
=head4 OUTPUT
{ type => 'hashref' }
=head3 MESSAGES
Key Meaning Resolution
config.parse the file is not valid JSON run filesync-admin reindex
io.write the file cannot be written check space
=head3 FORMAL SPECIFICATION
load(p) = json(FS(p)).entries if p â dom FS â§ is a hash, else â
save(p, e): FS'(p) = json(â¨version ⦠1, entries ⦠eâ©) â "\n"
=cut | |||||
| 469 | ||||||
| 470 | sub load_index { | |||||
| 471 | 189 | 1809729 | my ($self) = @_; | |||
| 472 | ||||||
| 473 | 189 | 475 | return $self->_load_entries($self->index_path()); | |||
| 474 | } | |||||
| 475 | ||||||
| 476 | sub save_index { | |||||
| 477 | 225 | 362 | my ($self, $entries) = @_; | |||
| 478 | ||||||
| 479 | 225 | 764 | return $self->_save_entries($self->index_path(), $entries); | |||
| 480 | } | |||||
| 481 | ||||||
| 482 | sub load_base { | |||||
| 483 | 167 | 301 | my ($self, $client) = @_; | |||
| 484 | ||||||
| 485 | 167 | 438 | return $self->_load_entries($self->base_path($client)); | |||
| 486 | } | |||||
| 487 | ||||||
| 488 | sub save_base { | |||||
| 489 | 143 | 324 | my ($self, $client, $entries) = @_; | |||
| 490 | ||||||
| 491 | 143 | 376 | return $self->_save_entries($self->base_path($client), $entries); | |||
| 492 | } | |||||
| 493 | ||||||
| 494 - 536 | =head2 walk_master
Purpose: scan the master, reusing indexed hashes where size and mtime match.
Args: optional C<\%cache> (usually the current index; C<{}> forces a rehash).
Returns: the L<App::FileSync::Scan/scan> result: C<{ manifest, stats, skipped }>.
Side Effects: reads the master tree.
Usage:
my $scan = $store->walk_master($store->load_index);
=head3 EXAMPLE
my $fresh = $store->walk_master({});
printf "%d files\n", $fresh->{stats}{files};
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'cache', type => 'hashref', optional => 1 },
]
=head4 OUTPUT
{ type => 'hashref' }
=head3 MESSAGES
Key Meaning Resolution
store.no_master the master directory is gone restore it or recreate the repo
=head3 FORMAL SPECIFICATION
pre isdir(master_dir)
walk_master(c) = scan(master_dir, ignore, c)
=cut | |||||
| 537 | ||||||
| 538 | sub walk_master { | |||||
| 539 | 174 | 302 | my ($self, $cache) = @_; | |||
| 540 | ||||||
| 541 | 174 | 433 | my $master = $self->master_dir(); | |||
| 542 | 174 | 1341 | $self->_throw('store.no_master', { path => $master }) unless -d $master; | |||
| 543 | ||||||
| 544 | return App::FileSync::Scan::scan( | |||||
| 545 | root => $master, | |||||
| 546 | ignore => $self->{ignore}, | |||||
| 547 | 174 | 796 | cache => $cache || {}, | |||
| 548 | ); | |||||
| 549 | } | |||||
| 550 | ||||||
| 551 - 590 | =head2 ensure_parent
Purpose: create the directory a master path will be written into.
Args: C<$rel>.
Returns: the parent directory.
Side Effects: creates missing directories.
Usage:
$store->ensure_parent($path);
=head3 EXAMPLE
$store->ensure_parent('new/deep/dir/file.txt');
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'rel', type => 'string', min => 1 },
]
=head4 OUTPUT
{ type => 'string', min => 1 }
=head3 MESSAGES
As for L</abs_path>, plus C<io.mkdir>.
=head3 FORMAL SPECIFICATION
isdir'(dirname(abs_path(rel)))
=cut | |||||
| 591 | ||||||
| 592 | sub ensure_parent { | |||||
| 593 | 181 | 192 | my ($self, $rel) = @_; | |||
| 594 | ||||||
| 595 | 181 | 324 | return ensure_dir(dirname($self->abs_path($rel))); | |||
| 596 | } | |||||
| 597 | ||||||
| 598 - 640 | =head2 temp_file
Purpose: an exclusive temporary file beside a master path.
Args: C<$rel>, the path the temporary file will become.
Returns: C<[ $filehandle, $path ]>.
Side Effects: creates the file (and its directory).
Usage:
my ($fh, $temp) = @{ $store->temp_file($path) };
=head3 EXAMPLE
my ($fh, $temp) = @{ $store->temp_file('notes/plan.txt') };
print {$fh} $content;
close $fh;
$store->commit_temp('notes/plan.txt', $temp, time, oct(644));
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'rel', type => 'string', min => 1 },
]
=head4 OUTPUT
{ type => 'arrayref', min => 2, max => 2 }
=head3 MESSAGES
As for L</abs_path>, plus C<io.temp>.
=head3 FORMAL SPECIFICATION
temp_file(rel) = App::FileSync::IO::temp_file(dirname(abs_path(rel)))
=cut | |||||
| 641 | ||||||
| 642 | sub temp_file { | |||||
| 643 | 176 | 210 | my ($self, $rel) = @_; | |||
| 644 | ||||||
| 645 | 176 | 229 | return App::FileSync::IO::temp_file(dirname($self->abs_path($rel))); | |||
| 646 | } | |||||
| 647 | ||||||
| 648 - 690 | =head2 commit_temp
Purpose: move a verified temporary file into the master.
Args: C<$rel>, C<$temp>, optional C<$mtime> and C<$mode>.
Returns: the absolute path written.
Side Effects: see L<App::FileSync::IO/install_file>.
Usage:
$store->commit_temp($path, $temp, $mtime, $mode);
=head3 EXAMPLE
$store->commit_temp('a.txt', $temp, 1_700_000_000, oct(600));
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'rel', type => 'string', min => 1 },
{ name => 'temp', type => 'string', min => 1 },
{ name => 'mtime', type => 'number', optional => 1 },
{ name => 'mode', type => 'integer', optional => 1 },
]
=head4 OUTPUT
{ type => 'string', min => 1 }
=head3 MESSAGES
As for L<App::FileSync::IO/install_file> and L</abs_path>.
=head3 FORMAL SPECIFICATION
commit_temp(rel, t, m, mode) = install_file(t, abs_path(rel), m, mode)
=cut | |||||
| 691 | ||||||
| 692 | sub commit_temp { | |||||
| 693 | 174 | 276 | my ($self, $rel, $temp, $mtime, $mode) = @_; | |||
| 694 | ||||||
| 695 | 174 | 287 | return install_file($temp, $self->abs_path($rel), $mtime, $mode); | |||
| 696 | } | |||||
| 697 | ||||||
| 698 - 739 | =head2 move_to_trash
Purpose: move a master path into the repo's trash.
Args: C<$rel>, optional C<$epoch>.
Returns: where it went, or C<undef> if it was not there.
Side Effects: see L<App::FileSync::Trash/move_to_trash>.
Usage:
my $trashed = $store->move_to_trash($path, time);
=head3 EXAMPLE
my $where = $store->move_to_trash('old/report.txt');
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'rel', type => 'string', min => 1 },
{ name => 'epoch', type => 'number', optional => 1 },
]
=head4 OUTPUT
{ type => 'string', optional => 1 }
=head3 MESSAGES
As for L<App::FileSync::Trash/move_to_trash> and L</abs_path>.
=head3 FORMAL SPECIFICATION
abs_path(rel) â dom FS â result = â¥
abs_path(rel) â dom FS â result = Trash::move_to_trash(abs_path(rel), rel, trash_dir, epoch)
=cut | |||||
| 740 | ||||||
| 741 | sub move_to_trash { | |||||
| 742 | 17 | 23 | my ($self, $rel, $epoch) = @_; | |||
| 743 | ||||||
| 744 | 17 | 28 | my $abs = $self->abs_path($rel); | |||
| 745 | 17 | 126 | return undef unless -e $abs || -l $abs; ## no critic (Subroutines::ProhibitExplicitReturnUndef) | |||
| 746 | ||||||
| 747 | 17 | 36 | return App::FileSync::Trash::move_to_trash( | |||
| 748 | src => $abs, | |||||
| 749 | rel => $rel, | |||||
| 750 | root => $self->trash_dir(), | |||||
| 751 | epoch => defined $epoch ? $epoch : time, | |||||
| 752 | ); | |||||
| 753 | } | |||||
| 754 | ||||||
| 755 - 804 | =head2 clean_stale_temps
Documented with L</prune_trash> below.
=head2 prune_trash
Purpose: housekeeping at the start of a session.
Args: C<clean_stale_temps(max_age =E<gt> SECONDS)>;
C<prune_trash(days =E<gt> N, dry_run =E<gt> BOOL)>. Defaults: one hour, and
the repo's C<trash_days> (30).
Returns: the number of temporaries removed; the arrayref of trash buckets
removed.
Side Effects: deletes stale temporaries from the master and expired trash.
Usage:
$store->clean_stale_temps();
$store->prune_trash();
=head3 EXAMPLE
my $pruned = $store->prune_trash(dry_run => 1);
=head3 API SPECIFICATION
=head4 INPUT
{
max_age => { type => 'integer', min => 0, optional => 1 },
days => { type => 'number', optional => 1 },
dry_run => { type => 'boolean', optional => 1 },
}
=head4 OUTPUT
{ type => 'scalar' }
=head3 MESSAGES
None.
=head3 FORMAL SPECIFICATION
clean_stale_temps = Scan::clean_stale_temps(master_dir, age)
prune_trash = Trash::prune(trash_dir, days)
=cut | |||||
| 805 | ||||||
| 806 | sub clean_stale_temps { | |||||
| 807 | 167 | 341 | my ($self, %args) = @_; | |||
| 808 | ||||||
| 809 | 167 | 432 | my $master = $self->master_dir(); | |||
| 810 | 167 | 1012 | return 0 unless -d $master; | |||
| 811 | ||||||
| 812 | return App::FileSync::Scan::clean_stale_temps( | |||||
| 813 | root => $master, | |||||
| 814 | 167 | 806 | max_age => defined $args{max_age} ? $args{max_age} : $STALE_TEMP_SECONDS, | |||
| 815 | ); | |||||
| 816 | } | |||||
| 817 | ||||||
| 818 | sub prune_trash { | |||||
| 819 | 161 | 264 | my ($self, %args) = @_; | |||
| 820 | ||||||
| 821 | return App::FileSync::Trash::prune( | |||||
| 822 | root => $self->trash_dir(), | |||||
| 823 | days => defined $args{days} ? $args{days} : ($self->{config}{trash_days} || $TRASH_DAYS), | |||||
| 824 | dry_run => $args{dry_run}, | |||||
| 825 | 161 | 382 | ); | |||
| 826 | } | |||||
| 827 | ||||||
| 828 - 868 | =head2 journal
Purpose: append an event to this month's journal.
Args: C<\%event>; a C<ts> (now) is added.
Returns: 1.
Side Effects: appends one JSON line, creating the journal directory.
Usage:
$store->journal({ client => $id, action => 'push', path => $path, result => 'ok' });
=head3 EXAMPLE
$store->journal({ action => 'reindex', files => 10, result => 'ok' });
=head3 API SPECIFICATION
=head4 INPUT
[
{ name => 'self', type => 'object' },
{ name => 'event', type => 'hashref' },
]
=head4 OUTPUT
{ type => 'integer', min => 1, max => 1 }
=head3 MESSAGES
Key Meaning Resolution
io.append the journal cannot be written check space
=head3 FORMAL SPECIFICATION
FS'(journal_path) = FS(journal_path) â json({ts ⦠now} â event) â "\n"
=cut | |||||
| 869 | ||||||
| 870 | sub journal { | |||||
| 871 | 542 | 640 | my ($self, $event) = @_; | |||
| 872 | ||||||
| 873 | 542 542 | 961 1747 | append_line($self->journal_path(), json_encode({ ts => time, %{$event} })); | |||
| 874 | 542 | 1255 | return 1; | |||
| 875 | } | |||||
| 876 | ||||||
| 877 | # _assert_no_symlinked_parent | |||||
| 878 | # Purpose: refuse to write through a symlinked directory in the master. | |||||
| 879 | # Entry: $rel has passed validate_relpath. | |||||
| 880 | # Exit: returns 1. | |||||
| 881 | # Side effects: raises store.symlink. Stops at the first missing directory: | |||||
| 882 | # nothing below it can be a symlink yet. | |||||
| 883 | sub _assert_no_symlinked_parent :Private { | |||||
| 884 | my ($self, $rel) = @_; | |||||
| 885 | ||||||
| 886 | my @parts = split m{/}, $rel; | |||||
| 887 | pop @parts; | |||||
| 888 | ||||||
| 889 | my $current = $self->master_dir(); | |||||
| 890 | for my $part (@parts) { | |||||
| 891 | $current = File::Spec->catfile($current, $part); | |||||
| 892 | last unless -e $current || -l $current; | |||||
| 893 | $self->_throw('store.symlink', { path => $current }) if -l $current; | |||||
| 894 | } | |||||
| 895 | ||||||
| 896 | return 1; | |||||
| 897 | 314 314 314 | 217171 276 3843 | } | |||
| 898 | ||||||
| 899 | # _load_entries | |||||
| 900 | # Purpose: read an index or base file. | |||||
| 901 | # Entry: $path may not exist. | |||||
| 902 | # Exit: returns the entries hashref (empty for a missing file or one | |||||
| 903 | # without an entries hash). | |||||
| 904 | # Side effects: raises config.parse for a corrupt file. | |||||
| 905 | sub _load_entries :Private { | |||||
| 906 | my ($self, $path) = @_; | |||||
| 907 | ||||||
| 908 | return {} unless -e $path; | |||||
| 909 | ||||||
| 910 | my $text = read_text($path); | |||||
| 911 | my $document = eval { json_decode($text) }; | |||||
| 912 | $self->_throw('config.parse', { path => $path, error => strip_location($@) }) if $@; | |||||
| 913 | ||||||
| 914 | return (ref($document) eq 'HASH' && ref($document->{entries}) eq 'HASH') ? $document->{entries} : {}; | |||||
| 915 | 314 314 314 | 115798 263 2619 | } | |||
| 916 | ||||||
| 917 | # _save_entries | |||||
| 918 | # Purpose: write an index or base file. | |||||
| 919 | # Entry: $entries may be undef (written as empty). | |||||
| 920 | # Exit: returns 1. | |||||
| 921 | # Side effects: replaces $path atomically. | |||||
| 922 | sub _save_entries :Private { | |||||
| 923 | my ($self, $path, $entries) = @_; | |||||
| 924 | ||||||
| 925 | write_atomic($path, json_encode({ version => 1, entries => $entries || {} }) . "\n"); | |||||
| 926 | return 1; | |||||
| 927 | 314 314 314 | 96872 262 2353 | } | |||
| 928 | ||||||
| 929 | 1; | |||||
| 930 | ||||||