From 3dca9978d910c2ca4c65d9c51fcdafa4f466a30f Mon Sep 17 00:00:00 2001 From: Fabian Wagner Date: Thu, 7 May 2026 10:05:46 +0200 Subject: [PATCH] fix(backup): widen mode + fail fast when backup dir isn't writable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/Services/BackupService.php | 43 +++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Services/BackupService.php b/src/Services/BackupService.php index 70d69dd..0960ed2 100644 --- a/src/Services/BackupService.php +++ b/src/Services/BackupService.php @@ -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, '/'); }