Python zipfile.Path: อ่านไฟล์ใน ZIP โดยไม่แตกทั้งก้อน
บางงานต้องอ่าน manifest ใบเดียวจาก ZIP ไม่ได้ต้องแตกทุกไฟล์ลง disk zipfile.Path ให้ API แบบ path-like สำหรับเดิน directory และอ่านสมาชิกตรง ๆ ลดไฟล์ชั่วคราวและทำให้ขอบเขตงานชัดขึ้นครับ
WHY: เปิดลิ้นชักเดียวแทนเทตู้ทั้งใบ
การ extract ทั้ง archive เหมือนเทเอกสารทุกแผ่นลงพื้นเพื่อหาใบเดียว นอกจากช้า ยังเพิ่มพื้นที่ที่ path แปลก ๆ จะเขียนไฟล์ zipfile.Path ช่วยอ่านสมาชิกเฉพาะจุดจาก archive แต่ผู้ใช้ยังต้องตรวจชื่อและขนาด เพราะ class นี้ไม่ได้ sanitize filename ให้เอง
import io
import unittest
import zipfile
def read_report(payload: bytes) -> str:
with zipfile.ZipFile(io.BytesIO(payload)) as archive:
target = zipfile.Path(archive, at="reports/today.txt")
if not target.is_file():
raise FileNotFoundError("reports/today.txt")
return target.read_text(encoding="utf-8")
class ZipPathTests(unittest.TestCase):
def test_reads_one_member(self) -> None:
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w") as archive:
archive.writestr("reports/today.txt", "ready")
archive.writestr("large/unused.txt", "x" * 1000)
self.assertEqual(read_report(buffer.getvalue()), "ready")
def test_missing_member(self) -> None:
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w"):
pass
with self.assertRaises(FileNotFoundError):
read_report(buffer.getvalue())
if __name__ == "__main__":
unittest.main()
HOW: อ่านตรง ๆ ไม่ได้แปลว่าปลอดภัยทุกด้าน
ก่อนรับ archive ที่ไม่เชื่อถือ ให้จำกัดขนาดไฟล์บีบอัด จำนวนสมาชิก ขนาดรวมหลังคลาย และ compression ratio เพื่อกัน zip bomb หากชื่อสมาชิกมาจาก input ให้ reject absolute path, .. และ path ที่ไม่ตรง allowlist แม้ไม่ได้ extract เพราะชื่อเหล่านี้อาจถูกนำไปใช้ในขั้นตอนต่อไป
Path.open() และ read_text() จะคลายข้อมูลสมาชิกที่เลือกอยู่ดี จึงต้องเช็ก ZipInfo.file_size ก่อนอ่าน อย่าโหลดสมาชิกหลายกิกะไบต์เข้า memory เพียงเพราะไม่ได้เขียนลง disk และปิด ZipFile ด้วย context manager เสมอ
Recap
- ใช้
zipfile.Pathเมื่ออ่านสมาชิกเฉพาะจุด - ตรวจว่า target เป็นไฟล์และมีอยู่จริง
- จำกัดจำนวนสมาชิก ขนาดรวม และ compression ratio
- validate ชื่อสมาชิก;
zipfile.Pathไม่ sanitize ให้ - เช็ก
ZipInfo.file_sizeก่อนอ่านไฟล์ใหญ่