Setup Storage
Pada production, PandaBear dapat menulis file ke empat lokasi berbeda:
- disk yang digunakan field upload;
- disk untuk export dan import;
- system temporary directory untuk membangun spreadsheet sementara;
bootstrap/cacheuntuk Panel manifest.
Tidak ada disk/path tersebut yang dikonfigurasi melalui config/panda-panel.php. Disk dan directory dideklarasikan pada FileUpload, Exporter, atau Importer, sehingga queued job dapat membaca konfigurasi yang sama tanpa bergantung pada request/container state.
Gunakan halaman ini ketika menyiapkan storage production, ketika preview upload menjadi broken image, atau ketika berpindah dari satu server ke multi-node deployment.
Contoh minimal deploy
Dengan default config, Anda hanya membutuhkan dua command:
php artisan storage:link # public disk, default untuk form upload
php artisan panel:cache # menulis bootstrap/cache/panels.php2
Pada config/filesystems.php standar Laravel, hasilnya kira-kira:
storage/app/public/uploads/… form uploads, dilayani dari /storage/uploads/…
storage/app/private/panel-exports/{userId}/… exports, hanya didownload melalui Panel
storage/app/private/panel-imports/… import uploads dan failure reports
bootstrap/cache/panels.php Panel manifest2
3
4
PandaBear tidak menulis compiled asset langsung ke public/. vendor:publish --tag=panda-panel-assets menyalin Vue/CSS source ke resources/; file public yang benar-benar disajikan browser berasal dari Vite build application.
File apa yang ditulis, dan ke mana
| Jenis | Disk dan path | Default | Ditulis oleh |
|---|---|---|---|
| Form upload | {FileUpload::getDisk()} pada {FileUpload::getDirectory()}/{hash}.{ext} | public / uploads | PandaPanel\Http\Controllers\PanelUploadController |
| Import upload | {Importer::disk()} pada {Importer::directory()}/{hash}.{ext} | local / panel-imports | upload endpoint yang sama; ImportAction membuat FileUpload dengan disk milik Importer |
| Export file | {Exporter::disk()} pada {Exporter::directory()}/{userKey}/{fileName}.{ext} | local / panel-exports | PandaPanel\Actions\Exports\ExportRun::write() |
| Failure report | {Importer::disk()} pada {Importer::directory()}/{userKey}/failed-rows-{Y-m-d-His}.csv | local / panel-imports | PandaPanel\Actions\Imports\ImportRun::run() |
| Spreadsheet temporary | sys_get_temp_dir(), prefix panel-export-* atau panel-import-failures-* | system temp | ExportRun dan ImportRun; dihapus setelah dipindahkan ke disk |
| Panel manifest | bootstrap/cache/panels.php melalui PanelManifest::path() | — | php artisan panel:cache |
Segment userKey pada export/report merupakan security boundary, bukan sekadar struktur folder. Download controller membangun directory berdasarkan user yang sedang meminta, bukan path dari request. User tidak dapat menunjuk file milik user lain hanya dengan memanipulasi nama file.
Public disk dan pentingnya storage:link
FileUpload default menggunakan public disk karena preview URL di-resolve oleh server dari disk tersebut:
// PandaPanel\Forms\Components\FileUpload::previewBase(), secara konsep
return rtrim(Storage::disk($this->disk)->url('/'), '/');2
String tersebut dikirim ke browser sebagai previewBase. Vue menggabungkannya dengan stored path. Browser tidak pernah mengubah disk name menjadi URL sendiri.
Pada Laravel default:
public disk root = storage/app/public
public disk url = APP_URL/storage2
Jadi storage:link yang membuat:
/storage/uploads/9f3c.pngbenar-benar menunjuk ke file upload.
Contoh field:
use PandaPanel\Forms\Components\FileUpload;
FileUpload::make('avatar')
->disk('public')
->directory('avatars')
->image()
->maxSize(1024);2
3
4
5
6
7
Pada release-directory deployment seperti Envoyer/Deployer/Forge, public/storage berada di release sedangkan storage/ biasanya shared. Karena itu symlink perlu tersedia pada setiap release:
php artisan storage:linkCommand aman dijalankan setiap deploy. Jika symlink sudah benar, command tetap exit 0. Gunakan --force jika link ada tetapi menunjuk ke target yang salah.
Private disk untuk export dan import
Exporter::disk() default ke local, bukan public, dan itu memang seharusnya demikian.
Export adalah salinan data yang user berhak lihat. Meletakkannya pada public disk akan membuat file tersedia pada URL langsung yang dapat ditebak.
Download seharusnya melewati authenticated Panel route:
| Route name | Path | Controller |
|---|---|---|
panel.{id}.export-file | {panel}/exports/{file} | PandaPanel\Http\Controllers\PanelExportController |
panel.{id}.import-file | {panel}/imports/{file} | PandaPanel\Http\Controllers\PanelImportController |
Keduanya menolak file yang mengandung:
/
\
..2
3
Directory dibangun dari $request->user()->getAuthIdentifier() dan request terhadap file yang tidak tersedia menghasilkan 404.
Caller hanya memberikan file name, bukan arbitrary filesystem path. Karena itu traversal tidak memiliki jalur untuk keluar dari per-user directory.
Jangan arahkan export/import report ke public disk.
Mendeklarasikan disk dan directory
Semua storage behavior berasal dari declaration berikut:
| Declaration | Signature | Default |
|---|---|---|
| Upload disk | PandaPanel\Forms\Components\FileUpload::disk(string $disk): self | 'public' |
| Upload directory | FileUpload::directory(string $directory): self | 'uploads' |
| Upload size limit | FileUpload::maxSize(int $kilobytes): self | 5120 |
| Upload count limit | FileUpload::maxFiles(int $max): self | null |
| Accepted types | FileUpload::acceptedTypes(array $types): self | [] |
| Read back | getDisk(), getDirectory(), getMaxSize(), getAcceptedTypes(), accepts() | — |
| Export disk | PandaPanel\Actions\Exports\Exporter::disk(): string | 'local' |
| Export directory | Exporter::directory(): string | 'panel-exports' |
| Export file name | Exporter::fileName(): string | kebab class name + timestamp Y-m-d-His |
| Import disk | PandaPanel\Actions\Imports\Importer::disk(): string | 'local' |
| Import directory | Importer::directory(): string | 'panel-imports' |
| ImageEntry disk | PandaPanel\Infolists\Components\ImageEntry::disk(string $disk): self | tidak ada; value digunakan apa adanya jika URL |
Perubahan terhadap declaration ini adalah code deploy, bukan config-only change.
Dedicated disk
Application dapat membuat disk khusus PandaBear:
// config/filesystems.php
'disks' => [
'panel-files' => [
'driver' => 'local',
'root' => storage_path('app/panel-files'),
'throw' => false,
],
],2
3
4
5
6
7
8
Exporter:
use PandaPanel\Actions\Exports\Exporter;
final class OrderExporter extends Exporter
{
public static function disk(): string
{
return 'panel-files';
}
public static function directory(): string
{
return 'exports/orders';
}
// …columns()
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Importer menggunakan pola yang sama:
use PandaPanel\Actions\Imports\Importer;
final class OrderImporter extends Importer
{
public static function disk(): string
{
return 'panel-files';
}
public static function directory(): string
{
return 'imports/orders';
}
// …model(), columns()
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Disk/directory Importer dibaca oleh upload endpoint dan failure report writer, sehingga keduanya harus mengarah ke storage yang sama.
ImageEntry
use PandaPanel\Infolists\Components\ImageEntry;
ImageEntry::make('avatar')
->disk('public')
->circular()
->size(64);2
3
4
5
6
ImageEntry::toValue() behavior:
- absolute
http:///https://value dikembalikan tanpa perubahan; - value lain di-resolve melalui
Storage::disk($disk)->url(); - jika
url()melemparRuntimeException, returnnullsehingga renderer menggunakan placeholder.
Remote disk
| Feature | Remote disk | Alasan |
|---|---|---|
FileUpload | Ya | endpoint menggunakan $file->store($directory, $disk) dan preview dari Storage::disk($disk)->url() |
ImageEntry::disk() | Ya | menggunakan url() |
Exporter::disk() | Ya | export dibuat di temporary local file lalu di-stream menggunakan put(); download memakai $disk->download() |
Importer::disk() | Tidak secara umum | importer membutuhkan readable filesystem path melalui path() untuk fopen() / ZipArchive |
Contoh S3 upload:
use PandaPanel\Forms\Components\FileUpload;
FileUpload::make('gallery')
->disk('s3')
->directory('posts/gallery')
->multiple()
->maxFiles(8)
->acceptedTypes(['image/png', 'image/jpeg']);2
3
4
5
6
7
8
Dua hal yang harus diperiksa pada remote disk:
Visibility
Upload endpoint menggunakan $file->store() tanpa memaksa visibility tertentu. Object mengikuti default disk.
Jika bucket private tetapi previewBase menghasilkan public-looking URL seperti AWS_URL, browser dapat menerima 403 saat preview.
Biaya per submit
FileUpload::accepts() memanggil:
Storage::disk($disk)->exists($path)Validation tersebut juga dapat berjalan ketika form disubmit. Pada remote storage, ini berarti satu network round trip per submitted path.
Directory yang harus writable
| Path | Ditulis oleh | Catatan |
|---|---|---|
| root dari disk upload/export/import | storage operations | permission harus sesuai user PHP/worker |
bootstrap/cache | panel:cache | manifest ditulis ke temporary file lalu di-rename; directory harus writable, bukan hanya existing panels.php |
sys_get_temp_dir() | ExportRun, ImportRun | tempnam() harus berhasil; read-only/full /tmp menyebabkan exception |
storage/framework, storage/logs | Laravel | session, compiled views, log warning, dll |
Jika temporary file export gagal dibuat, ExportRun melempar RuntimeException. Failure report import menggunakan SpreadsheetException, yang juga merupakan runtime exception.
Upload limit pada web/PHP edge
File diupload satu request per file. Vue menunggu upload selesai sebelum memulai file berikutnya; tidak ada chunked upload.
Karena itu file terbesar harus lolos seluruh limit berikut:
| Limit | Minimal sebesar |
|---|---|
upload_max_filesize, post_max_size | FileUpload::maxSize() terbesar yang digunakan application |
nginx client_max_body_size | ukuran file terbesar yang diizinkan |
Default FileUpload::maxSize() adalah:
5120 KB = 5 MBImportAction menggunakan limit:
20480 KB = 20 MBJadi application dengan import membutuhkan setidaknya sekitar 20 MB headroom di PHP dan web server.
Browser menolak file yang melampaui field limit sebelum upload. Endpoint juga memvalidasi kembali menggunakan rule server.
Tetapi file yang masih di bawah field limit namun di atas post_max_size tidak pernah sampai ke validation PandaBear. Laravel ValidatePostSize akan melempar PostTooLargeException, dan dari perspektif field user hanya melihat upload gagal. Karena itu web/PHP limit harus diselaraskan dengan field config.
Multi-server deployment
Storage adalah salah satu area pertama yang membedakan single-node dan multi-node deployment.
Queued export
Export dapat ditulis oleh queue worker tetapi didownload melalui web node.
Jika worker dan web berada di machine berbeda dan menggunakan disk local, keduanya melihat filesystem berbeda. Web node kemudian 404 ketika user mendownload file.
Gunakan shared disk/storage yang dapat diakses keduanya.
Queued import
Import worker membaca file yang sebelumnya disimpan web request. Masalah shared storage juga berlaku.
Selain itu Importer membutuhkan readable local filesystem path. Dalam praktiknya pilih salah satu:
- worker import berjalan pada host/shared filesystem yang sama;
- gunakan shared filesystem yang benar-benar dapat di-mount sebagai path lokal;
- jangan queue import jika storage architecture belum mendukungnya.
Session store
Session juga merupakan shared state.
Fitur seperti:
- persisted Table search/filter/sort;
- Email Code session marker
panel.mfa.email.confirmed_at;
bergantung pada session.
file session driver pada dua web node tanpa sticky/shared session menghasilkan behavior intermittent.
Panel manifest
bootstrap/cache/panels.php berada di release filesystem. Setiap node harus menjalankan panel:cache/optimize untuk release masing-masing.
Retention dan cleanup
PandaBear tidak otomatis membersihkan semua file yang pernah dihasilkan.
File berikut dapat terus bertambah:
- export files;
- failure reports;
- upload yang dialog/form-nya ditinggalkan user.
Tidak ada built-in panel:prune command.
Yang otomatis dihapus adalah import upload setelah proses selesai/gagal/missing required column, karena file tersebut hanya input sementara.
Jika seorang admin melakukan satu export per hari, satu user dapat menghasilkan ratusan file per tahun. Gunakan strategi pruning application. Lihat Storage and cleanup.
Hal yang perlu diperhatikan
- Private local disk dapat menghasilkan preview URL yang terlihat valid tetapi salah. Local disk tanpa
urldapat fallback ke/storage/{path}, yang pada Laravel default sebenarnya menunjuk public disk. File local/private tidak ada di sana sehingga preview 404. previewBasemenjadinullhanya ketikaurl()benar-benar melemparRuntimeException. Beberapa driver hanya menghasilkan URL fallback yang salah, bukan exception.- Queued import harus benar-benar dapat membuka path file dari worker. Jika worker hanya menerima relative stored path tetapi filesystem berbeda,
fopen()dapat gagal. Pastikan pernah menguji satu queued import nyata di environment production-like sebelum menurunkanqueueAfter(). ImageColumntidak memilikidisk(). Table image column menganggap attribute sudah berupa URL/value yang dapat dirender. Untuk stored path, buat accessor yang mengembalikanStorage::disk(...)->url($path).route:cachedapat memengaruhi served-file routes Laravel tertentu. Disk yang bergantung pada'serve' => truedapat bekerja di development tetapi 404 ketika route cache aktif. Public disk melaluistorage:linktidak bergantung pada route tersebut.optimize:clearmenghapus Panel manifest. Setelah clear/release rollback, rebuildpanel:cachedari code release yang aktif.- Menghapus file dari form UI tidak otomatis menghapus object storage. Record mungkin masih menggunakan file tersebut sampai save, dan lifecycle deletion adalah keputusan application.
- Mengubah
directory()dapat membuat stored path lama tidak lagi valid terhadap field.accepts()dapat menolak path lama dan save berikutnya menghapus value dari state. Storage::fake()cukup untuk testing sebagian besar behavior ini. Semua write menggunakan Laravel filesystem abstraction biasa.