From 8b2f968c2d33a4fd62263fbdec346ddbe79cfce5 Mon Sep 17 00:00:00 2001 From: Thomas Forgione Date: Mon, 29 Jul 2024 10:21:11 +0200 Subject: [PATCH] Working on zip --- app.py | 10 ++--- tar.py => archive.py | 91 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 20 deletions(-) rename tar.py => archive.py (56%) diff --git a/app.py b/app.py index a9c8d9b..5ec0563 100755 --- a/app.py +++ b/app.py @@ -7,7 +7,7 @@ import os from os.path import join import sqlite3 import uuid -from . import db, config, scanner, calibration, tar +from . import db, config, scanner, calibration, archive app = Flask(__name__) @@ -285,14 +285,14 @@ def download_object(id: int): ] # Create archive file to send - archive = tar.TarSender() + tar = archive.TarSender() for calibration_index, (calib, acquisitions) in enumerate(acquisitions_grouped): calibration_dir = join(config.CALIBRATION_DIR, str(calib.id)) # Add calibration images for image in os.listdir(calibration_dir): - archive.add_file( + tar.add_file( f'object/{calibration_index}/calibration/{image}', join(calibration_dir, image) ) @@ -302,12 +302,12 @@ def download_object(id: int): acquisition_dir = join(config.OBJECT_DIR, str(object.id), str(acquisition.id)) for image in os.listdir(acquisition_dir): - archive.add_file( + tar.add_file( f'object/{calibration_index}/{acquisition_index}/{image}', join(acquisition_dir, image) ) - return archive.response() + return tar.response() @app.route('/static/') diff --git a/tar.py b/archive.py similarity index 56% rename from tar.py rename to archive.py index 81b7226..7ffef68 100644 --- a/tar.py +++ b/archive.py @@ -1,7 +1,12 @@ import builtins +from datetime import datetime from flask import Response import functools import os +import zlib + +# Chunks for crc 32 computation +CRC32_CHUNK_SIZE = 65_536 # 4MiB chunks CHUNK_SIZE = 4_194_304 @@ -12,20 +17,8 @@ SPACE = ord(' ') # ASCII value for zero ZERO = ord('0') -# Specification de l'entête du fichier -# Numéro Nom Début Taille Description -# 1 name 0 100 Nom du fichier -# 2 mode 100 8 Permissions -# 3 uid 108 8 Propriétaire (inutilisé si format étendu) -# 4 gid 116 8 Groupe (inutilisé si format étendu) -# 5 size 124 12 Taille du fichier en octets. -# 6 mtime 136 12 Dernière modification en temps Unix. -# 7 chksum 148 8 Somme de contrôle de l'en-tête où ce champ est considéré comme rempli d'espaces (32) -# 8 type flag 156 1 Type de fichier -# 9 linkname 157 100 Nom du fichier pointé par ce lien symbolique (Si le type indique un lien symbolique) - -def header_chunk(filename: str, filepath: str) -> bytes: +def tar_header_chunk(filename: str, filepath: str) -> bytes: # Returns the octal representation without the initial def oct(i: int) -> str: @@ -71,17 +64,22 @@ def header_chunk(filename: str, filepath: str) -> bytes: return bytes(buffer) -class TarSender: +class ArchiveSender: def __init__(self): self.files: dict[str, str] = {} def add_file(self, filename: str, filepath: str): self.files[filename] = filepath + def response(self): + raise NotImplementedError("Abstract method") + + +class TarSender(ArchiveSender): def response(self): def generate(): for name, file in self.files.items(): - yield header_chunk(name, file) + yield tar_header_chunk(name, file) bytes_sent = 0 @@ -104,3 +102,66 @@ class TarSender: mimetype='application/x-tar', headers={'Content-Disposition': 'attachment; filename="archive.tar"'} ) + + +def crc32(filename): + with open(filename, 'rb') as fh: + hash = 0 + while True: + s = fh.read(CRC32_CHUNK_SIZE) + if not s: + break + hash = zlib.crc32(s, hash) + return hash + + +def zip_local_file_header(filename: str, filepath: str, fileindex: int) -> bytes: + buffer_size = 30 + len(filename) + buffer = bytearray(buffer_size) + stat = os.stat(filepath) + + # Field 1: local file header signature (buffer[0:4]) + buffer[0:4] = b'PK\x03\x04' + + # Field 2: version needed to extract (minimum) (buffer[4:6]) + buffer[4:6] = b'\x0a' + + # Field 3: general purpose bit flag (buffer[6:8]), leave at 0 + + # Field 4: compression mode (buffer[8:10]), leave at 0 (uncompressed) + + # Field 5: file last modification time (buffer[10:14]) + mtime = datetime.fromtimestramp(stat.st_mtime) + buffer[10:12] = (mtime.second // 2) | (mtime.minute << 5) | (mtime.hour << 11) + buffer[12:14] = mtime.day | (mtime.month << 5) | ((mtime.year - 1980) << 9) + + # Field 6: crc-32 of uncompressed data (buffer[14:18]) + buffer[14:18] = crc32(filepath).to_bytes(4) + + # Field 7: compressed size (buffer[18:22]) + buffer[18:22] = stat.st_size.to_bytes(4) + + # Field 8: uncompressed size (buffer[22:26]) + buffer[22:26] = stat.st_size.to_bytes(4) + + # Field 9: filename length (buffer[26:28]) + buffer[26:29] = len(filename).to_bytes(4) + + # Field 10: extra field length (buffer[28:30]) + + # Field 11: filename (buffer[30:30+len(filename)]) + buffer[30:30+len(filename)] = filename.encode('ascii') + + return buffer + + +class ZipSender(ArchiveSender): + def response(self): + def generate(): + yield 'oops' + + return Response( + generate(), + mimetype='application/zip', + headers={'Content-Disposition': 'attachment; filename="archive.zip"'} + )