Salta al contingut principal

Projecte Flask + S3 #3: Del servidor local a S3

 


Aquesta és la tercera part que parlo del projecte. Si encara no estàs seguint-lo, et recomano que miris les entrades anteriors dedicades al tema. A la primera part vam configurar l’entorn i decidir les pautes. A la segona, dedicàrem el seu temps a la importància de fer una bona classe S3Client i uns tests de pytest correctes.


A l’hora de pensar en desenvolupar l’app, hem de canviar la manera de pensar. Passem d’una filosofia de backend en la que pensem únicament en Python - A una full-stack amb Flask.
Què és un full-stack? La seva traducció literal és una “pila complerta”, es refereix a quan en programació s’escriu un codi des de zero. I disposa de tres parts: 
  1. Front-end, la interfície d'usuari, és el que interactua directament des del seu dispositiu i en el nostre cas és HTML/Jinja2.
  2. Back-end, el cervell que s'encarrega de processar les peticions del Front-end, executar la lògica de l’aplicació, les autoritzacions i la connexió amb la base de dades.
  3. Base de dades, on s'emmagatzema, s'organitza i gestiona tota la informació: nosaltres ho fem amb el servei AWS S3, on es guarden objectes, aquestes bases de dades poden ser relacionals i no relacionals també.
Com en cobrim aquestes tres necessitats, diem que estem en un desenvolupament “full-stack”.
Abans, quan estàvem pensant únicament en el back-end: ja ens eren suficient:
- Que ens retornès >try/except ClientError< per evitar que el codi col·lapsés.
- L’ús de os.path.basename ens garantia una seguretat local.
- La funció upload_file tal i com ja la teníem escrita al client S3.
Tots aquests punts compleixen les seves funcions de manera Back-end però ara els hem de canviar.

Objectius/Reptes d’avui

