Saltar al contenido

Controlador de Archivos y Terminando un simple Servicio de Almacenamiento de Archivos Usando VueJS, Flask, y RethinkDB

Los controladores de archivos se utilizarán para trabajar tanto con archivos como con carpetas, por lo que habrá algo más de lógica aquí que en nuestro controlador anterior. Empezamos creando un boilerplate para nuestro controlador en el módulo /api/controllers/files.py.

1234567891011121314151617181920212223242526272829importar osfrom solicitud de importación de frascos, gfrom flask_restful import reqparse, abortar, Resourcefrom werkzeug import secure_filenamefrom api.models import FileBASE_DIR = os.path.abspath( os.path.dirname(os.path. dirname(os.path.dirname(__file__)))classCreateList(Resource):defget(self, user_id):passdefpost(self, user_id):passclassViewEditDelete(Resource):defget(self, user_id, file_id):passdefput(self, user_id, file_id):passdefdelete(self, user_id, file_id):pass

pitón

Controlador de Archivos y Terminando un simple Servicio de Almacenamiento de Archivos Usando VueJS, Flask, y RethinkDB
Controlador de Archivos y Terminando un simple Servicio de Almacenamiento de Archivos Usando VueJS, Flask, y RethinkDB

La clase CreateList, como su nombre indica, se utilizará para crear y listar archivos para un usuario conectado. La clase ViewEditDelete, también como su nombre lo indica, se utilizará para ver, editar y borrar archivos. Los métodos que estamos usando en las clases se corresponden con las acciones HTTP apropiadas.

Decoradores

Comenzaremos la implementación creando un grupo de decoradores que usaremos en los métodos de nuestras clases de recursos. Querrás separar esto en el módulo /api/utils/decorators.py.

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455de jose import jwtfrom jose.exceptions import JWTErrorfrom functools import wrapsfrom flask import current_app, request, gfrom flask_restful import abortfrom api. models import User, Filedeflogin_required(f):$0027$0027$0027 Este decorador comprueba el encabezado para asegurarse de que un token válido está establecido $0027$0027$0027@wraps(f)deffunc(*args,**kwargs):try:if$0027authorization$0027notin request. headers: abort(404, message="Necesitas estar conectado para acceder a este recurso") token = request.headers.get($0027authorization$0027) payload = jwt.decode(token, current_app.config[$0027SECRET_KEY$0027], algorithms=[$0027HS256$0027]) user_id = payload[$0027id$0027] g. user = User.find(user_id)if g.user isNone: abort(404, message="El ID de usuario es inválido")return f(*args,**kwargs)except JWTError as e: abort(400, message="Hubo un problema al tratar de analizar su testigo -----; {}". format(e.message))return funcdefvalidate_user(f):$0027$0027$0027 Esta decoración asegura que el usuario conectado es el mismo usuario con el que estamos operando $0027$0027$0027@wraps(f)deffunc(*args,**kwargs): user_id = kwargs.get($0027user_id$0027)if user_id != g. user[$0027id$0027]: abort(404, message="No tienes permiso para el recurso al que intentas acceder")return f(*args,**kwargs)return funcdefbelongs_to_user(f):$0027$0027$0027 Este decorador se asegura de que el archivo al que intentamos acceder nos pertenece realmente $0027$0027$0027@wraps(f)deffunc(*args,**kwargs): file_id = kwargs. get($0027id_de_fichero$0027) id_usuario = kwargs.get($0027id_usuario$0027)file= Archivo.find(id_de_fichero,True)ifnotfileorfile[$0027creator$0027]!= id_usuario: abort(404, message="El archivo al que está intentando acceder no fue encontrado") g.file=filereturn f(*args,**kwargs)return func

pitón

El decorador de login_required se utiliza para validar que los usuarios están realmente conectados antes de acceder a la funcionalidad del método. Utilizamos este decorador para proteger ciertos puntos finales decodificando el testigo para asegurar su validez. Obtenemos el campo de identificación almacenado en el testigo e intentamos recuperar el objeto de usuario correspondiente. Luego, también almacenamos este objeto en g.user para el acceso dentro de la definición del método.

Del mismo modo, creamos el decorador validate_user que asegura que ningún otro usuario conectado pueda acceder a los patrones de URL etiquetados con el ID de otro usuario. Esta validación se basa puramente en la información de la URL.

Por último, el decorador de belongs_to_user se asegura de que sólo el usuario que ha creado un archivo pueda acceder a él. Este decorador comprueba el campo del creador en el documento del archivo con el user_id suministrado.

Aquí están las vistas para la creación de nuevos archivos y el listado de archivos:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071classCreateList(Resource): @login_required @validate_user @marshal_with(file_array_serializer)defget(self, user_id):try:return File.filter({$0027creator$0027: user_id,$0027parent_id$0027:$00270$0027})except Excepción como e: abort(500, message="Se ha producido un error al intentar obtener sus archivos --; {}".format(e.message)) @login_required @validate_user @marshal_with(file_serializer)defpost(self, user_id):try: parser = reqparse.RequestParser() parser.add_argument($0027name$0027,type=str,help="Este debería ser el nombre de la carpeta si se crea una carpeta") parser. add_argument($0027id_padre$0027,type=str,help=$0027Este debe ser el id de la carpeta padre$0027) parser.add_argument($0027es_carpeta$0027,type=bool,help="Esto indica si está intentando crear una carpeta o no") args = parser.parse_args() name = args.get($0027nombre$0027,Ninguno) id_padre = args.get($0027id_padre$0027,Ninguno) es_carpeta = args. get($0027es_carpeta$0027,Falso) parent =None# ¿Estamos añadiendo esto a una carpeta padre? if parent_id isotNone: parent = File.find(parent_id)if parent isNone:raise Exception("Esta carpeta no existe")ifnot parent[$0027es_carpeta$0027]:raise Exception("Selecciona una carpeta válida para subir")# ¿Estamos creando una carpeta? if is_folder:if name isNone:raise Excepción("Necesitas especificar un nombre para esta carpeta")return Folder.create( name=nombre, parent=parent, is_folder=es_carpeta, creator=id_usuario )else: files = request.files[$0027file$0027]if files y is_allowed(files.filename): directorio = os.path.join(BASE_DIR,$0027upload/{}/$0027.format(user_id))ifnot os.path.isdir(_dir): os.mkdir(_dir) filename = secure_filename(files. nombre_de_fichero) a_ruta = os.path.join(_dir, nombre_de_fichero) files.save(to_path) fileuri = os.path.join($0027upload/{}/$0027.format(user_id), nombre_de_fichero) filesize = os.path. getsize(to_path)return File.create( name=nombre_de_archivo, uri=fileuri, size=tamaño_de_archivo, parent=parental, creator=id_usuario )raise Excepción("No proporcionó un archivo válido en su solicitud")excepto Excepción como e: abort(500, message="Se produjo un error al procesar su solicitud --; {}".format(e.message))

pitón

El método de listado es bastante sencillo. Filtramos la tabla para todos los archivos creados por un determinado usuario y almacenados en el directorio raíz. Devolvemos estos datos para este punto final y lanzamos una excepción si hay algún error.

Crear

Para la acción de creación, es un poco más complicado. Para esta guía, asumimos que los archivos y las carpetas se crearán con el mismo objetivo. En el caso de los archivos, tendremos que proporcionar el archivo y un ID_padre, si lo subimos a una carpeta. Para las carpetas, necesitaremos un nombre y un valor parent_id, nuevamente si estamos creando esto dentro de otra carpeta. Para las carpetas, también necesitamos enviar un campo is_folder con nuestra solicitud para especificar que estamos creando una carpeta.

Si vamos a almacenar esto dentro de una carpeta, tenemos que asegurarnos de que la carpeta existe y es una carpeta válida. También nos aseguramos de que estamos suministrando un campo de nombre si estamos creando una carpeta.

Para la creación de un archivo, lo subimos a una carpeta con un nombre específico para los diferentes usuarios como se mencionó anteriormente. En nuestro caso, estamos usando el patrón /subir/ para los diferentes directorios de archivos de los usuarios. También hemos utilizado la información del archivo para rellenar el documento que vamos a almacenar en la tabla.

Concluimos llamando a los métodos de creación de archivos y carpetas – File.create() y Folder.create() respectivamente.

Serializadores

Fíjense que hemos usado el marshal_con decorador disponible con Flask-RESTful. Este decorador se utiliza para dar formato al objeto de respuesta y para indicar los diferentes nombres de campo y tipos que devolveremos. Ver la definición de file_array_serializer y file_serializer más abajo:

123456789101112131415161718192021222324archivo_arreglos_serializador ={$0027id$0027: fields.String,$0027nombre$0027: fields.String,$0027tamaño$0027: fields.Integer,$0027uri$0027: fields.String,$0027is_folder$0027: fields. Booleano,$0027parent_id$0027: fields.String,$0027creator$0027: fields.String,$0027date_created$0027: fields.DateTime(dt_format=$0027rfc822$0027),$0027date_modified$0027: fields.DateTime(dt_format=$0027rfc822$0027),}file_serializer ={$0027id$0027: fields. Cadena,$0027nombre$0027: campos.Cadena,$0027tamaño$0027: campos.Entero,$0027uri$0027: campos.Cadena,$0027es_carpeta$0027: campos.booleano,$0027objetos$0027: campos.Anidado(file_array_serializer, default=[]),$0027id_padre$0027: campos. Cadena,$0027creador$0027: fields.String,$0027fecha_creada$0027: fields.DateTime(dt_format=$0027rfc822$0027),$0027fecha_modificada$0027: fields.DateTime(dt_format=$0027rfc822$0027),}

pitón

Esto puede añadirse en la parte superior del módulo /api/controladores/archivos.py o en un módulo separado /api/utils/serializadores.py.

La diferencia entre ambos serializadores es que el serializador de archivos incluye la matriz de objetos en la respuesta. Utilizamos el file_array_serializer para las respuestas de la lista mientras que utilizamos el file_serializer para las respuestas de los objetos.

También hemos hecho uso de una función llamada is_allowed() para ayudar a asegurarnos de que apoyamos todos los archivos que estamos subiendo. Hemos creado una lista llamada ALLOWED_EXTENSIONS para contener la lista de todas las extensiones permitidas.

12345ALLOWED_EXTENSIONS =set([$0027txt$0027,$0027pdf$0027,$0027png$0027,$0027jpg$0027,$0027jpeg$0027,$0027gif$0027])defis_allowed(filename):return$0027.$0027in filename y  filename.rsplit($0027.$0027,1)[1]in ALLOWED_EXTENSIONS

pitón

Por último, concluimos añadiendo la clase de recursos para ViewEditDelete en el módulo /api/controllers/files.py.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384classVerEditDelete(Recurso): @login_required @validate_user @belongs_to_user @marshal_with(file_serializer)defget(self, user_id, file_id):try: should_download = request.args.get($0027download$0027,False)if should_download ==$0027true$0027: parts = os.path. split(g.file[$0027uri$0027])return send_from_directory(directory=parts[0], filename=parts[1])return g.fileexcept Exception as e: abort(500, message="There was an while processing your request --> {}".format(e.message)) @login_required @validate_user @belongs_to_user @marshal_with(file_serializer)defput(self, user_id, file_id):try: update_fields ={} parser = reqparse.RequestParser() parser.add_argument($0027name$0027,type=str,help="Nuevo nombre del archivo/carpeta") parser. add_argument($0027id_padre$0027,type=str,help="Nueva carpeta padre para el archivo/carpeta") args = parser.parse_args() name = args.get($0027nombre$0027,Ninguno) id_padre = args.get($0027id_padre$0027,Ninguno)if nombre isnotNone: update_fields[$0027nombre$0027]= nombre if id_padre isnotNoneand g. file[$0027id_parental$0027]!= id_parental:if id_parental !=$00270$0027 folder_access = Folder.filter({$0027id$0027: id_parental,$0027creator$0027: id_usuario})ifnot folder_access: abort(404, message="No tienes acceso a la carpeta a la que intentas mover este objeto")if g. file[$0027is_folder$0027]: update_fields[$0027tag$0027]= g.file[$0027id$0027]if parent_id ==$00270$0027else$0027{}#{}$0027.format(folder_access[$0027tag$0027], folder[$0027last_index$0027]) Folder.move(g.file, folder_access)else: Archivo.move(g.file, folder_access) update_fields[$0027id_padre$0027]= id_padre si g.file[$0027is_folder$0027]: Carpeta.update(id_archivo, campos_actualizados)else: Archivo.update(id_fichero, campos_actualizados)return Archivo.find(id_fichero)excepto Excepción como e: abort(500, mensaje="Hubo un tiempo procesando su solicitud --[; {}".format(e.mensaje)) @login_required @validate_user @belongs_to_user defdelete(self, user_id, file_id):try: hard_delete = request.args.get($0027hard_delete$0027,False)ifnot g.file[$0027is_folder$0027]:if hard_delete ==$0027true$0027: os.remove(g.file[$0027uri$0027]) File.delete(file_id)else: Archivo.update(id_fichero,{$0027status$0027:False})else:if hard_delete ==$0027true$0027: carpetas = Carpeta.filter(lambda carpeta: carpeta[$0027tag$0027].startswith(g.file[$0027tag$0027]))para la carpeta en las carpetas: archivos = Archivo. filter({$0027parent_id$0027: carpeta[$0027id$0027],$0027is_folder$0027:False}) Archivo.delete_where({$0027parent_id$0027: carpeta[$0027id$0027],$0027is_folder$0027:False})para f en archivos: os.remove(f[$0027uri$0027])else: Archivo.update(id_fichero,{$0027status$0027:False}) Archivo.update_donde({$0027id_padre$0027: id_fichero},{$0027status$0027:False})return "El archivo se ha eliminado con éxito",204except: abort(500, message="Se ha producido un error al procesar su solicitud --[; {}".format(e.message))

pitón

Creamos un método get() que devuelve un único archivo u objeto de carpeta basado en el ID. Para las carpetas, incluye información de listado. Puedes ver cómo se hace esto si miras al decorador belongs_to_user. Para los archivos, hemos incluido un parámetro de consulta should_download para que se establezca en true si queremos descargar un archivo.

El método put() se encarga de actualizar la información de archivos y carpetas. Esto también incluye mover archivos y carpetas. El movimiento de archivos se desencadena al actualizar el campo parent_id de un archivo/carpeta. La lógica de ambos se ha cubierto en los métodos move() para los modelos de archivos y carpetas.

El método delete() también viene con un parámetro de consulta que especifica si queremos o no realizar un borrado difícil. Para el hard delete, los registros se eliminan de la base de datos y los archivos se borran del sistema de archivos. Para el borrado suave, sólo actualizamos el campo de estado del archivo a false.

Hemos creado nuevos métodos aquí llamados update_where() y delete_where() en la clase RethinkDBModel para borrar y actualizar un conjunto filtrado de la tabla:

12345678910111213@classmethoddefupdate_where(cls, predicate, fields): status = r.table(cls._table).filter(predicate).update(fields).run(conn)if status[$0027errors$0027]:raise DatabaseProcessError("Could not complete the update action")returnTrue@classmethoddefdelete_where(cls, predicate): status = r. table(cls._table).filter(predicate).delete().run(conn)if status[$0027errors$0027]:raise DatabaseProcessError("Could not complete the delete action")returnTrue

pitón