publicado por Jose Nieto

Hacer un Dropbox Casero con Laravel y Vue

Introducción

En la actualidad disponemos de muchos servicios para compartir archivos por medio de la red como Dropbox, Google Drive, Nextcloud, ownCloud ademas de alternativas para nuestra red local como montar un servidor NAS o simplemente configurar en nuestro sistema operativo (sea Window, Linux o MacOS) una carpeta compartida. Todas estas soluciones son válidas para almacenar nuestros archivos y tenerlos disponibles en red para nosotros o para compartirlos; entonces por que voy a programar mi propio "Dropbox" minimalista. Bueno varias razones, servicios como Dropbox o Google Drive en su capa gratuita tienen un limite y para mis necesidades no queria pagar mensualmente una subscripción, y servicios como Nexcloud (si lo instalas en tu propia maquina) o un NAS también son soluciones muy overkill para lo que realmente quiero solucionar en mi caso personal; pero la razón principal para programar mi propio "Dropbox" llamado MyBox (Nombre muy original por cierto) es porque programar es entretenido y si tienes el conocimiento para construir tus propias herramientas que soluciones tus problemas por que no hacerlo, cierto.

El caso de uso que quiero solucionar es el siguiente: En varias ocaciones me he visto en la tarea de tomarle una foto a algo, digamos un documento en tamaño oficio porque el scanner con que viene mi impresora solo sirve para hojas tamaño carta, entonces tomo la foto y luego necesito mandarla a mi computador para editar comodamente en GIMP y luego pasarla a PDF, y hasta ahora mis opciones eran subir la foto a Drive y luego bajarla al computador (Mi internet no es muy rápido que digamos); o conectar el telefono al computador por cable y transferir el archivo (algo engorroso) o la peor y mas lenta de todas transferirlo por bluetooth. Entonces mejor me programo mi propio "Dropbox" en laravel y corro la aplicación en un puerto "http://192.168.0.20:8080" y así desde cualquier computador, tablet, o telefono conectado a la misma red poder subir y bajar archivos comodamente, claro que si se monta esta aplicación en un servidor en internet pues funcionará también y cambiar el código para que use por ejemplo S3 de AWS como almacen de archivos implicaría solo cambiar un par de líneas.

Funcionalidades

Entonces que es lo que esta pequeña aplicación va a poder realizar y que no:

  • Se pueden subir multiples archivos.
  • Se puede descargar un archivo seleccionado.
  • Se pueden crear carpetas.
  • Se pueden borrar archivos.
  • No se impletará ningún sistema de inicio de sesión y así todo el que acceda al enlace verá todos los archivos.
  • No se pueden renombrar archivos.
  • No requiere instalación de base de datos.

Creación del proyecto

Sin mas introducción empecemos con el código. Como dije antes voy a usar Laravel en su versión 5.8 para el backend, pero también voy a usar Vue.js para el frontend y como dependencia externa simplemente para darle una mejor estetica a la interfaz voy a usar google fonts https://fonts.google.com/specimen/Roboto y una libreria de iconos https://feathericons.com/.

Como primer paso creamos nuestro proyecto con el comando composer.

composer create-project --prefer-dist laravel/laravel mybox "5.8.*"

Laravel viene un package.json con varias dependecias pre instaladas, pero ahora instalamos las dependencias que faltan para compilar el javascript y el css correctamente.

npm install
npm install --save feather-icons@^4.28.0
npm install --save vue-feather-icons@^5.1.0
npm install --save-dev vue-template-compiler@^2.6.11

Creación del controlador de archivos

El siguiente paso es crear el controlador para el API de los archivos, donde vamos a consultar la lista de archivos, crear carpetas, subir archivos, etc. también vamos a crear de una vez un archivo Request para validar la subida de archivos.

php artisan make:controller FilesController --api
php artisan make:request FileRequest

Ya tenemos la estructura del api para los archivos y en este controlador importamos el request FileRequest en la función store para validar los archivos que se suban. también se importa la case Storage para manipular los archivos.

Controlador de archivos

En el request por ahora solo dejamos la función authorize para que devuelva siempre true, ya que la aplicación no va a limitar el uso de ninguna forma.

Request para Controlador de archivos

Ahora hay que publicar las funciones del controlador como urls en el archivo routes/web.php, y como se está usando la función resource hay que decirle que ignore las urls que no estamos usando.

