Dedup Upload File dengan SHA: Solusi Paling Masuk Akal untuk Double Upload
13 min read

Dedup Upload File dengan SHA: Solusi Paling Masuk Akal untuk Double Upload

Dalam sistem yang memiliki fitur upload file, kasus double upload hampir pasti akan terjadi suatu saat. Penyebabnya tidak selalu karena race condition atau bug di backend — sering kali justru murni faktor manusia dan UI yang kurang jelas. User lupa bahwa mereka sudah meng-upload file yang sama sebelumnya, admin membuka tab baru tanpa sadar dan upload ulang dokumen yang persis, UI tidak memberi feedback yang jelas sehingga user mengulang aksi karena ragu apakah upload pertamanya berhasil, atau retry manual dilakukan setelah koneksi internet yang lambat membuat proses terasa macet. Masalah intinya sederhana: file yang di-upload benar-benar sama, hanya berbeda waktu kejadiannya. Jika tidak ditangani, dampaknya bisa cukup serius — storage membengkak tanpa alasan yang jelas, data jadi redundant di banyak tempat, biaya penyimpanan meningkat seiring waktu, dan relasi data antar entity jadi ambigu karena tidak jelas file mana yang seharusnya jadi rujukan utama. Artikel ini membahas solusi yang paling tepat dan robust untuk kasus tersebut: deduplikasi berbasis SHA, atau yang lebih dikenal sebagai content-based deduplication.

Masalah Utama: Logical Duplicate, Bukan Race Condition

Sebelum membahas solusinya, penting untuk membedakan dua jenis masalah yang sering tercampur dalam diskusi tentang duplikasi data, karena keduanya membutuhkan pendekatan yang sama sekali berbeda. Race condition terjadi ketika dua request datang bersamaan dalam rentang waktu yang sangat sempit — masalah ini bisa diselesaikan dengan mekanisme locking atau atomic update di level database, karena yang dilawan adalah masalah timing. Logical duplicate, sebaliknya, terjadi ketika request datang terpisah jauh dalam waktu — bisa beberapa menit, beberapa jam, bahkan beberapa hari — tapi konten file yang di-upload persis sama.

flowchart TD
    A[File di-upload] --> B{Kapan terjadinya?}
    B -- Hampir bersamaan, milidetik --> C[Race Condition]
    B -- Terpisah waktu, menit/jam/hari --> D[Logical Duplicate]
    C --> E[Solusi: locking, atomic update]
    D --> F[Solusi: content-based deduplication]

Pada logical duplicate, pendekatan yang biasa dipakai untuk race condition justru tidak relevan sama sekali. Locking tidak membantu, karena tidak ada dua proses yang berebut resource di waktu yang sama — kedua upload itu sudah selesai diproses masing-masing tanpa konflik apapun secara teknis. Debounce di level UI juga tidak cukup, karena debounce hanya efektif mencegah klik ganda dalam hitungan detik, bukan mencegah user yang sengaja kembali besoknya dan upload file yang sama lagi. Time window check — misalnya menolak upload jika ada file dengan nama serupa dalam rentang lima menit terakhir — juga tidak reliable, karena heuristik semacam ini mudah salah baik dalam arah false positive maupun false negative.

Yang sebenarnya dibutuhkan adalah cara untuk menjawab satu pertanyaan mendasar: apakah file ini secara fisik benar-benar sama dengan yang pernah di-upload sebelumnya, terlepas dari kapan waktunya, siapa yang meng-upload, atau nama file yang dipakai?


Apa Itu File SHA?

SHA, kependekan dari Secure Hash Algorithm, adalah fungsi hash kriptografis yang menghasilkan fingerprint unik dari konten sebuah file. Konsep fingerprint di sini cukup tepat sebagai analogi — sama seperti sidik jari manusia yang unik untuk setiap individu, hash SHA dari sebuah file pada dasarnya unik untuk kombinasi byte tertentu yang menyusun file itu.

Yang benar-benar di-hash hanyalah raw byte content dari file — urutan byte mentah yang menyusun isi file tersebut. Beberapa hal yang secara sengaja tidak ikut dihitung dalam proses hashing ini meliputi nama file, extension, waktu upload, identitas user yang meng-upload, dan path penyimpanan di server. Semua atribut ini adalah metadata di sekitar file, bukan bagian dari konten file itu sendiri.

