diff --git a/archive.py b/archive.py index 7ffef68..4f8e5f4 100644 --- a/archive.py +++ b/archive.py @@ -104,7 +104,7 @@ class TarSender(ArchiveSender): ) -def crc32(filename): +def crc32(filename) -> int: with open(filename, 'rb') as fh: hash = 0 while True: @@ -115,7 +115,7 @@ def crc32(filename): return hash -def zip_local_file_header(filename: str, filepath: str, fileindex: int) -> bytes: +def zip_local_file_header(filename: str, filepath: str) -> bytes: buffer_size = 30 + len(filename) buffer = bytearray(buffer_size) stat = os.stat(filepath) @@ -145,7 +145,7 @@ def zip_local_file_header(filename: str, filepath: str, fileindex: int) -> bytes buffer[22:26] = stat.st_size.to_bytes(4) # Field 9: filename length (buffer[26:28]) - buffer[26:29] = len(filename).to_bytes(4) + buffer[26:28] = len(filename).to_bytes(4) # Field 10: extra field length (buffer[28:30]) @@ -155,10 +155,73 @@ def zip_local_file_header(filename: str, filepath: str, fileindex: int) -> bytes return buffer +def zip_central_directory_file_header(filename: str, filepath: str) -> bytes: + buffer_size = 46 + len(filename) + buffer = bytearray(buffer_size) + stat = os.stat(filepath) + + # Field 1: central directory file header signature (buffer[0:4]) + buffer[0:4] = b'\x02\x01\x4b\x50' + + # Field 2: version made by (buffer[4:6]) + buffer[4:6] = b'\x0a' + + # Field 3: version needed to extract (minimum) (buffer[6:8]) + buffer[6:8] = b'\x0a' + + # Field 3: general purpose bit flag (buffer[8:10]), leave at 0 + + # Field 4: compression mode (buffer[10:12]), leave at 0 (uncompressed) + + # Field 5: file last modification time (buffer[12:16]) + mtime = datetime.fromtimestramp(stat.st_mtime) + buffer[12:14] = (mtime.second // 2) | (mtime.minute << 5) | (mtime.hour << 11) + buffer[14:16] = mtime.day | (mtime.month << 5) | ((mtime.year - 1980) << 9) + + # Field 6: crc-32 of uncompressed data (buffer[16:20]) + buffer[16:20] = crc32(filepath).to_bytes(4) + + # Field 7: compressed size (buffer[20:24]) + buffer[20:24] = stat.st_size.to_bytes(4) + + # Field 8: uncompressed size (buffer[24:28]) + buffer[24:28] = stat.st_size.to_bytes(4) + + # Field 9: filename length (buffer[28:30]) + buffer[28:30] = len(filename).to_bytes(4) + + # Field 10: extra field length (buffer[30:32]) + + # Field 11: file comment length (buffer[32:34]) + + # Field 12: disk number where file starts (buffer[34:36]) + + # Field 13: internal file attributes (buffer[36:38]) + + # Field 14: external file attributes (buffer[38:42]) + + # Field 15: relative offset of the local file header (buffer[42:46]) + + # Field 16: filename (buffer[46:46+len(filename)]) + buffer[46:46+len(filename)] = filename.encode('ascii') + + return buffer + + class ZipSender(ArchiveSender): def response(self): def generate(): - yield 'oops' + for name, file in self.files.items(): + yield zip_local_file_header(name, file) + + with open(file, 'rb') as f: + while True: + bytes = f.read(CHUNK_SIZE) + + if len(bytes) == 0: + break + + yield bytes return Response( generate(),