Millora del Feedback: Com he comentat abans, canviem el try/except ClientError, per missatges amb errors explicatius. El seu objectiu és poder saber més sobre l’origen d’aquests. Per això haurem d'importar “logging”.
Gestió de regions (S3): El següent és modificar la part on vaig crear un #TODO, d’aquesta manera sol·lucionem poder crear el bucket a la regió a on ens sigui necessari. Per això, modifiquem la norma que parla de {“LocationConstraint”:region}, aquesta està definida en el nostre projecte, en una regió en particular: la us-west-1.
La línea en qüestió te la demana a totes les localitzacions, excepte per la us-east-1, què està situada a Virgínia(Estats Units d'Amèrica) i no la requereix. Llavors haurem de definir aquesta condició per a les altres.
Arquitectura escalable: En adonar-me que el programa estava creixent molt ràpid hem esmenat l’atribut (app = Flask(__name__)). I:
Hem creat una Blueprint per treure-li càrrega a l’arxiu fins ara “principal”.(__init__.py)
Una Blueprint és un fragment d'una aplicació, que es crea per poder descentralitzar. He creat un routes.py on agrupar totes les rutes del Gestor.
Hem passat a una “factory pattern”, que com el seu nom indica actua com una fàbrica, cada cop que la crides, construeix, configura (amb les blueprints, els límits de mida, etc.) i et torna una nova instància. Fa que la nostra infraestructura sigui escalable amb create_app().
    from flask import Flask
    from .routes import routes
...
    def create_app():
        app = Flask(__name__)
        app.config["MAX_CONTENT_LENGTH"] = 3 * 1024 * 1024 # 3MB
        # Registrar blueprint
        app.register_blueprint(routes)
        return app
Treballant en la capa visible

Quan parlem de l’aplicació Flask no hem de deixar de banda la part visible del projecte, la que connecta amb uns clics la part backend i la base de dades amb l’usuari. Per això també hem treballat en la carpeta /app/templates. Hem creat de moment una per la pàgina principal que és index.html i altra per upload.html. Jinja2 és el motor de les plantilles i ens permet mostrar missatges dinàmics amb % if message %. Hem creat una interfície senzilla que compleixi els requisits i que funciona.

La diferència entre “Guardar en Local” o “Pujar directe”

Al codi pensat pel backend i funcional(l'anterior), vam usar f.save(), que guarda l’arxiu de manera local. Si aquest patró seguís perpetuant-se en el temps podria fer que el nostre servidor es quedés sense memòria, és a dir, que es saturés, provocant una denegació de servei involuntaria.
Si múltiples servidors executen el codi, podria passar que si per exemple pujem l’arxiu al servidor A, però la lògica s’executa desde el Servidor B, el Servidor B no pot trobar l’arxiu. Que es repeteixin arxius o que s’oblidin dins el disc dur del servidor(creant arxius temporals, i obligant a crear altres funcions que s’encarreguin de borrar aquests arxius “orfes”).
Per tant és una mala pràctica que ens hem d’ocupar aviat.

Seguretat: Blindant la pujada

Noms perillosos
Per fer més segura la pujada d’arxius al bucket ja disposem de secure_filename, aquest s’encarrega de “sanititzar” qualsevol nom d’arxiu que entri. Reemplaça caracters estranys y problemàtics, elimina rutes que afectin canvis de directori i els converteix en ASCII bàsic.
Hem adherit a aquest nom d’arxiu un uuid4 que genera un identificador únic de manera aleatòria. Sumant-li una cadena de 32 caràcters més al nom sanititzant. Així evitem en cas de pujar dos arxius amb el mateix nom, que es sobrescriguin i evita col·lisions.

    if f and f.filename != "":
        secure_name = secure_filename(f.filename)
        if not allowed_file(secure_name):
            logging.warning("Object format not allowed")
            return render_template(
                "upload.html", message="Error: Extension not allowed"
)
else:
    unique_name = f"{uuid.uuid4().hex}_{secure_name}"
    bucket = current_app.config["DEFAULT_BUCKET"]
    s3.upload_fileobj(fileobj=f, Bucket=bucket, Key=unique_name)
    message = f"File uploaded and saved: {unique_name}"


Arxius gegants i extensions malicioses
Restringim l’accès als fitxers fent que només es puguin pujar uns amb una determinada extensió, jo he escollit txt, pdf, jpg, png i gif. I els he limitat a un màxim de tamany, uns 3MB. Tot el que estigui fora rebra o un missatge de que el format no es suportat o al haver posat un errorhandler(RequestEntityTooLarge) donarà un missatge elegant al usuari.

La solució tècnica: upload_fileobj

Com hem parlat abans sobre la mala pràctica de pujar fitxers des de la memòria, hem de tornar al cervell S3Client, que ja vam estar treballant en ell el dia passat i creem una funció nova anomenada upload_fileobj(), que aquesta en comptes de dir-li a boto3 “puja aquest arxiu des del disc”. Li diu: puja aquest flux de dades que tinc a la RAM. Això el que fa es que elimina una passa intermitja, optimitza: primer el procés i després, deixem de tenir les qüestions comentades anteriorment de males pràctiques.
Amb això, hem aconseguit que el nostre gestor d'arxius passi de ser un simple script local a una aplicació web modular i robusta. Hem implementat totes les millores d'arquitectura i seguretat necessàries per pujar arxius a S3 de manera professional.

Comentaris

Entrades populars d'aquest blog

A la vigesimoséptima, va la vencida

  Ayer sentía que estaba bien con Linux Mint pero a la vez lo veía demasiado cerrado y probé OpenSuse Leap(no me convenció) y hoy vuelvo a intentar instalar Fedora Workstation 42. Eh! Y Linux Mint lo recomiendo 100% para todo el mundo. Según yo mismo, la semana pasada no me funcionó Fedora por el tema de la BIOSLegacy. No le voy a dar más vueltas no es un portátil viejo pero por lo que sea ésta BIOS es la única manera que tiene de funcionar con Linux y un disco duro externo. En realidad no pasa nada (mientras nos quede Windows). Equisde que he estado rajando de Fedora cuando va de puta madre solo que no lo configure bien, no haría honor al nombre del blog sinó. De hecho empiezo a pensar que a lo mejor el UEFI sí funcionaría pero la lié al instalar, de momento lo voy a dejar así con MBR. Bof, empecé por la mañana y todo bien, mi error ha sido al querer instalarle los drivers de Nvidia, ha tenido un conflicto con Nouveau. A veces es mejor dejar las cosas como están. Si fuera mi único...

Projecte Flask + S3 #2: El cervell i la xarxa de seguretat

  Aquesta és la segona part del projecte. Si has parat aqui et convido a consultar la primera entrada , on vam configurar l’entorn i vam establir els requisits previs. Ara passem a la implementació del codi. Objectiu d’avui/Introducció Documentar la creació del backend . Tot el desenvolupament del codi i que faré menció el podeu trobar al repositori corresponent al meu perfil de Git , o sigui que pots anar directament allà o obrir-lo a una finestra mentre veus els comentaris que descric. No vull deixar-me a aquesta introducció la importància que ha tingut pel bon i correcte desenvolupament del codi:  La documentació oficial d’ Amazon Web Services de Boto3  i les seves guies de millors pràctiques, per exemple . La Classe S3Client i la seva importància La finalitat de crear una classe S3Client com a façana/embolcall(més conegut com a Wrapper) és que amaga tota la complexitat de boto3. Faig un parèntesi, diguem que:  Estem creant "un embolcall dins d’un altre embol...