SHA(file) = hash(byte[0] + byte[1] + ... + byte[N])

Secara konseptual, fungsi hash ini menerima seluruh rangkaian byte dari awal hingga akhir file, lalu menghasilkan satu string output dengan panjang fixed — untuk SHA-256 misalnya, selalu menghasilkan output 64 karakter heksadesimal, tidak peduli apakah file aslinya berukuran 10 KB atau 10 GB.

SHA-256 dipilih secara luas untuk kasus deduplikasi karena risiko collision-nya — dua input berbeda yang menghasilkan hash yang sama — secara praktis bisa diabaikan untuk kebutuhan aplikasi umum. Algoritma yang lebih lama seperti MD5 atau SHA-1 sudah terbukti rentan terhadap collision yang direkayasa secara sengaja, sehingga sebaiknya dihindari untuk kasus yang menyangkut integritas data.

Konsekuensi Penting dari File SHA

Memahami sifat dasar SHA membantu menjelaskan mengapa pendekatan ini bekerja dengan sangat baik untuk masalah double upload, sekaligus di mana batasannya berada.

File Identik Menghasilkan SHA Identik

Jika dua file memiliki byte demi byte yang sama persis, dengan urutan yang sama pula, maka SHA yang dihasilkan dari keduanya pasti identik. Properti ini bersifat deterministik — fungsi hash yang sama, diberi input yang sama, akan selalu menghasilkan output yang sama, berapa kali pun dijalankan dan di mesin manapun dijalankan.

flowchart LR
    A[File A: invoice.pdf] -->|hash| C[SHA: a3f5b8...]
    B[File B: invoice_copy.pdf] -->|hash| C
    C --> D[Sama persis, walau nama file berbeda]

Beda Satu Byte Saja, SHA Berubah Total

Di sisi lain, SHA sangat sensitif terhadap perubahan sekecil apapun. Beberapa contoh perubahan yang terlihat sepele tapi tetap menghasilkan SHA yang sama sekali berbeda meliputi metadata PDF yang berubah meski isi dokumen visualnya identik, EXIF timestamp pada file gambar yang berbeda meski foto yang ditampilkan sama, perbedaan line ending antara LF dan CRLF pada file teks, atau file yang di-save ulang oleh aplikasi editor tertentu yang menambahkan metadata tersembunyi tanpa mengubah apapun yang terlihat oleh mata.

Walaupun secara visual kedua file ini terlihat identik ketika dibuka, SHA yang dihasilkan bisa sama sekali berbeda. Ini adalah trade-off yang penting dipahami sejak awal — SHA mendeteksi kesamaan pada level byte mentah, bukan kesamaan pada level “makna” atau tampilan visual dari file tersebut.

Jika kasus penggunaan kamu melibatkan file yang sering di-export ulang dari aplikasi berbeda (misalnya PDF yang sama tapi diekspor dari Word di komputer berbeda, atau gambar yang sama tapi disimpan ulang lewat aplikasi editing), SHA murni mungkin tidak menangkap duplikasi yang secara visual sebenarnya identik. Pertimbangkan strategi tambahan yang dibahas di bagian akhir artikel ini untuk kasus seperti ini.

Kenapa SHA Adalah Solusi yang Tepat untuk Double Upload

SHA menjawab masalah duplikasi tepat di level yang seharusnya — pada konten asli file, bukan pada atribut-atribut di sekitarnya yang mudah berubah atau dimanipulasi tanpa sengaja.

PendekatanKelemahan
FilenameNama file bisa berbeda meski isinya identik
File sizeUkuran bisa sama persis tapi konten sebenarnya berbeda total
Time windowBersifat heuristik, tidak ada kepastian sama sekali
UI lock / debounceMudah dilewati dengan refresh halaman atau tab baru
SHABerdasarkan konten asli file, deterministik dan pasti

Pendekatan berbasis filename gagal karena dua file dengan konten persis sama bisa saja diberi nama yang berbeda total oleh user yang berbeda — misalnya laporan_final.pdf dan laporan_final_v2_REVISI.pdf yang ternyata isinya identik. Pendekatan berbasis file size juga tidak cukup, karena dua file yang ukurannya sama persis dalam byte bisa saja memiliki konten yang sepenuhnya berbeda — kemungkinan ini kecil tapi nyata, terutama untuk file dengan struktur seragam.

