File Coverage

File:blib/lib/App/FileSync/Server.pm
Coverage:96.5%

linestmtbrancondsubtimecode
1package App::FileSync::Server;
2
3# The server end of one sync session.  It is started by sshd (or the local
4# transport) with the repo, client and role pinned on its command line, speaks
5# the lock-step protocol on its connection, and only ever does what its own
6# plan allows: a client cannot write, read, delete or acknowledge a path the
7# server did not plan for it.
8
9
300
300
300
1709
243
4263
use App::FileSync::Platform;
10
11
300
300
300
574
247
2745
use strict;
12
300
300
300
436
226
7376
use warnings;
13
300
300
300
546
221
864
use autodie qw(:all);
14
15
300
300
300
649467
341
2347
use Digest::SHA ();
16
300
300
300
526
278
2635
use File::Temp ();
17
300
300
300
502
230
7905
use List::Util qw(any);
18
300
300
300
598
242
6336
use Readonly;
19
300
300
300
574
264
1146
use Sub::Private { mode => 'enforce' };
20
300
300
300
36852
270
2640
use Time::HiRes ();
21
22
300
300
300
489
222
935
use parent 'App::FileSync::Base';
23
24
300
300
300
8826
254
6941
use App::FileSync qw(validate_relpath validate_client_id validate_repo_name name_with_suffix);
25
300
300
300
524
237
6643
use App::FileSync::Args qw(named);
26
300
20851
use App::FileSync::Constants qw(
27        $PROTOCOL_VERSION $ROLE_CLIENT @ROLES $TYPE_FILE $TYPE_DIR
28        $MODE_MASK $DEFAULT_FILE_MODE $MAX_CHUNK_BYTES $SHA256_HEX
29        $EXIT_OK $EXIT_FATAL $EXIT_LOCKED $EXIT_WARNINGS $SERVER_PROGRAM
30
300
300
518
308
);
31
300
300
300
648
270
3737
use App::FileSync::Delta;
32
300
300
300
481
235
5378
use App::FileSync::I18N qw(message os_error strip_location);
33
300
300
300
50686
388
4134
use App::FileSync::Merge;
34
300
300
300
662
243
3017
use App::FileSync::Protocol;
35
300
300
300
555
259
2427
use App::FileSync::Settings;
36
300
300
300
489
225
2281
use App::FileSync::Store;
37
38
300
300
300
427
245
540
use version; our $VERSION = version->declare('0.001.1');
39
40# Package variables for compatibility: the CLI checks $PROTO, and tests may
41# localise $CHUNK.
42our $CHUNK = 262_144;
43our $PROTO = $PROTOCOL_VERSION;
44
45# Bounds on a pull signature the client uploads, so a hostile client cannot
46# make the server allocate or hash without limit.
47Readonly my $MAX_SIGNATURE_BLOCKS => 16_384;
48Readonly my $MIN_SIGNATURE_BLOCK  => 1024;
49Readonly my $MAX_SIGNATURE_BLOCK  => 1024 * 1024;
50
51# Wire cost of one delta record, for deciding whether a delta is worth it;
52# the constant covers the reply headers.
53Readonly my $DELTA_OP_BYTES => 12;
54Readonly my $DELTA_OVERHEAD => 64;
55
56# Unsigned decimal integers, and mtimes (which may be fractional or, for a
57# file dated before 1970, negative).
58Readonly my $UINT  => qr/\A\d+\z/;
59Readonly my $MTIME => qr/\A-?\d+(?:\.\d+)?\z/;
60
61# The protocol's commands and the method that serves each.
62Readonly my %COMMANDS => (
63        manifest  => '_manifest',
64        plan      => '_plan',
65        get       => '_get',
66        put       => '_put',
67        chunk     => '_chunk',
68        chunk_end => '_chunk_end',
69        sig       => '_signature',
70        delta     => '_delta_start',
71        dcopy     => '_delta_copy',
72        dlit      => '_delta_literal',
73        delta_end => '_delta_end',
74        dsig      => '_pull_signature',
75        dop       => '_pull_op',
76        mkdir     => '_mkdir',
77        del       => '_delete',
78        ack       => '_ack',
79        done      => '_finish',
80);
81
82# Which planned actions each kind of request may serve.  "conflict:client"
83# is a conflict the client's version won; "conflict:remote" one the master's won.
84Readonly my %MAY => (
85        read   => [ 'pull', 'conflict:remote' ],
86        write  => [ 'push', 'conflict:client' ],
87        delta  => [ 'push' ],
88        pull   => [ 'pull' ],
89        mkdir  => [ 'push', 'conflict:client' ],
90        delete => [ 'delete_remote' ],
91        ack    => [ 'pull', 'delete_local', 'conflict:remote' ],
92);
93
94# The flags a forced command may carry, and the pinned field each must match.
95Readonly my %FORCED_FLAGS => (repo => 'repo', client => 'client', role => 'role', proto => 'proto');
96
97=encoding utf8
98
99 - 174
=head1 NAME

App::FileSync::Server - serve one sync session against a repo's master

=head1 SYNOPSIS

        my $server = App::FileSync::Server->new(
                root => '/srv/filesync', repo => 'docs', client => 'laptop', role => 'client',
                connection => App::FileSync::Protocol->new(in => \*STDIN, out => \*STDOUT),
        );
        exit($server->run ? 0 : 1);

=head1 DESCRIPTION

A session is: hello, manifest, plan, then the transfers the plan lists, then
C<done>.  The server holds the repo lock throughout, so sessions on one repo
never interleave.  It updates the master index and the client's base only in
C<done>, after every planned action has been applied; a session that dies part
way leaves both as they were, and the next session replans from the tree.

=head1 METHODS

=head2 new

Purpose: prepare a session.

Args: C<root>, C<repo>, C<client>, C<role> (C<client> or C<replica>),
C<connection> (an L<App::FileSync::Protocol>), optional C<logger> and
C<language>.

Returns: the server object.

Side Effects: builds a logger from the settings when none is given.

Usage:

        my $server = App::FileSync::Server->new(%pinned, connection => $connection);

=head3 EXAMPLE

        my $server = App::FileSync::Server->new(
                root => $root, repo => 'docs', client => 'laptop', role => 'client', connection => $conn,
        );

=head3 API SPECIFICATION

=head4 INPUT

        {
                root       => { type => 'string', min => 1 },
                repo       => { type => 'string', min => 1, max => 64 },
                client     => { type => 'string', min => 1, max => 64 },
                role       => { type => 'string', memberof => [ 'client', 'replica' ] },
                connection => { type => 'object', can => [ 'send', 'recv' ] },
                logger     => { type => 'object', optional => 1 },
                language   => { type => 'string', optional => 1 },
        }

=head4 OUTPUT

        { type => 'object', isa => 'App::FileSync::Server' }

=head3 MESSAGES

        Key                  Meaning                         Resolution
        server.missing       a required argument is absent   pass it
        server.bad_role      role is not client or replica   fix the forced command
        server.bad_id        repo or client is not an id     fix the forced command
        server.no_connection no connection was given         pass connection

