Upload file adalah kebutuhan umum pada aplikasi web modern, misalnya untuk avatar pengguna, gambar produk, dokumen, atau lampiran lain. Pada stack Laravel + Inertia.js, alur upload relatif nyaman karena kita tetap bekerja dengan pola aplikasi server-side Laravel, tetapi pengalaman pengguna tetap terasa seperti SPA.
Meski terlihat sederhana, upload file memiliki beberapa detail penting: form harus dikirim sebagai multipart, objek file perlu ditangani secara khusus di sisi frontend, validasi harus ketat, penyimpanan file perlu dipilih dengan tepat, dan file lama harus dibersihkan saat data diperbarui agar storage tidak menumpuk. Jika Anda juga menampilkan preview gambar sebelum submit, ada tambahan perhatian terkait memori browser dan validasi sisi klien.
Artikel ini membahas implementasi praktis upload file dan preview gambar di Inertia.js dengan Laravel, termasuk useForm, validasi ukuran dan MIME type, progress upload, penyimpanan ke disk public atau S3, penghapusan file lama, serta praktik keamanan yang perlu diperhatikan.
Memahami alur upload file di Inertia.js dan Laravel
Secara umum alurnya seperti ini:
- Pengguna memilih file dari input
type="file". - Frontend menyimpan objek
Fileke state form. - Jika file berupa gambar, frontend dapat membuat preview sebelum submit.
- Ketika form dikirim, Inertia mengubah payload menjadi
FormDataagar file dapat dikirim sebagaimultipart/form-data. - Laravel menerima request, menjalankan validasi, lalu menyimpan file ke storage yang dipilih.
- Path file disimpan ke database.
- Jika ini proses update, file lama dapat dihapus setelah file baru berhasil disimpan.
Inertia membantu menyederhanakan langkah ini. Saat ada objek File di dalam data form, request biasanya akan dikirim sebagai FormData. Namun, memahami bahwa upload file pada dasarnya adalah request multipart tetap penting untuk debugging saat ada masalah.
Membuat form upload dengan useForm di Inertia
Berikut contoh komponen untuk upload avatar. Contoh ini menggunakan pola umum Inertia pada frontend JavaScript. Fokus utamanya adalah bagaimana file disimpan di state, bagaimana preview dibuat, dan bagaimana progress upload ditampilkan.
import { useEffect, useState } from 'react'
import { useForm } from '@inertiajs/react'
export default function ProfileForm({ user }) {
const [previewUrl, setPreviewUrl] = useState(user.avatar_url || null)
const form = useForm({
name: user.name || '',
avatar: null,
})
function handleFileChange(e) {
const file = e.target.files[0]
form.setData('avatar', file || null)
if (previewUrl && previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl)
}
if (file) {
const url = URL.createObjectURL(file)
setPreviewUrl(url)
} else {
setPreviewUrl(user.avatar_url || null)
}
}
function submit(e) {
e.preventDefault()
form.post(route('profile.update'), {
forceFormData: true,
onSuccess: () => {
form.reset('avatar')
},
})
}
useEffect(() => {
return () => {
if (previewUrl && previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl)
}
}
}, [previewUrl])
return (
<form onSubmit={submit}>
<div>
<label>Nama</label>
<input
type="text"
value={form.data.name}
onChange={e => form.setData('name', e.target.value)}
/>
{form.errors.name && <div>{form.errors.name}</div>}
</div>
<div>
<label>Avatar</label>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={handleFileChange}
/>
{form.errors.avatar && <div>{form.errors.avatar}</div>}
</div>
{previewUrl && (
<div>
<img src={previewUrl} alt="Preview avatar" style={{ width: 120, height: 120, objectFit: 'cover' }} />
</div>
)}
{form.progress && (
<div>
Upload progress: {form.progress.percentage}%
</div>
)}
<button type="submit" disabled={form.processing}>
{form.processing ? 'Menyimpan...' : 'Simpan'}
</button>
</form>
)
}Ada beberapa hal penting dari contoh di atas:
- File disimpan langsung ke form state melalui
form.setData('avatar', file). - Preview gambar dibuat dengan
URL.createObjectURL(file), bukan menunggu file diunggah ke server. - forceFormData berguna untuk memastikan request dikirim sebagai
FormData, terutama jika Anda ingin eksplisit dan menghindari perilaku yang membingungkan saat debugging. - form.progress dapat dipakai untuk menampilkan progres upload ke pengguna.
- URL.revokeObjectURL penting untuk membersihkan object URL agar tidak terjadi kebocoran memori pada browser.
Catatan: atribut
acceptpada input file hanya membantu pengalaman pengguna di browser. Ini bukan mekanisme keamanan. Validasi sesungguhnya tetap harus dilakukan di server.
Validasi file di Laravel: ukuran, MIME type, dan field opsional
Untuk upload file, validasi sisi server adalah lapisan utama. Jangan mengandalkan ekstensi file atau validasi frontend saja. Laravel menyediakan rule yang cukup jelas untuk memeriksa apakah file benar-benar file gambar, berapa ukuran maksimalnya, dan format apa yang diizinkan.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
public function update(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'avatar' => [
'nullable',
'file',
'image',
'mimes:jpg,jpeg,png,webp',
'max:2048',
],
]);
// proses penyimpanan berikutnya
}Penjelasan rule di atas:
nullable: field boleh kosong, berguna saat pengguna hanya mengganti nama tanpa mengganti file.file: memastikan input adalah file upload yang valid.image: membatasi file ke tipe gambar umum.mimes:jpg,jpeg,png,webp: membatasi format file yang diperbolehkan.max:2048: ukuran maksimal dalam kilobyte, jadi 2048 berarti 2 MB.
Kesalahan umum adalah menganggap mimes memeriksa nama file saja. Pada praktiknya Laravel memeriksa konten file untuk mengidentifikasi tipe yang lebih akurat. Tetap saja, untuk sistem yang sensitif, Anda sebaiknya menganggap file upload sebagai data yang tidak sepenuhnya dapat dipercaya.
Menyimpan file ke disk public atau S3
Menyimpan ke disk public
Jika aplikasi Anda berjalan di satu server atau kebutuhan penyimpanannya sederhana, disk public sering menjadi pilihan paling mudah. Contohnya:
public function update(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'avatar' => ['nullable', 'file', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
]);
$user = $request->user();
$user->name = $validated['name'];
if ($request->hasFile('avatar')) {
$path = $request->file('avatar')->store('avatars', 'public');
$oldAvatar = $user->avatar_path;
$user->avatar_path = $path;
$user->save();
if ($oldAvatar) {
Storage::disk('public')->delete($oldAvatar);
}
} else {
$user->save();
}
return redirect()->back()->with('success', 'Profil berhasil diperbarui.');
}Method store('avatars', 'public') akan membuat nama file unik secara otomatis dan mengembalikan path relatif, misalnya avatars/abc123.jpg. Path ini biasanya yang disimpan ke database.
Jika menggunakan disk public Laravel, pastikan symbolic link sudah dibuat:
php artisan storage:linkLalu URL file dapat diakses dari /storage/..., atau lebih aman dibentuk melalui helper backend/resource transformer sesuai kebutuhan aplikasi Anda.
Menyimpan ke Amazon S3
Jika aplikasi memerlukan skalabilitas lebih baik, akses lintas server, atau integrasi CDN, gunakan S3 atau storage object sejenis. Konsepnya sama, hanya disk yang digunakan berbeda.
if ($request->hasFile('avatar')) {
$path = $request->file('avatar')->store('avatars', 's3');
$oldAvatar = $user->avatar_path;
$user->avatar_path = $path;
$user->save();
if ($oldAvatar) {
Storage::disk('s3')->delete($oldAvatar);
}
}Beberapa pertimbangan saat memakai S3:
- Public vs private object: tidak semua file sebaiknya dapat diakses publik.
- URL akses: kadang lebih aman menggunakan signed URL untuk file private.
- Biaya dan latensi: lebih fleksibel, tetapi ada biaya request/storage dan performa bergantung pada region serta jaringan.
Jika file memang untuk aset publik seperti avatar atau gambar produk, S3 plus CDN biasanya cocok. Jika file bersifat sensitif, gunakan bucket private dan layani akses melalui signed URL atau controller yang memeriksa otorisasi.
Menghapus file lama saat update data
Salah satu kesalahan yang sering terjadi adalah menyimpan file baru tanpa menghapus file lama. Akibatnya storage menumpuk oleh file yatim yang tidak lagi dipakai database.
Pola aman untuk update adalah:
- Validasi request.
- Simpan file baru.
- Update path file di database.
- Setelah update berhasil, hapus file lama.
Mengapa file lama dihapus setelah file baru berhasil disimpan? Karena jika Anda menghapus file lama terlalu cepat lalu penyimpanan file baru gagal, data pengguna justru bisa kehilangan referensi file yang valid.
Untuk kasus yang lebih kompleks, Anda dapat membungkus update database dalam transaksi. Namun perlu diingat bahwa operasi file storage tidak selalu ikut rollback seperti database. Jadi desain alur error handling tetap penting.
DB::transaction(function () use ($request, $user, $validated) {
$user->name = $validated['name'];
if ($request->hasFile('avatar')) {
$newPath = $request->file('avatar')->store('avatars', 'public');
$oldPath = $user->avatar_path;
$user->avatar_path = $newPath;
$user->save();
if ($oldPath) {
Storage::disk('public')->delete($oldPath);
}
} else {
$user->save();
}
});Jika Anda ingin lebih hati-hati, Anda juga bisa mencatat file lama untuk dihapus melalui job asynchronous, terutama bila storage eksternal atau volume file cukup besar.
Progress upload, penanganan error, dan UX yang lebih baik
Menampilkan progress upload
Upload file, terutama gambar besar atau koneksi lambat, perlu memberi umpan balik ke pengguna. Inertia menyediakan informasi progres melalui form.progress. Anda bisa menampilkan progress bar sederhana:
{form.progress && (
<progress value={form.progress.percentage} max="100">
{form.progress.percentage}%
</progress>
)}Ini penting agar pengguna tidak menekan tombol submit berulang kali karena mengira aplikasi macet.
Menangani error validasi
Jika validasi Laravel gagal, error akan kembali ke halaman Inertia dan tersedia pada form.errors. Tampilkan pesan secara spesifik di dekat field yang bermasalah. Untuk file upload, error umum adalah:
- ukuran file terlalu besar,
- format file tidak diizinkan,
- tidak ada file yang terkirim saat field diwajibkan.
Selain validasi, tetap siapkan penanganan untuk error tak terduga seperti timeout jaringan, kredensial S3 salah, atau storage tidak bisa ditulis. Di sisi backend, error semacam ini sebaiknya dicatat ke log. Di sisi frontend, tampilkan pesan yang jelas tetapi tidak terlalu teknis untuk pengguna akhir.
Reset input file setelah sukses
Input file tidak sepenuhnya identik dengan input teks. Setelah upload sukses, Anda sering ingin me-reset state file agar tidak terkirim lagi tanpa sengaja. Pada contoh sebelumnya, form.reset('avatar') digunakan setelah sukses.
Jika Anda memakai komponen khusus untuk file input, terkadang Anda juga perlu me-reset nilai DOM input secara manual, misalnya dengan ref, karena browser memperlakukan file input secara lebih ketat dibanding input biasa.
Praktik aman untuk file yang diunggah pengguna
Upload file adalah salah satu titik masuk data yang paling berisiko. Beberapa praktik berikut sangat disarankan:
- Selalu validasi di server, bahkan jika frontend sudah membatasi jenis file.
- Jangan percaya nama file asli. Gunakan nama file yang dihasilkan sistem melalui
store()atau mekanisme serupa. - Batasi MIME type dan ukuran file sesuai kebutuhan bisnis.
- Simpan path file, bukan file mentah, di database.
- Hindari mengeksekusi file upload. Untuk server lokal/public disk, pastikan file disimpan di lokasi yang tidak menyebabkan file tersebut diperlakukan sebagai script executable.
- Pertimbangkan visibilitas file: publik untuk gambar yang memang terbuka, private untuk dokumen sensitif.
- Gunakan otorisasi saat update atau hapus file agar pengguna tidak bisa menimpa file milik pengguna lain.
- Batasi dimensi gambar jika perlu, terutama untuk aplikasi yang menerima upload dalam jumlah besar agar storage dan bandwidth lebih terkontrol.
Untuk aplikasi yang menerima file dari banyak pengguna, pertimbangkan juga antivirus scanning, image optimization, dan queue processing setelah upload. Itu di luar kebutuhan dasar artikel ini, tetapi sangat relevan untuk sistem produksi berskala besar.
Kesalahan umum dan tips debugging
- File selalu null di backend: periksa apakah state form benar-benar berisi objek
File, dan gunakanforceFormData: truebila perlu. - URL file public tidak bisa diakses: pastikan
php artisan storage:linksudah dijalankan. - Upload ke S3 gagal: periksa konfigurasi disk, kredensial, region, permission bucket, dan log aplikasi.
- Preview gambar tidak berubah: pastikan event
onChangebenar, dan object URL lama dibersihkan sebelum membuat yang baru. - Ukuran file terlalu besar meski validasi sudah benar: periksa juga batas upload di level PHP/web server seperti
upload_max_filesize,post_max_size, atau konfigurasi reverse proxy.
Tips lain yang sering membantu adalah memeriksa tab Network di browser developer tools. Di sana Anda bisa melihat apakah request benar-benar dikirim sebagai multipart, apakah payload berisi file, dan bagaimana respons validasinya.
Penutup
Upload file di Inertia.js dengan Laravel bukan hanya soal menambahkan input file ke form. Implementasi yang baik perlu memperhatikan cara request dibentuk, preview gambar sebelum submit, validasi yang ketat, target storage yang sesuai, penghapusan file lama saat update, serta pengalaman pengguna selama proses upload.
Untuk kebutuhan sederhana, disk public adalah pilihan praktis dan cepat. Untuk aplikasi yang lebih skalabel atau multi-server, S3 lebih fleksibel. Apa pun storage yang dipilih, prinsip dasarnya tetap sama: validasi file dengan ketat, simpan path secara aman, tampilkan feedback yang jelas ke pengguna, dan jangan biarkan file lama memenuhi storage tanpa kontrol.
Jika Anda menerapkan pola-pola di atas, alur upload file pada aplikasi Inertia.js dan Laravel akan lebih stabil, aman, dan mudah dirawat dalam jangka panjang.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!