Skip to content

Conversation

@notAreYouScared
Copy link
Member

@notAreYouScared notAreYouScared commented Nov 3, 2025

Reimplements Drag and Dropping for file uploads when viewing the file manager list. Fixes #1855 and Closes #1186

** Only Supported if the browser supports it! **
Adds support for folders 🥳 ( Also supports folderception )
Uploads directly to wings from the users pc, instead of user->panel->wings

Known Issues / Things that would be nice...
~ Check user permissions BEFORE showing the d&d

Video is outdated.... Feel free to Pull this PR and test it youself :)

SPOILER_final-form.mp4

@notAreYouScared notAreYouScared added this to the beta28 milestone Nov 3, 2025
@notAreYouScared notAreYouScared self-assigned this Nov 3, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2025

📝 Walkthrough

Walkthrough

Adds a Blade/Alpine drag-and-drop upload UI and a full-screen upload component; extends the Filament ListFiles page with view wiring, JWT upload URL/size endpoints, folder-creation and upload-logging methods; adds upload translations and bumps Vite devDependency.

Changes

Cohort / File(s) Summary
Backend: ListFiles page
app/Filament/Server/Resources/Files/Pages/ListFiles.php
Added protected string $view = 'filament.server.pages.list-files'. Added public methods: getUploadUrl(NodeJWTService $jwtService): string, getUploadSizeLimit(): int, createFolder(string $folderPath): void, and logUploadedFiles(array $files): void. Methods perform authorization checks, generate short-lived JWT upload URLs, return upload size limit, create directories via daemon repository with error handling, and emit upload events/logs.
Frontend: List files view
resources/views/filament/server/pages/list-files.blade.php
Replaced inline widget with a view-based upload component wired to Alpine.js: drag‑and‑drop handlers, file/folder extraction, upload queue, server folder-creation calls, token retrieval, concurrent client uploads with per-file progress/speed, and Filament notifications; triggers backend logging on completion.
Frontend: Full-screen file upload component
resources/views/filament/server/pages/file-upload.blade.php
New Blade + Alpine component implementing folder-aware drag‑and‑drop and picker UI, upload queue with per-file metadata, concurrent XHR uploads (max 3) with progress/speed tracking, Escape-to-cancel, directory-aware upload URL construction, notifications, and auto-close behavior.
Translations
lang/en/server/file.php
Added upload UI translation keys under actions.upload: drop_files, success, failed, header, error.
Tooling
package.json
Bumped devDependency vite from 6.2.6 to 7.1.11.

Sequence Diagram(s)

%%{init: {"themeVariables":{"actorBackground":"#F3F8FF","sequenceNumber":"#666"}}}%%
sequenceDiagram
    actor User
    participant UI as Upload UI (Blade + Alpine)
    participant Livewire as ListFiles (Livewire)
    participant Repo as DaemonFileRepository
    participant Auth as Authorization

    User->>UI: Drag & drop files/folders
    UI->>Livewire: getUploadSizeLimit()
    Livewire-->>UI: size limit

    loop ensure folders exist
        UI->>Livewire: createFolder(path)
        Livewire->>Auth: authorize create-directory
        alt authorized
            Livewire->>Repo: createDirectory(path)
            Repo-->>Livewire: success / exists
            Livewire-->>UI: ack / error
        else unauthorized
            Livewire-->>UI: 403
        end
    end

    UI->>Livewire: getUploadUrl()
    Livewire->>Auth: authorize file:create
    alt authorized
        Livewire-->>UI: signed JWT upload URL
    else unauthorized
        Livewire-->>UI: 403
    end

    loop per file (concurrent up to 3)
        UI->>UI: start XHR upload (JWT + directory)
        UI-->>Livewire: upload response (success/error)
        UI->>UI: update progress/status
    end

    alt all succeeded
        UI->>User: success notification
        UI->>Livewire: logUploadedFiles(files)
    else partial/failed
        UI->>User: partial/failure notification
        UI->>Livewire: logUploadedFiles(files)
    end
Loading

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Reimplement Drag & Drop for file uploading' clearly summarizes the primary change: reimplementing drag-and-drop functionality for file uploads, which matches the changeset content across multiple files.
Description check ✅ Passed The description is directly related to the changeset, explaining the reimplementation of drag-and-drop file uploads, folder support, direct Wings upload capability, and references specific linked issues.
Linked Issues check ✅ Passed The PR addresses both linked issues: #1186 is met via the reimplemented drag-and-drop UI in the view files; #1855 is partially addressed through direct-to-Wings upload capability via URL-based flow, though the original upload button path remains for mobile compatibility.
Out of Scope Changes check ✅ Passed All code changes are directly related to drag-and-drop file upload functionality and supporting infrastructure: UI views, backend endpoints, translations, and dependencies. No unrelated changes detected.

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.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2aff54 and 788a7ad.

📒 Files selected for processing (3)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (4 hunks)
  • lang/en/server/file.php (1 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (8)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/Repositories/Daemon/DaemonFileRepository.php (2)
  • DaemonFileRepository (12-248)
  • createDirectory (86-100)
app/helpers.php (1)
  • user (127-130)
app/Models/Permission.php (1)
  • Permission (11-221)
app/Models/Node.php (1)
  • getConnectionAddress (178-181)
app/Facades/Activity.php (1)
  • Activity (8-14)
app/Services/Activity/ActivityLogService.php (3)
  • event (48-53)
  • property (109-117)
  • log (136-155)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)
🔇 Additional comments (1)
resources/views/filament/server/pages/list-files.blade.php (1)

198-204: Align failure notifications with defined translations.

The notifications reference server/file.actions.upload.error_all / error_partial, which are not defined. Until the translation file gains those keys, these will surface raw translation paths to users. Please either add the missing language strings (see translation file comment) or point these notifications at the newly added failed key once it exists.