=head3 FORMAL SPECIFICATION

        pre root ≠ "" ∧ repo ∈ Id ∧ client ∈ Id ∧ role ∈ {client, replica} ∧ connection ≠ ⊥
        post finished = 0 ∧ plan = ∅ ∧ uploads = ∅

=cut
175
176sub new {
177
172
218
        my $class = shift;
178
172
1394
        my $args = named(\@_, {
179                root       => { type => 'string', optional => 1 },
180                repo       => { type => 'string', optional => 1 },
181                client     => { type => 'string', optional => 1 },
182                role       => { type => 'string', optional => 1 },
183                connection => { type => 'object', optional => 1 },
184                logger     => { type => 'object', optional => 1 },
185                language   => { type => 'string', optional => 1 },
186        }, 'Server->new');
187
188        # Checked here rather than by the schema so the messages read as a
189        # server refusal rather than an argument error.
190
172
571
        for my $required (qw(root repo client role)) {
191                App::FileSync::I18N::throw('server.missing', { what => $required })
192
688
1253
                        unless defined $args->{$required} && length $args->{$required};
193        }
194        App::FileSync::I18N::throw('server.bad_role', { role => $args->{role} })
195
172
200
624
766
                unless any { $_ eq $args->{role} } @ROLES;
196
197
172
1229
        for my $check ([ repo => \&validate_repo_name ], [ client => \&validate_client_id ]) {
198
344
344
307
419
                my ($field, $validate) = @{$check};
199
344
810
                my ($ok, $reason) = $validate->($args->{$field});
200
344
514
                App::FileSync::I18N::throw('server.bad_id', { reason => $reason }) unless $ok;
201        }
202
172
1008
        App::FileSync::I18N::throw('server.no_connection') unless $args->{connection};
203
204        return bless {
205
860
2482
                (map { $_ => $args->{$_} } qw(root repo client role language)),
206                conn            => $args->{connection},
207
172
290
                logger          => $args->{logger} || App::FileSync::Settings::logger(),
208                dry_run         => 0,
209                force           => 0,
210                finished        => 0,
211                uploads         => {},
212                pull_deltas     => {},
213                client_manifest => {},
214                plan            => {},
215                index           => {},
216                base            => {},
217                base_after      => {},
218                applied         => {},
219        }, $class;
220}
221
222 - 316
=head2 run

Purpose: serve the whole session.

Args: none.

Returns: 1 when the session finished, 0 when it failed (the client has been
sent the error).

Side Effects: takes the repo lock; reads and writes the master, its index, the
client's base, the trash and the journal; prunes the trash and removes stale
temporaries.  A failed session is journalled (exit 2 for a lock refusal,
otherwise 1) unless it never got as far as opening the repo.

Usage:

        exit($server->run ? 0 : 1);

=head3 EXAMPLE

        my $ok = $server->run;
        warn "session failed\n" unless $ok;

=head3 API SPECIFICATION

=head4 INPUT

        [ { name => 'self', type => 'object' } ]

=head4 OUTPUT

        { type => 'integer', memberof => [ 0, 1 ] }

=head3 MESSAGES

        Key                    Meaning                             Resolution
        server.locked          another session holds the lock      the client retries later
        server.not_allowed     the client is not registered        filesync-admin client allow
        server.wrong_role      the client is registered otherwise  fix the key's --role
        server.proto           the client speaks another protocol  upgrade one side
        server.pinned          the hello disagrees with the pin    fix the client config
        server.unknown         an unknown command arrived          upgrade one side

Raised once the session is open.  Each ends the session: the open uploads
are discarded, the failure is journalled, and the text (without Perl's
file and line) reaches the client as C<< { ok => 0, fatal => 1, error => ... } >>.
Most mean a client bug or a hostile client; the resolution is to fix the
client, or simply to rerun the sync.

        Key                     Meaning
        server.manifest_list    the manifest is not a list
        server.manifest_entry   a manifest entry is not an object
        server.manifest_path    a manifest path is unsafe
        server.manifest_field   a manifest t, h, s, m or mode field is malformed
        server.refused          the deletion guard stopped the plan (rerun with --force if intended)
        server.unplanned        an operation names a path that is not in the plan
        server.not_planned      the path's planned action does not allow this operation
        server.bad_field        an off, max, len, s, mode or h field is malformed
        server.conflict_name    the conflict copy is named other than the plan says
        server.conflict_unsafe  the planned conflict name is not a safe path
        server.conflict_same    the planned conflict name equals the path
        server.conflict_exists  something already exists at the conflict name
        server.conflict_move    the master's copy could not be moved aside
        server.bad_ack          an ack disagrees with the planned action
        server.busy             a second put for a path already being received
        server.no_put           chunk or chunk_end without a put
        server.too_large        one chunk is larger than the chunk limit
        server.overflow         the upload is larger than its announced size
        server.hash_mismatch    the received content does not have the announced hash
        server.size_mismatch    the received content does not have the announced size
        server.no_delta         delta data without a delta start
        server.bad_copy         a delta copy reaches outside the base file
        server.bad_signature    a pull signature is malformed
        server.no_signature     a pull delta operation without a signature
        server.cannot_stat      a planned file vanished from the master
        server.not_file         a get for a path that is a directory
        server.unfinished       done arrived before every planned action completed

=head3 FORMAL SPECIFICATION

        Î”FS
        run = 1 ⇔ hello ⨾ open ⨾ lock ⨾ loop ⨾ done succeed
        run = 0 ⇒ index' = index ∧ base' = base

=head3 PSEUDOCODE

        read and check the hello against the pinned arguments
        open the repo; check the client is allowed in its role
        take the lock (refuse with code "locked" otherwise)
        load the index and the client's base; tidy temporaries and the trash
        send the policy, then serve commands until done
        on any error: log it, journal the failed session, discard partial
        uploads, and send the error to the client

=cut
317
318sub run {
319
172
259
        my ($self) = @_;
320
321
172
457
        my $connection = $self->{conn};
322
323
172
215
        my $ok = eval {
324
172
491
                $self->_validate_hello($connection->recv());
325
326                my $store = $self->{store} = App::FileSync::Store->new(
327                        root => $self->{root}, repo => $self->{repo}, language => $self->{language},
328
171
1278
                );
329
171
490
                $self->_check_client_allowed();
330
331
170
489
                $self->{lock} = $store->acquire_lock();
332
170
351
                unless ($self->{lock}) {
333
3
6
                        $self->{refusal} = 'locked';
334
3
24
                        $self->_throw('server.locked', { repo => $self->{repo} });
335                }
336
337
167
472
                $self->{index} = $store->load_index();
338
167
712
                $self->{base}  = $store->load_base($self->{client});
339
167
556
                $store->clean_stale_temps();
340
167
600
                $store->prune_trash() unless $self->{dry_run};
341
342
167
501
414
1329
                $self->{logger}->info('session opened', { map { $_ => $self->{$_} } qw(repo client role) });
343
344
167
6379
                my $policy = $store->config();
345                $connection->send({
346                        ok     => 1,
347                        proto  => $PROTO,
348                        server => { os => $^O },
349                        repo   => { name => $self->{repo} },
350
167
668
694
1475
                        policy => { map { $_ => $policy->{$_} } qw(ignore trash_days max_deletes max_delete_percent) },
351                });
352
353
167
715
                $self->_loop();
354
149
268
                1;
355        };
356
357
172
1045
        return 1 if $ok;
358
359        # The client shows this text to a person, so no Perl file and line.
360
23
59
        my $error = strip_location($@) || $self->i18n('server.unknown_error');
361
23
97
        $self->{logger}->error($error);
362
23
6318
        $self->_discard_uploads();
363
23
64
        $self->_journal_failed_session($error);
364
365
23
25
        eval {
366
23
122
                $connection->send({ ok => 0, error => $error, fatal => 1, ($self->{refusal} ? (code => $self->{refusal}) : ()) });
367        };
368
23
86
        return 0;
369}
370
371 - 438
=head2 validate_forced_command

