Compare commits

...

2 Commits

Author SHA1 Message Date
Fabian Wagner ➖ a6a2f5842 56ad8a8f82
fix(backup): widen mode + fail fast when backup dir isn't writable (#1)
mkdir was 0755 — owner-only write. The dir often gets created once at
deploy time as root or whoever ran the first artisan command, then
www-data tries to write to it at runtime and bash redirects fail with
"Permission denied" mid-pipeline (after mysqldump has already started
streaming, leaving a 0-byte .enc behind).

- mkdir(0775) so group writes too; ensure-group-write is the typical
  pattern for shared deploy/runtime users.
- Best-effort chmod 0775 on existing dirs to repair narrow modes when
  we own the path.
- is_writable() pre-flight before kicking off any pipeline. Throws a
  RuntimeException with the exact `chown` + `chmod` command to run,
  including the discovered owner and the current process user — so the
  fix is one paste away instead of grepping man pages.

This pairs with docker-laravel's start-container change that pre-creates
storage/backups/ owned by www-data on every boot. Either layer alone is
enough; both together means the failure mode disappears whether the
deployment uses our image or not.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:22:12 +02:00
Fabian Wagner b893a3a594 I readme 2026-05-07 07:18:00 +02:00
2 changed files with 47 additions and 1 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
[![Blax Software OSS](https://raw.githubusercontent.com/blax-software/laravel-workkit/master/art/oss-initiative-banner.svg)](https://github.com/blax-software)
# Laravel Workkit
A Laravel collection of helpers and utilities to reduce redundant code over multiple projects.

View File

@ -153,13 +153,54 @@ class BackupService
/**
* Path of the host's backup directory, created if missing. Defaults
* to storage/backups; overridable via config('workkit.backup.path').
*
* Group-writable on purpose the dir is often created once at deploy
* time (as root or whoever ran the first artisan command) and then
* written by www-data at runtime. If we can't make it writable for
* the current user, we throw with the exact `chown`/`chmod` to run,
* because the alternative is the bash redirect failing mid-pipeline
* with `Permission denied` after mysqldump has already started.
*/
public static function backupDirectory(): string
{
$path = config('workkit.backup.path') ?: storage_path('backups');
if (! is_dir($path)) {
mkdir($path, 0755, true);
// Suppress because a tight parent dir or umask can race here;
// the is_dir() check below is the real gate.
@mkdir($path, 0775, true);
if (! is_dir($path)) {
throw new RuntimeException("Failed to create backup directory: {$path}");
}
}
// Best-effort widen — only succeeds when we own the dir, which is
// exactly when the perms were already too narrow to begin with.
@chmod($path, 0775);
if (! is_writable($path)) {
$owner = '?';
$current = '?';
if (function_exists('posix_geteuid') && function_exists('posix_getpwuid')) {
$statOwner = @fileowner($path);
if ($statOwner !== false) {
$owner = posix_getpwuid($statOwner)['name'] ?? (string) $statOwner;
}
$current = posix_getpwuid(posix_geteuid())['name'] ?? (string) posix_geteuid();
}
throw new RuntimeException(sprintf(
"Backup directory not writable: %s\n"
. " owner=%s, current user=%s\n"
. " Fix as root: chown -R %s %s && chmod 0775 %s",
$path,
$owner,
$current,
$current,
$path,
$path
));
}
return rtrim($path, '/');
}