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

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...

Droidian, cap a l'ordinador a la butxaca o no

  Al nostre dia a dia, poc ens parem a pensar en un company que ens fa la vida més fàcil , ens facilita l'accés a la informació, ens connecta amb els amics locals i d'arreu del món. Hi podem escoltar música, el podem utilitzar de GPS, hi podem editar vídeos o fins i tot dibuixar-hi (tot i que encara no es gaire còmode), podem consultar les nostres pàgines web favorites, aprendre nous idiomes... Si ens hi fixem, encara podem seguir dient més sobre els smartphones. En les seves característiques negatives podem trobar addiccions, estimulacions constants, desconnexió del món real , control sobre nosaltres i de la nostra privacitat. Potser estic fent una introducció massa llarga únicament per descriure i per posar en valor el hardware que tenen aquests petits dispositius. Si parlem d'hardware (o de maquinari) i comparem els primers models amb els dels últims 5 anys, ja estem parlant de supercomputadores . Sense anar més lluny, fa uns pocs mesos, va sortir al mercat el Macbook Ne...