Purpose: check the command a client asked sshd to run against the arguments
pinned in F<authorized_keys>.

The pinned arguments are the authority for a session.  sshd passes the command
the client asked for in C<SSH_ORIGINAL_COMMAND>; it must agree with the pinned
repo, client, role and protocol and ask for nothing else: in particular it may
not choose its own C<--root>.

Args: C<$original> (the requested command; undef or blank is accepted),
C<\%pinned> (C<repo>, C<client>, C<role>).

Returns: C<(1, undef)> or C<(0, $reason)>.  A list, deliberately: callers
write C<my ($ok, $why) = ...>, matching L<App::FileSync/validate_relpath>.

Side Effects: none.

Usage:

        my ($ok, $reason) = App::FileSync::Server::validate_forced_command($ENV{SSH_ORIGINAL_COMMAND}, \%pinned);

=head3 EXAMPLE

        my ($ok, $why) = App::FileSync::Server::validate_forced_command(
                'filesync-server --repo docs --client laptop --role client --proto 2',
                { repo => 'docs', client => 'laptop', role => 'client' },
        );      # (1, undef)

=head3 API SPECIFICATION

=head4 INPUT

        [
                { name => 'original', type => 'string', optional => 1 },
                { name => 'pinned',   type => 'hashref' },
        ]

=head4 OUTPUT

        { type => 'arrayref', min => 2, max => 2 }     # returned as a two-element list

=head3 MESSAGES

        Key                   Meaning
        forced.program        the command runs some other program
        forced.argument       a token is not a --flag
        forced.no_value       a flag has no value
        forced.flag           the flag is not repo, client, role or proto
        forced.proto          the protocol differs from this server's
        forced.pinned         the value differs from the pinned one

=head3 FORMAL SPECIFICATION

        let ts = tokens(original)
        ok ⇔ (ts = ⟨⟩) ∨ ((head ts ∈ FLAG ∨ basename(head ts) = filesync-server) ∧
              âˆ€ (f, v) : flags(ts) • f ∈ {repo, client, role, proto} ∧
              (f = proto ⇒ v = PROTO) ∧ (f ≠ proto ⇒ v = pinned(f)))

=head3 PSEUDOCODE

        accept an empty command
        split on white space; allow a leading filesync-server path
        for each --flag[=value] (value from the next token if not attached):
                reject an unknown flag, a missing value, a wrong protocol
                reject a value that differs from the pinned one