URLs de controlador

Para probar si las urls está bien definidas se usa el siguiente comando:

php artisan route:list

Verificar urls

Configuración de la carpeta de archivos

Antes de continuar agregando la logica al controlador, vamos a crear una carpeta llamada box donde van a quedar los archivos almacenados, y para que no haya problemas con git, sobre todo al descargar el repositorio agregamos un archivo llamado .gitignore

mkdir storage/app/box
touch storage/app/box/.gitignore

también se modifica el archivo storage/app/.gitignore agregando la tercera linea para que la carpeta box/ no sea ignorada

*
!public/
!box/
!.gitignore

Dentro del archivo storage/app/box/.gitignore se coloca el siguiente código para que el contenido de esta carpeta sea ignorado excepto el mismo archivo .gitignore

*
!.gitignore

API: Consultar listado de archivos

Ya teniendo la configuración de la carpeta donde van a ir nuestros archivos al subirlos, seguimos con la logica del controlador FilesController

La función index() la vamos a usar para que nos traiga todas las carpetas y archivos de la ruta que le digamos, para eso es la variable Request $request que es desde el cual obtenemos el parámetro path que viene desde el frontend con la ruta a inspeccionar.

Controlador index

Las primeras líneas de la función muestran que se está reemplazando todas las coincidencias que encuentre de -@folder@- y -@space@- por / y respectivamente.

$path = $request->get('path');
$path = str_replace('-@folder@-', '/', $path);
$path = str_replace('-@space@-', ' ', $path);

Esto es porque desde el frontend no se puede enviar una ruta de carpetas con slashes y espacios dentro de una url, entonces para evitar problemas se formatean las rutas.

Causa problemas para que el backend entienda correctamente la url.

http://mybox.test/files?path=box/ruta/de/mi carpeta

Solución utilizada, formatear la url.

http://mybox.test/files?path=box-@folder@-ruta-@folder@-de-@folder@-mi-@space@-carpeta

Por último la función index busca los archivos y carpetas que hay en la ruta seleccionada (de forma separada) y los entrega en formato json para consumo del frontend.

API: Guardar archivos

La función store va a cumplir dos tareas; 1. crear carpetas y 2. subir archivos. y esto lo hace dependiendo del parametro type (file/folder). Si recibe archivos simplemente los guarda tal cual con el nombre con el que vienen en la carpeta que se indica también por parametro con la variable directory. Para crear carpetas simplemente recibe el parametro name y la crea en el directorio indicado con el parametro directory.

Controlador store

API: Descargar archivo

La función manipula la ruta que recibe por parametro igual que la función index y luego verifica si el archivo existe. Si existe entonces lo descarga, sino devuelve un error 404.

Controlador show

API: Eliminar archivo

Por último, para finalizar la logica del API de archivos, la función destroy valida la url y la existencia del archivo y lo elimina.

Controlador destroy

Agregar validaciones a la subida de archivos

Ahora agregamos unas cuantas reglas a la creación de carpetas y subida de archivos con el request FileRequest

  • Reglas generales
    • type: es obligatorio, y solo puede tener uno de estos valores (folder,file)
    • directory: es obligatorio, y debe preexistir en el sistema
  • Reglas para carpetas
    • name: es obligatorio, la cantidad de caracteres es entre 1 y 100 y no debe coincidir con una carpeta ya existente
  • Reglas para archivos
    • files: es obligatorio, debe ser un array de archivos (aunque sea un solo archivo)
    • files.*: cada elemento del array debe ser un archivo

Controlador destroy

Frontend

Todo el código del backend deberia estar listo y ahora es tiempo de pasar al frontend y hacer un poco de código Javascript con Vue.JS y también CSS con el prepocesador SASS.

primero vamos a eliminar algunos archivos que no necesitamos pero que Laravel ha creado; los voy a eliminar usando el comando git rm para que git detecte correctamente ese cambio.

git rm resources/js/components/ExampleComponent.vue
git rm resources/sass/_variables.scss

Luego de borrar estos archivos hay que hacer algo de limpieza en el archivo resources/js/app.js porque aquí se estaba importando ese componente de ejemplo ExampleComponent y también vamos a hacer limpieza en el archivo resources/sass/app.scss. Los archivos deben quedar así.

