Latar Belakang
Alat dan Bahan
A. Perangkat Lunak
- Web browser (Google Chrome)
- Code editor (Visual Studio Code)
- Terminal
- Web server
- Composer
- PHP
- Laravel
B. Perangkat Keras
- Laptop
Kenapa File Upload Perlu Ditangani dengan Benar?
- Masalah teknis: storage penuh, performa lambat, file corrupt
- Masalah keamanan: malware upload, directory traversal attacks, file inclusion vulnerabilities
- Masalah pengalaman pengguna: file gagal upload, proses lambat, file hilang
- Masalah maintenance: sulit backup, migrasi data rumit, file sampah menumpuk
Risiko Jika Salah Mengelola File
1. Storage Membengkak Tanpa Kendali
2. Konflik Nama File
3. Celah Keamanan
4. Performa Menurun
5. Kesulitan Migrasi
Validasi File Upload
Validasi Tipe File
// Validasi dengan mimes
'foto' => 'required|mimes:jpg,jpeg,png|max:2048'
// Validasi dengan mimetypes (lebih akurat)
'dokumen' => 'required|mimetypes:application/pdf,image/jpeg|max:5120'- mimes : hanya mengecek ekstensi file dari nama file. Seseorang bisa dengan mudah mengubah nama virus.exe menjadi virus.jpg dan lolos validasi ini.
- mimetypes : mengecek MIME type yang sebenarnya dengan membaca isi file. Ini lebih aman karena bisa mendeteksi file yang diubah ekstensinya.
Validasi Ukuran File
'video' => 'required|mimes:mp4,mov,avi|max:102400' // maksimal 100MB- upload_max_filesize - batas maksimal ukuran file upload
- post_max_size - batas maksimal total data POST (termasuk file)
- max_execution_time - waktu maksimal eksekusi script
Validasi Lainnya
'gambar' => 'required|file|image|dimensions:min_width=100,min_height=100|max:2048'
'dokumen' => 'nullable|file|mimes:pdf|min:50' // minimal 50KB
- image - memastikan file adalah gambar (jpg, png, bmp, gif, svg)
- dimensions - validasi dimensi gambar (lebar, tinggi, rasio)
- min - ukuran minimal file
Contoh Penggunaan di Controller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function store(Request $request)
{
// Pesan error untuk validasi field product_image
$messages = [
// Validasi jika file tidak diupload
'product_image.required' => 'Foto produk wajib diupload',
// Validasi jika format file tidak sesuai ketentuan
'product_image.mimes' => 'Foto harus berformat JPG, JPEG, atau PNG',
// Validasi jika ukuran file melebihi 2MB (2048 KB)
'product_image.max' => 'Ukuran foto maksimal 2MB',
// Validasi jika dimensi gambar kurang dari 300x300 piksel
'product_image.dimensions' => 'Foto minimal berukuran 300x300 piksel'
];
// Melakukan validasi terhadap input dari form
$request->validate([
// Field name:
// - required: wajib diisi
// - string: harus berupa teks
// - max:255: maksimal 255 karakter
'name' => 'required|string|max:255',
// Field price:
// - required: wajib diisi
// - numeric: harus berupa angka
// - min:0: tidak boleh bernilai negatif
'price' => 'required|numeric|min:0',
// Field product_image:
// - required: wajib diupload
// - file: memastikan input berupa file
// - mimes: membatasi tipe file yang diperbolehkan
// - max:2048: ukuran maksimal 2MB
// - dimensions: minimal lebar dan tinggi 300 piksel
'product_image' => 'required|file|mimes:jpg,jpeg,png|max:2048|dimensions:min_width=300,min_height=300'
], $messages);
// Jika validasi gagal, Laravel otomatis akan:
// - Mengembalikan ke halaman sebelumnya
// - Mengirimkan pesan error ke session
// Jika validasi berhasil, maka proses penyimpanan data dapat dilanjutkan
// Proses penyimpanan file akan dibahas pada bagian berikutnya
}
}
Penyimpanan File di Storage/App
Penggunaan Method store() dan storeAs()
// Cara 1: store() - nama file di-generate otomatis
$path = $request->file('product_image')->store('product-images');
// Cara 2: storeAs() - kita bisa menentukan nama file sendiri
$path = $request->file('product_image')->storeAs(
'product-images',
'product-' . time() . '.' . $request->file('product_image')->extension()
);
// Cara 3: menyimpan dengan disk tertentu
$path = $request->file('product_image')->store('product-images', 'p ublic');
Disk Storage
// config/filesystems.php
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
],
],
- local: Untuk file internal yang tidak perlu diakses publik. Cocok untuk backup, file temporary, atau file yang diproses di background.
- public: Untuk file yang perlu diakses publik seperti gambar profil, foto produk, dll. File di disk ini bisa diakses melalui symlink.
- s3: Untuk Amazon S3 atau layanan compatible S3 lainnya (DigitalOcean Spaces, MinIO, dll)
Kenapa File Tidak Disimpan Langsung di Folder Public?
$file->move(public_path('uploads'), $filename);
$path = $file->store('uploads', 'public');
Membuat Symbolic Link
Fungsi php artisan storage:link
php artisan storage:link
# Di Linux/Mac
ln -s /var/www/project/storage/app/public /var/www/project/public/storage
# Di Windows (menggunakan mklink)
mklink /D C:\project\public\storage C:\project\storage\app\public
Hubungan storage/app/public dan public/storage
Kenapa Ini Penting?
// Route untuk mengakses file yang dilindungi
Route::get('/protected-file/{filename}', function ($filename) {
// Mengecek apakah user sudah login (terautentikasi)
// Jika belum login, maka akan menampilkan error 403 (Forbidden)
if (!auth()->check()) {
abort(403);
}
// Menentukan path lengkap file di dalam folder storage/app/protected
// storage_path() akan mengarahkan ke direktori storage Laravel
$path = storage_path('app/protected/' . $filename);
// Mengecek apakah file benar-benar ada di lokasi tersebut
// Jika tidak ditemukan, akan menampilkan error 404 (Not Found)
if (!file_exists($path)) {
abort(404);
}
// Mengembalikan file sebagai response agar dapat ditampilkan atau diunduh
return response()->file($path);
});
Penanganan Nama File agar Tidak Bentrok
Penggunaan hashName()
$file = $request->file('product_image');
$path = $file->storeAs('product-images', $file->hashName());
Timestamp atau UUID
$filename = time() . '_' . $file->getClientOriginalName();
$path = $file->storeAs('product-images', $filename);
$filename = time() . '_' . uniqid() . '.' . $file->extension();
$filename = Str::uuid() . '.' . $file->extension();
$path = $file->storeAs('product-images', $filename);
Membuat Struktur Folder Berdasarkan Tanggal
$folder = 'product-images/' . date('Y') . '/' . date('m');
$path = $file->store($folder, 'public');
Menyimpan Informasi Lengkap di Database
<?php
// Mengambil file dari request dengan nama field 'product_image'
$file = $request->file('product_image');
// Mengambil nama asli file saat diupload oleh user
$originalName = $file->getClientOriginalName();
// Mengambil MIME type file (contoh: image/jpeg, image/png)
$mimeType = $file->getMimeType();
// Mengambil ukuran file dalam satuan byte
$size = $file->getSize();
// Mengambil ekstensi file (jpg, png, dll)
$extension = $file->extension();
// Membuat nama file baru yang unik
// time() → timestamp saat ini
// Str::random(10) → string acak 10 karakter
// Tujuannya untuk menghindari konflik nama file
$filename = time() . '_' . Str::random(10) . '.' . $extension;
// Menyimpan file ke folder storage/app/public/product-images
// 'product-images' → nama folder
// $filename → nama file yang sudah dibuat unik
// 'public' → disk storage yang digunakan (sesuai config/filesystems.php)
$path = $file->storeAs('product-images', $filename, 'public');
// Menyimpan data produk ke database
// termasuk metadata file yang telah diambil sebelumnya
Product::create([
'name' => $request->name,
'image_path' => $path, // path relatif file di storage
'image_original_name' => $originalName, // nama asli file
'image_mime_type' => $mimeType, // tipe file
'image_size' => $size, // ukuran file
]);
- Menampilkan ukuran file ke user
- Validasi tambahan di level database
- Proses migrasi atau restore jika diperlukan
- Audit trail
Mengapa Disarankan untuk Tidak Menggunakan Nama Asli File?
Update & Replace File Lama
Alur Update File yang Aman
- Validasi input termasuk file baru
- Simpan file baru terlebih dahulu (jika ada)
- Jika penyimpanan file baru berhasil, hapus file lama
- Update database dengan path file baru
- Jika ada error, rollback (file baru dihapus, data tidak berubah)
Contoh Implementasi di Controller
public function update(Request $request, $id)
{
// Mengambil data produk berdasarkan ID
// Jika tidak ditemukan, otomatis akan menampilkan error 404
$product = Product::findOrFail($id);
// Validasi input
$request->validate([
// name wajib diisi, string, maksimal 255 karakter
'name' => 'required|string|max:255',
// product_image opsional (nullable),
// tetapi jika diupload harus berupa file jpg/jpeg/png,
// maksimal 2MB dan minimal lebar 300px
'product_image' => 'nullable|file|mimes:jpg,jpeg,png|max:2048|dimensions:min_width=300'
]);
// Ambil hanya field yang boleh diupdate
$data = $request->only('name', 'price', 'description');
// Cek apakah ada file baru yang diupload
if ($request->hasFile('product_image')) {
try {
// Simpan file baru terlebih dahulu
// Disimpan ke storage/app/public/product-images
$newPath = $request->file('product_image')
->store('product-images', 'public');
// Jika penyimpanan berhasil, masukkan path baru ke array update
$data['image_path'] = $newPath;
// Setelah file baru aman tersimpan,
// baru hapus file lama (jika ada)
if ($product->image_path) {
// Cek apakah file lama benar-benar ada
if (Storage::disk('public')->exists($product->image_path)) {
// Hapus file lama dari storage
Storage::disk('public')->delete($product->image_path);
}
}
} catch (\Exception $e) {
// Jika terjadi error saat upload,
// dan file baru sempat tersimpan, maka hapus kembali
if (isset($newPath) &&
Storage::disk('public')->exists($newPath)) {
Storage::disk('public')->delete($newPath);
}
// Kembalikan ke halaman sebelumnya dengan pesan error
return back()->with(
'error',
'Gagal mengupload file: ' . $e->getMessage()
);
}
}
// Update data produk di database
$product->update($data);
// Redirect ke halaman index dengan pesan sukses
return redirect()
->route('products.index')
->with('success', 'Produk berhasil diupdate');
}
Delete File Saat Record Dihapus
Penggunaan Storage::delete()
public function destroy($id)
{
$product = Product::findOrFail($id);
// Hapus file dari storage
if ($product->image_path) {
$fullPath = storage_path('app/public/' . $product->image_path);
if (file_exists($fullPath)) {
unlink($fullPath);
}
// Atau menggunakan Storage facade
// Storage::disk('public')->delete($product->image_path);
}
// Hapus data dari database
$product->delete();
return redirect()->route('products.index')
->with('success', 'Produk berhasil dihapus');
}
Menggunakan Model Event untuk Otomatisasi
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
class Product extends Model
{
protected $fillable = ['name', 'price', 'image_path'];
protected static function booted()
{
// Hapus file saat model dihapus
static::deleted(function ($product) {
if ($product->image_path) {
Storage::disk('public')->delete($product->image_path);
}
});
// Hapus file lama saat diupdate dengan file baru
static::updating(function ($product) {
// Cek apakah image_path berubah
if ($product->isDirty('image_path')) {
$oldPath = $product->getOriginal('image_path');
if ($oldPath) {
Storage::disk('public')->delete($oldPath);
}
}
});
}
}
Optimasi File Upload untuk Produksi
Menggunakan Queue untuk File Besar
app/Http/Controllers/VideoController.php
public function store(Request $request)
{
// Validasi file video
// - wajib
// - format mp4
// - maksimal 500MB (512000 KB)
$request->validate([
'video' => 'required|mimes:mp4|max:512000'
]);
// Simpan file sementara terlebih dahulu
// agar tidak hilang setelah request selesai
$tempPath = $request->file('video')->store('temp-videos');
// Buat record upload di database
$upload = PendingUpload::create([
'user_id' => auth()->id(),
'original_name' => $request->file('video')->getClientOriginalName(),
'size' => $request->file('video')->getSize(),
'status' => 'pending'
]);
// Dispatch job ke queue (hanya kirim ID & path string)
ProcessVideoUpload::dispatch($upload->id, $tempPath);
// Response langsung dikirim tanpa menunggu proses selesai
return response()->json([
'message' => 'Video sedang diproses',
'upload_id' => $upload->id
]);
}
app/Jobs/ProcessVideoUpload.php
use Illuminate\Support\Facades\Storage;
class ProcessVideoUpload implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $uploadId;
protected $tempPath;
// Constructor hanya menerima ID dan path file
public function __construct($uploadId, $tempPath)
{
$this->uploadId = $uploadId;
$this->tempPath = $tempPath;
}
public function handle()
{
// Ambil ulang data upload dari database
$upload = PendingUpload::find($this->uploadId);
if (!$upload) {
return;
}
try {
// Update status menjadi processing
$upload->update(['status' => 'processing']);
// Tentukan path final
$finalPath = 'videos/' . time() . '_' . $upload->original_name;
// Pindahkan file dari temp ke folder final
Storage::move($this->tempPath, $finalPath);
// Update database setelah berhasil
$upload->update([
'status' => 'completed',
'path' => $finalPath
]);
// Di sini bisa tambahkan:
// - kirim email
// - broadcast event
// - trigger encoding ffmpeg
} catch (\Exception $e) {
// Jika gagal, update status menjadi failed
$upload->update([
'status' => 'failed'
]);
// Hapus file temp jika masih ada
if (Storage::exists($this->tempPath)) {
Storage::delete($this->tempPath);
}
}
}
}
Kompresi dan Optimasi Gambar
use Intervention\Image\Facades\Image;
use Illuminate\Http\UploadedFile;
public function uploadWithOptimization(UploadedFile $file)
{
// Membuat instance image dari file upload
// Intervention akan membaca file dan mengubahnya menjadi object yang bisa dimanipulasi
$image = Image::make($file);
// Cek apakah lebar gambar lebih dari 1200px
// Jika ya, lakukan resize agar tidak terlalu besar
if ($image->width() > 1200) {
// Resize lebar menjadi 1200px
// Tinggi diset null agar mengikuti aspect ratio
$image->resize(1200, null, function ($constraint) {
// Menjaga rasio asli agar gambar tidak gepeng
$constraint->aspectRatio();
// Mencegah gambar kecil diperbesar (no upscale)
$constraint->upsize();
});
}
// Membuat nama file baru (format JPG)
// Menggunakan timestamp agar unik
$filename = time() . '.jpg';
// Menentukan lokasi penyimpanan file di storage
$path = storage_path('app/public/product-images/' . $filename);
// Menyimpan gambar dengan kualitas kompresi 80%
// Nilai 0–100 (semakin kecil, semakin terkompres)
$image->save($path, 80);
// Mengembalikan path relatif untuk disimpan ke database
return 'product-images/' . $filename;
}
Multiple File Upload
public function uploadMultiple(Request $request)
{
// Validasi:
// - photos wajib ada dan harus berupa array
// - maksimal 5 file
// - setiap item dalam photos harus file jpg/jpeg/png
// - ukuran maksimal 2MB per file
$request->validate([
'photos' => 'required|array|max:5',
'photos.*' => 'required|file|mimes:jpg,jpeg,png|max:2048'
]);
// Array untuk menyimpan path file yang berhasil diupload
$paths = [];
// Loop setiap file yang dikirim
foreach ($request->file('photos') as $file) {
// Simpan file ke storage/app/public/gallery
// dan simpan path relatifnya ke dalam array
$paths[] = $file->store('gallery', 'public');
}
// Kembalikan response JSON berisi pesan dan daftar path file
return response()->json([
'message' => 'Upload berhasil',
'paths' => $paths
]);
}
Chunk Upload untuk File Besar
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
public function uploadChunk(Request $request)
{
// Validasi input chunk
$request->validate([
'file_id' => 'required|string',
'chunk' => 'required|file',
'chunk_index' => 'required|integer|min:0',
'total_chunks' => 'required|integer|min:1'
]);
$fileId = $request->file_id;
$chunkIndex = $request->chunk_index;
$totalChunks = $request->total_chunks;
$chunk = $request->file('chunk');
// Folder temporary berdasarkan file_id
$tempPath = storage_path("app/temp/chunks/{$fileId}");
// Buat folder jika belum ada
if (!file_exists($tempPath)) {
mkdir($tempPath, 0755, true);
}
// Simpan chunk sesuai index
$chunk->move($tempPath, "chunk_{$chunkIndex}.part");
// Cek apakah semua chunk sudah terkumpul
$uploadedChunks = glob("{$tempPath}/*.part");
if (count($uploadedChunks) == $totalChunks) {
$this->assembleChunks($fileId, $tempPath);
}
return response()->json([
'status' => 'success',
'message' => 'Chunk diterima'
]);
}
private function assembleChunks($fileId, $tempPath)
{
// Tentukan lokasi file final
$finalDirectory = storage_path("app/public/uploads");
if (!file_exists($finalDirectory)) {
mkdir($finalDirectory, 0755, true);
}
$finalPath = "{$finalDirectory}/{$fileId}.mp4";
// Buka file final dalam mode write binary
$finalFile = fopen($finalPath, 'wb');
if (!$finalFile) {
return;
}
// Ambil semua chunk
$chunks = glob("{$tempPath}/*.part");
// Urutkan natural agar chunk_1, chunk_2, chunk_10 tidak salah urut
natsort($chunks);
// Gabungkan menggunakan stream (hemat memory)
foreach ($chunks as $chunkPath) {
$handle = fopen($chunkPath, 'rb');
if ($handle) {
while (!feof($handle)) {
fwrite($finalFile, fread($handle, 1024 * 1024)); // 1MB per baca
}
fclose($handle);
}
}
fclose($finalFile);
// Hapus seluruh folder temporary setelah selesai
Storage::deleteDirectory("temp/chunks/{$fileId}");
}
Best Practice
Jangan Simpan File Langsung di Public
Pisahkan Logika Upload
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class FileUploadService
{
/**
* Upload gambar produk ke storage public
*
* @param UploadedFile $file
* @return string Path relatif file yang tersimpan
*/
public function uploadProductImage(UploadedFile $file)
{
// Membuat nama file unik berdasarkan timestamp
// Ditambahkan nama asli file untuk keterbacaan
$filename = time() . '_' . $file->getClientOriginalName();
// Menyimpan file ke:
// storage/app/public/product-images
// dan mengembalikan path relatifnya
return $file->storeAs('product-images', $filename, 'public');
}
/**
* Menghapus file dari storage public
*
* @param string|null $path
* @return void
*/
public function deleteFile($path)
{
// Cek apakah path ada dan file benar-benar tersedia
if ($path && Storage::disk('public')->exists($path)) {
// Hapus file dari storage
Storage::disk('public')->delete($path);
}
}
}
Cek Keberadaan File Sebelum Menghapus
if (Storage::disk('public')->exists($oldPath)) {
Storage::disk('public')->delete($oldPath);
}
Gunakan Queue untuk File Besar
Simpan Informasi File Lengkap di Database
Hasil Pembelajaran
multipart/form-data, validasi file pada controller, serta penyimpanan file menggunakan sistem storage yang telah disediakan oleh Laravel. File yang diunggah dapat disimpan di folder tertentu, baik secara lokal maupun menggunakan disk penyimpanan yang telah dikonfigurasi.Kesimpulan
Daftar Pustaka
Laravel. (n.d.). File storage. Diakses pada 3 Maret 2026, dari https://laravel.com/docs/filesystem
Laravel. (n.d.). File uploads. Diakses pada 3 Maret 2026, dari https://laravel.com/docs/requests#storing-uploaded-files
Laravel. (n.d.). Validation. Diakses pada 3 Maret 2026, dari https://laravel.com/docs/validation
Laravel. (n.d.). Queues. Diakses pada 3 Maret 2026, dari https://laravel.com/docs/queues
Laravel. (n.d.). Eloquent: Events. Diakses pada 3 Maret 2026, dari https://laravel.com/docs/eloquent#events
Intervention Image. (n.d.). Image manipulation library for PHP. Diakses pada 3 Maret 2026, dari http://image.intervention.io/
Amazon Web Services. (n.d.). Amazon S3 documentation. Diakses pada 3 Maret 2026, dari https://docs.aws.amazon.com/s3/
Niagahoster. (2023). Cara upload file di Laravel dengan aman. Diakses pada 3 Maret 2026, dari https://www.niagahoster.co.id/blog/
Petani Kode. (2022). Tutorial upload file di Laravel. Diakses pada 3 Maret 2026, dari https://www.petanikode.com/