{"record":{"id":"714a127963fcc112","repo":"getgrav/grav","slug":"backup-location-backup-root-does-not-exist","errorCode":null,"errorMessage":"Backup location: {$backup_root} does not exist...","messagePattern":"Backup location: (.+?) does not exist\\.\\.\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Backup/Backups.php","lineNumber":256,"sourceCode":"            throw new RuntimeException('No backups defined...');\n        }\n\n        $name = $grav['inflector']->underscorize($backup->name);\n        $date = date(static::BACKUP_DATE_FORMAT, time());\n        $filename = trim((string) $name, '_') . '--' . $date . '.zip';\n        $grav['backups']->setup();\n        $destination = static::$backup_dir . DS . $filename;\n        $max_execution_time = ini_set('max_execution_time', '600');\n        $backup_root = $backup->root;\n\n        if ($locator->isStream($backup_root)) {\n            $backup_root = $locator->findResource($backup_root);\n        } else {\n            $backup_root = rtrim(GRAV_ROOT . $backup_root, DS) ?: DS;\n        }\n\n        if (!$backup_root || !file_exists($backup_root)) {\n            throw new RuntimeException(\"Backup location: {$backup_root} does not exist...\");\n        }\n\n        // Security: Resolve real path and ensure it's within GRAV_ROOT to prevent path traversal\n        $realBackupRoot = realpath($backup_root);\n        $realGravRoot = realpath(GRAV_ROOT);\n\n        if ($realBackupRoot === false || $realGravRoot === false) {\n            throw new RuntimeException(\"Invalid backup location: {$backup_root}\");\n        }\n\n        // Positive containment (GHSA-fch7-cpv4-w7hg): the resolved backup root must\n        // BE GRAV_ROOT or a directory beneath it. The previous deny-list only rejected\n        // a fixed set of system paths, so a non-blocklisted external directory (e.g.\n        // /opt, /mnt, /srv) still fell through and had its contents archived. Comparing\n        // against GRAV_ROOT with a trailing separator also prevents a sibling directory\n        // (e.g. `/var/www/site-evil` next to `/var/www/site`) from matching by prefix.\n        $isWithinGravRoot = $realBackupRoot === $realGravRoot\n            || strpos($realBackupRoot, $realGravRoot . DIRECTORY_SEPARATOR) === 0;","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Backup/Backups.php#L238-L274","documentation":"After resolving a backup profile's root — either through the stream wrapper (locator->findResource) or by concatenating GRAV_ROOT with the relative root — Backups::backup() verifies the path exists. If the resolved $backup_root is empty or file_exists() returns false, it throws RuntimeException 'Backup location ... does not exist...'. The profile's configured root points at a directory that is not on disk.","triggerScenarios":"A profile with root: '/nonexistent-dir'; a stream root like 'user://pages' when the stream resolves to false (undefined scheme or missing directory); a root that was valid when the profile was saved but whose directory has since been moved, renamed, or deleted; a leading double slash or typo producing a bogus GRAV_ROOT concatenation.","commonSituations":"Site migrated to a new server and custom profile roots were not created; environment-specific configuration resolving streams differently than where the profile was authored; typo in the root field of a hand-edited backups.yaml; a directory removed by cache purging or cleanup scripts.","solutions":["Check the profile's root field in backups.profiles and confirm the directory actually exists under GRAV_ROOT","Use the default root '/' (whole site) or a stream root like 'user://' that is guaranteed to resolve","If using a stream, verify the scheme is defined in streams.schemes and the target directory exists","Recreate the missing directory or repoint the profile before scheduling/calling the backup"],"exampleFix":"# user/config/backups.yaml — before\nprofiles:\n  - name: 'Pages Backup'\n    root: '/content'   # directory does not exist\n\n# after\nprofiles:\n  - name: 'Pages Backup'\n    root: '/user/pages'","handlingStrategy":"validation","validationCode":"$profile = Backups::getBackupProfiles()[$id] ?? null;\n$root = $profile['root'] ?? '/';\n$grav = Grav::instance();\n/** @var UniformResourceLocator $locator */\n$locator = $grav['locator'];\n$resolved = $locator->isStream($root) ? $locator->findResource($root) : rtrim(GRAV_ROOT . $root, DS);\nif (!$resolved || !is_dir($resolved)) {\n    throw new RuntimeException(\"Backup root {$root} resolves to a missing directory\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    Backups::backup($id);\n} catch (RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'does not exist')) {\n        // config points at a moved/deleted directory — fix the profile\n    }\n    throw $e;\n}","preventionTips":["Prefer stream roots (user://, /) over absolute paths — streams resolve against current layout","Validate profile roots after site migrations and environment changes","Alert on scheduled-backup failure so a silently-moved directory is noticed early"],"tags":["grav","backup","filesystem","path-not-found","configuration"],"backgroundTag":"path-not-found","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}