Leave the archive readable by whoever collects from it
tests / pest (push) Failing after 7m32s Details
tests / assets (push) Successful in 21s Details
tests / release (push) Has been skipped Details

Reported as "the folder arrives empty". It was not empty. The folder was 0700
and owned by the account that wrote it, so the account that COLLECTS — an rsync
over ssh, a backup agent, anything that is not the writer — could not enter it,
and an unreadable directory looks exactly like an empty one. The invoice had
been sitting in it the whole time.

The cause is that the default follows the process umask, so under a restrictive
one 0755 quietly becomes 0700. The permissions are declared on the disk now
rather than left to inherit, and a test writes under umask 0077 and asserts the
mode — the ordinary umask hides this completely, which is why it was found on a
server and not here.

Two mistakes of mine on the way to it, both worth recording. I read the folder
as the wrong user, got no entries back, and concluded the file had been deleted
— it was a permission failure reading as an unprivileged account, not a missing
file. And the first fix was a chmod on a variable that does not exist in that
method, silenced by an @ so it failed without a word. Flysystem takes its
permissions from configuration; a chmod afterwards was the wrong layer even
before it was the wrong variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat/granted-plans
nexxo 2026-07-29 03:48:03 +02:00
parent 79564947d4
commit 132be7fdd5
2 changed files with 35 additions and 0 deletions

View File

@ -148,6 +148,18 @@ final class InvoiceArchive
'driver' => 'local',
'root' => $target->path,
'throw' => false,
// Explicit, because the default follows the process umask and a
// restrictive one leaves the folder 0700. That is not cosmetic:
// whoever COLLECTS from a handover directory is a different account
// — an rsync over ssh, a backup agent — and a directory it cannot
// enter looks to it exactly like an empty one. Reported as "the
// folder arrives empty", with the invoice sitting in it the whole
// time.
'visibility' => 'public',
'permissions' => [
'file' => ['public' => 0o644, 'private' => 0o600],
'dir' => ['public' => 0o755, 'private' => 0o700],
],
]);
}
}

View File

@ -216,3 +216,26 @@ it('does not prune a destination whose mount has gone away', function () {
expect(is_dir($gone))->toBeFalse();
});
it('leaves the folder readable by whoever collects from it', function () {
// Not cosmetic. mkdir's mode is masked by the process umask, and a
// restrictive one leaves the folder 0700 — at which point the account that
// collects (an rsync over ssh, a backup agent, anything that is not the
// account that wrote it) cannot enter it, and an unreadable directory looks
// exactly like an empty one. Reported as "the folder arrives empty", with
// the invoice sitting in it the whole time.
$invoice = anInvoice();
$old = umask(0o077);
try {
(new ArchiveInvoice($invoice, $this->target))->handle(app(InvoiceArchive::class));
} finally {
umask($old);
}
$folder = $this->archive.'/'.now()->format('Y');
$file = $folder.'/'.$invoice->number.'.pdf';
expect(substr(sprintf('%o', fileperms($folder)), -3))->toBe('755')
->and(substr(sprintf('%o', fileperms($file)), -3))->toBe('644');
});