getgrav/grav · error · RuntimeException

Backup location: {$backup_root} does not exist...

Error message

Backup location: {$backup_root} does not exist...

What it means

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.

Source

Thrown at system/src/Grav/Common/Backup/Backups.php:256

            throw new RuntimeException('No backups defined...');
        }

        $name = $grav['inflector']->underscorize($backup->name);
        $date = date(static::BACKUP_DATE_FORMAT, time());
        $filename = trim((string) $name, '_') . '--' . $date . '.zip';
        $grav['backups']->setup();
        $destination = static::$backup_dir . DS . $filename;
        $max_execution_time = ini_set('max_execution_time', '600');
        $backup_root = $backup->root;

        if ($locator->isStream($backup_root)) {
            $backup_root = $locator->findResource($backup_root);
        } else {
            $backup_root = rtrim(GRAV_ROOT . $backup_root, DS) ?: DS;
        }

        if (!$backup_root || !file_exists($backup_root)) {
            throw new RuntimeException("Backup location: {$backup_root} does not exist...");
        }

        // Security: Resolve real path and ensure it's within GRAV_ROOT to prevent path traversal
        $realBackupRoot = realpath($backup_root);
        $realGravRoot = realpath(GRAV_ROOT);

        if ($realBackupRoot === false || $realGravRoot === false) {
            throw new RuntimeException("Invalid backup location: {$backup_root}");
        }

        // Positive containment (GHSA-fch7-cpv4-w7hg): the resolved backup root must
        // BE GRAV_ROOT or a directory beneath it. The previous deny-list only rejected
        // a fixed set of system paths, so a non-blocklisted external directory (e.g.
        // /opt, /mnt, /srv) still fell through and had its contents archived. Comparing
        // against GRAV_ROOT with a trailing separator also prevents a sibling directory
        // (e.g. `/var/www/site-evil` next to `/var/www/site`) from matching by prefix.
        $isWithinGravRoot = $realBackupRoot === $realGravRoot
            || strpos($realBackupRoot, $realGravRoot . DIRECTORY_SEPARATOR) === 0;

View on GitHub (pinned to 6040efed04)

Solutions

  1. Check the profile's root field in backups.profiles and confirm the directory actually exists under GRAV_ROOT
  2. Use the default root '/' (whole site) or a stream root like 'user://' that is guaranteed to resolve
  3. If using a stream, verify the scheme is defined in streams.schemes and the target directory exists
  4. Recreate the missing directory or repoint the profile before scheduling/calling the backup

Example fix

# user/config/backups.yaml — before
profiles:
  - name: 'Pages Backup'
    root: '/content'   # directory does not exist

# after
profiles:
  - name: 'Pages Backup'
    root: '/user/pages'
Defensive patterns

Strategy: validation

Validate before calling

$profile = Backups::getBackupProfiles()[$id] ?? null;
$root = $profile['root'] ?? '/';
$grav = Grav::instance();
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$resolved = $locator->isStream($root) ? $locator->findResource($root) : rtrim(GRAV_ROOT . $root, DS);
if (!$resolved || !is_dir($resolved)) {
    throw new RuntimeException("Backup root {$root} resolves to a missing directory");
}

Try / catch

try {
    Backups::backup($id);
} catch (RuntimeException $e) {
    if (str_contains($e->getMessage(), 'does not exist')) {
        // config points at a moved/deleted directory — fix the profile
    }
    throw $e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/714a127963fcc112. Report an issue: GitHub.