cfzip: mampat dan ekstrak ZIP dalam CFML
Panduan lengkap cfzip untuk Lucee: mampat, senarai, ekstrak dan padam entri arkib ZIP dalam CFML, termasuk keselamatan zip slip dan muat turun arkib ZIP.

Kandungan
Arkib ZIP ialah cara paling mudah untuk mengumpulkan banyak fail menjadi satu, sama ada untuk sandaran, penghantaran atau import data. Dalam CFML, kerja ini dilakukan oleh tag cfzip, dan ia tersedia secara teras dalam Lucee tanpa sebarang extension tambahan. Artikel ini menunjukkan empat tindakan utama cfzip, iaitu zip, unzip, list dan delete, cara menghantar arkib kepada pelayar, serta langkah keselamatan yang wajib diambil sebelum mengekstrak arkib yang dimuat naik pengguna.
Apa itu cfzip
cfzip ialah tag CFML yang memanipulasi arkib ZIP dan JAR. Ia menyokong enam tindakan: zip untuk mencipta atau mengemas kini arkib, unzip untuk mengekstrak, list untuk menyenaraikan kandungan, read dan readbinary untuk membaca satu entri sebagai teks atau binari, dan delete untuk membuang entri daripada arkib. Artikel ini memfokuskan empat tindakan yang paling kerap digunakan dalam aplikasi seharian.
Berbeza dengan cffile, yang membaca dan menulis fail biasa, cfzip memahami struktur dalaman arkib. Ia boleh menambah entri baharu kepada arkib sedia ada tanpa menulis semula keseluruhannya, menapis fail mengikut corak wildcard, dan memulangkan senarai entri sebagai objek query yang boleh digelungkan seperti hasil cfquery. Kalau anda baru berkenalan dengan perbezaan sintaks tag lawan skrip, rujuk tag vs script syntax.
Mampatkan fail dengan action="zip"
Tindakan zip mengambil kandungan sebuah folder (source) dan menulisnya ke dalam satu fail arkib (file). Parameter recurse menentukan sama ada subfolder turut disertakan, filter mengehadkan fail yang masuk mengikut corak, dan overwrite membenarkan arkib lama ditimpa. Contoh ringkas dalam cfscript:
if (!directoryExists("/tmp/uji-zip")) directoryCreate("/tmp/uji-zip", true);
fileWrite("/tmp/uji-zip/a.txt", "isi A");
fileWrite("/tmp/uji-zip/b.txt", "isi B");
// 1) Mampat: source ialah folder, file ialah arkib keluaran.
cfzip(action="zip", file="/tmp/uji-zip/data.zip", source="/tmp/uji-zip", recurse=false, filter="*.txt", overwrite=true);
// 2) Senarai kandungan arkib.
cfzip(action="list", file="/tmp/uji-zip/data.zip", name="senarai");
for (r in senarai) {
writeOutput(r.name & " (" & r.size & " bait)" & chr(10));
}
// 3) Ekstrak ke folder lain.
if (!directoryExists("/tmp/uji-zip/keluar")) directoryCreate("/tmp/uji-zip/keluar", true);
cfzip(action="unzip", file="/tmp/uji-zip/data.zip", destination="/tmp/uji-zip/keluar", overwrite=true);
// 4) Padam satu entri daripada arkib.
cfzip(action="delete", file="/tmp/uji-zip/data.zip", entryPath="a.txt");
filter menerima corak wildcard yang dipisahkan oleh paip, contohnya "*.txt|*.csv". Sejak Lucee 5 ia juga menerima fungsi atau closure yang menerima laluan entri dan memulangkan boolean, jadi penapisan boleh jadi lebih halus daripada sekadar padanan nama. Parameter prefix menambah nama subdirektori di hadapan setiap entri, berguna apabila anda mahu arkib menyimpan struktur folder tertentu walaupun fail sumbernya berselerak.
Sintaks tag memberikan hasil yang sama, dan kadang kala lebih mudah dibaca dalam templat .cfm:
<cfset dirSumber = "/tmp/uji-zip-tag">
<cfif not directoryExists(dirSumber)>
<cfdirectory action="create" directory="#dirSumber#">
</cfif>
<cffile action="write" file="#dirSumber#/satu.txt" output="kandungan satu">
<cffile action="write" file="#dirSumber#/dua.txt" output="kandungan dua">
<cfzip action="zip" file="#dirSumber#/hasil.zip" source="#dirSumber#" recurse="false" filter="*.txt" overwrite="true">
<cfset dirKeluar = "#dirSumber#/keluar">
<cfif not directoryExists(dirKeluar)>
<cfdirectory action="create" directory="#dirKeluar#">
</cfif>
<cfzip action="unzip" file="#dirSumber#/hasil.zip" destination="#dirKeluar#" overwrite="true">
<cfoutput>zip_wujud=#fileExists("#dirSumber#/hasil.zip")#; ekstrak_satu=#fileExists("#dirKeluar#/satu.txt")#</cfoutput>
Menyenaraikan kandungan dengan action="list"
action="list" memulangkan query yang setiap barisnya mewakili satu entri dalam arkib. Parameter name menentukan nama pemboleh ubah query itu. Lajur yang disediakan lebih banyak daripada yang dijangkakan: name, size, type, datelastmodified, directory, crc, compressedsize, comment, encrypted, encryptionalgorithm dan compressionmethod. Ini membolehkan anda bukan sahaja memaparkan senarai fail, malah menyemak sama ada entri disulitkan atau membandingkan saiz asal dengan saiz termampat sebelum memutuskan untuk mengekstrak.
Sifat query ini penting kerana ia berpadanan dengan cara anda memproses hasil cfquery. Anda boleh menggelungkannya dengan cfloop query, mengisihnya, atau menukarnya kepada struktur. Dalam contoh cfscript di atas, r.size memulangkan saiz asal fail dalam bait, manakala r.compressedsize memulangkan saiz selepas mampatan.
Ekstrak dengan action="unzip"
action="unzip" mengambil file arkib dan menulis kandungannya ke destination. Folder destinasi mesti wujud terlebih dahulu, jadi panggil directoryCreate sebelum cfzip seperti dalam contoh. Parameter overwrite="true" membenarkan fail sedia ada ditimpa; tanpanya, fail yang sudah ada akan menyebabkan ralat atau ditinggalkan bergantung kepada pelaksanaan.
Satu kelebihan unzip ialah ia menyokong filter, jadi anda boleh mengekstrak hanya subset fail. Misalnya, mengekstrak hanya fail .csv daripada arkib yang mengandungi pelbagai jenis dokumen. entryPath pula membolehkan anda mengekstrak satu entri tertentu sahaja tanpa menyentuh yang lain.
Padam entri dengan action="delete"
action="delete" membuang entri daripada arkib sedia ada menggunakan entryPath untuk menamakan entri sasaran, atau filter untuk membuang sekumpulan entri sekaligus. Ini berguna untuk mengemas arkib lama, contohnya membuang log nyahpepijat sebelum menghantar sandaran kepada pelanggan. Arkib diubah di tempatnya; tiada fail perantaraan perlu dicipta.
Jadual rujukan tindakan cfzip
| Tindakan | Parameter penting | Keterangan |
|---|---|---|
zip | file, source, recurse, filter, overwrite, prefix | Cipta atau kemas kini arkib ZIP/JAR daripada folder sumber |
unzip | file, destination, filter, entryPath, overwrite | Ekstrak seluruh atau sebahagian kandungan arkib |
list | file, name, filter, entryPath | Pulangkan query entri dengan lajur name, size, crc dan banyak lagi |
delete | file, entryPath, filter | Buang satu atau lebih entri daripada arkib |
read / readbinary | file, entryPath, charset, variable | Baca satu entri sebagai teks atau binari tanpa mengekstrak ke cakera |
Semua tindakan berkongsi parameter password untuk arkib yang disulitkan, dan filter yang menerima corak wildcard ataupun closure.
Menghantar ZIP kepada pelayar
Untuk membolehkan pengguna memuat turun arkib, gabungkan cfzip dengan cfcontent. Dua perkara perlu diberi perhatian: jenis kandungan mesti application/zip supaya pelayar tidak cuba memaparkannya sebagai teks, dan selepas cfcontent dipanggil anda tidak boleh menulis sebarang output lain kerana ia akan merosakkan aliran binari.
dirSementara = "/tmp/uji-zip-muat";
if (!directoryExists(dirSementara)) directoryCreate(dirSementara, true);
fileWrite(dirSementara & "/laporan.csv", "nama,jumlah" & chr(10) & "Ali,120");
cfzip(action="zip", file=dirSementara & "/laporan.zip", source=dirSementara, recurse=false, filter="*.csv", overwrite=true);
cfheader(name="Content-Type", value="application/zip");
cfheader(name="Content-Disposition", value='attachment; filename="laporan.zip"');
cfcontent(file=dirSementara & "/laporan.zip", type="application/zip", deleteFile="yes");
deleteFile="yes" pada cfcontent memadam arkib selepas dihantar, jadi fail sementara tidak menimbun pada pelayan. Ini corak biasa untuk eksport data: bina arkib, hantar, kemudian bersihkan. Kalau anda membina eksport yang lebih besar, pertimbangkan untuk menggabungkannya dengan cache untuk menjana semula arkib hanya apabila data berubah, seperti yang dibincangkan dalam caching untuk prestasi.
Keselamatan: zip slip dan had saiz
Bahaya terbesar dalam memproses arkib ZIP ialah serangan zip slip. Arkib yang dimuat naik pengguna boleh mengandungi entri bernama ../keluar.txt, dan apabila diekstrak, fail itu ditulis di luar folder destinasi, berpotensi menimpa fail sistem atau kod aplikasi. Lucee tidak menyekat ini secara automatik, jadi semakan mesti dilakukan sendiri sebelum memanggil unzip.
// Arkib yang dimuat naik pengguna; semak sebelum ekstrak.
failZip = "/tmp/uji-zip-slip/muatnaik.zip";
sasaran = "/tmp/uji-zip-slip/ekstrak";
hadSaiz = 10 * 1024 * 1024; // 10 MB
if (!directoryExists("/tmp/uji-zip-slip")) directoryCreate("/tmp/uji-zip-slip", true);
// Contoh arkib dengan entri traversal ../keluar.txt untuk demo.
fos = createObject("java", "java.io.FileOutputStream").init(failZip);
zos = createObject("java", "java.util.zip.ZipOutputStream").init(fos);
e1 = createObject("java", "java.util.zip.ZipEntry").init("../keluar.txt");
zos.putNextEntry(e1);
zos.write("keluar".getBytes());
zos.closeEntry();
zos.close();
fos.close();
if (!directoryExists(sasaran)) directoryCreate(sasaran, true);
// 1) Had saiz arkib.
if (getFileInfo(failZip).size > hadSaiz) {
writeOutput("TOLAK: arkib melebihi had saiz");
abort;
}
// 2) Semak nama entri sebelum unzip untuk cegah zip slip.
cfzip(action="list", file=failZip, name="entri");
for (e in entri) {
n = replace(e.name, "\", "/", "all");
if (left(n, 3) == "../" || n == ".." || find("/../", n) > 0) {
writeOutput("TOLAK: entri mencurigakan " & e.name);
abort;
}
}
cfzip(action="unzip", file=failZip, destination=sasaran, overwrite=true);
writeOutput("LULUS: diekstrak");
Semakan ini menormalkan pemisah backslash kepada slash, kemudian menolak sebarang nama yang bermula dengan ../, sama dengan .., atau mengandungi /../ di tengah. Had saiz menghalang serangan zip bomb, iaitu arkib kecil yang mengembang menjadi data gergasi apabila diekstrak. Kedua-dua semakan ini mesti berlaku sebelum unzip. Untuk panduan lebih luas tentang mengendalikan muat naik fail dengan selamat, baca muat naik fail selamat, dan untuk konteks keselamatan aplikasi web secara umum, lihat keselamatan web XSS dan CSRF.
cfzip lawan gzip untuk sandaran
Untuk sandaran, ZIP dan gzip sering disebut bersama tetapi ia menyelesaikan masalah berbeza. ZIP ialah format arkib yang menyimpan banyak fail dalam satu bekas dengan direktori dalaman; gzip hanya memampatkan satu aliran data tanpa struktur fail. Lucee menyediakan fungsi compress dan extract untuk gzip, yang sesuai untuk memampatkan satu dokumen besar atau respons HTTP, manakala cfzip lebih sesuai untuk mengumpulkan folder projek atau set fail pangkalan data.
Praktikalnya, kalau anda mengeksport satu dump SQL besar, gzip memberi fail lebih kecil. Kalau anda mahu menghantar keseluruhan folder dengan struktur subdirektori, ZIP ialah pilihan yang betul kerana penerima boleh membuka dan menyemak senarai kandungan sebelum mengekstrak. Memandangkan arkib ZIP boleh disenaraikan dengan action="list" tanpa mengekstrak, ia juga lebih mudah diaudit.
Soalan Lazim
Adakah cfzip memerlukan extension tambahan dalam Lucee?
Tidak. cfzip ialah tag teras Lucee dan berfungsi tanpa extension. Empat tindakan zip, unzip, list dan delete semuanya diuji dalam Lucee 7.1 tanpa pemasangan tambahan.
Bolehkah cfzip mencipta arkib yang disulitkan?
Ya. Parameter password pada zip mencipta arkib terenkripsi, dan parameter sama pada unzip, list dan read digunakan untuk membuka arkib yang dilindungi kata laluan.
Bagaimana hendak mengekstrak hanya satu fail daripada arkib besar?
Guna action="unzip" dengan parameter entryPath yang menamakan entri sasaran. Ini lebih cepat daripada mengekstrak seluruh arkib hanya untuk mengambil satu fail.
Apakah perbezaan cfzip dengan fungsi compress/extract?
cfzip mengendalikan arkib ZIP dan JAR dengan struktur berbilang fail dan direktori. compress dan extract mengendalikan gzip, yang memampatkan satu aliran data sahaja tanpa senarai kandungan.
Adakah cfzip selamat untuk arkib yang dimuat naik pengguna?
Tidak secara automatik. Anda mesti menyemak nama entri untuk mengesan traversal ../ dan menetapkan had saiz sebelum memanggil unzip, seperti yang ditunjukkan di bahagian keselamatan di atas.