En el archivo app.js borramos también los comentarios.

resources/js/app.js

Frontend app.js

En el archivo app.scss borramos todo, no necesitamos nada de lo que había ahí.

resources/sass/app.scss

Texto pendiente

Despues de hacer eso podemos compilar los archivos js/css y no deberí haber ningún error.

Con este comando compilamos el js y css una sola vez.

npm run dev

Con este comando dejamos que npm compile los archivos cada vez que realizemos un cambio en un archivo de js o scss.

npm run watch

Ahora para dejar listo el frontend y empezar a agregar el código de las funcionalidades de listamos arriba, que modificar la plantillas de blade en la que se va a visualizar la aplicación.

En el archivo web.php la url / de inicio apunta a una plantilla llamada welcome que es un archivo ubicado en resources/views/welcome.blade.php que es la que hace que la pagina se vea así:

Texto pendiente

Reemplazamos todo el código de la plantilla por este, donde simplemente importamos nuestro css y js, ademas importamos la fuente Roboto de Google Fonts y por último a señalar aquí es que creamos un div con el id app que es el espacio que va a usar Vue.js para cargar lo que programemos en el archivo app.js en el componente que por ahora está vacío.

Texto pendiente

Preparar el dashboard de la aplicación

La interfaz que vamos a construir es bastante simple, se compone de una cabecera donde solo se ve un botón para ir a la carpeta raíz, un botón para volver a la carpeta anterior, un botón para abrir una ventana con un formulario para crear una carpeta y por último un botón que abre otra ventana con un formulario para subir archivos. Luego sigue un bloque blanco donde ser visualizará la ruta de archivos que estamos viendo actualmente y por ultimo un bloque grande donde se verá la lista de todos los archivos.

Empezamos colocando el etiquetado de todos los botones que vamos luego a dar funcionalidad y también agregamos los divs para indicar las zonas de la cabecera, la ruta actual y la zona de los archivos.

resources/views/welcome.blade.php

Texto pendiente

En el archivo de app.scss está todo el css que vamos a necesitar para el resto de la aplicación y no hay mucho que explicar aquí, simplemente es el css del dashboard.

resources/sass/app.scss

Texto pendiente

Y para finalizar es necesario hacer un cambio en el archivo app.js para que cuando carguemos nuestra pagina en el navegador, los iconos de la librería FeatherIcons se vean correctamente, de lo contrario nisiquiera se verán.

resources/js/app.js

Texto pendiente

El resultado final debe verse de esta forma, y es un diseño que puede usar inmediatamente en cualquier resolución desde móvil a escritorio.

Texto pendiente

Componente de Archivo y carpeta

Para visualizar por fin los archivos en pantalla primero vamos a crear un componente llamado File.vue que es el componente que va a encasular la funcionalidad de un archivo o carpeta.

mkdir resources/js/components
touch resources/js/components/File.vue

Ahora modificamos la estructura inicial del componente File.vue y lo importamos en app.js, y con esto podemos empezar a hacer llamadas al API.

resources/js/components/File.vue

Texto pendiente

resources/js/app.js

Texto pendiente

Cargar archivos

Primero vamos a colocar varios archivos de prueba en la carpeta box.

storage/app/box

Texto pendiente

En el componente principal agregamos 4 atributos, el primero solo queda con el nombre de la carpeta raíz, el segundo mantiene el nombre de la carpeta actual que se está visualizando y los otros dos guardan la lista de carpetas y archivos que carga del backend.

En la función mounted() ejecutamos la carga inicial de archivos, entonces siempre que abramos la aplicación se cargarán automáticamente los archivos de la carpeta raíz.

La carga de archivos se ejecuta en la función loadFiles(path) que recibe como parametro la ruta completa de la carpeta que quieres abrir, pero como había dicho al programar el controlador FilesController primero formatea la ruta de carpetas de tal forma que no genere conflicto con las urls que maneja el protocolo HTTP.

resources/js/app.js

Texto pendiente

Y como prueba que de funciona abrimos el navegador y si tenemos instalado el plugin para programar con Vue.js entonces podemos ver que el componente raíz si cargó los archivos y carpetas que tenemos en la carpeta box.

Texto pendiente

Visualizar archivos en pantalla