⛔ Skipped due to learnings
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1671
File: lang/en/server/database.php:22-25
Timestamp: 2025-09-06T20:20:48.706Z
Learning: In the pelican-dev/panel project, translation workflows for non-English languages are handled in separate PRs and processes, so missing translations in other locales should be ignored during code reviews.

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 (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (1)

670-695: Nested folder paths still need to be split before calling createDirectory.

This issue was flagged in a previous review but hasn't been addressed. When $folderPath contains nested paths (e.g., photos/2023), passing it directly to createDirectory will fail because the daemon expects only the folder basename as the first parameter, not a path with slashes.

The previous review suggested splitting the path into its parent segments and basename. For example:

         try {
-            $this->getDaemonFileRepository()->createDirectory($folderPath, $this->path);
+            $fullPath = trim($folderPath, '/');
+            $segments = array_values(array_filter(explode('/', $fullPath), static fn (string $segment): bool => $segment !== ''));
+            $name = array_pop($segments);
+
+            if ($name === null) {
+                return;
+            }
+
+            $targetPath = $this->path;
+
+            if (!empty($segments)) {
+                $targetPath = join_paths($this->path, implode('/', $segments));
+            }
+
+            $this->getDaemonFileRepository()->createDirectory($name, $targetPath);
 
             Activity::event('server:file.create-directory')
-                ->property(['directory' => $this->path, 'name' => $folderPath])
+                ->property(['directory' => $targetPath, 'name' => $name])
                 ->log();
 
         } catch (FileExistsException) {
🧹 Nitpick comments (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (1)

665-694: Update exception handling to match method signature.

The method signature declares @throws ConnectionException, but the exception is caught and not rethrown (Lines 688-694). This creates an inconsistency between the documented behavior and actual implementation.

Choose one of the following approaches:

Option 1: Remove the @throws ConnectionException annotation if catching and showing a notification is the desired behavior:

     /**
-     * @throws ConnectionException
      * @throws FileExistsException
      * @throws \Throwable
      */

Option 2: Rethrow the exception after sending the notification if callers need to handle it:

         } catch (ConnectionException $e) {
             Notification::make()
                 ->body($e->getMessage())
                 ->danger()
                 ->send();
+
+            throw $e;
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 788a7ad and 8eef1d1.

📒 Files selected for processing (1)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (8)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/helpers.php (1)
  • user (127-130)
app/Models/Node.php (1)
  • getConnectionAddress (178-181)
app/Filament/Server/Resources/Files/Pages/EditFiles.php (1)
  • getDaemonFileRepository (270-277)
app/Repositories/Daemon/DaemonFileRepository.php (2)
  • createDirectory (86-100)
  • DaemonFileRepository (12-248)
app/Facades/Activity.php (1)
  • Activity (8-14)
app/Services/Activity/ActivityLogService.php (3)
  • event (48-53)
  • property (109-117)
  • log (136-155)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)
🔇 Additional comments (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (1)

657-663: LGTM!

The conversion from MiB to bytes is correct, and the implementation is straightforward.

@JoanFo1456
Copy link
Contributor

@notAreYouScared This won't fix the upload issue on the panel (#1855)
(You linked it) As the original upload button will still pass by php.

@notAreYouScared
Copy link
Member Author

@notAreYouScared This won't fix the upload issue on the panel (#1855) (You linked it) As the original upload button will still pass by php.

Referring to our previous discussion in discord, im well aware. The only reason it still exists is due to me nlt having a solution for mobile users yet.

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 (2)
resources/views/filament/server/pages/list-files.blade.php (1)

175-192: Fix the concurrency limiter cleanup

Promise objects never expose .status, so activeUploads.filter(p => p.status …) never removes settled uploads. After the first Promise.race the array keeps the resolved promises and the limiter stops working, letting every remaining upload start at once. Wrap each upload promise in a finally that removes itself from activeUploads so the cap actually holds.

-                    for (let i = 0; i < this.uploadQueue.length; i++) {
-                        const uploadPromise = this.uploadFile(i)
-                            .then(() => { completedCount++; this.currentFileIndex = completedCount; })
-                            .catch(() => { completedCount++; this.currentFileIndex = completedCount; });
-
-                        activeUploads.push(uploadPromise);
-
-                        if (activeUploads.length >= maxConcurrent) {
-                            await Promise.race(activeUploads);
-                            activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
-                        }
-                    }
-
-                    await Promise.allSettled(activeUploads);
+                    for (let i = 0; i < this.uploadQueue.length; i++) {
+                        const uploadPromise = this.uploadFile(i)
+                            .then(() => { completedCount++; this.currentFileIndex = completedCount; })
+                            .catch(() => { completedCount++; this.currentFileIndex = completedCount; });
+
+                        const trackedPromise = uploadPromise.finally(() => {
+                            activeUploads = activeUploads.filter(p => p !== trackedPromise);
+                        });
+
+                        activeUploads.push(trackedPromise);
+
+                        if (activeUploads.length >= maxConcurrent) {
+                            await Promise.race(activeUploads);
+                        }
+                    }
+
+                    await Promise.allSettled(activeUploads);
app/Filament/Server/Resources/Files/Pages/ListFiles.php (1)

661-666: Split nested folder paths before calling the daemon

DaemonFileRepository::createDirectory() expects a folder name and its parent path separately. Passing "foo/bar" as $name yields a 400 from the daemon, so uploading directories with nesting always fails. Normalize the incoming relative path, take the basename as the name, and join the remaining segments onto $this->path before delegating.

-        try {
-            $this->getDaemonFileRepository()->createDirectory($folderPath, $this->path);
-
-            Activity::event('server:file.create-directory')
-                ->property(['directory' => $this->path, 'name' => $folderPath])
-                ->log();
+        $fullPath = trim($folderPath, '/');
+
+        if ($fullPath === '') {
+            return;
+        }
+
+        $segments = array_values(array_filter(explode('/', $fullPath), static fn (string $segment): bool => $segment !== ''));
+        $name = array_pop($segments);
+
+        if ($name === null) {
+            return;
+        }
+
+        $targetPath = $this->path;
+
+        if ($segments !== []) {
+            $targetPath = join_paths($this->path, implode('/', $segments));
+        }
+
+        try {
+            $this->getDaemonFileRepository()->createDirectory($name, $targetPath);
+
+            Activity::event('server:file.create-directory')
+                ->property(['directory' => $targetPath, 'name' => $name])
+                ->log();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8eef1d1 and d013b6b.

📒 Files selected for processing (3)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (6 hunks)
  • resources/views/filament/server/pages/file-upload.blade.php (1 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, the Forms component Placeholder was deprecated and removed. Use TextEntry from Filament\Infolists\Components\TextEntry in forms instead, binding values with ->state(). For HTML content, use ->html().

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, Wizard Step components use the Filament\Schemas namespace (Filament\Schemas\Components\Wizard\Step), not Filament\Forms.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (6)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/helpers.php (1)
  • user (127-130)
app/Models/Permission.php (1)
  • Permission (11-221)
app/Models/Node.php (1)
  • getConnectionAddress (178-181)
app/Repositories/Daemon/DaemonFileRepository.php (1)
  • createDirectory (86-100)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)

@JoanFo1456
Copy link
Contributor

Drag and drop is now working only when you're inside the upload button. Not by default on the ListFiles view.

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 (2)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (1)

655-680: Nested folder paths must be split before calling createDirectory.

Line 665 passes $folderPath directly as the $name parameter, but DaemonFileRepository::createDirectory(string $name, string $path) expects a single folder name, not a nested path. When uploading nested directories (e.g., photos/2023), the daemon will reject this with a validation error.

Split the incoming $folderPath into parent segments and basename, then create each level recursively or pass the correct parent path and folder name.

resources/views/filament/server/pages/list-files.blade.php (1)

175-193: Fix the concurrency limiter—settled promises never vacate their slots.

Line 188 attempts to filter promises by checking a .status property that doesn't exist on Promise objects. As a result, activeUploads never shrinks, and the concurrency limit breaks after the first batch. Wrap each upload promise with a .finally() hook that removes itself from the array once settled.

🧹 Nitpick comments (1)
resources/views/filament/server/pages/list-files.blade.php (1)

217-281: Consider reusing upload tokens across files to reduce server round-trips.

Line 221 calls getUploadUrl() for every file, generating a new JWT token each time. For large folder uploads (hundreds of files), this creates unnecessary server requests. Since tokens have a 15-minute expiry, you could fetch one token and reuse it across all files in the batch, refreshing only when needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d013b6b and 5774019.

📒 Files selected for processing (2)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (6 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, the Forms component Placeholder was deprecated and removed. Use TextEntry from Filament\Infolists\Components\TextEntry in forms instead, binding values with ->state(). For HTML content, use ->html().

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, Wizard Step components use the Filament\Schemas namespace (Filament\Schemas\Components\Wizard\Step), not Filament\Forms.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (5)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/helpers.php (1)
  • user (127-130)
app/Models/Permission.php (1)
  • Permission (11-221)
app/Repositories/Daemon/DaemonFileRepository.php (1)
  • createDirectory (86-100)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)
🔇 Additional comments (4)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (2)

620-640: LGTM! JWT-based upload URL generation is properly secured.

The permission check, token expiry, and URL construction are all correct. The 15-minute expiry provides a reasonable window for uploads while maintaining security.


642-648: LGTM! Upload size limit conversion is correct.

The MiB-to-bytes conversion is accurate, and exposing this limit allows client-side validation before upload attempts.

resources/views/filament/server/pages/list-files.blade.php (2)

84-120: LGTM! Recursive directory traversal correctly handles large directories.

The continuation loop in readEntries properly handles the fact that entries are returned in batches. The Promise-based recursive approach cleanly collects all nested files while preserving their relative paths.


13-57: LGTM! Drag-and-drop handlers are well-structured.

The dragCounter pattern correctly handles nested drag events, the modal check prevents conflicts, and the feature detection with fallback ensures broad compatibility while supporting folder uploads where available.

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)
resources/views/filament/server/pages/list-files.blade.php (2)

149-155: Folder creation failures are silently ignored, leading to subsequent upload failures.

When createFolder fails (e.g., for nested paths like photos/2023), Line 153 only logs a warning and continues. Files targeting non-existent folders will then fail during upload with unclear error messages. Surface folder-creation failures more prominently or halt the upload workflow if critical folders can't be created.


171-189: Fix the concurrency limiter so settled uploads vacate their slots.

Promise instances never expose a .status field, so Line 184's filter never removes settled promises from activeUploads. After the first race resolves, the array keeps growing and the maxConcurrent cap becomes ineffective. Wrap each upload promise with a finally hook that removes itself once settled.

Apply this diff to fix the concurrency limiter:

-                    for (let i = 0; i < this.uploadQueue.length; i++) {
-                        const uploadPromise = this.uploadFile(i)
-                            .then(() => { completedCount++; this.currentFileIndex = completedCount; })
-                            .catch(() => { completedCount++; this.currentFileIndex = completedCount; });
-
-                        activeUploads.push(uploadPromise);
-
-                        if (activeUploads.length >= maxConcurrent) {
-                            await Promise.race(activeUploads);
-                            activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
-                        }
-                    }
-
-                    await Promise.allSettled(activeUploads);
+                    for (let i = 0; i < this.uploadQueue.length; i++) {
+                        const uploadPromise = this.uploadFile(i)
+                            .then(() => { completedCount++; this.currentFileIndex = completedCount; })
+                            .catch(() => { completedCount++; this.currentFileIndex = completedCount; });
+
+                        const trackedPromise = uploadPromise.finally(() => {
+                            activeUploads = activeUploads.filter(p => p !== trackedPromise);
+                        });
+
+                        activeUploads.push(trackedPromise);
+
+                        if (activeUploads.length >= maxConcurrent) {
+                            await Promise.race(activeUploads);
+                        }
+                    }
+
+                    await Promise.allSettled(activeUploads);
app/Filament/Server/Resources/Files/Pages/ListFiles.php (1)

652-677: Split nested folder paths before calling createDirectory.

DaemonFileRepository::createDirectory() expects the folder name and its parent path as separate arguments (see snippet showing signature createDirectory(string $name, string $path)). Passing a nested path like photos/2023 as $folderPath results in daemon-side validation errors, so nested directory uploads fail. Split the incoming relative path into parent segments and basename before delegating to the repository.

Apply this diff to properly handle nested folder paths:

+        $fullPath = trim($folderPath, '/');
+        $segments = array_values(array_filter(explode('/', $fullPath), static fn (string $segment): bool => $segment !== ''));
+        $name = array_pop($segments);
+
+        if ($name === null) {
+            return;
+        }
+
+        $targetPath = $this->path;
+
+        if (!empty($segments)) {
+            $targetPath = join_paths($this->path, implode('/', $segments));
+        }
+
         try {
-            $this->getDaemonFileRepository()->createDirectory($folderPath, $this->path);
+            $this->getDaemonFileRepository()->createDirectory($name, $targetPath);

             Activity::event('server:file.create-directory')
-                ->property(['directory' => $this->path, 'name' => $folderPath])
+                ->property(['directory' => $targetPath, 'name' => $name])
                 ->log();
🧹 Nitpick comments (1)
resources/views/filament/server/pages/list-files.blade.php (1)

190-211: LGTM!

The completion handling correctly categorizes outcomes (all success, partial failure, all failure) and triggers appropriate notifications. The auto-close timer is properly cleaned up.

Consider increasing the auto-close delay from 1 second to 2-3 seconds to give users more time to review the upload results, especially for large batches:

-                    this.autoCloseTimer = setTimeout(() => {
-                        this.isUploading = false;
-                        this.uploadQueue = [];
-                    },1000);
+                    this.autoCloseTimer = setTimeout(() => {
+                        this.isUploading = false;
+                        this.uploadQueue = [];
+                    }, 2000);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5774019 and f41cf67.

📒 Files selected for processing (3)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (6 hunks)
  • resources/views/filament/server/pages/file-upload.blade.php (1 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • resources/views/filament/server/pages/file-upload.blade.php
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, the Forms component Placeholder was deprecated and removed. Use TextEntry from Filament\Infolists\Components\TextEntry in forms instead, binding values with ->state(). For HTML content, use ->html().

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, Wizard Step components use the Filament\Schemas namespace (Filament\Schemas\Components\Wizard\Step), not Filament\Forms.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (7)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/helpers.php (1)
  • user (127-130)
app/Models/Permission.php (1)
  • Permission (11-221)
app/Models/Node.php (1)
  • getConnectionAddress (178-181)
app/Repositories/Daemon/DaemonFileRepository.php (1)
  • createDirectory (86-100)
app/Services/Activity/ActivityLogService.php (3)
  • event (48-53)
  • property (109-117)
  • log (136-155)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)
🔇 Additional comments (9)
resources/views/filament/server/pages/list-files.blade.php (5)

13-28: LGTM!

The drag event handlers correctly use a counter-based approach to manage nested drag enter/leave events, preventing premature dismissal of the drag overlay when hovering over child elements.


29-53: LGTM!

The drop handler correctly prioritizes the FileSystem Access API (webkitGetAsEntry) for folder support, with a fallback to the standard FileList for browsers that don't support it. This progressive enhancement approach ensures broad compatibility.


55-78: LGTM!

The file extraction logic properly handles both file and directory entries with appropriate fallbacks, and uses Promise.all to ensure all asynchronous traversals complete before proceeding.


80-116: LGTM!

The recursive directory traversal correctly implements the readEntries() loop pattern (required for handling directories with many entries) and properly builds nested paths for uploaded files.


213-277: LGTM!

The upload implementation correctly uses XMLHttpRequest (rather than fetch) to enable upload progress tracking, properly constructs the upload path, and handles both success and error cases. The speed calculation throttling (Line 242) prevents excessive updates.

app/Filament/Server/Resources/Files/Pages/ListFiles.php (4)

15-15: LGTM!

The new imports and view property binding are appropriate for the drag-and-drop upload functionality. The custom view replaces the default Filament list page template to enable the Alpine.js-powered upload UI.

Also applies to: 18-18, 41-41, 48-48, 61-61


617-637: LGTM!

The upload URL generation correctly enforces permissions, creates short-lived JWT tokens (15-minute expiry), and constructs URLs pointing directly to the Wings node. This enables the client-side code to upload directly to Wings, bypassing Panel and avoiding PHP memory limits.

Ensure your deployment uses HTTPS for Panel-to-Wings communication, as the JWT token is transmitted in the URL query string. This is a deployment requirement rather than a code issue.


639-645: LGTM!

The size limit retrieval correctly converts the node's upload size from MiB to bytes for client-side validation.


541-541: LGTM!

The upload action changes correctly reflect the new architecture where file uploads are handled via drag-and-drop in the main view, while the Upload button modal now primarily serves URL-based imports. The "files" tab now uses a custom View component instead of the built-in FileUpload.

Also applies to: 559-559

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)
resources/views/filament/server/pages/list-files.blade.php (3)

149-155: Folder creation failures should halt the upload with a clear error message.

When createFolder fails (line 151), the code logs a warning but continues. If folders cannot be created due to permission issues or other errors, subsequent file uploads targeting those folders will fail with less clear error messages, confusing users.


171-189: Critical: Concurrency limiter never frees slots because Promises lack a .status property.

Line 184 filters promises using .status, but JavaScript Promise objects don't expose this property. The activeUploads array never shrinks, so after the first Promise.race resolves, all promises remain in the array and the maxConcurrent limit stops working.


213-277: Escape key cancellation doesn't abort in-flight XHR uploads.

The XHR instance created at line 228 is not stored in fileData, so when the user presses Escape (handled at lines 289-294), the UI clears but active uploads continue in the background. This can cause unexpected server load and user confusion.

🧹 Nitpick comments (2)
lang/en/server/file.php (1)

20-24: Consider adding differentiated failure messages for partial vs. complete upload failures.

The Blade view (lines 193-199) uses the same failed translation key whether some files fail or all files fail, making it unclear to users what happened. Adding error_partial and error_all keys would improve the user experience.

Add differentiated keys to provide clearer feedback:

             'drop_files' => 'Drop files to upload',
             'success' => 'Files uploaded successfully',
             'failed' => 'Failed to upload files',
+            'error_partial' => 'Some files failed to upload',
+            'error_all' => 'All files failed to upload',
             'header' => 'Uploading Files',
             'error' => 'An error occurred while uploading',

Then update the Blade file to use them:

// In list-files.blade.php around lines 195-198
} else if (failed.length === this.totalFiles) {
    new window.FilamentNotification().title('{{ trans('server/file.actions.upload.error_all') }}').danger().send();
} else {
    new window.FilamentNotification().title('{{ trans('server/file.actions.upload.error_partial') }}').danger().send();
}
resources/views/filament/server/pages/list-files.blade.php (1)

332-390: Consider adding accessibility features to the upload modal.

The upload progress modal (lines 332-390) lacks accessibility features:

  • No focus trap to prevent keyboard users from tabbing outside the modal
  • Missing role="dialog" and aria-modal="true" attributes
  • No aria-label or aria-labelledby for screen readers
  • Escape key closes the modal but doesn't announce the action

Consider enhancing accessibility:

         <div
             x-show="isUploading"
             x-cloak
+            role="dialog"
+            aria-modal="true"
+            aria-labelledby="upload-modal-title"
             class="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/50 dark:bg-gray-100/20 p-4"
         >
             <div
                 class="rounded-lg bg-white shadow-xl dark:bg-gray-800 max-w-[50%] max-h-[50vh] overflow-hidden flex flex-col">
                 <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-center">
-                    <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
+                    <h3 id="upload-modal-title" class="text-lg font-semibold text-gray-900 dark:text-gray-100">

Additionally, consider implementing focus trapping using Alpine.js's x-trap directive or a similar solution.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f41cf67 and 6cec5da.

📒 Files selected for processing (2)
  • lang/en/server/file.php (1 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)

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/Server/Resources/Files/Pages/ListFiles.php (1)

652-677: Split nested folder paths before calling createDirectory.

The same issue identified in the previous review remains: DaemonFileRepository::createDirectory() expects a single folder name and its parent path separately. Passing a nested path like photos/2023 as $folderPath will cause daemon-side validation errors, preventing nested directory uploads from working.

Break the incoming relative path into parent segments and basename before delegating to the repository, as suggested in the previous review.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cec5da and 339efc7.

📒 Files selected for processing (3)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (6 hunks)
  • resources/views/filament/server/pages/file-upload.blade.php (1 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • resources/views/filament/server/pages/list-files.blade.php
  • resources/views/filament/server/pages/file-upload.blade.php
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, the Forms component Placeholder was deprecated and removed. Use TextEntry from Filament\Infolists\Components\TextEntry in forms instead, binding values with ->state(). For HTML content, use ->html().

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, Wizard Step components use the Filament\Schemas namespace (Filament\Schemas\Components\Wizard\Step), not Filament\Forms.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (7)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/helpers.php (1)
  • user (127-130)
app/Models/Node.php (1)
  • getConnectionAddress (178-181)
app/Filament/Server/Resources/Files/Pages/EditFiles.php (1)
  • getDaemonFileRepository (270-277)
app/Repositories/Daemon/DaemonFileRepository.php (1)
  • createDirectory (86-100)
app/Services/Activity/ActivityLogService.php (3)
  • event (48-53)
  • property (109-117)
  • log (136-155)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)
🔇 Additional comments (5)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (5)

15-15: LGTM!

The new imports support the direct-to-Wings upload functionality and are all utilized in the new methods.

Also applies to: 18-18, 41-41, 48-48


61-61: LGTM!

Setting a custom view is the correct approach to integrate the drag-and-drop upload component into the file list page.


541-541: LGTM!

Transitioning from server-side FileUpload to a View-based component correctly implements direct-to-Wings uploads, bypassing Panel as a file relay.

Also applies to: 559-559


617-637: LGTM!

The JWT token generation for direct uploads includes proper permission checks, follows established codebase patterns for token identifiers, and correctly constructs the Wings upload URL.


639-645: LGTM!

The upload size limit conversion from megabytes to bytes is correct and will properly enforce node-level upload constraints.

@Boy132 Boy132 removed their request for review November 8, 2025 20:49
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 (2)
resources/views/filament/server/pages/list-files.blade.php (1)

181-208: Fix the concurrency limiter so settled uploads vacate their slots.

Promise instances never expose a .status field, so line 203's filter never removes settled promises from activeUploads. After the first 3 uploads start, the array keeps all promises, effectively breaking the intended concurrent upload limit.

Apply this fix to properly track and remove settled promises:

-                    for (let i = 0; i < this.uploadQueue.length; i++) {
-                        const uploadPromise = this.uploadFile(i)
-                            .then(() => {
-                                completedCount++;
-                                this.currentFileIndex = completedCount;
-                                const item = this.uploadQueue[i];
-                                const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
-                                uploadedFiles.push(relativePath);
-                            })
-                            .catch(() => {
-                                completedCount++;
-                                this.currentFileIndex = completedCount;
-                            });
-
-                        activeUploads.push(uploadPromise);
-
-                        if (activeUploads.length >= maxConcurrent) {
-                            await Promise.race(activeUploads);
-                            activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
-                        }
-                    }
-
-                    await Promise.allSettled(activeUploads);
+                    for (let i = 0; i < this.uploadQueue.length; i++) {
+                        const uploadPromise = this.uploadFile(i)
+                            .then(() => {
+                                completedCount++;
+                                this.currentFileIndex = completedCount;
+                                const item = this.uploadQueue[i];
+                                const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
+                                uploadedFiles.push(relativePath);
+                            })
+                            .catch(() => {
+                                completedCount++;
+                                this.currentFileIndex = completedCount;
+                            });
+
+                        const trackedPromise = uploadPromise.finally(() => {
+                            activeUploads = activeUploads.filter(p => p !== trackedPromise);
+                        });
+
+                        activeUploads.push(trackedPromise);
+
+                        if (activeUploads.length >= maxConcurrent) {
+                            await Promise.race(activeUploads);
+                        }
+                    }
+
+                    await Promise.allSettled(activeUploads);
resources/views/filament/server/pages/file-upload.blade.php (1)

132-158: Fix the concurrency limiter so settled uploads vacate their slots.

Promise instances never expose a .status field, so line 154's filter never removes settled promises from activeUploads. This breaks the intended 3-concurrent-upload limit, causing uploads to effectively serialize after the first 3 start.

Apply this fix:

-            for (let i = 0; i < this.uploadQueue.length; i++) {
-                const uploadPromise = this.uploadFile(i)
-                    .then(() => {
-                        completedCount++;
-                        this.currentFileIndex = completedCount;
-                        const item = this.uploadQueue[i];
-                        const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
-                        uploadedFiles.push(relativePath);
-                    })
-                    .catch(() => {
-                        completedCount++;
-                        this.currentFileIndex = completedCount;
-                    });
-
-                activeUploads.push(uploadPromise);
-
-                if (activeUploads.length >= maxConcurrent) {
-                    await Promise.race(activeUploads);
-                    activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
-                }
-            }
-
-            await Promise.allSettled(activeUploads);
+            for (let i = 0; i < this.uploadQueue.length; i++) {
+                const uploadPromise = this.uploadFile(i)
+                    .then(() => {
+                        completedCount++;
+                        this.currentFileIndex = completedCount;
+                        const item = this.uploadQueue[i];
+                        const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
+                        uploadedFiles.push(relativePath);
+                    })
+                    .catch(() => {
+                        completedCount++;
+                        this.currentFileIndex = completedCount;
+                    });
+
+                const trackedPromise = uploadPromise.finally(() => {
+                    activeUploads = activeUploads.filter(p => p !== trackedPromise);
+                });
+
+                activeUploads.push(trackedPromise);
+
+                if (activeUploads.length >= maxConcurrent) {
+                    await Promise.race(activeUploads);
+                }
+            }
+
+            await Promise.allSettled(activeUploads);
🧹 Nitpick comments (3)
resources/views/filament/server/pages/list-files.blade.php (1)

159-165: Consider surfacing folder creation errors to users.

When folder creation fails (line 163), only a console warning is logged. If Wings doesn't auto-create parent folders, subsequent file uploads to those paths will fail with potentially unclear error messages, degrading the user experience.

Consider surfacing folder-creation failures or collecting them to show a single aggregated notification if any critical paths can't be created.

resources/views/filament/server/pages/file-upload.blade.php (2)

9-69: Extract duplicated folder traversal logic into a shared module.

The extractFilesFromItems and traverseFileTree functions (lines 9-69) are duplicated identically in resources/views/filament/server/pages/list-files.blade.php (lines 58-119). This 60+ line duplication makes maintenance harder and increases the risk of inconsistent behavior if one copy is updated but not the other.

Consider extracting these functions into a shared JavaScript module or Alpine.js store that both components can import, or create a shared Alpine.js data object that can be reused across components.


71-207: Consider refactoring duplicated upload logic.

The entire uploadFilesWithFolders method (137 lines) is nearly identical to the one in list-files.blade.php (lines 120-256), differing only in minor error message details. This represents substantial code duplication that makes maintenance harder.

Consider extracting this shared upload logic into a reusable Alpine.js component, store, or JavaScript module that both components can use.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 339efc7 and ce3589f.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (3)
  • package.json (1 hunks)
  • resources/views/filament/server/pages/file-upload.blade.php (1 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1858
File: resources/views/filament/server/pages/list-files.blade.php:304-309
Timestamp: 2025-11-07T11:30:21.662Z
Learning: In resources/views/filament/server/pages/list-files.blade.php, the handleEscapeKey function intentionally does not abort active XMLHttpRequest uploads when Escape is pressed. The UI is hidden but uploads continue in the background by design.
📚 Learning: 2025-11-07T11:30:21.662Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1858
File: resources/views/filament/server/pages/list-files.blade.php:304-309
Timestamp: 2025-11-07T11:30:21.662Z
Learning: In resources/views/filament/server/pages/list-files.blade.php, the handleEscapeKey function intentionally does not abort active XMLHttpRequest uploads when Escape is pressed. The UI is hidden but uploads continue in the background by design.

Applied to files:

  • resources/views/filament/server/pages/list-files.blade.php
  • resources/views/filament/server/pages/file-upload.blade.php
📚 Learning: 2025-11-05T22:21:31.863Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1865
File: app/Filament/Admin/Resources/Nodes/Pages/EditNode.php:682-682
Timestamp: 2025-11-05T22:21:31.863Z
Learning: In app/Filament/Admin/Resources/Nodes/Pages/EditNode.php, the diagnostics tab's upload action intentionally does not use the iconButton() modifier, while the pull action does. This UI difference is intentional.

Applied to files:

  • resources/views/filament/server/pages/list-files.blade.php
  • resources/views/filament/server/pages/file-upload.blade.php
📚 Learning: 2025-11-03T23:44:24.737Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1858
File: resources/views/filament/server/pages/list-files.blade.php:338-338
Timestamp: 2025-11-03T23:44:24.737Z
Learning: In the pelican-dev/panel repository, Tailwind CSS v4 is used, which supports fractional utility class syntax such as `max-w-1/2` (equivalent to `max-width: calc(1/2 * 100%)`). This syntax is valid and should not be flagged as invalid.

Applied to files:

  • resources/views/filament/server/pages/list-files.blade.php
📚 Learning: 2025-10-07T14:44:18.583Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1779
File: app/Filament/Admin/Resources/Users/Pages/EditUser.php:51-51
Timestamp: 2025-10-07T14:44:18.583Z
Learning: In the Pelican Panel codebase, when using Filament's FileUpload component for the avatar field in UserResource, the 'avatar' key is intentionally unset from the data array in EditUser::handleRecordUpdate before passing to UserUpdateService. This is by design because the avatar is not stored in the database directly—Filament's FileUpload component handles file storage, retrieval, and deletion through its own lifecycle hooks (formatStateUsing, deleteUploadedFileUsing, etc.) independently of the database update service.

Applied to files:

  • resources/views/filament/server/pages/list-files.blade.php
🔇 Additional comments (1)
package.json (1)

17-17: Verify and address Vite 7 breaking changes before merging.

Vite 7.1.11 is a valid version with no known security vulnerabilities. However, the upgrade from 6.2.6 introduces multiple breaking changes that require action:

  • Node version requirement: Upgrade Node to one of the supported minimums (20.19+ or 22.12+)
  • Config changes: Rename build.target → baseline-widely-available and update tooling that reads build.target
  • Build changes: Remove splitVendorChunkPlugin usages and switch to alternative chunking strategies
  • Sass changes: Update code that depended on the legacy Sass integration; Vite 7 uses the Sass compiler API
  • Plugin and SSR updates: Update plugins to versions compatible with Vite 7 and remove experimental.skipSsrTransform and HotBroadcaster-related code
  • Test tooling: Upgrade Vitest to a version with Vite 7 support (3.2+ recommended)

Verify your vite.config.js and package.json against these requirements before merging.

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 (8)
resources/views/filament/server/pages/list-files.blade.php (3)

181-208: Fix the concurrency limiter so settled uploads vacate their slots.

Promise instances never expose a .status field, so the activeUploads array never shrinks. After the first race resolves you still keep all settled promises, effectively disabling the intended maxConcurrent cap.

Apply this diff to wrap each upload promise with a finally hook that removes itself once settled:

-                    for (let i = 0; i < this.uploadQueue.length; i++) {
-                        const uploadPromise = this.uploadFile(i)
-                            .then(() => { completedCount++; this.currentFileIndex = completedCount; })
-                            .catch(() => { completedCount++; this.currentFileIndex = completedCount; });
-
-                        activeUploads.push(uploadPromise);
-
-                        if (activeUploads.length >= maxConcurrent) {
-                            await Promise.race(activeUploads);
-                            activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
-                        }
-                    }
-
-                    await Promise.allSettled(activeUploads);
+                    for (let i = 0; i < this.uploadQueue.length; i++) {
+                        const uploadPromise = this.uploadFile(i)
+                            .then(() => { completedCount++; this.currentFileIndex = completedCount; })
+                            .catch(() => { completedCount++; this.currentFileIndex = completedCount; });
+
+                        const trackedPromise = uploadPromise.finally(() => {
+                            activeUploads = activeUploads.filter(p => p !== trackedPromise);
+                        });
+
+                        activeUploads.push(trackedPromise);
+
+                        if (activeUploads.length >= maxConcurrent) {
+                            await Promise.race(activeUploads);
+                        }
+                    }
+
+                    await Promise.allSettled(activeUploads);

159-165: Folder creation failures are silently ignored, leading to subsequent upload failures.

When createFolder fails for nested paths (e.g., photos/2023), Line 163 logs a warning but continues. Files targeting those non-existent folders will then fail during upload with less clear error messages.

Consider surfacing folder-creation failures more prominently, or halt the upload workflow if critical folders can't be created.


223-227: Partial failure notification uses the same message as total failure.

When some (but not all) uploads fail, the notification on Line 224 displays 'server/file.actions.upload.failed', which is identical to the total failure message (Line 219). This doesn't clearly communicate to users that some files uploaded successfully.

Consider using a distinct translation key like 'server/file.actions.upload.partial' or modifying the message to indicate partial success.

resources/views/filament/server/pages/file-upload.blade.php (3)

136-158: Restore active upload slot reclamation.

Filtering on p.status never removes settled promises, so activeUploads only grows and the three-upload cap disappears after the first completion.

Apply this diff to clean up each tracked promise in a finally so the limiter keeps working:

-            for (let i = 0; i < this.uploadQueue.length; i++) {
-                const uploadPromise = this.uploadFile(i)
-                    .then(() => {
-                        completedCount++;
-                        this.currentFileIndex = completedCount;
-                        const item = this.uploadQueue[i];
-                        const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
-                        uploadedFiles.push(relativePath);
-                    })
-                    .catch(() => {
-                        completedCount++;
-                        this.currentFileIndex = completedCount;
-                    });
-
-                activeUploads.push(uploadPromise);
-
-                if (activeUploads.length >= maxConcurrent) {
-                    await Promise.race(activeUploads);
-                    activeUploads = activeUploads.filter(p => p.status !== 'fulfilled' && p.status !== 'rejected');
-                }
-            }
-
-            await Promise.allSettled(activeUploads);
+            for (let i = 0; i < this.uploadQueue.length; i++) {
+                const uploadPromise = this.uploadFile(i)
+                    .then(() => {
+                        completedCount++;
+                        this.currentFileIndex = completedCount;
+                        const item = this.uploadQueue[i];
+                        const relativePath = (item.path ? item.path.replace(/^\/+/, '') + '/' : '') + item.name;
+                        uploadedFiles.push(relativePath);
+                    })
+                    .catch(() => {
+                        completedCount++;
+                        this.currentFileIndex = completedCount;
+                    });
+
+                const trackedPromise = uploadPromise.finally(() => {
+                    activeUploads = activeUploads.filter(p => p !== trackedPromise);
+                });
+
+                activeUploads.push(trackedPromise);
+
+                if (activeUploads.length >= maxConcurrent) {
+                    await Promise.race(activeUploads);
+                }
+            }
+
+            await Promise.allSettled(activeUploads);

110-116: Folder creation failures are silently ignored.

Same issue as in list-files.blade.php: failed folder creation is only logged to console, which may lead to less clear upload errors later.


173-178: Partial failure notification uses the same message as total failure.

Identical issue as in list-files.blade.php: partial success (some files uploaded, some failed) displays the same 'server/file.actions.upload.failed' message as total failure.

app/Filament/Server/Resources/Files/Pages/ListFiles.php (2)

668-676: Add permission check to logUploadedFiles method—currently exploitable from frontend.

This public method is callable directly from frontend JavaScript without permission validation. While getUploadUrl protects the actual file upload endpoint, logUploadedFiles has no such check. An attacker could invoke logUploadedFiles(['fake-file.txt']) directly to create false activity log entries without uploading any files, polluting the audit trail.

Add a permission check matching the pattern used in createFolder (Line 643):

 public function logUploadedFiles(array $files): void
 {
+    /** @var Server $server */
+    $server = Filament::getTenant();
+
+    if (!user()?->can(Permission::ACTION_FILE_CREATE, $server)) {
+        abort(403, 'You do not have permission to log uploads.');
+    }
+
     $filesCollection = collect($files);
 
     Activity::event('server:files.uploaded')
         ->property('directory', $this->path)
         ->property('files', $filesCollection)
         ->log();
 }

672-672: Use consistent event naming convention.

The event name 'server:files.uploaded' uses plural "files" while all other activity events in this file use the singular form: 'server:file.rename', 'server:file.copy', 'server:file.write', 'server:file.pull', etc. This inconsistency could cause confusion when filtering or searching activity logs.

Apply this diff to use consistent singular naming:

-        Activity::event('server:files.uploaded')
+        Activity::event('server:file.upload')
             ->property('directory', $this->path)
             ->property('files', $filesCollection)
             ->log();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce3589f and 0b6683d.

📒 Files selected for processing (3)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (5 hunks)
  • resources/views/filament/server/pages/file-upload.blade.php (1 hunks)
  • resources/views/filament/server/pages/list-files.blade.php (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-11-07T11:30:21.662Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1858
File: resources/views/filament/server/pages/list-files.blade.php:304-309
Timestamp: 2025-11-07T11:30:21.662Z
Learning: In resources/views/filament/server/pages/list-files.blade.php, the handleEscapeKey function intentionally does not abort active XMLHttpRequest uploads when Escape is pressed. The UI is hidden but uploads continue in the background by design.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
  • resources/views/filament/server/pages/list-files.blade.php
  • resources/views/filament/server/pages/file-upload.blade.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, the Forms component Placeholder was deprecated and removed. Use TextEntry from Filament\Infolists\Components\TextEntry in forms instead, binding values with ->state(). For HTML content, use ->html().

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, Wizard Step components use the Filament\Schemas namespace (Filament\Schemas\Components\Wizard\Step), not Filament\Forms.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-11-05T22:21:31.863Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1865
File: app/Filament/Admin/Resources/Nodes/Pages/EditNode.php:682-682
Timestamp: 2025-11-05T22:21:31.863Z
Learning: In app/Filament/Admin/Resources/Nodes/Pages/EditNode.php, the diagnostics tab's upload action intentionally does not use the iconButton() modifier, while the pull action does. This UI difference is intentional.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
  • resources/views/filament/server/pages/list-files.blade.php
  • resources/views/filament/server/pages/file-upload.blade.php
📚 Learning: 2025-11-03T23:44:24.737Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1858
File: resources/views/filament/server/pages/list-files.blade.php:338-338
Timestamp: 2025-11-03T23:44:24.737Z
Learning: In the pelican-dev/panel repository, Tailwind CSS v4 is used, which supports fractional utility class syntax such as `max-w-1/2` (equivalent to `max-width: calc(1/2 * 100%)`). This syntax is valid and should not be flagged as invalid.

Applied to files:

  • resources/views/filament/server/pages/list-files.blade.php
📚 Learning: 2025-10-07T14:44:18.583Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1779
File: app/Filament/Admin/Resources/Users/Pages/EditUser.php:51-51
Timestamp: 2025-10-07T14:44:18.583Z
Learning: In the Pelican Panel codebase, when using Filament's FileUpload component for the avatar field in UserResource, the 'avatar' key is intentionally unset from the data array in EditUser::handleRecordUpdate before passing to UserUpdateService. This is by design because the avatar is not stored in the database directly—Filament's FileUpload component handles file storage, retrieval, and deletion through its own lifecycle hooks (formatStateUsing, deleteUploadedFileUsing, etc.) independently of the database update service.

Applied to files:

  • resources/views/filament/server/pages/list-files.blade.php
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (4)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/Repositories/Daemon/DaemonFileRepository.php (2)
  • pull (215-226)
  • createDirectory (86-100)
app/Models/Node.php (1)
  • getConnectionAddress (178-181)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)

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 (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (1)

668-676: Critical: Add permission check to prevent unauthorized activity log manipulation.

This public method remains callable from the frontend without permission validation, allowing malicious users to inject false upload activity entries. While getUploadUrl (line 608) protects the actual upload endpoint, this logging method has no such guard.

Add the permission check:

 public function logUploadedFiles(array $files): void
 {
+    /** @var Server $server */
+    $server = Filament::getTenant();
+
+    if (!user()?->can(Permission::ACTION_FILE_CREATE, $server)) {
+        abort(403, 'You do not have permission to log uploads.');
+    }
+
     $filesCollection = collect($files);

     Activity::event('server:files.uploaded')
         ->property('directory', $this->path)
         ->property('files', $filesCollection)
         ->log();
 }

Fix event naming inconsistency.

The event name 'server:files.uploaded' (line 672) uses plural form, breaking the established convention. All other file activity events use singular: 'server:file.rename', 'server:file.copy', 'server:file.write', etc.

Apply this diff:

-    Activity::event('server:files.uploaded')
+    Activity::event('server:file.uploaded')
         ->property('directory', $this->path)
         ->property('files', $filesCollection)
         ->log();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b6683d and decf423.

📒 Files selected for processing (1)
  • app/Filament/Server/Resources/Files/Pages/ListFiles.php (6 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1858
File: resources/views/filament/server/pages/list-files.blade.php:304-309
Timestamp: 2025-11-07T11:30:21.662Z
Learning: In resources/views/filament/server/pages/list-files.blade.php, the handleEscapeKey function intentionally does not abort active XMLHttpRequest uploads when Escape is pressed. The UI is hidden but uploads continue in the background by design.
📚 Learning: 2025-11-05T22:21:31.863Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1865
File: app/Filament/Admin/Resources/Nodes/Pages/EditNode.php:682-682
Timestamp: 2025-11-05T22:21:31.863Z
Learning: In app/Filament/Admin/Resources/Nodes/Pages/EditNode.php, the diagnostics tab's upload action intentionally does not use the iconButton() modifier, while the pull action does. This UI difference is intentional.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-11-07T11:30:21.662Z
Learnt from: notAreYouScared
Repo: pelican-dev/panel PR: 1858
File: resources/views/filament/server/pages/list-files.blade.php:304-309
Timestamp: 2025-11-07T11:30:21.662Z
Learning: In resources/views/filament/server/pages/list-files.blade.php, the handleEscapeKey function intentionally does not abort active XMLHttpRequest uploads when Escape is pressed. The UI is hidden but uploads continue in the background by design.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, the Forms component Placeholder was deprecated and removed. Use TextEntry from Filament\Infolists\Components\TextEntry in forms instead, binding values with ->state(). For HTML content, use ->html().

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
📚 Learning: 2025-10-15T11:55:53.461Z
Learnt from: rmartinoscar
Repo: pelican-dev/panel PR: 1801
File: app/Extensions/OAuth/Schemas/AuthentikSchema.php:7-10
Timestamp: 2025-10-15T11:55:53.461Z
Learning: In Filament v4, Wizard Step components use the Filament\Schemas namespace (Filament\Schemas\Components\Wizard\Step), not Filament\Forms.

Applied to files:

  • app/Filament/Server/Resources/Files/Pages/ListFiles.php
🧬 Code graph analysis (1)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (6)
app/Services/Nodes/NodeJWTService.php (4)
  • NodeJWTService (16-100)
  • setExpiresAt (50-55)
  • setUser (43-48)
  • setClaims (32-37)
app/helpers.php (1)
  • user (127-130)
app/Filament/Server/Resources/Files/Pages/EditFiles.php (1)
  • getDaemonFileRepository (274-281)
app/Repositories/Daemon/DaemonFileRepository.php (2)
  • pull (215-226)
  • createDirectory (86-100)
app/Models/Node.php (1)
  • getConnectionAddress (178-181)
app/Exceptions/Repository/FileExistsException.php (1)
  • FileExistsException (7-7)
🔇 Additional comments (6)
app/Filament/Server/Resources/Files/Pages/ListFiles.php (6)

15-15: LGTM!

The new imports support the direct-to-Wings upload functionality: NodeJWTService for token generation, CarbonImmutable for expiry timestamps, and ConnectionException for network error handling.

Also applies to: 18-18, 45-45


58-58: LGTM!

Setting a custom view property is the correct approach to wire the drag-and-drop Blade template for this page.


508-508: LGTM!

Refactoring to use the createFolder method eliminates duplication and ensures consistent error handling, addressing the previous review suggestion.


532-556: LGTM!

The refactoring cleanly separates direct file upload (now handled via the Blade view at line 534) from URL-based pulls (uploadURL). Both actions maintain appropriate authorization checks.


603-623: LGTM!

The method correctly generates a JWT token for direct browser-to-Wings uploads with appropriate permission checks and a reasonable 15-minute expiry window. This implementation enables the bypass of PHP memory limits for large uploads.


638-663: LGTM!

The method correctly handles folder creation with appropriate permission checks, comprehensive error handling, and activity logging. The idempotent behavior (silently ignoring FileExistsException at line 655) is appropriate for the drag-and-drop upload flow where folders may be created multiple times.

@notAreYouScared notAreYouScared merged commit 0891db5 into main Nov 9, 2025
25 checks passed
@notAreYouScared notAreYouScared deleted the charles/drag&drop branch November 9, 2025 14:24
@github-actions github-actions bot locked and limited conversation to collaborators Nov 9, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Uploads broken Reimplement direct Drag & Drop for file upload

5 participants