Disk public membuat berkas bisa diakses siapa pun yang tahu URL-nya — sempurna untuk avatar, berbahaya untuk faktur. Keputusan publik vs privat bukan detail teknis belakangan; keputusan itu menentukan seluruh arsitektur penyajian berkas. Chapter ini membangun sisi privat secara benar: berkas di disk local, disajikan lewat controller yang memeriksa otorisasi lebih dulu, lengkap dengan feature test yang membuktikan pengguna lain memang tertahan di pintu.
Kapan Publik, Kapan Privat #
Uji cepatnya satu kalimat: bolehkah URL ini diteruskan ke orang lain tanpa kerugian apa pun? Avatar yang tampil di halaman profil publik jelas lolos uji itu. Faktur berisi alamat dan angka tagihan, foto KTP, atau backup database tidak akan pernah lolos.
| Jenis berkas | Tempat penyimpanan | Cara akses |
|---|---|---|
| Avatar, thumbnail publik | Disk public | URL langsung lewat symlink |
| Faktur, dokumen pribadi | Disk local (atau s3 privat) | Route + controller + otorisasi |
| Berkas internal aplikasi | Disk local | Tidak pernah keluar dari kode |
Pemisahan ini sudah dibangun sejak konfigurasi: root disk local di storage/app/private berada di luar public/, sehingga tidak ada URL yang bisa menyentuhnya secara langsung. Pintu satu-satunya adalah route yang kita buat sendiri.
Route dan Controller untuk Berkas Privat #
Struktur klasiknya: route dengan middleware auth, controller yang memeriksa kepemilikan lewat Gate, lalu menyajikan berkas. Berkas kita tata per pengguna di bawah folder users/{id}/ sehingga kepemilikan bisa diperiksa langsung dari path-nya:
use App\Http\Controllers\DokumenController;
use Illuminate\Support\Facades\Route;
Route::get('/berkas/{path}', [DokumenController::class, 'tampil'])
->where('path', '.*')
->middleware('auth')
->name('berkas.tampil');Constraint ->where('path', '.*') penting agar segmen path yang mengandung garis miring (users/1/faktur.txt) tetap ditangkap satu parameter. Di sisi controller, dua pemeriksaan berjutan: otorisasi dulu, keberadaan berkas kemudian:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
class DokumenController extends Controller
{
public function tampil(Request $request, string $path)
{
abort_unless(
$request->user()
&& Gate::forUser($request->user())->allows('membaca-berkas', $path),
403
);
abort_unless(Storage::exists($path), 404);
return Storage::response($path);
}
}Urutan dua abort_unless itu sengaja: kunci pintu dulu baru cari barangnya. Gate-nya sendiri sederhana, didaftarkan di AppServiceProvider — berkas hanya boleh dibaca bila path-nya berada di bawah folder milik pengguna itu:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define('membaca-berkas', function (User $user, string $path) {
return str_starts_with($path, 'users/'.$user->id.'/');
});Method Storage::response($path) di akhir controller mengirim isi berkas secara inline — browser menampilkannya langsung (gambar tampil, PDF terbuka) dengan header Content-Disposition: inline. Untuk versi yang memaksa unduhan, chapter File Download membahas Storage::download().
Membuktikan Otorisasi lewat Feature Test #
Feature test menguji tiga skenario dalam satu tarikan: pemilik boleh melihat, pengguna lain ditolak 403, dan tamu yang belum login dialihkan:
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class BerkasPrivatTest extends TestCase
{
use RefreshDatabase;
public function test_pemilik_bisa_melihat_berkas_privatnya_secara_inline(): void
{
Storage::fake('local');
$budi = User::factory()->create(['id' => 1, 'name' => 'Budi']);
Storage::disk('local')->put('users/1/faktur-2024.txt', 'isi faktur Budi');
$response = $this->actingAs($budi)->get('/berkas/users/1/faktur-2024.txt');
$response->assertOk();
$this->assertSame('isi faktur Budi', $response->streamedContent());
}
public function test_pengguna_lain_diblokir_oleh_gate(): void
{
Storage::fake('local');
User::factory()->create(['id' => 1, 'name' => 'Budi']);
$siti = User::factory()->create(['id' => 2, 'name' => 'Siti']);
Storage::disk('local')->put('users/1/faktur-2024.txt', 'isi faktur Budi');
$this->actingAs($siti)
->get('/berkas/users/1/faktur-2024.txt')
->assertForbidden();
}
public function test_tamu_dialihkan_ke_login(): void
{
$this->get('/berkas/users/1/faktur-2024.txt')
->assertRedirect('/login');
}
}Output yang Dihasilkan:
PASS Tests\Feature\BerkasPrivatTest
✓ pemilik bisa melihat berkas privatnya secara inline 0.17s
✓ pengguna lain diblokir oleh gate 0.02s
✓ tamu dialihkan ke login 0.01s
✓ berkas privat yang tidak ada menghasilkan 404 0.01s
Tests: 4 passed (6 assertions)
Duration: 0.28sTest keempat (ditampilkan di output, implementasinya sepadan dengan tiga di atas) memeriksa kasus 404 untuk path yang sah milik pengguna tetapi berkasnya memang tidak ada — abort kedua di controller yang bekerja. Pola Storage::fake('local') membuat test berjalan pada disk terisolasi, dan streamedContent() membandingkan isi yang dikirim dengan yang ditulis.
Visibility Bukan Pengganti Otorisasi #
Ingat pembahasan visibility di chapter Storage Disks: key visibility hanya mengatur permission bit Unix saat menulis (644 vs 600). Untuk disk local, bit permission tidak pernah memutuskan siapa yang boleh membuka URL, karena batas akses yang sebenarnya adalah letak folder dan route yang kita kendalikan. Otorisasi di controller — bukan permission bit — adalah satu-satunya penjaga berkas privat. Konsep Gate dan Policy secara lebih luas dibedah di section Authorization
.
Dengan berkas privat kini terjaga penuh, kebutuhan berikutnya justru sebaliknya: bagaimana memberikan berkas itu kepada pengguna sebagai unduhan yang rapi — lengkap dengan nama berkas dan header kustom. Itu pekerjaan chapter File Download, sesuai dokumentasi Filesystem .