Para poder visualizar los archivos y carpetas en el navegador primero modificamos el script del componente File.vue. Primero importamos de la libreria FeatherIcons los iconos FileIcon y FolderIcon, luego le agregarmos dos propiedades al componente File; la propiedad file file va a recibir la ruta del archivo o la carpeta y la vamos a usar como la necesitemos y por ahora solo será mostrar el nombre del archivo en pantalla y el atributo type es para saber si se trata de un archivo o una carpeta y por último creamos un filtro que va a formatear la ruta del archivo que recibimos como propiedad y solo nos entrega el nombre del archivo o carpeta.

resources/js/components/File.vue

Texto pendiente

En la sección del etiquetado solo escribimos las etiquetas de los iconos a mostrar dependiendo del tipo de archivo y el nombre del archivo formateado para que no muestre la ruta completa.

Texto pendiente

Y en la zona de estilos del componente File pues agregamos los estilos correspondientes para darle un buen acabado a los archivos.

Texto pendiente

Por último solo queda imprimir los archivos y carpetas usando un bucle v-for para el array de files y otro bucle para el array directories en el archivo welcome.blade.php y revisar el resultado final.

resources/views/welcome.blade.php

Texto pendiente Texto pendiente

Agregar navegación por carpetas

Entonces ya se pueden ver los archivos y carpetas, pero falta agregar la funcionalidad de navegar a travéz de ellas. En el archvo app.js creamos la función changeDirectory(directory) que va a recibir la ruta hacia la cual ir esta misma consulta los nuevos archivos y carpetas, luego tenemos ya simplemente funciones utilitarias como goRoot que cambia el directorio al directorio raíz o goBack que vuelve al directorio inmediatamente anterior.

resources/js/app.js

Texto pendiente

La funciones goRoot y goBack las agregamos a los botones (líneas 20 y 24) que ya habíamos creado en la plantilla welcome.blade.php y también imprimimos en la línea 41 la ruta actual que se está visualizando.

resources/views/welcome.blade.php

Texto pendiente

Para agregar las funcionalidades a las carpetas y archivos vamos a agregar un pequeño botón en la parte superior derecha de cada archivo y carpeta que al oprimir desplegará una ventana modal con las opciones que permite realizar ese archivo, por ahora en el caso de las carpetas mostrará una opción de abrir carpeta y cerrar modal y en el caso de los archivos solo se podrá cerrar de nuevo.

En el archivo File.vue primero importamos mas iconos (El icono de tres puntos verticales y una x con circulo), luego le agregamos el atributo showOptions al componente que es un booleano y cuando esté en verdadero mostrará el modal. por último agregamos la función changeDirectory para darle funcionalidad a la opción "Abrir Carpeta" cuando la seleccionemos; esta función emitirá evento que se propaga hasta el componente padre, y es este componente que cambia el directorio.

resources/js/components/File.vue

Texto pendiente

En la sección de html del componente File.vue agregamos el botón con el icono de tre puntos verticales después de donde imprimimos el nombre del archivo y luego en la sección de los estilos le damos diseño a ese botón.

Texto pendiente Texto pendiente

El código del dialogo modal que muestra las opciones que podemos realizar con las carpetas o archivos es un poco largo, pero lo que se ve es lo siguiente

  • Líneas 18-21: Un div que ocupa todo el viewport y se pone de fondo del modal para que cuando el usuario de click fuera del modal este se cierre.
  • Líneas 23-39: El contenedor del modal que en orden imprime el nombre del archivo, la opción de abrir carpeta (que solo se visualiza si es una carpeta) y un botón para cerrar el modal.
  • Líneas 145-158: Estilos para el fondo del modal.
  • Líneas 160-189: Estilos para el modal.

Texto pendiente Texto pendiente

Por último en la plantilla welcome.blade.php se agregan los eventos (líneas 51 y 57) para que al seleccionar "Abrir Carpeta" el componente padre sepa que hacer.

resources/views/welcome.blade.php

Texto pendiente

Con esto ya la aplicación tiene navegación completa por todas las carpetas y el resultado queda así.

Texto pendiente

Descargar archivos

