File Uploads
PandaPanel\Forms\Components\FileUpload merepresentasikan file tersimpan yang direferensikan melalui path. Field ini tidak pernah membawa isi file di dalam form: browser mengirim file ke upload endpoint milik Panel, endpoint menyimpannya lalu mengembalikan path, dan form kemudian mengirim path tersebut seperti string biasa. Gunakan field ini ketika record membutuhkan avatar, attachment, atau gallery.
Contoh minimal
use PandaPanel\Forms\Components\FileUpload;
use PandaPanel\Forms\FormSchema;
public static function form(FormSchema $schema): FormSchema
{
return $schema->schema([
FileUpload::make('avatar')
->disk('public')
->directory('avatars')
->image()
->maxSize(1024),
]);
}2
3
4
5
6
7
8
9
10
11
12
13
Column menyimpan path seperti avatars/abc123.png. Cara menampilkan file tersebut merupakan tanggung jawab application; field hanya menyimpan dan mengembalikan path.
Referensi method
| Method | Signature | Default |
|---|---|---|
disk() | disk(string $disk): self | 'public' |
directory() | directory(string $directory): self | 'uploads' |
multiple() | multiple(bool $multiple = true): self | false |
maxSize() | maxSize(int $kilobytes): self | 5120 (5 MB), minimum 1 |
maxFiles() | maxFiles(int $max): self | null, minimum 1 |
acceptedTypes() | acceptedTypes(list<string> $types): self | [] — MIME types |
image() | image(bool $image = true): self | false |
getDisk() | getDisk(): string | |
getDirectory() | getDirectory(): string | |
getMaxSize() | getMaxSize(): int | |
isMultiple() | isMultiple(): bool | |
getAcceptedTypes() | getAcceptedTypes(): list<string> | |
accepts() | accepts(string $path): bool |
use PandaPanel\Forms\Components\FileUpload;
FileUpload::make('gallery')
->disk('s3')
->directory('posts/gallery')
->multiple()
->maxFiles(8)
->maxSize(2048)
->acceptedTypes(['image/png', 'image/jpeg'])
->image();2
3
4
5
6
7
8
9
10
image() mengaktifkan preview dan, jika belum ada type yang dideklarasikan, mengisi acceptedTypes() dengan image/jpeg, image/png, image/gif, image/webp, dan image/avif. Jika Anda mendeklarasikan type sendiri lebih dulu, daftar tersebut tetap digunakan.
directory() dinormalisasi: .. dihapus dan slash di awal/akhir dibuang. Jaminan keamanan field bergantung pada prefix comparison, sehingga directory dengan trailing slash atau traversal segment tidak boleh menghasilkan path yang ambigu.
Tiga deklarasi, diverifikasi dua kali
- Disk, agar path tidak dapat diarahkan ke disk lain.
- Directory, agar path dari luar directory yang dideklarasikan ditolak dan tidak dapat ditempelkan ke record.
- Accepted types dan size, karena pembatasan file picker di browser hanyalah control UX, bukan constraint keamanan.
Upload endpoint menerapkan ketiganya ketika file pertama kali diterima. Saat form disubmit, disk/directory/path diperiksa kembali. Keduanya adalah dua request terpisah dan hanya request kedua yang benar-benar mengaitkan path dengan record.
use Illuminate\Support\Facades\Storage;
use PandaPanel\Forms\Components\FileUpload;
Storage::fake('local');
Storage::disk('local')->put('avatars/one.png', 'x');
Storage::disk('local')->put('elsewhere/two.png', 'x');
$field = FileUpload::make('avatar')->disk('local')->directory('avatars');
$field->accepts('avatars/one.png'); // true
$field->accepts('elsewhere/two.png'); // false — di luar directory
$field->accepts('avatars/missing.png'); // false — file tidak ada
$field->accepts('avatars/../elsewhere/two.png'); // false — mencoba keluar directory2
3
4
5
6
7
8
9
10
11
12
13
FileUpload::mutate() secara silent membuang path yang tidak mungkin dihasilkan field. Path yang gagal check bukan dianggap input teks user, tetapi value yang tidak valid untuk field tersebut:
$field->mutate('avatars/never-uploaded.png', null); // null
FileUpload::make('gallery')->disk('local')->directory('avatars')->multiple()
->mutate(['avatars/one.png', 'avatars/fake.png'], null);
// ['avatars/one.png']2
3
4
5
Validation
| Kondisi | Rules |
|---|---|
| Single | string |
| Multiple | array, ditambah max:{maxFiles} bila diset, dan string pada field.* |
Seperti field lain, rule diawali required atau nullable. Size dan MIME check tidak berada di validation form ini karena keduanya harus dijalankan pada upload request, yaitu saat file asli masih tersedia.
Endpoint
Route bernama panel.{panel_id}.uploads dan ditangani oleh PandaPanel\Http\Controllers\PanelUploadController. Request berupa POST dengan tepat dua body field:
| Body field | Arti |
|---|---|
field | Nama field. 404 jika schema tidak mendeklarasikannya, 400 jika field tersebut bukan FileUpload |
file | File yang di-upload |
Context lainnya—resource, Page, record, relation, action—berasal dari query string yang dibentuk server. Request hanya menyebut resource dan field, tidak pernah boleh menentukan disk atau directory.
Response berupa JSON:
{ "path": "avatars/9f3c.png", "name": "portrait.png" }File disimpan menggunakan $file->store($field->getDirectory(), $field->getDisk()), sehingga nama file tersimpan adalah hash Laravel. Nama file asli hanya dikembalikan untuk kebutuhan display.
Permission yang dibutuhkan upload
Upload memerlukan permission yang sama dengan permission untuk mensubmit form asal field tersebut, tidak lebih lemah.
| Context pada URL | Schema yang dibangun | Ability yang diperiksa |
|---|---|---|
page=create | create form milik resource | canCreate() |
page=edit + record | edit form milik resource | canEdit($record) |
relation + operation (+ related) | relation form | canView($owner), canViewAny($owner) milik manager, serta ability operation |
action + scope (+ record) | action form | isAuthorizedFor($record) milik action |
Permission membaca resource saja tidak cukup. Upload menulis file ke disk pada directory yang dipilih application, sehingga kemampuan melihat list tidak sama dengan kemampuan menyimpan file. Nilai page selain create atau edit menghasilkan 422, bukan diam-diam memilih form yang tidak membutuhkan record.
Untuk nested resource, key parent dikirim sebagai parent dan di-bind sebelum query berjalan. Artinya upload memperoleh scope yang sama dengan Page asalnya.
Membentuk URL
Seluruh URL dibangun oleh PandaPanel\Support\FormEndpoints, sehingga frontend Vue tidak pernah merakit URL Panel sendiri:
use PandaPanel\Support\FormEndpoints;
FormEndpoints::upload(PostResource::class, 'create');
FormEndpoints::upload(PostResource::class, 'edit', $post);
FormEndpoints::uploadForRelation(PostResource::class, CommentsRelationManager::class, $post, 'create');
FormEndpoints::uploadForAction(PostResource::class, 'import', 'table');2
3
4
5
6
Resource Page mengirim URL create/edit sebagai uploadUrl melalui Inertia. Relation dan action dialog menggunakan URL masing-masing.
Preview
Definisi field yang diserialisasi membawa multiple, maxSize, maxFiles, acceptedTypes, image, dan previewBase. previewBase berasal dari Storage::disk($disk)->url('/') dengan trailing slash dihapus. URL diselesaikan di server sehingga browser tidak pernah mengubah nama disk menjadi URL sendiri. Disk yang tidak memiliki public URL—misalnya private disk—menghasilkan null, dan frontend menampilkan nama file alih-alih broken image.
Menyimpan beberapa file
use PandaPanel\Forms\Components\FileUpload;
FileUpload::make('gallery')->multiple()->maxFiles(5);2
3
Value berupa list path, sehingga model memerlukan tempat untuk menyimpan array:
protected function casts(): array
{
return ['gallery' => 'array'];
}2
3
4
Setiap file di-upload menggunakan request terpisah. Field mengumpulkan seluruh path dan mengirimkannya bersama saat form disubmit.
Catatan
- Menghapus file dari control form tidak menghapus file fisik. Form belum tentu disubmit dan record lama mungkin masih menggunakan file tersebut. Penghapusan storage merupakan keputusan application, misalnya melalui model event atau action.
maxSize()menggunakan kilobyte, mengikuti behavior rule filemax:Laravel.- MIME check membaca isi file, bukan extension nama. PDF yang hanya diganti extension menjadi
.pngtetap ditolak olehmimetypes:. - Upload gagal adalah validation failure biasa. XHR yang menerima JSON mendapat 422; plain form post menerima redirect. Tidak ada file yang disimpan jika validation gagal.
acceptedTypes()juga dipakai oleh file picker browser, tetapi endpoint tetap menjadi authority. Attributeaccepthanya convenience UI.- Path diperiksa ulang saat save. Path harus benar-benar ada, berada di bawah directory yang dideklarasikan, dan tidak melakukan traversal. Karena itu path dari field lain, form lain, atau disk lain tidak dapat ditempelkan ke record.
accepts()mengakses disk. Method ini memanggilStorage::disk($disk)->exists($path), yang berarti remote call bila driver storage bersifat remote.- Mengubah
directory()dapat membuat path lama menjadi orphan. Existing value yang tidak lagi memiliki prefix baru akan dibuang pada save berikutnya.