Pendekatan content-based deduplication seperti ini bukan hal baru atau eksperimental — pendekatan ini sudah dipakai secara luas di sistem-sistem besar yang sudah teruji dalam skala produksi, seperti Git yang menggunakan hash konten untuk mendeteksi perubahan file, layer Docker image yang dideduplikasi berdasarkan hash kontennya, dan content-addressable storage (CAS) yang menjadikan hash sebagai identifier utama dari data yang disimpan.


Contoh Flow Dedup Upload

Secara garis besar, implementasi deduplikasi berbasis SHA mengikuti enam langkah yang relatif sederhana untuk dipahami dan diimplementasikan.

sequenceDiagram
    participant User
    participant Backend
    participant DB
    participant Storage

    User->>Backend: Upload file
    Backend->>Backend: Hitung SHA-256 dari file stream
    Backend->>DB: Cek apakah SHA sudah ada?
    alt SHA sudah ada
        DB-->>Backend: Ditemukan
        Backend-->>User: Tolak / reuse file lama
    else SHA belum ada
        DB-->>Backend: Tidak ditemukan
        Backend->>Storage: Simpan file baru
        Backend->>DB: Simpan SHA ke database
        Backend-->>User: Upload berhasil
    end

User meng-upload file seperti biasa tanpa perlu tahu ada proses tambahan di belakangnya. Backend kemudian membaca file stream yang masuk dan menghitung SHA-nya, biasanya menggunakan SHA-256 sebagai pilihan standar. Hasil hash ini lalu dicek ke database untuk melihat apakah SHA yang sama sudah pernah tercatat sebelumnya. Jika ditemukan, sistem punya dua pilihan: menolak upload sepenuhnya, atau melakukan reuse terhadap file lama yang sudah tersimpan tanpa menyimpan duplikat baru. Jika tidak ditemukan, file baru disimpan ke storage seperti biasa, dan SHA-nya dicatat ke database sebagai referensi untuk pengecekan berikutnya di masa depan.


Contoh Implementasi di Golang

Hitung SHA-256 dari File secara Streaming

Menghitung hash dengan cara streaming, bukan membaca seluruh file ke memory sekaligus, adalah praktik yang penting terutama untuk file berukuran besar.

func ComputeSHA256(r io.Reader) (string, error) {
    hash := sha256.New()

    if _, err := io.Copy(hash, r); err != nil {
        return "", err
    }

    sum := hash.Sum(nil)
    return hex.EncodeToString(sum), nil
}

Fungsi io.Copy di sini bekerja dengan membaca data dari io.Reader dalam potongan kecil secara berurutan, lalu langsung memasukkannya ke dalam state hash yang sedang dibangun — tanpa pernah menyimpan seluruh konten file dalam memory pada satu waktu. Pendekatan ini membuat penghitungan hash tetap efisien meski file yang diproses berukuran ratusan megabyte atau lebih.

Contoh Penggunaan di Handler Upload

file, _, err := r.FormFile("file")
if err != nil {
    return err
}
defer file.Close()

sha, err := ComputeSHA256(file)
if err != nil {
    return err
}

// ANTI-PATTERN: langsung simpan file tanpa cek duplikat
// saveFile(file)
// -- file yang sama bisa tersimpan berkali-kali tanpa terdeteksi

// BENAR: cek SHA ke database sebelum menyimpan
exists := repo.ExistsBySHA(sha)
if exists {
    return errors.New("file already uploaded")
}

// reset reader ke posisi awal karena sudah terbaca habis saat hashing
file.Seek(0, io.SeekStart)
saveFile(file)
repo.SaveSHA(sha)

Detail teknis yang penting untuk diperhatikan di sini adalah pemanggilan file.Seek(0, io.SeekStart) setelah proses hashing selesai. Karena io.Copy membaca stream dari awal hingga akhir untuk menghitung hash, posisi pointer baca file akan berada di ujung setelah hashing selesai. Tanpa me-reset posisi ini kembali ke awal, upaya menyimpan file setelahnya akan menghasilkan file kosong, karena tidak ada lagi data yang tersisa untuk dibaca dari posisi tersebut.