Para agregar la funcionalidad de descargar archivos primero importamos un icono mas (Icono de descarga) y creamos la función getDownloadUrl(file) que se encarga de formatear la ruta del archivo como una url lista para hacer la petición al backend, y para usar esta función agregamos una nueva opción al modal del componente File.vue, despues de la opción "Abrir Carpeta" y la llamamos "Descargar"; esta opción no va a ser un "button" sino un enlace y en su atributo href ejecutamos la función getDownloadUrl(file). Por último esta opción solo está disponible para archivos, no para carpetas.

resources/js/components/File.vue

Texto pendiente Texto pendiente Texto pendiente

Borrar archivos

Para borrar archivos creamos la función deleteFile(file) que permitirá borrar archivos y carpetas por igual, ejecutando una llamada ajax usando el método delete del protocolo HTTP enviando la ruta del archivo o la carpeta. Tambien en el código importamos otro icono mas Trash2Icon y para completar esta funcionalidad de borrar archivos agregamos una opción mas al dialogo modal y lo conectamos con la función deleteFile(file).

resources/js/components/File.vue

Texto pendiente

Texto pendiente

Texto pendiente

Texto pendiente

Crear los componentes para subir archivos y crear carpetas

Vamos ahora a crear dos componentes que en estructura son similares, van a abrir un modal cuando le demos click a los botones que están en la cabecera de "Crear Carpeta" y "Subir Archivos".

Creamos primero dos archivos para los dos componentes.

touch resources/js/components/FileForm.vue
touch resources/js/components/FolderForm.vue

Componente FileForm

Todo el código del componente FileForm parece demasiado, pero su funcionalidad es practicamente la misma que el modal de las opciones de archivos y carpetas, lo unico que cambia es que la variable que controla si este modal es visible o no, no está aquí sino en el archivo app.js. Este modal ahora solo muestra un título y un botón de cerrar el modal que emite un evento para que el componente padre sea el que ejecute la logica de cerrar el modal.

resources/js/components/FileForm.vue

Texto pendiente

En el archivo app.js que es donde está el componente raíz importamos el componente FileForm (Líneas 4 y 19) y también creamos el atributo showFileFormModal para controlar que el modal sea visible o no (Línea 28).

resources/js/app.js

Texto pendiente

Por último en la plantilla welcome colocamos el componente del FileForm (Líneas 17-19), y validamos que solo sea visible si la variable showFileFormModal es verdadera con la directiva v-if. Por otro lado, también agregamos el evento on-click al botón de Subir Archivo en la linea 38.

resources/views/welcome.blade.php

Texto pendiente

Solo queda compilar los assets y probar que el modal abra y cierre correctamente.

Texto pendiente

Componente FolderForm

Los pasos para crear el componente FolderForm que va a abrir un modal con un formulario para crear carpetas son realmente los mismos que los del componente FileForm y como repetir código no es para nada una buena practica, lo correcto sería encapsular esta funcionalidad de crear dialogos modal aparte y reutilizar ese código, pero por tiempo se realizó de esta manera.

resources/js/components/FolderForm.vue

Texto pendiente

resources/js/app.js

Texto pendiente

resources/views/welcome.blade.php

Texto pendiente

Texto pendiente

Al fin vamos a crear carpetas

Ahora sí vamos a agregar la lógica del frontend para crear carpetas. Primero importamos el icono de guardar.

resources/js/components/FolderForm.vue

Texto pendiente Texto pendiente

El componente necesita saber en que directorio debe guardar la nueva carpeta, entonces se le pasa por parametro y usando el atributo props hacemos que sea obligatorio este parametro cuando usemos el componente en la plantilla welcome.blade.php.

Texto pendiente

Como datos internos del componente FolderForm, colocamos el atributo name para usarlo en el formulario y saber el nombre de la carpeta a crear.

Texto pendiente

Creamos la función saveFolder() que prepara los datos necesario para crear una carpeta en el backend, que son:

  • directory: carpeta base en la que se crea la nueva carpeta.
  • name: el nombre de la carpeta.
  • type: especificamos que la llamada ajax que vamos a hacer es para crear una carpeta, porque la otra opción es file y es para indicarle que vamos a usar esa llamada para subir archivos.

Luego ejecutamos la llamada ajax al backend y si tiene exito ejecutamos dos eventos uno es el de cerrar el modal y el otro evento (success) es para que el componente padre sepa que debe reaccionar a ese evento recargando la lista de archivos.

Texto pendiente

