Solución:
Lo que tengo que hacer es darle al cliente la capacidad de descargarlos todos en un ZIP (o similar), pero no puedo hacerlo en la memoria ni en el almacenamiento del servidor, ya que esta es una configuración sin servidor.
Cuando dice servidor menos, si lo que quiere decir es que le gustaría usar Lambda para crear un archivo zip en S3, se encontrará con algunas limitaciones:
- Lambda tiene un límite de tiempo sobre cuánto tiempo se pueden ejecutar las funciones.
- Como Lambda tiene un límite de memoria, es posible que tenga problemas para ensamblar un archivo grande en una función Lambda
- Lambda tiene un límite en el tamaño máximo de una llamada PUT.
Por las razones anteriores, creo que el siguiente enfoque es mejor:
- Cuando los archivos sean necesarios, cree una instancia EC2 sobre la marcha. Quizás su función lambda pueda desencadenar la creación de la instancia EC2.
- copie todos los archivos en el almacén de instancias de la máquina o incluso en EFS.
- Comprime los archivos en un zip
- Cargue el archivo de nuevo en S3 o sirva el archivo directamente
- Elimine la instancia EC2.
En mi opinión, esto simplificaría enormemente el código que tiene que escribir, ya que cualquier código que se ejecute en su computadora portátil / escritorio probablemente funcionará en la instancia EC2. Tampoco tendrá las limitaciones de tiempo / espacio de lambda.
Como puede deshacerse de la instancia EC2 una vez que el archivo zip se carga de nuevo en S3, no tiene que preocuparse por el costo de que el servidor esté siempre en funcionamiento; simplemente encienda uno cuando lo necesite y elimínelo cuando lo necesite. Listo.
El código para comprimir varios archivos en una carpeta podría ser tan simple como:
De: https://code.tutsplus.com/tutorials/compressing-and-extracting-files-in-python–cms-26816
import os
import zipfile
fantasy_zip = zipfile.ZipFile('C:\Stories\Fantasy\archive.zip', 'w')
for folder, subfolders, files in os.walk('C:\Stories\Fantasy'):
for file in files:
if file.endswith('.pdf'):
fantasy_zip.write(os.path.join(folder, file), os.path.relpath(os.path.join(folder,file), 'C:\Stories\Fantasy'), compress_type = zipfile.ZIP_DEFLATED)
fantasy_zip.close()
import io
class S3File(io.RawIOBase):
def __init__(self, s3_object):
self.s3_object = s3_object
self.position = 0
def __repr__(self):
return "<%s s3_object=%r>" % (type(self).__name__, self.s3_object)
@property
def size(self):
return self.s3_object.content_length
def tell(self):
return self.position
def seek(self, offset, whence=io.SEEK_SET):
if whence == io.SEEK_SET:
self.position = offset
elif whence == io.SEEK_CUR:
self.position += offset
elif whence == io.SEEK_END:
self.position = self.size + offset
else:
raise ValueError("invalid whence (%r, should be %d, %d, %d)" % (
whence, io.SEEK_SET, io.SEEK_CUR, io.SEEK_END
))
return self.position
def seekable(self):
return True
def read(self, size=-1):
if size == -1:
# Read to the end of the file
range_header = "bytes=%d-" % self.position
self.seek(offset=0, whence=io.SEEK_END)
else:
new_position = self.position + size
# If we're going to read beyond the end of the object, return
# the entire object.
if new_position >= self.size:
return self.read()
range_header = "bytes=%d-%d" % (self.position, new_position - 1)
self.seek(offset=size, whence=io.SEEK_CUR)
return self.s3_object.get(Range=range_header)["Body"].read()
def readable(self):
return True
if __name__ == "__main__":
import zipfile
import boto3
s3 = boto3.resource("s3")
s3_object = s3.Object(bucket_name="bukkit", key="bagit.zip")
s3_file = S3File(s3_object)
with zipfile.ZipFile(s3_file) as zf:
print(zf.namelist())
Tu pregunta es extremadamente compleja, porque resolverla puede enviarte por muchos agujeros de conejo.
Creo que Rahul Iyer está en el camino correcto, porque en mi humilde opinión sería más fácil iniciar una nueva instancia EC2 y comprimir los archivos en esta instancia y moverlos de nuevo a un depósito S3 que solo sirve archivos zip al cliente.
Si sus archivos fueran más pequeños, podría usar AWS Cloudfront para manejar la compresión cuando un cliente solicita un archivo.
Durante mi investigación, noté que otros lenguajes, como .Net y Java, tenían API que manejan la transmisión en archivos zip. También miré zipstream, que se ha bifurcado varias veces. No está claro cómo se puede usar zipstream para transmitir un archivo para comprimirlo.
El siguiente código fragmentará un archivo y escribirá los mandriles en un archivo zip. Los archivos de entrada estaban cerca de 12 Gbs y el archivo de salida era de casi 5 Gbs.
Durante las pruebas, no vi ningún problema importante con el uso de la memoria o grandes picos.
Agregué un código pseudo S3 a una de las publicaciones a continuación. Creo que se requieren más pruebas para comprender cómo funciona este código en archivos en S3.
from io import RawIOBase
from zipfile import ZipFile
from zipfile import ZipInfo
from zipfile import ZIP_DEFLATED
# This module is needed for ZIP_DEFLATED
import zlib
class UnseekableStream(RawIOBase):
def __init__(self):
self._buffer = b''
def writable(self):
return True
def write(self, b):
if self.closed:
raise ValueError('The stream was closed!')
self._buffer += b
return len(b)
def get(self):
chunk = self._buffer
self._buffer = b''
return chunk
def zipfile_generator(path, stream):
with ZipFile(stream, mode="w") as zip_archive:
z_info = ZipInfo.from_file(path)
z_info.compress_type = ZIP_DEFLATED
with open(path, 'rb') as entry, zip_archive.open(z_info, mode="w") as dest:
for chunk in iter(lambda: entry.read(16384), b''): # 16384 is the maximum size of an SSL/TLS buffer.
dest.write(chunk)
yield stream.get()
yield stream.get()
stream = UnseekableStream()
# each on the input files was 4gb
files = ['input.txt', 'input2.txt', 'input3.txt']
with open("test.zip", "wb") as f:
for item in files:
for i in zipfile_generator(item, stream):
f.write(i)
f.flush()
stream.close()
f.close()
pseudocódigo s3 / código postal
Este código es estrictamente hipotético, porque necesita ser probado.
from io import RawIOBase
from zipfile import ZipFile
from zipfile import ZipInfo
from zipfile import ZIP_DEFLATED
import os
import boto3
# This module is needed for ZIP_DEFLATED
import zlib
session = boto3.Session(
aws_access_key_id='XXXXXXXXXXXXXXXXXXXXXXX',
aws_secret_access_key='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
region_name="XXXXXXXXXX")
s3 = session.resource('s3')
bucket_name = s3.Bucket('bucket name')
class UnseekableStream(RawIOBase):
def __init__(self):
self._buffer = b''
def writable(self):
return True
def write(self, b):
if self.closed:
raise ValueError('The stream was closed!')
self._buffer += b
return len(b)
def get(self):
chunk = self._buffer
self._buffer = b''
return chunk
def zipfile_generator(path, stream):
with ZipFile(stream, mode="w") as zip_archive:
z_info = ZipInfo.from_file(path)
z_info.compress_type = ZIP_DEFLATED
with open(path, 'rb') as entry, zip_archive.open(z_info, mode="w") as dest:
for chunk in iter(lambda: entry.read(16384), b''):
dest.write(chunk)
yield stream.get()
yield stream.get()
stream = UnseekableStream()
with open("test.zip", "wb") as f:
for file in bucket_name.objects.all():
obj = s3.get_object(Bucket=bucket_name, Key=file.key)
for i in zipfile_generator(obj.get(), stream):
f.write(i)
f.flush()
stream.close()
f.close()