Skip to content

Backup hosts#2125

Open
Boy132 wants to merge 24 commits intomainfrom
boy132/backup-hosts
Open

Backup hosts#2125
Boy132 wants to merge 24 commits intomainfrom
boy132/backup-hosts

Conversation

@Boy132
Copy link
Member

@Boy132 Boy132 commented Jan 20, 2026

Closes #1919
Also allows to register custom adapters (#1600)

Largely untested!!!

@Boy132 Boy132 self-assigned this Jan 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Introduces a pluggable BackupHost / BackupAdapter system: adds adapter interface, registry, S3/Wings schemas, BackupHost model/resource/migration, provider wiring, and updates controllers/services/tests; removes legacy BackupManager/S3Filesystem and disk-based backup config; migrates backups to BackupHost references.

Changes

Cohort / File(s) Summary
Adapter Core
app/Extensions/BackupAdapter/BackupAdapterSchemaInterface.php, app/Extensions/BackupAdapter/BackupAdapterService.php, app/Extensions/BackupAdapter/Schemas/BackupAdapterSchema.php
Adds adapter interface, registry service, and abstract base schema implementing getName().
Adapter Implementations
app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php, app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php
New S3 and Wings schema classes providing create/delete, getDownloadLink, and multipart upload lifecycle (S3) or JWT-based download (Wings).
Domain Model & Migration
app/Models/BackupHost.php, database/migrations/..._create_backup_hosts_table.php, database/Factories/BackupHostFactory.php
Adds BackupHost model, factory and migration (backup_hosts table, pivot, adds backup_host_id to backups and drops disk); migration migrates existing config into a BackupHost record.
Backup Model & Node
app/Models/Backup.php, app/Models/Node.php, database/Factories/BackupFactory.php
Replaces Backup.disk with backup_host_id, adds backupHost relation; Node gains backupHosts relation; backup factory no longer emits disk.
Filament Admin UI
app/Filament/Admin/Resources/BackupHosts/*, app/Filament/Admin/Pages/Settings.php, app/Filament/Admin/Resources/Servers/Pages/EditServer.php, app/Filament/Server/Resources/Backups/BackupResource.php
Adds BackupHost Filament resource, pages, relation manager and translations; removes old APP_BACKUP_DRIVER/S3 UI; integrates BackupAdapterService into server/backup UI flows and unifies download URL generation.
Controllers
app/Http/Controllers/Api/Client/Servers/BackupController.php, app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php, app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
Controllers now inject BackupAdapterService, resolve schemas by backupHost.schema, and delegate upload/download/multipart flows to schema implementations; S3-specific in-controller logic removed.
Services Refactored
app/Services/Backups/InitiateBackupService.php, app/Services/Backups/DeleteBackupService.php, app/Services/Backups/DownloadLinkService.php
Services now resolve schemas via BackupAdapterService and delegate create/delete/download to adapters; constructors and payloads updated to use BackupHost/schema.
Provider & Bootstrap
app/Providers/Extensions/BackupAdapterServiceProvider.php, app/Providers/BackupsServiceProvider.php (removed), bootstrap/providers.php
New provider registers BackupAdapterService and default schemas; old BackupsServiceProvider removed and bootstrap providers updated.
Legacy Removal
app/Extensions/Backups/BackupManager.php, app/Extensions/Filesystem/S3Filesystem.php
Removes legacy BackupManager, S3Filesystem class and associated provider registration.
Daemon Repository
app/Repositories/Daemon/DaemonBackupRepository.php
Renames backup()→create(), removes internal adapter state; daemon payloads now use backupHost->schema.
Tasks & Transfer Flow
app/Extensions/Tasks/Schemas/CreateBackupSchema.php, app/Services/Servers/TransferServerService.php, app/Filament/Admin/Resources/Servers/Pages/EditServer.php
Renames injected property in CreateBackupSchema; transfer flows updated to filter Wings backups via BackupAdapterService and accept backup UUID arrays.
Config
config/backups.php
Removes default driver and disks block; retains presigned/max part/prune/throttles settings only.
Tests
tests/Integration/.../Backup/*, tests/Integration/Services/Backups/DeleteBackupServiceTest.php
Tests updated to create and associate BackupHost with backups; S3-specific deletion test removed; fixtures/imports updated.
Policies
app/Policies/BackupHostPolicy.php
New policy gating BackupHost actions by checking linked nodes.
Enum update
app/Enums/RolePermissionModels.php
Adds enum case BackupHost = 'backupHost'.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Initiator as InitiateBackupService
    participant AdapterSvc as BackupAdapterService
    participant Schema as BackupSchema (S3/Wings)
    participant DB as Database
    participant External as External System

    User->>Initiator: request create backup
    Initiator->>DB: create Backup record (backup_host_id)
    Initiator->>AdapterSvc: get(backupHost.schema)
    AdapterSvc-->>Initiator: BackupSchema instance
    Initiator->>Schema: createBackup(backup)
    Schema->>External: perform storage action (daemon or S3)
    External-->>Schema: confirmation
    Schema-->>Initiator: completion
    Initiator-->>User: respond backup created
Loading
sequenceDiagram
    participant Client as Client
    participant Controller as BackupController
    participant AdapterSvc as BackupAdapterService
    participant Schema as BackupSchema (S3/Wings)
    participant External as External System

    Client->>Controller: request download link
    Controller->>AdapterSvc: get(backup.backupHost.schema)
    AdapterSvc-->>Controller: BackupSchema instance
    Controller->>Schema: getDownloadLink(backup, user)
    Schema->>External: generate presigned URL or JWT URL
    External-->>Schema: url
    Schema-->>Controller: download URL
    Controller-->>Client: return URL
Loading

Possibly related PRs

  • Refactor schedule tasks #1911 — overlaps CreateBackupSchema constructor/property changes and InitiateBackupService updates; strong code-level relation.
  • Allow backup transfers #2068 — modifies TransferServerService and server transfer backup filtering; related to transfer/backup flow changes.
  • Add changes from upstream #2076 — touches backup upload/multipart and config/backups changes; relates to S3 multipart and controller/service refactors.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Backup hosts' directly summarizes the main change: introducing a new backup hosts system with dedicated models, services, and UI for managing multiple backup configurations.
Description check ✅ Passed The description is related to the changeset, referencing issue #1919 and mentioning the ability to register custom adapters, which aligns with the implementation of the BackupAdapterService and schema-based architecture.
Linked Issues check ✅ Passed The PR fulfills all requirements from issue #1919: moves backup configuration from settings to a dedicated resource (BackupHostResource), enables multiple backup configurations (BackupHost model with schema field), allows assigning nodes to backup hosts (BelongsToMany relation), and preserves local Wings backup as an option (WingsBackupSchema).
Out of Scope Changes check ✅ Passed All changes are within scope of the backup hosts refactoring: introducing the BackupAdapterService architecture, BackupHost model, new Filament resources, schema-based adapters (Wings, S3), and updating existing backup services to use the new system. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Boy132
Copy link
Member Author

Boy132 commented Jan 20, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Fix all issues with AI agents
In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`:
- Around line 106-154: In getUploadParts, accessing
$backup->backupHost->configuration['storage_class'] directly can emit warnings
when that key is missing; change the $storageClass assignment to safely read the
key (e.g. $storageClass = $backup->backupHost->configuration['storage_class'] ??
null or use array_key_exists) and keep the subsequent null check (if
(!is_null($storageClass)) { $params['StorageClass'] = $storageClass; }) so
missing config no longer triggers PHP notices.
- Around line 19-36: The S3 client is using wrong config keys and a single
cached client for all hosts; in S3BackupSchema::createClient(BackupHost
$backupHost) map the form fields to AWS SDK names (map access_key → key,
secret_key → secret, default_region → region) and preserve token if present,
build credentials via Arr::only on those mapped keys, and include region/version
in $config before constructing S3Client; change the client cache to be per-host
(keyed by $backupHost->id) instead of the single ?S3Client $client to avoid
cross-host credential leakage; also ensure any use sites like getUploadParts
read and respect storage_class from the host configuration (defaulting if
absent).

In `@app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php`:
- Around line 27-31: The DeleteAction in EditBackupHost.php currently only
disables deletion when the BackupHost has associated backups; update
DeleteAction::make() so it also disables (and adjusts its label) when
BackupHost::count() === 1 to prevent deleting the last host. Specifically,
change the ->disabled(...) callback to return true if
$backupHost->backups()->count() > 0 OR BackupHost::count() === 1, and update the
->label(...) callback to return a distinct translatable message when
BackupHost::count() === 1 (e.g., a "cannot delete last host" message) while
preserving the existing backups-related message; reference DeleteAction::make(),
BackupHost::backups(), and BackupHost::count() to locate the code.

In `@app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php`:
- Around line 57-63: The code in BackupRemoteUploadController currently resolves
the schema from collect($node->backupHosts)->first(), which can pick the wrong
host; instead resolve the schema from the Backup model's host property (use
$backup->host or equivalent) and validate it is present before using it. Replace
the get(collect($node->backupHosts)->first()->schema) call with fetching the
host associated with $backup (guarding against null), then call
$this->backupService->get($backupHost->schema) and keep the instanceof
S3BackupSchema check; if the backup's host is null or the schema lookup fails,
throw a BadRequestHttpException.

In `@app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php`:
- Around line 72-76: The current logic in BackupStatusController resolves the S3
schema using collect($node->backupHosts)->first(), which can pick the wrong host
or be null; instead resolve the schema from the backup's own host and guard
against missing hosts: call $this->backupService->get(...) using the host
associated with the backup model (the host/backupHost property on $model) and
only invoke S3BackupSchema->completeMultipartUpload($model, $successful,
$request->input('parts')) if the resolved schema is an S3BackupSchema and the
host/schema is not null.

In `@app/Services/Backups/DeleteBackupService.php`:
- Around line 34-37: The code in DeleteBackupService uses
$backup->backupHost->schema without checking whether $backup->backupHost is
null, which can cause a fatal error; update the method in DeleteBackupService to
first verify $backup->backupHost is not null (e.g., if null throw a clear
Exception or handle gracefully) before calling
$this->backupService->get($backup->backupHost->schema), and then proceed to
retrieve $schema and the subsequent logic only when the host exists.

In `@app/Services/Backups/DownloadLinkService.php`:
- Around line 22-25: The code accesses $backup->backupHost->schema without
checking if backupHost exists; add a null check in DownloadLinkService before
calling $this->backupService->get(...) so that if $backup->backupHost is null
you throw a specific exception (create e.g., MissingBackupHostException or
BackupAdapterException) instead of a generic Exception; update the method to
validate $backup->backupHost, throw the new exception with a clear message when
missing, and ensure any callers/handlers are adjusted to catch the new exception
type.

In `@app/Services/Backups/InitiateBackupService.php`:
- Around line 113-133: The current BackupHost selection (using
BackupHost::doesntHave('nodes')->orWhereHas('nodes', fn($q) => $q->where('id',
$server->node->id))->firstOrFail()) can pick a global host even when a
node-specific host exists; change the logic to first attempt to find a host
assigned to the server's node (e.g. BackupHost::whereHas('nodes', fn($q) =>
$q->where('id', $server->node->id))->first()), and if that returns null then
fall back to a global host (e.g.
BackupHost::doesntHave('nodes')->firstOrFail()); keep the remaining flow
(loading $schema, transaction, Backup::create, and $schema->createBackup) the
same but use the explicitly chosen $backupHost variable.

In `@app/Services/Servers/TransferServerService.php`:
- Line 31: The notify() method in TransferServerService.php currently ignores
the incoming $backup_uuids by overwriting them with $backups = [], causing
transfers to omit selected backups; update notify() to map/validate the provided
$backup_uuids into the $backups payload instead of emptying it (preserve
$backup_uuids, transform to the structure expected by the daemon API), ensure
you reference the $backup_uuids parameter and $backups variable inside notify(),
and adjust the payload format to match the daemon API spec (e.g., array of
objects or keyed field) before sending the transfer notification.

In `@database/migrations/2026_01_16_081858_create_backup_hosts_table.php`:
- Line 52: The migration's config array sets the 'prefix' key using the wrong
env var AWS_BACKUPS_BUCKET (copy-paste); update the 'prefix' entry to use the
dedicated environment variable (e.g., AWS_BACKUPS_PREFIX) instead of
AWS_BACKUPS_BUCKET in the migration file (create_backup_hosts_table.php) so the
'prefix' value is read from env('AWS_BACKUPS_PREFIX', '') and not from the
bucket variable.
- Around line 72-83: In the down() method the migration drops backup_hosts
before removing the backup_host_id foreign key on the backups table, which will
violate the FK constraint; update down() to first modify the backups table (in
the Schema::table('backups', ...) block) by adding the disk column, then remove
the foreign key (use $table->dropForeign(['backup_host_id']) or the explicit
constraint name) and then dropColumn('backup_host_id'), and only after that drop
the pivot table (backup_host_node) and finally drop backup_hosts (i.e., reorder
operations so foreign key is removed before dropping the referenced table).

In `@lang/en/admin/backuphost.php`:
- Line 5: The translation string 'model_label_plural' currently reads "Database
Hosts" but should be "Backup Hosts"; update the value for the
'model_label_plural' key in this translation file to "Backup Hosts" (preserve
surrounding quotes and comma) so the plural label matches the backup host
context.
🧹 Nitpick comments (7)
app/Policies/BackupHostPolicy.php (1)

21-24: Avoid N+1 queries in node authorization checks.
canTarget() can hit the DB per node; a set-based check reduces queries for hosts with many nodes.

♻️ Example set-based check
-        foreach ($backupHost->nodes as $node) {
-            if (!$user->canTarget($node)) {
-                return false;
-            }
-        }
+        $nodeIds = $backupHost->nodes->modelKeys();
+        if ($nodeIds !== [] && $user->accessibleNodes()->whereIn('id', $nodeIds)->count() !== count($nodeIds)) {
+            return false;
+        }
app/Models/Node.php (1)

279-282: Use canonical belongsToMany casing for consistency.

PHP is case‑insensitive here, but consistent casing avoids IDE/static analysis confusion.

♻️ Proposed tweak
-    public function backupHosts(): BelongsToMany
-    {
-        return $this->BelongsToMany(BackupHost::class);
-    }
+    /** `@return` BelongsToMany<BackupHost, $this> */
+    public function backupHosts(): BelongsToMany
+    {
+        return $this->belongsToMany(BackupHost::class);
+    }
database/migrations/2026_01_16_081858_create_backup_hosts_table.php (2)

60-64: Avoid using Eloquent models in migrations.

Using BackupHost::create() in a migration is fragile. If the model's $fillable, validation rules, or other attributes change in the future, this migration may break when run on a fresh database. Use query builder instead.

Proposed fix
-        $backupHost = BackupHost::create([
-            'name' => $oldDriver === 's3' ? 'Remote' : 'Local',
-            'schema' => $oldDriver,
-            'configuration' => $oldConfiguration,
-        ]);
+        $backupHostId = DB::table('backup_hosts')->insertGetId([
+            'name' => $oldDriver === 's3' ? 'Remote' : 'Local',
+            'schema' => $oldDriver,
+            'configuration' => $oldConfiguration ? json_encode($oldConfiguration) : null,
+            'created_at' => now(),
+            'updated_at' => now(),
+        ]);

-        DB::table('backups')->update(['backup_host_id' => $backupHost->id]);
+        DB::table('backups')->update(['backup_host_id' => $backupHostId]);

Also remove the use App\Models\BackupHost; import at the top.


36-41: Consider making backup_host_id nullable initially during migration.

Adding a non-nullable foreign key column and then immediately updating all rows works, but if the backups table is large, this could cause issues. Additionally, if the migration fails partway, you may end up with rows having a 0 or invalid backup_host_id.

A safer pattern is to:

  1. Add the column as nullable
  2. Create the backup host and update existing records
  3. Alter the column to be non-nullable

This is a minor concern given the migration context, but worth considering for robustness.

app/Services/Backups/DeleteBackupService.php (1)

16-19: Update docblock to reflect adapter-based architecture.

The comment mentions "If the backup is stored in S3" but the implementation is now adapter-agnostic. Consider updating the documentation to reflect the new architecture.

Proposed update
     /**
-     * Deletes a backup from the system. If the backup is stored in S3 a request
-     * will be made to delete that backup from the disk as well.
+     * Deletes a backup from the system. The backup adapter handles
+     * removing the backup data from the configured storage backend.
      *
      * `@throws` Throwable
      */
app/Models/BackupHost.php (1)

46-50: Align casts with CarbonImmutable docblocks.

Consider casting id, created_at, and updated_at to match the declared types and other models’ patterns.

♻️ Suggested adjustment
     protected function casts(): array
     {
         return [
+            'id' => 'int',
             'configuration' => 'array',
+            'created_at' => 'immutable_datetime',
+            'updated_at' => 'immutable_datetime',
         ];
     }
app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php (1)

57-62: Use a Form/Schema component for the “no configuration” message.

TextEntry is an Infolists component and TextEntry::make(trans(...)) uses a translation string as the state path. If this schema is rendered in a form, it may not display correctly. Prefer a Forms component like Placeholder (or a Schema-compatible view field).

♻️ Suggested adjustment
-use Filament\Infolists\Components\TextEntry;
+use Filament\Forms\Components\Placeholder;
@@
     /** `@return` Component[] */
     public function getConfigurationForm(): array
     {
         return [
-            TextEntry::make(trans('admin/backuphost.no_configuration')),
+            Placeholder::make('no_configuration')
+                ->content(trans('admin/backuphost.no_configuration')),
         ];
     }

@Boy132
Copy link
Member Author

Boy132 commented Jan 20, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Boy132
Copy link
Member Author

Boy132 commented Feb 24, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Filament/Server/Resources/Backups/BackupResource.php (1)

205-216: ⚠️ Potential issue | 🟡 Minor

Handle download-link failures gracefully before starting the restore.

DownloadLinkService::handle() can throw (e.g., unknown adapter). Since this is now unconditional, a failure will surface as a generic UI error and may still log a restore attempt. Consider precomputing the URL with a friendly notification and only logging/continuing on success.

💡 Suggested adjustment
-                            $log = Activity::event('server:backup.restore')
-                                ->subject($backup)
-                                ->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
-
-                            $log->transaction(function () use ($downloadLinkService, $daemonRepository, $backup, $server, $data) {
-                                $url = $downloadLinkService->handle($backup, user());
+                            try {
+                                $url = $downloadLinkService->handle($backup, user());
+                            } catch (Throwable $e) {
+                                return Notification::make()
+                                    ->title(trans('server/backup.actions.restore.notification_fail'))
+                                    ->body(trans('server/backup.actions.restore.notification_fail_body_adapter'))
+                                    ->danger()
+                                    ->send();
+                            }
+
+                            $log = Activity::event('server:backup.restore')
+                                ->subject($backup)
+                                ->property(['name' => $backup->name, 'truncate' => $data['truncate']]);
+
+                            $log->transaction(function () use ($daemonRepository, $backup, $server, $data, $url) {
                                 // Update the status right away for the server so that we know not to allow certain
                                 // actions against it via the Panel API.
                                 $server->update(['status' => ServerState::RestoringBackup]);
 
                                 $daemonRepository->setServer($server)->restore($backup, $url, $data['truncate']);
                             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Server/Resources/Backups/BackupResource.php` around lines 205 -
216, Wrap the call to DownloadLinkService::handle($backup, user()) in a
try/catch before you create/commit the Activity log and before changing server
state: if handle() throws, catch the exception, return a friendly
notification/error to the user and abort the restore flow so no
Activity::event('server:backup.restore') is committed and
$server->update(['status' => ServerState::RestoringBackup]) is not executed;
only after successfully obtaining $url should you create the $log, call
$log->transaction(...) and invoke
$daemonRepository->setServer($server)->restore($backup, $url,
$data['truncate']).
🧹 Nitpick comments (1)
app/Models/Node.php (1)

281-284: Fix method call casing and add missing @return annotation.

$this->BelongsToMany(...) at Line 283 has an uppercase B. PHP method calls are case-insensitive so this works at runtime, but it's inconsistent with databaseHosts() (and every other relation in the file) that use lowercase $this->belongsToMany(...). The method also lacks the generic @return annotation that was just added to databaseHosts().

♻️ Proposed fix
+   /** `@return` BelongsToMany<BackupHost, $this> */
    public function backupHosts(): BelongsToMany
    {
-       return $this->BelongsToMany(BackupHost::class);
+       return $this->belongsToMany(BackupHost::class);
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Node.php` around lines 281 - 284, The relation method
backupHosts() uses incorrect casing and lacks a return annotation: replace the
call to $this->BelongsToMany(BackupHost::class) with the conventional
$this->belongsToMany(BackupHost::class) and add the docblock `@return`
\Illuminate\Database\Eloquent\Relations\BelongsToMany (or the project's
BelongsToMany import) above the backupHosts() method to match databaseHosts()
and other relations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 1088-1094: Extract the repeated filter into a private helper on
the EditServer class to avoid null dereferences and DRY: add a private function
getWingsBackups(Server $server, BackupAdapterService $backupService) that
returns $server->backups->filter(fn($backup) => $backup->backupHost !== null &&
$backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema);
then update the three closures to call this helper: for options use
$this->getWingsBackups($server, $backupService)->mapWithKeys(...), for columns
compute ceil($this->getWingsBackups($record, $backupService)->count() / 4), and
for hidden use $this->getWingsBackups($server, $backupService)->count() === 0 so
you only check backupHost once and avoid null-dereference.
- Around line 987-992: The closure that iterates backups accesses
$backup->backupHost->schema which can be null and causes a fatal error; update
the closure in the ->each(...) block to first fetch $host = $backup->backupHost
and guard it (if !$host then call $backup->delete() and return) before calling
$backupService->get($host->schema), then continue with the existing check for
instanceof WingsBackupSchema and $backup->delete() as needed.
- Around line 987-992: The current iterator calls $backup->delete() directly
which only removes the DB record and leaves files on the Wings daemon; retrieve
the schema via $backupService->get(...) and if it's an instance of
WingsBackupSchema call $schema->deleteBackup($backup) before deleting the DB
record, and perform both actions inside a transaction (follow the pattern in
DeleteBackupService::handle()) so the daemon DELETE is attempted before calling
$backup->delete().

---

Outside diff comments:
In `@app/Filament/Server/Resources/Backups/BackupResource.php`:
- Around line 205-216: Wrap the call to DownloadLinkService::handle($backup,
user()) in a try/catch before you create/commit the Activity log and before
changing server state: if handle() throws, catch the exception, return a
friendly notification/error to the user and abort the restore flow so no
Activity::event('server:backup.restore') is committed and
$server->update(['status' => ServerState::RestoringBackup]) is not executed;
only after successfully obtaining $url should you create the $log, call
$log->transaction(...) and invoke
$daemonRepository->setServer($server)->restore($backup, $url,
$data['truncate']).

---

Nitpick comments:
In `@app/Models/Node.php`:
- Around line 281-284: The relation method backupHosts() uses incorrect casing
and lacks a return annotation: replace the call to
$this->BelongsToMany(BackupHost::class) with the conventional
$this->belongsToMany(BackupHost::class) and add the docblock `@return`
\Illuminate\Database\Eloquent\Relations\BelongsToMany (or the project's
BelongsToMany import) above the backupHosts() method to match databaseHosts()
and other relations.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7da9d8c and c215e95.

📒 Files selected for processing (6)
  • app/Enums/RolePermissionModels.php
  • app/Filament/Admin/Pages/Settings.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Filament/Server/Resources/Backups/BackupResource.php
  • app/Models/Node.php
  • app/Providers/AppServiceProvider.php
💤 Files with no reviewable changes (2)
  • app/Filament/Admin/Pages/Settings.php
  • app/Providers/AppServiceProvider.php

@Boy132 Boy132 marked this pull request as ready for review February 24, 2026 12:38
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

985-993: ⚠️ Potential issue | 🟠 Major

Call the Wings schema delete before removing the DB record.

Deleting only the DB row can leave orphaned backups on the daemon. Use the schema delete first (or the existing delete service) and then remove the record.

🔧 Suggested fix
                                                         ->each(function ($backup) use ($backupService) {
                                                             $schema = $backupService->get($backup->backupHost->schema);

                                                             // Wings backups that aren't transferred only need to be delete on the panel, wings will cleanup the backup files automatically
                                                             if ($schema instanceof WingsBackupSchema) {
+                                                                $schema->deleteBackup($backup);
                                                                 $backup->delete();
                                                             }
                                                         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 985 -
993, In the loop over $server->backups (the block using
$backupService->get($backup->backupHost->schema) and checking "instanceof
WingsBackupSchema"), call the schema's deletion routine (or the project-wide
backup delete service) to remove the backup from the daemon before calling
$backup->delete() on the DB record; i.e., for WingsBackupSchema invoke its
delete/cleanup method (or use the existing backup deletion service) and only
after that succeeds remove the backup row with $backup->delete() to avoid
leaving orphaned files on the daemon.
app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php (2)

24-33: ⚠️ Potential issue | 🟠 Major

Filter S3 client config to avoid invalid options and isolate credentials.

Line 26 passes the entire backup host configuration into S3Client. If the AWS SDK validates options, unknown keys like bucket, key, and secret can trigger InvalidArgumentException at runtime. Build a clean config array and move credentials under credentials.

🛠️ Suggested fix
-    private function createClient(BackupHost $backupHost): S3Client
-    {
-        $config = $backupHost->configuration;
-        $config['version'] = 'latest';
-
-        if (!empty($config['key']) && !empty($config['secret'])) {
-            $config['credentials'] = Arr::only($config, ['key', 'secret', 'token']);
-        }
-
-        return new S3Client($config);
-    }
+    private function createClient(BackupHost $backupHost): S3Client
+    {
+        $cfg = $backupHost->configuration;
+        $config = [
+            'version' => 'latest',
+            'region' => $cfg['region'] ?? null,
+            'endpoint' => $cfg['endpoint'] ?? null,
+            'use_path_style_endpoint' => $cfg['use_path_style_endpoint'] ?? false,
+        ];
+
+        if (!empty($cfg['key']) && !empty($cfg['secret'])) {
+            $config['credentials'] = [
+                'key' => $cfg['key'],
+                'secret' => $cfg['secret'],
+                'token' => $cfg['token'] ?? null,
+            ];
+        }
+
+        return new S3Client(array_filter($config, fn ($v) => $v !== null));
+    }
AWS SDK for PHP S3Client configuration options: does it reject unknown keys like "bucket", "key", or "secret" at the top level?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php` around lines 24 -
33, The createClient(BackupHost $backupHost) currently passes the entire
$backupHost->configuration into new S3Client which can include invalid top-level
keys (e.g., bucket, key, secret); instead, build a sanitized $config array
containing only allowed AWS options (at minimum 'version' and 'region' if
present), move credentials into a 'credentials' sub-array (use
Arr::only($configRaw, ['key','secret','token']) to populate it) and then pass
that sanitized $config to new S3Client; update the createClient method to read
$backupHost->configuration into a temp, pick allowed keys, set 'version' =>
'latest', add 'credentials' when keys exist, and then instantiate S3Client with
the cleaned config.

103-151: ⚠️ Potential issue | 🟡 Minor

Guard storage_class access to prevent PHP notices.

Line 115 directly indexes configuration['storage_class']. If the key is missing, PHP emits a notice. Use null coalescing before the check.

🔧 Suggested fix
-        $storageClass = $backup->backupHost->configuration['storage_class'];
-        if (!is_null($storageClass)) {
+        $storageClass = $backup->backupHost->configuration['storage_class'] ?? null;
+        if ($storageClass !== null) {
             $params['StorageClass'] = $storageClass;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php` around lines 103 -
151, The code in getUploadParts accesses
$backup->backupHost->configuration['storage_class'] directly which can emit a
PHP notice if the key is missing; update the access to use a null-coalescing
lookup (e.g. $backup->backupHost->configuration['storage_class'] ?? null) or
fetch into a variable via $storageClass =
$backup->backupHost->configuration['storage_class'] ?? null before the if-check,
then use that $storageClass for the subsequent if (!is_null($storageClass)) and
when setting $params['StorageClass'] to prevent notices.
🧹 Nitpick comments (1)
app/Models/Node.php (1)

281-284: Add @return generic annotation for consistency with databaseHosts().

databaseHosts() at line 275 was just updated to carry @return BelongsToMany<DatabaseHost, $this>, but the new backupHosts() method is missing the equivalent annotation.

♻️ Proposed fix
+    /** `@return` BelongsToMany<BackupHost, $this> */
     public function backupHosts(): BelongsToMany
     {
         return $this->belongsToMany(BackupHost::class);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Node.php` around lines 281 - 284, Add the same generic `@return`
docblock to the backupHosts relation as used on databaseHosts: update the PHPDoc
for the backupHosts() method to declare `@return BelongsToMany<BackupHost,
$this>` so IDEs and static analyzers get the precise relation types; locate the
backupHosts() method and add the matching annotation referencing BackupHost and
$this.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`:
- Around line 56-70: The getDownloadLink(Backup $backup, User $user): string
method currently ignores the $user parameter which trips PHPMD; to fix, either
use $user for audit/logging inside getDownloadLink (e.g., record who requested
the URL) or explicitly mark it as intentionally unused to silence PHPMD by
adding a no-op usage like casting (void)$user; at the top of the getDownloadLink
body; update the PHPDoc if you choose suppression so reviewers know it's
intentional.

---

Duplicate comments:
In `@app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`:
- Around line 24-33: The createClient(BackupHost $backupHost) currently passes
the entire $backupHost->configuration into new S3Client which can include
invalid top-level keys (e.g., bucket, key, secret); instead, build a sanitized
$config array containing only allowed AWS options (at minimum 'version' and
'region' if present), move credentials into a 'credentials' sub-array (use
Arr::only($configRaw, ['key','secret','token']) to populate it) and then pass
that sanitized $config to new S3Client; update the createClient method to read
$backupHost->configuration into a temp, pick allowed keys, set 'version' =>
'latest', add 'credentials' when keys exist, and then instantiate S3Client with
the cleaned config.
- Around line 103-151: The code in getUploadParts accesses
$backup->backupHost->configuration['storage_class'] directly which can emit a
PHP notice if the key is missing; update the access to use a null-coalescing
lookup (e.g. $backup->backupHost->configuration['storage_class'] ?? null) or
fetch into a variable via $storageClass =
$backup->backupHost->configuration['storage_class'] ?? null before the if-check,
then use that $storageClass for the subsequent if (!is_null($storageClass)) and
when setting $params['StorageClass'] to prevent notices.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 985-993: In the loop over $server->backups (the block using
$backupService->get($backup->backupHost->schema) and checking "instanceof
WingsBackupSchema"), call the schema's deletion routine (or the project-wide
backup delete service) to remove the backup from the daemon before calling
$backup->delete() on the DB record; i.e., for WingsBackupSchema invoke its
delete/cleanup method (or use the existing backup deletion service) and only
after that succeeds remove the backup row with $backup->delete() to avoid
leaving orphaned files on the daemon.

---

Nitpick comments:
In `@app/Models/Node.php`:
- Around line 281-284: Add the same generic `@return` docblock to the
backupHosts relation as used on databaseHosts: update the PHPDoc for the
backupHosts() method to declare `@return BelongsToMany<BackupHost, $this>` so
IDEs and static analyzers get the precise relation types; locate the
backupHosts() method and add the matching annotation referencing BackupHost and
$this.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c215e95 and c3b597d.

📒 Files selected for processing (6)
  • app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/ListBackupHosts.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Models/Node.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/Filament/Admin/Resources/BackupHosts/Pages/ListBackupHosts.php
  • app/Filament/Admin/Resources/BackupHosts/Pages/EditBackupHost.php
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/pelican-dev/panel/issues/comments/3771874444","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nReplaces disk-based backups with a pluggable BackupHost / BackupAdapter system: adds adapter interfaces, registry and S3/Wings schemas; introduces BackupHost model/resource/migration; rewires services/controllers to delegate to adapters; removes legacy BackupManager/S3Filesystem and related config/UI; updates tests and providers.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Adapter Core** <br> `app/Extensions/BackupAdapter/BackupAdapterSchemaInterface.php`, `app/Extensions/BackupAdapter/BackupAdapterService.php`, `app/Extensions/BackupAdapter/Schemas/BackupAdapterSchema.php`|Adds adapter interface, a registry service for schemas, and an abstract base schema implementing getName.|\n|**Adapter Implementations** <br> `app/Extensions/BackupAdapter/Schemas/S3BackupSchema.php`, `app/Extensions/BackupAdapter/Schemas/WingsBackupSchema.php`|New S3 and Wings schema implementations handling create/delete, download link generation; S3 implements multipart upload/presigned URLs and client caching.|\n|**BackupHost Domain** <br> `app/Models/BackupHost.php`, `database/migrations/..._create_backup_hosts_table.php`, `database/Factories/BackupHostFactory.php`, `lang/en/admin/backuphost.php`|Adds BackupHost model, migration (backup_hosts + pivot), factory and translations; migration migrates existing backups to a BackupHost.|\n|**Backup Model & Node** <br> `app/Models/Backup.php`, `app/Models/Node.php`, `database/Factories/BackupFactory.php`|Replaces Backup.disk with backup_host_id, adds backupHost relation; Node gets backupHosts relation; factory removes disk fixture.|\n|**Filament Admin UI** <br> `app/Filament/Admin/Resources/BackupHosts/*`, `app/Filament/Admin/Pages/Settings.php`, `app/Filament/Admin/Resources/Servers/Pages/EditServer.php`, `app/Filament/Server/Resources/Backups/BackupResource.php`|Adds BackupHost Filament resource, pages and relation manager; removes APP_BACKUP_DRIVER/S3 UI; server edit and backup UI wired to BackupAdapterService; download link generation consolidated.|\n|**Service Providers & Bootstrap** <br> `app/Providers/Extensions/BackupAdapterServiceProvider.php`, `app/Providers/BackupsServiceProvider.php` (removed), `bootstrap/providers.php`|Registers BackupAdapterService and default schemas; removes old BackupsServiceProvider and updates bootstrap providers.|\n|**Services Refactored** <br> `app/Services/Backups/InitiateBackupService.php`, `app/Services/Backups/DeleteBackupService.php`, `app/Services/Backups/DownloadLinkService.php`|Services now resolve schemas via BackupAdapterService and delegate create/delete/download to adapter schemas; constructors and payloads updated to use BackupHost/schema.|\n|**Controllers Updated** <br> `app/Http/Controllers/Api/Client/Servers/BackupController.php`, `app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php`, `app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php`|Controllers inject BackupAdapterService, resolve schemas, validate schema presence, and delegate upload/download/multipart operations to schemas; removed adapter-specific checks.|\n|**Legacy Removal** <br> `app/Extensions/Backups/BackupManager.php`, `app/Extensions/Filesystem/S3Filesystem.php`|Removed legacy BackupManager and S3Filesystem class and their provider binding.|\n|**Daemon Repository** <br> `app/Repositories/Daemon/DaemonBackupRepository.php`|Renamed backup()→create(), removed internal adapter state; daemon payloads now use backupHost->schema.|\n|**Tasks & Transfer Flow** <br> `app/Extensions/Tasks/Schemas/CreateBackupSchema.php`, `app/Services/Servers/TransferServerService.php`, `app/Filament/Admin/Resources/Servers/Pages/EditServer.php`|Small ctor/property rename; transfer flows updated to filter Wings-backed backups via BackupAdapterService and to pass backup UUIDs.|\n|**Config** <br> `config/backups.php`|Removed default driver and disks block; retains presigned/max part/prune/throttles settings only.|\n|**Tests** <br> `tests/Integration/.../Backup/*`, `tests/Integration/Services/Backups/DeleteBackupServiceTest.php`|Tests updated to create and associate BackupHost with backups; S3-specific deletion test removed.|\n|**Policies** <br> `app/Policies/BackupHostPolicy.php`|New policy gating BackupHost actions by linked nodes.|\n|**Enum update** <br> `app/Enums/RolePermissionModels.php`|Added enum case `BackupHost = 'backupHost'`.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant User as User/Client\n    participant Initiator as InitiateBackupService\n    participant AdapterSvc as BackupAdapterService\n    participant Schema as BackupSchema (S3 or Wings)\n    participant DB as Database\n    participant External as External System\n\n    User->>Initiator: initiate backup request\n    Initiator->>DB: create Backup record (backup_host_id)\n    Initiator->>AdapterSvc: get(backupHost.schema)\n    AdapterSvc-->>Initiator: BackupSchema instance\n    Initiator->>Schema: createBackup(backup)\n    Schema->>External: perform storage action (upload/daemon)\n    External-->>Schema: confirm\n    Schema-->>Initiator: completion\n    Initiator-->>User: backup created\n```\n\n```mermaid\nsequenceDiagram\n    participant Client as Client/User\n    participant Controller as BackupController\n    participant AdapterSvc as BackupAdapterService\n    participant Schema as BackupSchema (S3 or Wings)\n    participant External as External System\n\n    Client->>Controller: request download link\n    Controller->>AdapterSvc: get(backup.backupHost.schema)\n    AdapterSvc-->>Controller: BackupSchema instance\n    Controller->>Schema: getDownloadLink(backup, user)\n    alt S3\n        Schema->>External: generate presigned URL\n    else Wings\n        Schema->>External: generate JWT and daemon URL\n    end\n    External-->>Schema: url\n    Schema-->>Controller: download URL\n    Controller-->>Client: return URL\n```\n\n## Possibly related PRs\n\n- pelican-dev/panel#2068 — modifies server transfer flow and backup filtering in a way that overlaps this PR's TransferServerService/EditServer changes.\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>\n\n### ❌ Failed checks (1 warning)\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 45.98% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                                                                                                                                                                                                                                          |\n| :------------------------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|         Title check        | ✅ Passed | The title 'Backup hosts' is concise and directly reflects the primary change: introducing a new BackupHost model and infrastructure to manage backup configurations.                                                                                                                                                                                 |\n|      Description check     | ✅ Passed | The description mentions closing issue `#1919` and enabling custom adapter registration, which align with the changeset's introduction of BackupAdapterService and adapter-based architecture.                                                                                                                                                         |\n|     Linked Issues check    | ✅ Passed | The PR implements all primary objectives from issue `#1919`: creates BackupHost model with schema/configuration fields [BackupHostResource, BackupHost model], enables multiple backup configurations [BackupAdapterService, S3BackupSchema], allows node assignment [BelongsToMany relation], and preserves Wings (local) option [WingsBackupSchema]. |\n| Out of Scope Changes check | ✅ Passed | All changes align with objectives: BackupAdapter interfaces and implementations support schema-based adapter management; migrations and models support BackupHost; UI resources enable backup host management; service layer refactoring redirects to adapter schemas. No unrelated changes detected.                                                |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=pelican-dev/panel&utm_content=2125)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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= -->\n\n<!-- internal state end -->"},"request":{"retryCount":3,"retries":3,"retryAfter":16}}}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

985-993: ⚠️ Potential issue | 🟠 Major

Verify Wings cleanup before deleting backup DB rows.
Line 991–992 deletes only the panel record. If Wings doesn’t auto-prune untransferred backups, this leaves orphaned files. Consider calling deleteBackup() before delete(), or confirm the daemon’s cleanup behavior.

🧩 Suggested fix (if cleanup is not automatic)
-                                                            if ($schema instanceof WingsBackupSchema) {
-                                                                $backup->delete();
-                                                            }
+                                                            if ($schema instanceof WingsBackupSchema) {
+                                                                $schema->deleteBackup($backup);
+                                                                $backup->delete();
+                                                            }
Pelican Wings server transfer: does the daemon automatically delete backups not included in a transfer, or must the panel call the backup delete API?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 985 -
993, The code currently deletes only the DB row for Wings backups (inside the
closure using $backupService->get(...) and checking instanceof
WingsBackupSchema) which may leave orphaned files; update the logic to verify
daemon cleanup and if not guaranteed, call the backup model's remote deletion
method (e.g., deleteBackup() or the appropriate API method on the backup
model/service) before calling $backup->delete(), ensuring you still use the same
WingsBackupSchema check and $backupService->get(...) to locate Wings backups.
🧹 Nitpick comments (1)
app/Filament/Server/Resources/Backups/BackupResource.php (1)

173-173: user() passes a nullable type to DownloadLinkService::handle

user() returns Authenticatable|null, and while the ->authorize() guard ensures the user is authenticated when this callback runs, passing user() without asserting non-null may trip static analysis. The same pattern applies at line 210.

🛡️ Optional null assertion
-->url(fn (DownloadLinkService $downloadLinkService, Backup $backup) => $downloadLinkService->handle($backup, user()), true)
+->url(fn (DownloadLinkService $downloadLinkService, Backup $backup) => $downloadLinkService->handle($backup, user()!), true)

Or, if the project uses PHPDoc assertions:

-$url = $downloadLinkService->handle($backup, user());
+/** `@var` \App\Models\User $user */
+$user = user();
+$url = $downloadLinkService->handle($backup, $user);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Server/Resources/Backups/BackupResource.php` at line 173, The
callback passes user() (nullable) into DownloadLinkService::handle which can
trigger static-analysis errors; capture the result into a local variable, assert
it's non-null (e.g. $authUser = user(); assert($authUser instanceof
\Illuminate\Contracts\Auth\Authenticatable); or throw if null) and then pass
$authUser to DownloadLinkService::handle; apply the same change for the second
occurrence referenced (the other callback around line 210) so both calls use an
asserted non-null $authUser variable before calling handle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php`:
- Around line 123-137: The configuration section always reads the persisted
model schema first (BackupHost->schema) so changing the schema in the live form
doesn't update the configuration UI; update the selection order to prefer the
live form state from Get $get('schema') and fall back to the model's schema only
if the live value is empty. Locate the closure passed to Section::make(...) that
receives (?BackupHost $backupHost, Get $get, BackupAdapterService $service) and
change the logic that sets $schema so it checks $get('schema') first, then
$backupHost->schema as a fallback, then call $service->get($schema) and return
$schema->getConfigurationForm() as before.

In `@app/Services/Servers/TransferServerService.php`:
- Around line 30-45: The backup lookup in the notify method is using
Backup::find($uuid) which looks up by integer PK and returns null for UUID
strings; change the lookup in notify (method name: notify, class:
TransferServerService) to resolve by the uuid column (e.g., query Backup where
'uuid' equals the $uuid and take first()) so $backup is correctly loaded and
WingsBackupSchema checks run; update any related logic that uses $backup
accordingly.

---

Duplicate comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 985-993: The code currently deletes only the DB row for Wings
backups (inside the closure using $backupService->get(...) and checking
instanceof WingsBackupSchema) which may leave orphaned files; update the logic
to verify daemon cleanup and if not guaranteed, call the backup model's remote
deletion method (e.g., deleteBackup() or the appropriate API method on the
backup model/service) before calling $backup->delete(), ensuring you still use
the same WingsBackupSchema check and $backupService->get(...) to locate Wings
backups.

---

Nitpick comments:
In `@app/Filament/Server/Resources/Backups/BackupResource.php`:
- Line 173: The callback passes user() (nullable) into
DownloadLinkService::handle which can trigger static-analysis errors; capture
the result into a local variable, assert it's non-null (e.g. $authUser = user();
assert($authUser instanceof \Illuminate\Contracts\Auth\Authenticatable); or
throw if null) and then pass $authUser to DownloadLinkService::handle; apply the
same change for the second occurrence referenced (the other callback around line
210) so both calls use an asserted non-null $authUser variable before calling
handle.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6ba81e and f300259.

📒 Files selected for processing (5)
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
  • app/Filament/Admin/Resources/BackupHosts/RelationManagers/BackupsRelationManager.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Filament/Server/Resources/Backups/BackupResource.php
  • app/Services/Servers/TransferServerService.php

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
app/Filament/Admin/Resources/Servers/Pages/EditServer.php (1)

985-993: Confirm Wings cleanup before deleting the DB record.
If Wings does not automatically remove backup archives during transfer, $backup->delete() will orphan storage. Please verify the cleanup behavior; otherwise call $schema->deleteBackup($backup) before deleting the model.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php` around lines 985 -
993, The current loop deletes backup DB records for backups with Wings schema
without ensuring storage cleanup; update the logic in the block handling
$server->backups->whereNotIn('uuid', $selectedBackupUuids)->each(...) to verify
Wings actually removed the archive and, if not, call the schema-level cleanup
method (use $backupService->get($backup->backupHost->schema) to obtain $schema
and then call $schema->deleteBackup($backup) or equivalent) before invoking
$backup->delete(); ensure you still only call $backup->delete() after successful
cleanup or on a confirmed Wings-auto-cleanup path (keep usage of
WingsBackupSchema to branch the behavior).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Services/Servers/TransferServerService.php`:
- Around line 67-68: The handle method's signature allows ?array $backup_uuids
but later passes it into notify() which requires array, causing a TypeError; fix
by making $backup_uuids non-nullable (change signature to array $backup_uuids =
[]) or, inside TransferServerService::handle, coerce null to an empty array
(e.g. $backup_uuids = $backup_uuids ?? []) before calling notify(); update the
same pattern for the other occurrence mentioned (line ~118) to ensure notify()
always receives an array.
- Around line 36-45: The loop is doing an unscoped, per-UUID lookup
(Backup::where('uuid', ...) inside the foreach over $backup_uuids) causing
potential cross-server matches and N+1 queries; replace it by querying all
backups for the transfer server once (e.g. fetch Backup::where('server_id',
$server->id)->whereIn('uuid', $backup_uuids')->get()), then iterate that
collection and call $this->backupService->get(...) to check instanceof
WingsBackupSchema and populate $backups, ensuring you only consider backups
belonging to the target server and avoid repeated DB calls.

---

Duplicate comments:
In `@app/Filament/Admin/Resources/Servers/Pages/EditServer.php`:
- Around line 985-993: The current loop deletes backup DB records for backups
with Wings schema without ensuring storage cleanup; update the logic in the
block handling $server->backups->whereNotIn('uuid',
$selectedBackupUuids)->each(...) to verify Wings actually removed the archive
and, if not, call the schema-level cleanup method (use
$backupService->get($backup->backupHost->schema) to obtain $schema and then call
$schema->deleteBackup($backup) or equivalent) before invoking $backup->delete();
ensure you still only call $backup->delete() after successful cleanup or on a
confirmed Wings-auto-cleanup path (keep usage of WingsBackupSchema to branch the
behavior).

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6ba81e and 5057d72.

📒 Files selected for processing (5)
  • app/Filament/Admin/Resources/BackupHosts/BackupHostResource.php
  • app/Filament/Admin/Resources/BackupHosts/RelationManagers/BackupsRelationManager.php
  • app/Filament/Admin/Resources/Servers/Pages/EditServer.php
  • app/Filament/Server/Resources/Backups/BackupResource.php
  • app/Services/Servers/TransferServerService.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/Filament/Server/Resources/Backups/BackupResource.php

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/Services/Servers/TransferServerService.php (2)

33-39: Avoid N+1 on backupHost during filtering.
Each $backup->backupHost access can trigger an extra query; eager load the relation before filtering.

♻️ Proposed tweak
-        $backups = Backup::where('server_id', $transfer->server_id)
-            ->whereIn('uuid', $backup_uuids)
-            ->get()
+        $backups = Backup::where('server_id', $transfer->server_id)
+            ->whereIn('uuid', $backup_uuids)
+            ->with('backupHost')
+            ->get()
             ->filter(fn (Backup $backup) => $this->backupService->get($backup->backupHost->schema) instanceof WingsBackupSchema)
             ->pluck('uuid')
             ->all();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Servers/TransferServerService.php` around lines 33 - 39, The
code filters backups by accessing $backup->backupHost inside a collection
filter, causing N+1 queries; eager load the backupHost relation before
retrieving/filtering to prevent extra queries. Update the Backup query used to
build $backups (the chain that calls Backup::where(...)->whereIn(...)) to
include the backupHost relation (e.g., via with('backupHost')) so the subsequent
->filter(fn (Backup $backup) =>
$this->backupService->get($backup->backupHost->schema) instanceof
WingsBackupSchema) runs in-memory without triggering additional queries.

30-30: Track the TODO for persisting backup UUIDs.
Line 30 notes missing persistence; consider opening a follow-up issue so selected backups aren’t lost if transfers need retry/audit later.

Would you like me to draft an issue or propose a migration sketch?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/Servers/TransferServerService.php` at line 30, The comment flags
a TODO to persist backup UUIDs for transfers; open a follow-up issue and
implement it by adding a new column (e.g. backup_uuids as JSON or text) to the
ServerTransfer model via a migration, add the corresponding $casts or attribute
on ServerTransfer to treat it as an array, update the TransferServerService
logic where the docblock and TODO appear to save the provided backup_uuids into
the ServerTransfer record when creating/updating transfers, and ensure any
validation/serialization is handled consistently when reading/writing that
field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/Services/Servers/TransferServerService.php`:
- Around line 33-39: The code filters backups by accessing $backup->backupHost
inside a collection filter, causing N+1 queries; eager load the backupHost
relation before retrieving/filtering to prevent extra queries. Update the Backup
query used to build $backups (the chain that calls
Backup::where(...)->whereIn(...)) to include the backupHost relation (e.g., via
with('backupHost')) so the subsequent ->filter(fn (Backup $backup) =>
$this->backupService->get($backup->backupHost->schema) instanceof
WingsBackupSchema) runs in-memory without triggering additional queries.
- Line 30: The comment flags a TODO to persist backup UUIDs for transfers; open
a follow-up issue and implement it by adding a new column (e.g. backup_uuids as
JSON or text) to the ServerTransfer model via a migration, add the corresponding
$casts or attribute on ServerTransfer to treat it as an array, update the
TransferServerService logic where the docblock and TODO appear to save the
provided backup_uuids into the ServerTransfer record when creating/updating
transfers, and ensure any validation/serialization is handled consistently when
reading/writing that field.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5057d72 and cdd69c2.

📒 Files selected for processing (1)
  • app/Services/Servers/TransferServerService.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor Backup Options

1 participant