Para evitar problemas como que intentó crear una carpeta sin nombre o quizo colocarle de nombre las palabras clave que usamos para formatear la ruta, debemos validar que no se pueda ejecutar botón "Guardar" si no se cumple la validación que está en la función canSummitForm()

Texto pendiente

Como último paso en el archivo FolderForm.vue agregamos un input de tipo text y un botón para confirmar el formulario, y por supuesto le agregamos estilos a estas etiquetas.

Texto pendiente

Texto pendiente

En la plantilla welcome modificamos la llamada al componente folder-form-box para agregarle el evento success que acabamos de crear y le pasamos el currentDirectory como directorio base.

resources/views/welcome.blade.php

Texto pendiente

El resultado es un formulario simple con un solo campo pero con el que podemos crear todas las carpetas que queramos.

Texto pendiente

Han pasado 8... Vamos a subir archivos

Para finalizar este largo tutorial sobre como hacerle la "competencia" a Dropbox solo queda poder subir archivos a la aplicación. Empezamos importando el icono de guardar para el botón "Guardar" en el formulario.

Texto pendiente

Texto pendiente

Ahora similar al componente de FolderForm necesitamos saber cual es el directorio base al que se van a subir los archivos, entonces ponemos la propiedad directory, la cual se usará en la plantilla welcome.blade.php

Texto pendiente

Para subir archivos vamos a usar un input de tipo "file" con la directiva "multiple" habilitada para poder subir varios archivos a la vez, y la forma en que Vue.js va a poder manipular esos archivos que se suban en ese input es por medio del evento "change" que se ejecuta cuando se detecta que el contenido de ese input a cambiado y vamos a capturar esos cambios en la función "fileSelected(event)" que va a llenar el atributo "files" (que es un array) que definimos en la sección de data()

Texto pendiente

Texto pendiente

Input de tipo file con el evento change y sus estilos css.

Texto pendiente Texto pendiente

Ahora agregamos el botón que ejecuta la petición de guardar los archivos en el backend; la transferencia de archivos se hará tambien por javascript con ayuda del objeto FormData en la función saveFile(). El objeto FormData se llena primero con los datos de que esta petición es para guardar archivos y no carpetas, luego se indica el directorio base con la variable "directory" y usando un for se itera por cada uno de los archivos que se capturaron en el evento fileSelected con el atributos files, Y ya solo con esto se pueden enviar los datos y archivos al backend y si recibimos una respuesta exitosa limpiamos el input de archivos y ejecutamos los eventos necesarios para cerrar el modal y avisarle a componente padre que actualize la lista de archivos del directorio actual.

Texto pendiente

Texto pendiente

Al botón de guardar solo le ponemos una validación de que el input haya al menos un archivo cargado.

Texto pendiente

Por último solo queda actualizar la plantilla welcome para que la llamada del componente file-form-box ejecute correctamente los eventos que produce al guardar un archivo.

Texto pendiente

El formulario de subir archivos debe verse así, en este ejemplo se están subiendo 4 archivos a la vez.

Texto pendiente

Conclusión

Ya con esto queda totalmente funcional esta aplicación para subir y compartir archivos estando en la misma red, aunque es posible cambiar un par de lineas y subirlo a un servidor en la nube y usarlo desde cualquier parte y dispositivo, pero eso requiere agregar mas código sobre todo de seguiridad. El proposito de esta actividad en mi caso era simplemente solucionar un problema personal y en el proceso compartir algo de conocimiento sobre todo a los programadores que estén empezando.

Esta aplicación claramente requiere mejoras como no repetir código, agregar funcionalidades de renombrar archivos, cosas interesantes como arrastrar y soltar archivos, agregar cuentas de usuarios, limitar y compartir archivos entre cuentas, etc. Pero para un MVP (palabra fancy) y mis necesidades por ahora es mas que suficiente y espero este contenido le sea de ayuda a alguien ahí afuera.

Repositorio GIT

https://gitlab.com/nisujo/mybox/

Compartelo!

Logo Facebook Logo Twitter LinkedIn

También te puede interesar

Que es Responsive Web Design? SEO (Search Engine Optimization) ó Posicionamiento Web Gestores de Contenido (CMS), Todo lo que debes saber
Escribenos y nos pondremos en contacto con usted lo antes posible