Skip to content

Strategi Hapus Permanen Bersihkan Media di Database KotaPortal

Adityo Guni Waluyo

Baris media ikut terhapus bersama recordnya, tetapi file di server tetap ada. Delete permanen yang benar mengumpulkan referensi sebelum baris dihapus.

Ringkasan

Di KotaPortal file media sering nyangkut di server padahal datanya udah kehapus dari DB. Makanya sekarang URL-nya dikumpulin dulu sebelum hapus, dicek di 7 kolom lewat modul mediausage Unused. Abis DB kehapus baru file yang nggak kepake dibersihin, kalau gagal hapus ya cuma di-log aja.

Meta (untuk sistem): Investigasi proses penghapusan data media di KotaPortal yang mengungkap kebutuhan pengumpulan referensi URL sebelum eksekusi hapus baris database untuk mencegah file yatim piatu di disk. Slug (untuk sistem): hapus-permanen-bersihkan-media

Saat melakukan pemeriksaan kualitas pada modul administrasi media KotaPortal, saya menemukan anomali R4-5. Baris data di tabel database telah menghilang, namun file fisik di direktori server tetap ada.

Awalnya saya berasumsi bahwa mekanisme cascade pada foreign key database akan menangani pembersihan ini secara otomatis, atau proses cleanup dapat dijadwalkan pada tahap berikutnya.

Kenyataannya, jejak referensi hilang tepat saat baris dihapus. Kandidat harus dikumpulkan sebelum penghapusan terjadi. Keputusan apakah sebuah URL masih digunakan harus tinggal di satu tempat dengan kontrak pemeliharaan yang jelas. Penghapusan file dijalankan secara best-effort setelah penghapusan database berhasil, dilengkapi dengan penjaga jalur sebelum memanggil fungsi penghapusan sistem operasi.

Arsitektur Solusi Pengumpulan Referensi

Pada pembaruan kode ini, saya mengimplementasikan modul mediausage.Unused yang menjalankan sensus COUNT pada 7 kolom yang dapat menyimpan URL unggahan. Kolomnya meliputi media.url, announcement_media.url, entities.featured_image_url, announcements.cover_image, menus.image_url, users.avatar_url, dan settings.setting_value. Fungsi ini mengembalikan nilai benar ketika jumlah referensi sama dengan nol.

Fungsi RemoveLocal hanya menyentuh URL di bawah direktori /uploads/ dan hanya setelah validasi melalui uploads.ValidDatedRel atau uploads.ValidMediaRel. Jika penghapusan gagal, sistem mencatatnya dengan slog.Warn, sementara os.IsNotExist ditoleransi tanpa menghentikan proses. os.Remove mengembalikan *PathError ketika ada kesalahan, dan os.RemoveAll mengembalikan nil kalau path memang tidak ada [1].

Di sisi repositori, MediaURLsForDelete mengambil URL sebelum penghapusan baris utama menggunakan query UNION antara announcement_media.url dan announcements.cover_image. Fungsi ReleaseMedia kemudian menghapus baris yatim piatu di tabel media menggunakan sqlx.In untuk parameterized IN clause, lalu mengembalikan URL yang hitungan mediausage.Unused-nya nol.

Pola ini dicerminkan ke modul entityadmin (repository.go, service.go), diwiring di api/cmd/server/main.go dengan direktori uploads diteruskan ke NewService. Seluruh alur ini diuji secara ketat dalam delete_permanent_media_test.go (announcementadmin +116 baris, entityadmin +122 baris) plus penyesuaian layanan dan integrasi. Perubahan ini melibatkan 17 berkas dengan diferensial +472/-18 baris kode.

Kontrak pemeliharaan baru juga didokumentasikan dalam komentar paket mediausage: setiap migrasi yang menambahkan kolom URL unggahan wajib menambahkan penghitungan COUNT di dalam fungsi Unused, satu tempat untuk keputusan, bukan di pemanggil.

Alur DeletePermanent: Kumpulkan Sebelum Hapus, Bersihkan Setelah Berhasil

Di service.go, DeletePermanent memanggil MediaURLsForDelete terlebih dahulu untuk mengumpulkan URL gallery (announcement_media) dan cover_image dari announcements — karena setelah baris announcements dihapus, rujukan cover_image tidak bisa ditelusuri lagi. Baru kemudian repo.DeletePermanent dijalankan, cache publik dibust, lalu cleanupMedia menanggung melepaskan baris media yatim piatu via ReleaseMedia dan menghapus berkas lokal yang sudah tak dirujuk.

Gagal hapus berkas atau baris yatim hanya di-log dengan slog.Warn: delete utama sudah sukses, integritas data lebih dulu daripada berkas (R4-5). Kegagalan bersifat non-fatal secara desain.

Verifikasinya bisa dijalankan sendiri: go test ./internal/announcementadmin/... dan go test ./internal/entityadmin/... Dua test file baru itu memaksa skenario baris media yatim — setelah DeletePermanent, query SELECT COUNT(*) FROM media WHERE url = ? wajib 0 dan file di direktori uploads ikut hilang. Test ini tidak menangkap migrasi yang lupa menambah COUNT; kerusakan semacam itu diam-diam membuat sensus berbohong. Karena itu kontraknya ditaruh di komentar paket, tempat pengembang membacanya tepat saat membuka file.

Tiga Alasan di Balik Keputusan Ini

Bentuk arsitektur ini masuk akal karena beberapa alasan mendasar. Pertama, aksi referensial MySQL bersifat tingkat baris dan antar tabel saja [2]. Sebuah operasi cascade hanya memindahkan baris data, tidak pernah menghapus file di disk [1]. Foreign key hanya menghubungkan tabel untuk menjaga konsistensi data terkait [5].

Kedua, sensus kolom yang membawa URL harus berasal dari skema langsung, bukan dari riwayat migrasi. Tabel COLUMNS menyediakan informasi definitif tentang kolom dalam tabel [3].

Ketiga, menggabungkan URL yang tersimpan menjadi jalur sistem berkas adalah bentuk kerentanan traversal jalur klasik jika tidak dinetralisasi dengan benar [4]. Karena itu, memisahkan pengumpulan referensi, penghapusan baris database, dan penghapusan file secara berurutan adalah satu-satunya cara untuk menjamin integritas data saat proses hapus permanen bersihkan media dijalankan.

Sources:

Artikel terkait