Untuk file yang sangat besar, hashing sambil streaming seperti contoh di atas sangat disarankan dibanding membaca seluruh file ke dalam slice byte di memory terlebih dahulu. Pendekatan streaming menjaga penggunaan memory tetap konstan, tidak peduli seberapa besar ukuran file yang diproses.

Best Practice Wajib

Beberapa praktik berikut perlu diterapkan secara konsisten agar mekanisme deduplikasi ini benar-benar robust di produksi, bukan sekadar bekerja pada skenario uji coba yang sederhana.

Hash Dihitung di Server

Jangan pernah mempercayai SHA yang dikirim dari client. Jika client diizinkan mengirim hash-nya sendiri dan server hanya memverifikasi tanpa menghitung ulang, client yang nakal bisa mengirim hash palsu untuk berbagai tujuan — baik untuk menghindari deteksi duplikat yang sebenarnya, atau sebaliknya, untuk membuat sistem salah mengira file berbeda sebagai duplikat. Hash harus selalu dihitung ulang di sisi server dari konten file yang benar-benar diterima.

Gunakan Unique Index di Database

CREATE UNIQUE INDEX uniq_file_sha ON uploaded_files (sha256);

Unique index ini memberikan dua manfaat penting sekaligus. Pertama, atomic guarantee — database akan menolak secara otomatis jika ada upaya menyimpan baris dengan SHA yang sama, tanpa perlu logic tambahan di level aplikasi untuk memastikan ini. Kedua, perlindungan dari race condition di masa depan — jika suatu saat ada dua request upload dengan file identik yang datang hampir bersamaan, constraint di level database ini akan mencegah duplikasi tersimpan, melengkapi pengecekan yang sudah dilakukan di level aplikasi.

Simpan File Size

Menyimpan ukuran file di samping SHA-nya berguna untuk validasi tambahan dan keperluan debugging. Jika suatu saat kamu menemukan dua baris dengan SHA yang sama tapi ukuran file yang tercatat berbeda, ini adalah sinyal kuat bahwa ada bug di proses penghitungan hash atau penyimpanan metadata yang perlu segera diinvestigasi.

Tentukan Scope Dedup

Pertanyaan penting yang harus dijawab di awal desain adalah pada level mana deduplikasi ini berlaku. Apakah dedup berlaku secara global di seluruh sistem, per user secara individual, atau per tenant pada sistem multi-tenant? Jawaban ini sangat bergantung pada konteks bisnis aplikasi kamu.

-- Dedup secara global: satu SHA hanya boleh ada satu baris di seluruh tabel
CREATE UNIQUE INDEX uniq_file_sha ON uploaded_files (sha256);

-- Dedup per tenant: SHA yang sama boleh ada di tenant berbeda,
-- tapi tidak boleh duplikat dalam tenant yang sama
CREATE UNIQUE INDEX uniq_tenant_sha ON uploaded_files (tenant_id, sha256);

Sistem multi-tenant misalnya, hampir selalu membutuhkan scope dedup per tenant, bukan global — karena dua tenant yang berbeda sangat mungkin meng-upload file yang isinya identik secara independen tanpa ada hubungan apapun, dan keduanya seharusnya tetap diizinkan tersimpan sebagai entitas terpisah.

Tentukan Behavior Saat Duplicate Terdeteksi

Beberapa pilihan umum yang bisa diterapkan ketika sistem mendeteksi file duplikat meliputi menolak upload secara langsung dengan pesan error yang jelas, mengembalikan referensi ke file lama yang sudah ada tanpa menyimpan file baru, melakukan attach file lama tersebut ke entity baru yang sedang dibuat tanpa user perlu tahu bahwa file fisiknya sebenarnya sama dengan yang sudah ada, atau sekadar mencatat kejadian ini di log tanpa menghalangi proses upload sama sekali — pendekatan yang kadang disebut soft dedup.

PILIHAN BEHAVIOR SAAT DUPLICATE TERDETEKSI:
  ✓ Reject upload         -- tegas, cocok untuk dokumen unik per transaksi
  ✓ Return reference lama -- hemat storage, file lama tetap dipakai
  ✓ Attach ke entity baru -- transparan ke user, tetap hemat storage
  ✓ Log saja (soft dedup) -- observability tanpa menghalangi user