=cut
439
440sub validate_forced_command {
441
28
95848
        my ($original, $pinned) = @_;
442
443
28
67
        my @tokens = defined $original ? split(' ', $original) : ();
444
28
35
        return (1, undef) unless @tokens;
445
446
25
41
        if ($tokens[0] !~ /\A--/) {
447
24
23
                my $program = shift @tokens;
448
24
49
                return (0, message('forced.program', { program => $program }))
449                        unless $program =~ m{(?:\A|/)\Q$SERVER_PROGRAM\E\z};
450        }
451
452
24
217
        while (@tokens) {
453
75
46
                my $token = shift @tokens;
454
75
122
                return (0, message('forced.argument', { token => $token }))
455                        unless $token =~ /\A--([a-z][a-z-]*)(?:=(.*))?\z/s;
456
457
74
83
                my ($flag, $value) = ($1, $2);
458
74
63
                unless (defined $value) {
459
69
56
                        return (0, message('forced.no_value', { flag => $flag })) unless @tokens;
460
68
48
                        $value = shift @tokens;
461                }
462
463
73
106
                my $field = $FORCED_FLAGS{$flag} or return (0, message('forced.flag', { flag => $flag }));
464
465
70
257
                if ($field eq 'proto') {
466
13
23
                        return (0, message('forced.proto', { proto => $value })) unless $value eq $PROTO;
467
12
17
                        next;
468                }
469
470
57
41
                my $expected = $pinned->{$field};
471
57
111
                return (0, message('forced.pinned', { field => $field, value => $value, expected => $expected // '' }))
472                        unless defined $expected && $value eq $expected;
473        }
474
475
13
19
        return (1, undef);
476}
477
478# _journal_failed_session
479# Purpose:     record a session that never reached done, so the admin sees
480#              the attempt with the same exit code the client records.
481# Entry:       $error is the message sent to the client.
482# Exit:        returns 1 if journalled, else 0.
483# Side effects: appends to the journal.  Nothing is journalled without a store
484#              (a bad hello, say) or on a dry run, and a journal write that
485#              fails is logged, never allowed to mask the original error.
486sub _journal_failed_session :Private {
487        my ($self, $error) = @_;
488
489        return 0 if $self->{finished} || $self->{session_journalled} || $self->{dry_run} || !$self->{store};
490
491        my $locked = ($self->{refusal} || '') eq 'locked';
492        my $ok = eval {
493                $self->_journal({
494                        action => 'session',
495                        result => 'error',
496                        exit   => $locked ? $EXIT_LOCKED : $EXIT_FATAL,
497                        error  => $error,
498                        ($locked ? (locked => 1) : ()),
499                });
500                1;
501        };
502        $self->{logger}->warn('cannot journal the failed session', { error => strip_location($@) }) unless $ok;
503
504        return $ok ? 1 : 0;
505
300
300
300
221875
306
3428
}
506
507# _validate_hello
508# Purpose:     check the client's hello against the pinned session.
509# Entry:       $hello is the first message.
510# Exit:        returns 1.
511# Side effects: records dry_run and force; raises server.proto or
512#              server.pinned.
513sub _validate_hello :Private {
514        my ($self, $hello) = @_;
515
516        $self->_throw('server.proto') unless defined $hello->{proto} && $hello->{proto} eq $PROTO;
517
518        for my $field (qw(client repo role)) {
519                my $given = $hello->{$field} // '';
520                $self->_throw('server.pinned', { field => $field, expected => $self->{$field}, given => $given })
521                        unless $given eq $self->{$field};
522        }
523
524        # case_insensitive is part of the hello but only the client acts on it.
525        $self->{dry_run} = $hello->{dry_run} ? 1 : 0;
526        $self->{force}   = $hello->{force} ? 1 : 0;
527        return 1;
528
300
300
300
104054
265
2458
}
529
530# _check_client_allowed
531# Purpose:     enforce the repo's client list.
532# Entry:       the store is open.
533# Exit:        returns 1.
534# Side effects: raises server.not_allowed or server.wrong_role.  An empty
535#              list allows everyone (a repo nobody has restricted yet).
536sub _check_client_allowed :Private {
537        my ($self) = @_;
538
539        my $clients = $self->{store}->config()->{clients} || {};
540        return 1 unless %{$clients};
541
542        my $entry = $clients->{ $self->{client} }
543                or $self->_throw('server.not_allowed', { client => $self->{client}, repo => $self->{repo} });
544
545        my $role = (ref($entry) eq 'HASH' && $entry->{role}) || $ROLE_CLIENT;
546        $self->_throw('server.wrong_role', { client => $self->{client}, registered => $role, asked => $self->{role} })
547                unless $role eq $self->{role};
548
549        return 1;
550
300
300
300
97508
284
2384
}
551
552# _loop
553# Purpose:     serve commands until done.
554# Entry:       the hello has been answered.
555# Exit:        returns 1 once _finish has run.
556# Side effects: whatever the commands do; raises server.unknown.
557sub _loop :Private {
558        my ($self) = @_;
559
560        until ($self->{finished}) {
561                my $message = $self->{conn}->recv();
562                my $command = $message->{cmd} // '';
563                my $method = $COMMANDS{$command} or $self->_throw('server.unknown', { command => $command });
564                $self->$method($message);
565        }
566
567        return 1;
568
300
300
300
91707
256
2352
}
569
570# _manifest
571# Purpose:     accept (part of) the client's manifest.
572# Entry:       $message->{entries} is an arrayref of { p, t, s, m, h, mode }.
573# Exit:        returns 1 after replying ok.
574# Side effects: adds normalised entries to client_manifest; raises
575#              server.manifest_* for anything malformed, so the plan never
576#              sees an unchecked path or field.
577sub _manifest :Private {
578        my ($self, $message) = @_;
579
580        my $entries = $message->{entries};
581        $self->_throw('server.manifest_list') unless ref($entries) eq 'ARRAY';
582
583        for my $entry (@{$entries}) {
584                $self->_throw('server.manifest_entry') unless ref($entry) eq 'HASH';
585
586                my $path = $entry->{p};
587                my ($safe, $reason) = validate_relpath($path);
588                $self->_throw('server.manifest_path', { reason => $reason }) unless $safe;
589
590                my $type = $entry->{t} // $TYPE_FILE;
591                $self->_throw('server.manifest_field', { field => 't', path => $path })
592                        unless $type eq $TYPE_FILE || $type eq $TYPE_DIR;
593
594                if ($type eq $TYPE_DIR) {
595                        $self->{client_manifest}{$path} = { t => $TYPE_DIR };
596                        next;
597                }
598
599                my %field = (h => $SHA256_HEX, s => $UINT, m => $MTIME, mode => $UINT);
600                my %entry = (m => 0, mode => $DEFAULT_FILE_MODE, %{$entry});
601                for my $name (sort keys %field) {
602                        $self->_throw('server.manifest_field', { field => $name, path => $path })
603                                unless defined $entry{$name} && $entry{$name} =~ $field{$name};
604                }
605
606                $self->{client_manifest}{$path} = {
607                        t    => $TYPE_FILE,
608                        h    => $entry{h},
609                        (map { $_ => $entry{$_} + 0 } qw(s m mode)),
610                };
611        }
612
613        $self->{conn}->send({ ok => 1 });
614        return 1;
615
300
300
300
125243
289
2651
}
616
617# _plan
618# Purpose:     plan the session and send it to the client.
619# Entry:       the manifest has arrived.
620# Exit:        returns 1 after sending the actions.
621# Side effects: rescans the master (reusing the index's hashes); stores the
622#              plan; journals it unless dry run; raises the delete guard's
623#              refusal when the plan deletes too much.
624sub _plan :Private {
625        my ($self) = @_;
626
627        my $store = $self->{store};
628        my $master = $self->{index} = $store->walk_master($self->{index})->{manifest};
629        my $config = $store->config();
630
631        my $plan = App::FileSync::Merge::plan(
632                base      => $self->{base},
633                client    => $self->{client_manifest},
634                master    => $master,
635                client_id => $self->{client},
636                role      => $self->{role},
637                now       => time,
638        );
639
640        my $refusal = App::FileSync::Merge::check_delete_limit(
641                deletions          => $plan->{deletions},
642                base_size          => scalar(keys %{ $self->{base} }),
643                max_deletes        => $config->{max_deletes},
644                max_delete_percent => $config->{max_delete_percent},
645                force              => $self->{force},
646        );
647        $self->_throw('server.refused', { reason => $refusal }) if defined $refusal;
648
649        $self->_unique_loser_names($plan->{actions});
650
651        $self->{plan} = { map { $_->{p} => $_ } @{ $plan->{actions} } };
652        $self->{base_after} = { %{ $self->{client_manifest} } };
653
654        my %counts = (
655                actions   => scalar @{ $plan->{actions} },
656                deletions => $plan->{deletions},
657                conflicts => $plan->{conflicts},
658                dry_run   => $self->{dry_run},
659        );
660        $self->{logger}->debug('plan', { repo => $self->{repo}, client => $self->{client}, %counts });
661        $self->_journal({ action => 'plan', %counts }) unless $self->{dry_run};
662
663        $self->{conn}->send({ ok => 1, actions => $plan->{actions}, warnings => $plan->{warnings} });
664        return 1;
665
300
300
300
125693
261
2493
}
666
667# _unique_loser_names
668# Purpose:     make every conflict-copy name unique against the master and
669#              the rest of the plan.
670# Entry:       $actions from Merge::plan.
671# Exit:        returns 1.
672# Side effects: rewrites loser_name in place.  Two conflicts resolved in the
673#              same second would otherwise pick the same name.
674sub _unique_loser_names :Private {
675        my ($self, $actions) = @_;
676
677        my %used;
678        for my $action (grep { $_->{a} eq 'conflict' && defined $_->{loser_name} } @{$actions}) {
679                my ($name, $suffix) = ($action->{loser_name}, 1);
680                $name = name_with_suffix($action->{loser_name}, ++$suffix)
681                        while $used{$name} || exists $self->{index}{$name};
682                $used{$name} = 1;
683                $action->{loser_name} = $name;
684        }
685
686        return 1;
687
300
300
300
100050
252
2414
}
688
689# _expect
690# Purpose:     the one gate every path-bearing request passes.
691# Entry:       $kind is a key of %MAY; $path is from the client.
692# Exit:        returns the planned action for $path.
693# Side effects: raises server.unplanned when the plan has no such path and
694#              server.not_planned when it does not allow this request.
695sub _expect :Private {
696        my ($self, $kind, $path) = @_;
697
698        my $what = $self->i18n("server.op.$kind");
699        my $action = defined $path ? $self->{plan}{$path} : undef;
700        $self->_throw('server.unplanned', { what => $what, path => $path // '' }) unless $action;
701
702        my $label = $action->{a} eq 'conflict' ? "conflict:" . ($action->{winner} // '') : $action->{a};
703        $self->_throw('server.not_planned', { what => $what, path => $path })
704                unless any { $_ eq $label } @{ $MAY{$kind} };
705
706        return $action;
707
300
300
300
103828
256
2425
}
708
709# _get
710# Purpose:     send one chunk of a master file the client is pulling.
711# Entry:       $message has p, optional off and max.
712# Exit:        returns 1 after sending the chunk.
713# Side effects: reads the master; raises server.bad_field for a malformed
714#              offset or size, server.not_file and io.* errors.  max is
715#              clamped to MAX_CHUNK_BYTES so a client cannot ask the server to
716#              buffer an entire large file.
717sub _get :Private {
718        my ($self, $message) = @_;
719
720        my $path = $message->{p};
721        $self->_expect('read', $path);
722
723        my $offset = $self->_uint($message, 'off', 0);
724        my $max    = $self->_uint($message, 'max', $CHUNK);
725        $max = $MAX_CHUNK_BYTES if $max > $MAX_CHUNK_BYTES;
726
727        my $abs = $self->{store}->abs_path($path);
728        my $file = $self->_file_stat($abs, $path);
729        $self->_throw('server.bad_field', { field => 'off', path => $path }) if $offset > $file->{s};
730
731        my $length = $file->{s} - $offset;
732        $length = $max if $length > $max;
733
734        my $bytes = '';
735        if ($length > 0) {
736                my $fh = App::FileSync::IO::open_file($abs, '<:raw');
737                seek($fh, $offset, 0);
738                $bytes = App::FileSync::Delta::read_exact($fh, $length, $abs);
739                close $fh;
740        }
741
742        $self->{conn}->send_data({ ok => 1, eof => ($offset + $length >= $file->{s}) ? 1 : 0, %{$file} }, $bytes);
743        return 1;
744
300
300
300
113578
256
2518
}
745
746# _put
747# Purpose:     start receiving a whole file the client is pushing.
748# Entry:       $message has p, s, h, m, mode; conflict_name is optional and,
749#              when sent, must equal the plan's loser_name.
750# Exit:        returns 1 after replying ok with off 0.
751# Side effects: for a conflict the client won, moves the master's version
752#              aside under the plan's name first - whatever the client
753#              sends, so a client that omits the name cannot overwrite the
754#              master's losing version.  Opens a temporary file.
755sub _put :Private {
756        my ($self, $message) = @_;
757
758        my $path = $message->{p};
759        my $action = $self->_expect('write', $path);
760
761        if ($action->{a} eq 'conflict' && defined $action->{loser_name}) {
762                $self->_throw('server.conflict_name', { path => $path, expected => $action->{loser_name} })
763                        if defined $message->{conflict_name} && $message->{conflict_name} ne $action->{loser_name};
764                $self->_preserve_loser($path, $action->{loser_name}, { sha256 => $message->{h}, mtime => $message->{m} });
765        }
766
767        $self->_open_upload($path, $message);
768        $self->{conn}->send({ ok => 1, off => 0 });
769        return 1;
770
300
300
300
102048
288
2371
}
771
772# _chunk
773# Purpose:     receive one chunk of a whole-file upload.
774# Entry:       a put for $message->{p} is open; len gives the payload size.
775# Exit:        returns 1 after replying with the new offset.
776# Side effects: appends to the temporary file and the running hash.
777sub _chunk :Private {
778        my ($self, $message) = @_;
779
780        my $path = $message->{p} // '';
781        my $upload = $self->{uploads}{$path} or $self->_throw('server.no_put', { path => $path });
782
783        my $length = $self->_uint($message, 'len', 0);
784        $self->_throw('server.too_large', { path => $path }) if $length > $MAX_CHUNK_BYTES;
785
786        $self->_append_upload($path, $upload, $length ? $self->{conn}->read_data($message) : '');
787        $self->{conn}->send({ ok => 1, off => $upload->{got} });
788        return 1;
789
300
300
300
96779
295
2342
}
790
791# _chunk_end
792# Purpose:     finish a whole-file upload.
793# Entry:       a put for $message->{p} is open.
794# Exit:        returns 1 after replying ok.
795# Side effects: verifies and installs the file (see _commit_upload).
796sub _chunk_end :Private {
797        my ($self, $message) = @_;
798
799        my $path = $message->{p} // '';
800        my $upload = delete $self->{uploads}{$path} or $self->_throw('server.no_put', { path => $path });
801
802        $self->_commit_upload($path, $upload);
803        $self->{conn}->send({ ok => 1 });
804        return 1;
805
300
300
300
90948
270
2229
}
806
807# _signature
808# Purpose:     describe the master's current version so the client can push
809#              only the blocks that differ.
810# Entry:       $message->{p} is a planned push.
811# Exit:        returns 1 after replying; delta 0 when the file is too small
812#              or absent.
813# Side effects: reads the master file.
814sub _signature :Private {
815        my ($self, $message) = @_;
816
817        my $path = $message->{p};
818        $self->_expect('delta', $path);
819
820        my $abs = $self->{store}->abs_path($path);
821        my $signature = (-f $abs && (-s _) >= $App::FileSync::Delta::MIN_SIZE)
822                ? App::FileSync::Delta::signature($abs)
823                : undef;
824
825        return $self->_reply({ ok => 1, delta => 0 }) unless $signature;
826
827        $self->{conn}->send_data({
828                ok    => 1,
829                delta => 1,
830                s     => $signature->{size},
831                b     => $signature->{blocklen},
832                n     => scalar @{ $signature->{blocks} },
833        }, App::FileSync::Delta::encode_signature($signature));
834        return 1;
835
300
300
300
103491
249
2371
}
836
837# _delta_start
838# Purpose:     start receiving a pushed file as a delta against the master.
839# Entry:       $message has p, h, s, m, mode.
840# Exit:        returns 1 after replying ok.
841# Side effects: opens a temporary file and, when the master has the file,
842#              a read handle on it for copy records.
843sub _delta_start :Private {
844        my ($self, $message) = @_;
845
846        my $path = $message->{p};
847        $self->_expect('delta', $path);
848        $self->_throw('server.bad_field', { field => 'h', path => $path })
849                unless defined $message->{h} && $message->{h} =~ $SHA256_HEX;
850        $self->_uint($message, 's');
851
852        my $upload = $self->_open_upload($path, $message);
853        $upload->{delta} = 1;
854
855        my $old = $self->{store}->abs_path($path);
856        if (-f $old) {
857                @{$upload}{qw(old old_fh old_size)} = ($old, App::FileSync::IO::open_file($old, '<:raw'), -s _);
858        }
859
860        $self->{conn}->send({ ok => 1 });
861        return 1;
862
300
300
300
108612
301
2361
}
863
864# _delta_copy
865# Purpose:     apply a copy record: bytes from the master's old version.
866# Entry:       a delta for p is open; off and len are integers, len > 0.
867# Exit:        returns 1 after replying with the new offset.
868# Side effects: appends to the upload; raises server.bad_copy for a range
869#              outside the old file.
870sub _delta_copy :Private {
871        my ($self, $message) = @_;
872
873        my ($path, $upload) = $self->_open_delta($message);
874        my $offset = $self->_uint($message, 'off');
875        my $length = $self->_uint($message, 'len');
876
877        $self->_throw('server.bad_copy', { path => $path })
878                unless $length > 0 && $upload->{old_fh} && $offset + $length <= $upload->{old_size};
879
880        seek($upload->{old_fh}, $offset, 0);
881        $self->_append_upload($path, $upload, App::FileSync::Delta::read_exact($upload->{old_fh}, $length, $upload->{old}));
882        $self->{conn}->send({ ok => 1, off => $upload->{got} });
883        return 1;
884
300
300
300
106039
275
2351
}
885
886# _delta_literal
887# Purpose:     apply a literal record: bytes sent by the client.
888# Entry:       a delta for p is open.
889# Exit:        returns 1 after replying with the new offset.
890# Side effects: appends to the upload.
891sub _delta_literal :Private {
892        my ($self, $message) = @_;
893
894        my ($path, $upload) = $self->_open_delta($message);
895        $self->_append_upload($path, $upload, $self->{conn}->read_data($message));
896        $self->{conn}->send({ ok => 1, off => $upload->{got} });
897        return 1;
898
300
300
300
94161
277
2271
}
899
900# _delta_end
901# Purpose:     finish a delta upload.
902# Entry:       a delta for p is open.
903# Exit:        returns 1 after replying ok.
904# Side effects: verifies and installs the file; journals it as a delta push.
905sub _delta_end :Private {
906        my ($self, $message) = @_;
907
908        my ($path, $upload) = $self->_open_delta($message);
909        delete $self->{uploads}{$path};
910
911        $self->_commit_upload($path, $upload, delta => 1);
912        $self->{conn}->send({ ok => 1, delta => 1 });
913        return 1;
914
300
300
300
91515
271
2205
}
915
916# _pull_signature
917# Purpose:     take the client's description of its old version and spool a
918#              delta from the master's current one.
919# Entry:       $message->{p} is a planned pull; n and b describe the blocks.
920# Exit:        returns 1 after replying; delta 0 when a delta is not worth it.
921# Side effects: reads the master file; keeps the spool for _pull_op.
922sub _pull_signature :Private {
923        my ($self, $message) = @_;
924
925        my $path = $message->{p};
926        $self->_expect('pull', $path);
927
928        my $count = $self->_uint($message, 'n');
929        my $block = $self->_uint($message, 'b');
930        $self->_throw('server.bad_signature', { path => $path })
931                unless $count <= $MAX_SIGNATURE_BLOCKS && $block >= $MIN_SIGNATURE_BLOCK && $block <= $MAX_SIGNATURE_BLOCK;
932
933        my $blocks = App::FileSync::Delta::decode_signature($self->{conn}->read_data($message), $count);
934
935        my $abs = $self->{store}->abs_path($path);
936        my $file = $self->_file_stat($abs, $path);
937        return $self->_reply({ ok => 1, delta => 0 }) if $file->{s} < $App::FileSync::Delta::MIN_SIZE;
938
939        my $spool = File::Temp->new(UNLINK => 1);
940        binmode $spool;
941        my $stats = App::FileSync::Delta::diff($abs, { blocklen => $block, blocks => $blocks }, $spool);
942        $spool->flush();
943
944        # Only the literal bytes and record headers go on the wire; compression can
945        # shrink them further, so this is a conservative estimate.
946        my $estimated = $stats->{literal} + $DELTA_OP_BYTES * $stats->{ops} + $DELTA_OVERHEAD;
947        return $self->_reply({ ok => 1, delta => 0 }) if $estimated >= $file->{s};
948
949        seek($spool, 0, 0);
950        $self->{pull_deltas}{$path} = { spool => $spool, %{$file} };
951        $self->{conn}->send({ ok => 1, delta => 1, %{$file}, b => $block });
952        return 1;
953
300
300
300
127765
271
2461
}
954
955# _pull_op
956# Purpose:     serve the next record of a spooled pull delta.
957# Entry:       _pull_signature spooled a delta for p.
958# Exit:        returns 1 after sending a copy, a literal, or done.
959# Side effects: advances the spool; forgets it at the end.
960sub _pull_op :Private {
961        my ($self, $message) = @_;
962
963        my $path = $message->{p} // '';
964        my $state = $self->{pull_deltas}{$path} or $self->_throw('server.no_signature', { path => $path });
965
966        my ($type, @rest) = App::FileSync::Delta::read_op($state->{spool});
967        unless ($type) {
968                delete $self->{pull_deltas}{$path};
969                return $self->_reply({ ok => 1, done => 1 });
970        }
971
972        return $self->_reply({ ok => 1, op => 'copy', off => $rest[0], len => $rest[1] }) if $type eq 'copy';
973
974        $self->{conn}->send_data({ ok => 1, op => 'lit' }, $rest[0]);
975        return 1;
976
300
300
300
108831
298
2382
}
977
978# _mkdir
979# Purpose:     create a directory the client pushed.
980# Entry:       $message->{p} is a planned push of a directory.
981# Exit:        returns 1 after replying ok.
982# Side effects: creates the directory in the master; records it.
983sub _mkdir :Private {
984        my ($self, $message) = @_;
985
986        my $path = $message->{p};
987        $self->_expect('mkdir', $path);
988
989        App::FileSync::IO::ensure_dir($self->{store}->abs_path($path));
990        $self->{index}{$path} = { t => $TYPE_DIR };
991        $self->{base_after}{$path} = { t => $TYPE_DIR };
992        $self->_journal({ action => 'mkdir', path => $path, result => 'ok' });
993
994        $self->{applied}{$path} = 1;
995        return $self->_reply({ ok => 1 });
996
300
300
300
102338
261
2303
}
997
998# _delete
999# Purpose:     move a master path the client deleted into the trash.
1000# Entry:       $message->{p} is a planned delete_remote.
1001# Exit:        returns 1 after replying with the trash path.
1002# Side effects: moves the path to the trash; records it.
1003sub _delete :Private {
1004        my ($self, $message) = @_;
1005
1006        my $path = $message->{p};
1007        $self->_expect('delete', $path);
1008
1009        my $trashed = $self->{store}->move_to_trash($path, time);
1010        delete $self->{index}{$path};
1011        delete $self->{base_after}{$path};
1012        $self->_journal({ action => 'delete', path => $path, trash => $trashed, result => 'ok' });
1013
1014        $self->{applied}{$path} = 1;
1015        return $self->_reply({ ok => 1, trash => $trashed });
1016
300
300
300
100757
280
2266
}
1017
1018# _ack
1019# Purpose:     record what the client now holds after a change on its side.
1020# Entry:       $message->{p} is a planned pull, delete_local or conflict the
1021#              master won.  deleted is only valid for delete_local, and only
1022#              a delete_local may omit the file description.
1023# Exit:        returns 1 after replying ok.
1024# Side effects: updates base_after; journals a conflict the master won.
1025#              Restricting acks to these actions stops a client from writing
1026#              arbitrary entries into its own base, which would make the
1027#              next plan delete or skip master files.
1028sub _ack :Private {
1029        my ($self, $message) = @_;
1030
1031        my $path = $message->{p};
1032        my $action = $self->_expect('ack', $path);
1033        my $deleting = $action->{a} eq 'delete_local';
1034
1035        $self->_throw('server.bad_ack', { path => $path }) unless !$message->{deleted} == !$deleting;
1036
1037        if ($deleting) {
1038                delete $self->{base_after}{$path};
1039        } elsif (($message->{t} // '') eq $TYPE_DIR) {
1040                $self->{base_after}{$path} = { t => $TYPE_DIR };
1041        } else {
1042                $self->_throw('server.bad_field', { field => 'h', path => $path })
1043                        unless defined $message->{h} && $message->{h} =~ $SHA256_HEX;
1044                $self->{base_after}{$path} = {
1045                        t    => $TYPE_FILE,
1046                        h    => $message->{h},
1047                        s    => $self->_uint($message, 's', 0),
1048                        m    => ($message->{m} // 0) + 0,
1049                        mode => $self->_uint($message, 'mode', $DEFAULT_FILE_MODE),
1050                };
1051        }
1052
1053        if ($action->{a} eq 'conflict') {
1054                $self->_journal_conflict($path, 'remote', $action->{loser_name},
1055                        $self->{index}{$path}, $self->{client_manifest}{$path});
1056        }
1057
1058        $self->{applied}{$path} = 1;
1059        delete $self->{pull_deltas}{$path};
1060        return $self->_reply({ ok => 1 });
1061
300
300
300
123186
275
2419
}
1062
1063# _preserve_loser
1064# Purpose:     move the master's losing version aside before the client's
1065#              version is written.
1066# Entry:       $conflict_rel is the plan's loser_name for $path.
1067# Exit:        returns 1.
1068# Side effects: renames the master file; moves its index entry; journals the
1069#              copy and the conflict.  Does nothing if the master no longer
1070#              has the file.
1071sub _preserve_loser :Private {
1072        my ($self, $path, $conflict_rel, $winner) = @_;
1073
1074        my ($safe, $reason) = validate_relpath($conflict_rel);
1075        $self->_throw('server.conflict_unsafe', { reason => $reason }) unless $safe;
1076        $self->_throw('server.conflict_same', { path => $path }) if $conflict_rel eq $path;
1077
1078        my $store = $self->{store};
1079        my $abs = $store->abs_path($path);
1080        return 1 unless -e $abs || -l $abs;
1081
1082        my $destination = $store->abs_path($conflict_rel);
1083        $self->_throw('server.conflict_exists', { path => $conflict_rel }) if -e $destination || -l $destination;
1084
1085        $store->ensure_parent($conflict_rel);
1086        eval { rename $abs, $destination; 1 }
1087                or $self->_throw('server.conflict_move', { path => $path, to => $conflict_rel, error => os_error($@) });
1088
1089        my $old = delete $self->{index}{$path};
1090        $self->{index}{$conflict_rel} = $old if $old;
1091
1092        $self->_journal({ action => 'conflict_copy', path => $conflict_rel, from => $path, sha256 => $old ? $old->{h} : undef, result => 'ok' });
1093        $self->_journal_conflict($path, 'client', $conflict_rel, { h => $winner->{sha256}, m => $winner->{mtime} }, $old);
1094
1095        return 1;
1096
300
300
300
118593
289
2496
}
1097
1098# _journal_conflict
1099# Purpose:     record how a conflict was resolved.
1100# Entry:       $side is client or remote; entries may be undef.
1101# Exit:        returns 1.
1102# Side effects: appends a conflict event.
1103sub _journal_conflict :Private {
1104        my ($self, $path, $side, $loser_name, $winner, $loser) = @_;
1105
1106        ($winner, $loser) = ($winner || {}, $loser || {});
1107        return $self->_journal({
1108                action        => 'conflict',
1109                path          => $path,
1110                winner        => $side,
1111                loser_name    => $loser_name,
1112                winner_sha256 => $winner->{h},
1113                winner_mtime  => $winner->{m},
1114                loser_sha256  => $loser->{h},
1115                loser_mtime   => $loser->{m},
1116                result        => 'ok',
1117        });
1118
300
300
300
102826
282
2323
}
1119
1120# _finish
1121# Purpose:     end the session: save the index and the client's base.
1122# Entry:       every planned action has been applied (unless dry run).
1123# Exit:        returns 1 after sending the summary.
1124# Side effects: writes the index, the base and a session event; sets
1125#              finished.  Raises server.unfinished, naming the missing paths
1126#              in order, when the client stopped early.
1127sub _finish :Private {
1128        my ($self) = @_;
1129
1130        my @actions = values %{ $self->{plan} };
1131        unless ($self->{dry_run}) {
1132                my @unfinished = sort map { $_->{p} } grep { !$self->{applied}{ $_->{p} } } @actions;
1133                $self->_throw('server.unfinished', { paths => join(', ', @unfinished) }) if @unfinished;
1134
1135                $self->{store}->save_index($self->{index});
1136                $self->{store}->save_base($self->{client}, $self->{base_after});
1137        }
1138
1139        my %summary = map { my $kind = $_; $kind => scalar(grep { $_->{a} =~ /\A$kind/ } @actions) }
1140                qw(pull push delete conflict);
1141        %summary = (
1142                pulled    => $summary{pull},
1143                pushed    => $summary{push},
1144                deleted   => $summary{delete},
1145                conflicts => $summary{conflict},
1146        );
1147
1148        $self->{logger}->info('session finished', { repo => $self->{repo}, client => $self->{client}, %summary, dry_run => $self->{dry_run} });
1149
1150        # The client exits 3 when it had to keep conflict copies; record that here
1151        # too, so --exit means the same thing on both sides.
1152        $self->_journal({
1153                action  => 'session',
1154                result  => 'ok',
1155                exit    => $summary{conflicts} ? $EXIT_WARNINGS : $EXIT_OK,
1156                dry_run => $self->{dry_run},
1157                %summary,
1158        }) unless $self->{dry_run};
1159        $self->{session_journalled} = 1;
1160
1161        $self->{conn}->send({ ok => 1, summary => \%summary });
1162        $self->{finished} = 1;
1163        return 1;
1164
300
300
300
134487
294
2554
}
1165
1166# _open_upload
1167# Purpose:     start an upload for a planned path.
1168# Entry:       $message has s, h, m, mode (s and h may be absent for a put).
1169# Exit:        returns the upload record.
1170# Side effects: creates the parent directory and a temporary file; raises
1171#              server.busy if the path is already being received.
1172sub _open_upload :Private {
1173        my ($self, $path, $message) = @_;
1174
1175        $self->_throw('server.busy', { path => $path }) if $self->{uploads}{$path};
1176
1177        my $size = defined $message->{s} ? $self->_uint($message, 's') : undef;
1178        $self->{store}->ensure_parent($path);
1179        my ($fh, $temp) = @{ $self->{store}->temp_file($path) };
1180
1181        return $self->{uploads}{$path} = {
1182                fh    => $fh,
1183                temp  => $temp,
1184                sha   => Digest::SHA->new(256),
1185                got   => 0,
1186                size  => $size,
1187                hash  => $message->{h},
1188                mtime => $message->{m},
1189                mode  => $message->{mode},
1190        };
1191
300
300
300
112483
306
2444
}
1192
1193# _open_delta
1194# Purpose:     find the delta upload a record belongs to.
1195# Entry:       $message->{p} names it.
1196# Exit:        returns ($path, $upload).
1197# Side effects: raises server.no_delta when there is none, or the open
1198#              upload for that path is a whole-file put.
1199sub _open_delta :Private {
1200        my ($self, $message) = @_;
1201
1202        my $path = $message->{p} // '';
1203        my $upload = $self->{uploads}{$path};
1204        $self->_throw('server.no_delta', { path => $path }) unless $upload && $upload->{delta};
1205        return ($path, $upload);
1206
300
300
300
99274
283
2337
}
1207
1208# _append_upload
1209# Purpose:     add received bytes to an upload.
1210# Entry:       $upload is open.
1211# Exit:        returns 1.
1212# Side effects: writes to the temporary file and the hash.  Refuses more
1213#              bytes than were announced, so a client cannot fill the disk
1214#              behind a small size; on any failure the upload is discarded.
1215sub _append_upload :Private {
1216        my ($self, $path, $upload, $bytes) = @_;
1217
1218        my $length = length $bytes;
1219        if (defined $upload->{size} && $upload->{got} + $length > $upload->{size}) {
1220                $self->_discard_upload($path);
1221                $self->_throw('server.overflow', { path => $path });
1222        }
1223
1224        unless (print { $upload->{fh} } $bytes) {
1225                my $error = os_error();
1226                $self->_discard_upload($path);
1227                $self->_throw('io.write', { path => $upload->{temp}, error => $error });
1228        }
1229
1230        $upload->{sha}->add($bytes);
1231        $upload->{got} += $length;
1232        return 1;
1233
300
300
300
109971
311
2293
}
1234
1235# _commit_upload
1236# Purpose:     close, verify and install an upload, whole or delta.
1237# Entry:       $upload is no longer in uploads.
1238# Exit:        returns the new index entry.
1239# Side effects: installs the file in the master; updates the index and
1240#              base_after; journals the push.  A hash or size mismatch
1241#              discards the temporary file and raises.
1242sub _commit_upload :Private {
1243        my ($self, $path, $upload, %extra) = @_;
1244
1245        my $closed = eval { close $upload->{fh}; 1 };
1246        my $error = $closed ? undef : os_error($@);
1247        eval { close $upload->{old_fh} } if $upload->{old_fh};
1248
1249        my $hash = $upload->{sha}->hexdigest();
1250        my ($key, %args) =
1251                  !$closed ? ('io.write', path => $upload->{temp}, error => $error)
1252                : (defined $upload->{hash} && $hash ne $upload->{hash}) ? ('server.hash_mismatch', path => $path, expected => $upload->{hash}, got => $hash)
1253                : (defined $upload->{size} && $upload->{got} != $upload->{size}) ? ('server.size_mismatch', path => $path, expected => $upload->{size}, got => $upload->{got})
1254                : ();
1255        if ($key) {
1256                eval { unlink $upload->{temp} };
1257                $self->_throw($key, \%args);
1258        }
1259
1260        $self->{store}->commit_temp($path, $upload->{temp}, $upload->{mtime}, $upload->{mode});
1261
1262        my $entry = {
1263                t    => $TYPE_FILE,
1264                s    => $upload->{got},
1265                m    => $upload->{mtime} // time,
1266                h    => $hash,
1267                mode => $upload->{mode} // $DEFAULT_FILE_MODE,
1268        };
1269        $self->{index}{$path}      = $entry;
1270        $self->{base_after}{$path} = { %{$entry} };
1271
1272        $self->_journal({ action => 'push', path => $path, size => $upload->{got}, sha256 => $hash, result => 'ok', %extra });
1273        $self->{applied}{$path} = 1;
1274        return $entry;
1275
300
300
300
137159
305
2472
}
1276
1277# _discard_upload / _discard_uploads
1278# Purpose:     abandon one, or every, partial upload.
1279# Entry:       none.
1280# Exit:        returns 1.
1281# Side effects: closes handles and removes temporary files, so a failed
1282#              session leaves nothing for clean_stale_temps to find an hour
1283#              later.
1284sub _discard_upload :Private {
1285        my ($self, $path) = @_;
1286
1287        my $upload = delete $self->{uploads}{$path} or return 1;
1288        eval { close $_ } for grep { defined } @{$upload}{qw(fh old_fh)};
1289        eval { unlink $upload->{temp} };
1290        return 1;
1291
300
300
300
113257
284
2469
}
1292
1293sub _discard_uploads :Private {
1294        my ($self) = @_;
1295
1296        $self->_discard_upload($_) for keys %{ $self->{uploads} };
1297        return 1;
1298
300
300
300
104528
277
2299
}
1299
1300# _file_stat
1301# Purpose:     size, mtime and mode of a master file the client is reading.
1302# Entry:       $abs is inside the master.
1303# Exit:        returns { s, m, mode }.
1304# Side effects: raises server.cannot_stat or server.not_file.
1305sub _file_stat :Private {
1306        my ($self, $abs, $path) = @_;
1307
1308        my @stat = Time::HiRes::stat($abs);
1309        $self->_throw('server.cannot_stat', { path => $path, error => os_error() }) unless @stat;
1310        $self->_throw('server.not_file', { path => $path }) unless -f _;
1311        return { s => $stat[7], m => $stat[9], mode => $stat[2] & $MODE_MASK };
1312
300
300
300
105080
291
2397
}
1313
1314# _uint
1315# Purpose:     read an unsigned integer field from a client message.
1316# Entry:       $default is used when the field is absent; without one the
1317#              field is required.
1318# Exit:        returns the number.
1319# Side effects: raises server.bad_field for anything else (a negative or
1320#              fractional offset, a string, a missing required field).
1321sub _uint :Private {
1322        my ($self, $message, $field, $default) = @_;
1323
1324        my $value = $message->{$field} // $default;
1325        $self->_throw('server.bad_field', { field => $field, path => $message->{p} // '' })
1326                unless defined $value && $value =~ $UINT;
1327        return $value + 0;
1328
300
300
300
99881
259
2300
}
1329
1330# _journal
1331# Purpose:     append an event for this client.
1332# Entry:       $event is a hashref without client.
1333# Exit:        returns 1.
1334# Side effects: appends to the journal.
1335sub _journal :Private {
1336        my ($self, $event) = @_;
1337
1338        $self->{store}->journal({ client => $self->{client}, %{$event} });
1339        return 1;
1340
300
300
300
101686
298
2288
}
1341
1342# _reply
1343# Purpose:     send a reply and end the handler.
1344# Entry:       $reply is a hashref.
1345# Exit:        returns 1.
1346# Side effects: writes to the connection.
1347sub _reply :Private {
1348        my ($self, $reply) = @_;
1349
1350        $self->{conn}->send($reply);
1351        return 1;
1352
300
300
300
99305
286
2223
}
1353
13541;
1355