Keputusan ini sepenuhnya bergantung pada kebutuhan bisnis spesifik yang sedang kamu hadapi — tidak ada jawaban yang secara universal benar untuk semua kasus penggunaan.


Kapan SHA Tidak Cukup?

Ada situasi di mana file “secara logis sama” tapi binary-nya berbeda, dan SHA murni tidak akan menangkap kesamaan ini. Contohnya meliputi gambar dengan isi visual yang sama persis tapi metadata EXIF-nya berbeda, atau dokumen PDF dengan isi yang identik tapi metadata internalnya — seperti tanggal modifikasi atau aplikasi yang dipakai untuk generate — berbeda.

flowchart TD
    A[File asli] --> B[Disimpan ulang via aplikasi editor]
    B --> C[Metadata berubah, konten visual sama]
    C --> D{SHA sama?}
    D -- Tidak --> E[SHA murni tidak mendeteksi duplikat ini]

Untuk kasus seperti ini, ada beberapa solusi lanjutan yang bisa dipertimbangkan, meski levelnya jauh lebih kompleks dibanding pendekatan SHA murni: melakukan strip metadata sebelum hashing sehingga hanya konten inti yang dihitung, melakukan canonicalization terhadap format file agar representasi byte-nya konsisten sebelum dihash, atau menggunakan perceptual hash seperti pHash khusus untuk gambar, yang dirancang untuk mendeteksi kesamaan visual meski byte mentahnya berbeda.

Solusi lanjutan seperti perceptual hash dan canonicalization adalah teknik level lanjut yang tidak selalu diperlukan. Sebelum mengimplementasikannya, pastikan kasus penggunaan kamu memang benar-benar membutuhkannya — bagi mayoritas sistem upload file pada umumnya, deduplikasi berbasis SHA murni sudah lebih dari cukup untuk menyelesaikan masalah double upload yang sebenarnya terjadi di lapangan.

Kesimpulan

File SHA pada dasarnya adalah fingerprint dari konten file itu sendiri, terlepas dari metadata apapun yang menyertainya. Deduplikasi berbasis SHA adalah solusi paling tepat untuk masalah double upload yang sifatnya non-race — yaitu ketika dua file identik di-upload pada waktu yang terpisah jauh, bukan karena konflik timing. Pendekatan ini terbukti robust terhadap perbedaan waktu, user, maupun device yang dipakai untuk meng-upload, dan yang tidak kalah penting, relatif mudah diimplementasikan serta scalable untuk volume data yang besar.

Jika kamu pernah mengalami kasus upload ulang dengan file yang persis sama, maka deduplikasi berbasis SHA bukan hanya solusi yang secara teknis benar, tapi juga solusi yang sudah matang dan teruji luas di berbagai sistem produksi skala besar.


Ringkasan

  • Logical duplicate berbeda dari race condition — masalahnya bukan timing, melainkan konten file yang sama di-upload pada waktu yang terpisah jauh.
  • SHA adalah fingerprint dari konten file, dihitung dari raw byte content, tidak terpengaruh nama file, waktu upload, atau siapa yang meng-upload.
  • File identik selalu menghasilkan SHA identik, tapi perbedaan satu byte saja — termasuk metadata tersembunyi — bisa mengubah SHA secara total.
  • SHA lebih reliable dibanding filename, file size, time window, atau UI lock, karena bekerja langsung pada konten asli file.
  • Flow dedup: hitung SHA saat upload, cek ke database, tolak atau reuse jika sudah ada, simpan jika belum ada.
  • Hash harus dihitung di server, bukan dipercayakan dari client, untuk mencegah manipulasi.
  • Unique index pada kolom SHA memberi atomic guarantee dan perlindungan tambahan dari race condition di masa depan.
  • Scope dedup (global, per user, atau per tenant) harus ditentukan sejak awal, karena memengaruhi struktur unique constraint di database.
  • SHA murni punya keterbatasan untuk file yang “secara visual sama” tapi metadatanya berbeda — solusi lanjutan seperti perceptual hash hanya diperlukan untuk kasus khusus.

Portofolio