Cómo usar el almacenamiento de objetos Vultr en AdonisJS

Introducción

Drive es una biblioteca de abstracción de almacenamiento en AdonisJS. Proporciona una API coherente que funciona en todos los proveedores de almacenamiento.

Drive tiene un controlador S3 para admitir el almacenamiento en la nube compatible con S3, como Vultr Object Storage. Esta guía explica cómo configurar AdonisJS Drive para Vultr Object Storage y usarlo para almacenar y leer archivos.

requisitos previos

Antes de comenzar, debe:

Instalar Node.js

AdonisJS requiere al menos la versión 14 de Node.js. Puede instalar la última versión de Node.js mediante Node Version Manager (NVM).

  1. Instalar NVM:

                              
                                $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
    
                              
                            
  2. Desconecte y vuelva a conectar su sesión ssh.

  3. Instale Node.js:

                              
                                $ nvm install node
    
                              
                            
  4. Compruebe la versión de Node.js:

                              
                                $ node -v
    
    v19.3.0
    
                              
                            

Crear almacenamiento de objetos

  1. Iniciar sesión en Portal de clientes Vultr .

  2. Navegar a Productos -> Objetos .

  3. Agregar almacenamiento de objetos . Elija la región y asígnele una etiqueta.

  4. Haga clic en su almacenamiento de objetos y vaya a la pestaña Cubo.

  5. Cree un depósito y asígnele un nombre.

  6. Toma nota de la Hostname la Secret Key la Access Key y el Bucket Name .

Crear nueva aplicación AdonisJS

Vaya al directorio de inicio.

                      
                        $ cd ~

                      
                    

Cree un nuevo directorio para su aplicación.

                      
                        $ mkdir app

                      
                    

Ve a la app directorio y generar una nueva aplicación AdonisJS usando el npm init dominio.

                      
                        $ cd app

$ npm init [email protected] website

                      
                    
  • Seleccione la estructura del proyecto web.

  • Elija ‘y’ cuando le solicite que configure el Webpack Encore.

Genera tu app en el website directorio. Para las tareas restantes, debe ejecutarlas en el website directorio.

                      
                        $ cd website

                      
                    

Instalar Redis

La aplicación de muestra de esta guía utiliza Redis para almacenar los nombres de archivo de las imágenes.

Instalar Redis:

                      
                        $ sudo apt install redis-server

                      
                    

Configure el modo persistente de Redis:

  1. Abra el archivo de configuración de Redis:

                              
                                $ sudo nano /etc/redis/redis.conf
    
                              
                            
  2. Cambiar el appendonly no a appendonly yes .

  3. Guardar archivo y salir.

  4. Reinicie Redis.

                              
                                $ sudo systemctl restart redis-server
    
                              
                            

Instale y configure el paquete AdonisJS Redis:

                      
                        $ npm i @adonisjs/redis

$ node ace configure @adonisjs/redis

                      
                    

Abre el env.ts expediente:

                      
                        $ nano env.ts

                      
                    

Agregue las siguientes reglas:

                      
                        REDIS_CONNECTION: Env.schema.enum(['local'] as const),

REDIS_HOST: Env.schema.string({ format: 'host' }),

REDIS_PORT: Env.schema.number(),

REDIS_PASSWORD: Env.schema.string.optional(),

                      
                    

Configurar la unidad AdonisJS

AdonisJS Drive tiene un controlador S3 para interactuar con almacenamiento en la nube compatible con S3 como Vultr Object Storage.

Instale y configure el controlador S3:

                      
                        $ npm i @adonisjs/drive-s3

$ node ace configure @adonisjs/drive-s3

                      
                    

Abre el env.ts expediente:

                      
                        $ nano env.ts

                      
                    

Actualizar el DRIVE_DISK normas:

                      
                        DRIVE_DISK: Env.schema.enum(['local','s3'] as const),

                      
                    

Agregue las siguientes reglas:

                      
                        S3_KEY: Env.schema.string(),

S3_SECRET: Env.schema.string(),

S3_BUCKET: Env.schema.string(),

S3_REGION: Env.schema.string(),

S3_ENDPOINT: Env.schema.string.optional(),

                      
                    

Abre el config/drive.ts expediente:

                      
                        $ nano config/drive.ts

                      
                    

Agregue las configuraciones de S3 dentro del disks objeto:

                      
                        s3: {

    driver: 's3',

    visibility: 'public',

    key: Env.get('S3_KEY'),

    secret: Env.get('S3_SECRET'),

    region: Env.get('S3_REGION'),

    bucket: Env.get('S3_BUCKET'),

    endpoint: Env.get('S3_ENDPOINT'),

}

                      
                    

Abre el .env expediente:

                      
                        $ nano .env

                      
                    

Actualizar el DRIVE_DISK valor a s3 :

                      
                        DRIVE_DISK=s3

                      
                    

Agregue las credenciales de Vultr Object Storage:

                      
                        S3_KEY=

S3_SECRET=

S3_BUCKET=adonis-drive

S3_REGION=sgp1

S3_ENDPOINT=https://sgp1.vultrobjects.com

                      
                    
  • S3_KEY es su clave de acceso al almacenamiento de objetos Vultr.

  • S3_SECRET es su clave secreta de almacenamiento de objetos Vultr.

  • S3_BUCKET` es el nombre de su depósito de almacenamiento de objetos Vultr.

  • S3_ENDPOINT es su nombre de host de almacenamiento de objetos Vultr.

  • S3_REGION es su región de almacenamiento de objetos Vultr.

Añadir Tailwind CSS

Esta guía utiliza Tailwind CSS para el marco CSS. Instale Tailwind CSS y sus dependencias a través de NPM:

                      
                        $ npm install -D tailwindcss postcss autoprefixer postcss-loader

                      
                    

Abre el webpack.config.js expediente:

                      
                        $ nano webpack.config.js

                      
                    

Habilite el cargador PostCSS:

                      
                        Encore.enablePostCssLoader()

                      
                    

Cree y abra el archivo de configuración Tailwind CSS:

                      
                        $ npx tailwindcss init -p

$ nano tailwind.config.js

                      
                    

Cambia el contenido a:

                      
                        /** @type {import('tailwindcss').Config} */

module.exports = {

    content: [

        "./resources/**/*.edge",

        "./resources/**/*.js",

    ],

    theme: {

        extend: {},

    },

    plugins: [],

}

                      
                    

Abre el resources/css/app.css y reemplace el contenido por las directivas CSS de Tailwind:

                      
                        @tailwind base;

@tailwind components;

@tailwind utilities;

                      
                    

Agregar JavaScript

Abre el resources/js/app.js y agregue los siguientes scripts:

                      
                        import '../css/app.css'



document.getElementById('fileImage').addEventListener('change',function(){

  if( this.files.length > 0 ){

      document.getElementById('uploadBtn').removeAttribute('disabled');

  }

});

                      
                    

Los scripts habilitan el botón de carga después de que el usuario seleccione el archivo de imagen.

Crear vista

Crear un resources/views/gallery.edge expediente:

                      
                        $ nano resources/views/gallery.edge

                      
                    

Agrega el siguiente código:

                      
                        <html>

    <head>

        <title>Gallery</title>



        @entryPointStyles('app')

    </head>

    <body>

        <div class="max-w-7xl m-auto">

            <h1 class="text-3xl font-bold text-gray-900 text-center py-8 uppercase">Gallery</h1>

            <form action="" method="post" enctype="multipart/form-data" class="flex flex-wrap text-center justify-center items-start p-4 rounded-lg">

                <label class="block py-1">

                  <input id="fileImage" type="file" name="fileImage" class="block w-full text-sm text-slate-500 pr-6

                    file:cursor-pointer

                    file:mr-4 file:py-2 file:px-4

                    file:rounded-full file:border-0

                    file:text-sm file:font-semibold

                    file:bg-indigo-50 file:text-indigo-700

                    hover:file:bg-indigo-100

                  "/>



                  @if (flashMessages.has('errors.fileImage'))

                    <span class="block text-red-700 py-4 text-left">{{ flashMessages.get('errors.fileImage') }}</span>

                  @endif

                </label>

                <button id="uploadBtn" disabled class="rounded border border-transparent bg-indigo-600 px-6 py-2 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50" type="submit">

                    Upload Image

                </button>

            </form>



            <div class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8 ">

                @each(image in images)

                    <div>

                        <img class="rounded" src="">

                    </div>

                @end

            </div>

        </div>



        @entryPointScripts('app')

    </body>

</html>

                      
                    

Crear controlador

Crea y abre el GalleryController.ts expediente:

                      
                        $ node ace make:controller GalleryController -e

$ nano app/Controllers/Http/GalleryController.ts

                      
                    

Importe bibliotecas y ayudantes en la parte superior del archivo:

                      
                        import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

import Drive from '@ioc:Adonis/Core/Drive'

import Redis from '@ioc:Adonis/Addons/Redis'

import { string } from '@ioc:Adonis/Core/Helpers'

import { schema } from '@ioc:Adonis/Core/Validator'

                      
                    

Crear el index acción. Muestra el formulario de carga y enumera todas las imágenes de su almacenamiento de objetos Vultr. Obtiene los nombres de archivo de imagen de Redis y llama al getUrl de la biblioteca de Drive para obtener la URL de cada imagen.

                      
                        public async index({ view }: HttpContextContract) {

    const galleryString = await Redis.get('gallery')

    const gallery = (galleryString) ? JSON.parse(galleryString) : []



    let images:string[] = []



    for (const filename of gallery) {

        const url = await Drive.getUrl(`gallery/${filename}`)

        images.push(url)

    }



    return view.render('gallery', { images })

}

                      
                    

Crear el upload acción. Maneja cuando un usuario sube sus imágenes. Valida el archivo, guarda el nombre del archivo en Redis y luego almacena el archivo en Vultr Object Storage usando el moveToDisk método.

                      
                        public async upload({ request, response }: HttpContextContract) {

    const imageSchema = schema.create({

        fileImage: schema.file({

            extnames: ['jpg', 'png', 'gif']

        }),

    })



    const payload = await request.validate({ schema: imageSchema })

    const filename = `${string.generateRandom(32)}.${payload.fileImage.extname}`



    if (payload.fileImage) {

        await payload.fileImage.moveToDisk(", {

            name: `gallery/${filename}`

        }, 's3')



        const galleryString = await Redis.get('gallery')



        let gallery:string[] = []



        if (galleryString) {

            gallery = JSON.parse(galleryString)

        }



        gallery.push(filename)

        await Redis.set('gallery', JSON.stringify(gallery))

    }



    return response.redirect().toPath("https://www.vultr.com/")

}

                      
                    

Debe guardar y obtener los nombres de archivo de imagen de Redis porque el controlador AdonisJS S3 no puede obtener una lista de archivos en un depósito o carpeta. Solo puede obtener un archivo a la vez.

El siguiente es el contenido completo del GalleryController.ts expediente:

                      
                        import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

import Drive from '@ioc:Adonis/Core/Drive'

import Redis from '@ioc:Adonis/Addons/Redis'

import { string } from '@ioc:Adonis/Core/Helpers'

import { schema } from '@ioc:Adonis/Core/Validator'



export default class GalleryController {

    public async index({ view }: HttpContextContract) {

        const galleryString = await Redis.get('gallery')

        const gallery = (galleryString) ? JSON.parse(galleryString) : []



        let images:string[] = []



        for (const filename of gallery) {

            const url = await Drive.getUrl(`gallery/${filename}`)

            images.push(url)

        }



        return view.render('gallery', { images })

    }



    public async upload({ request, response }: HttpContextContract) {

        const imageSchema = schema.create({

            fileImage: schema.file({

                extnames: ['jpg', 'png', 'gif']

            }),

        })



        const payload = await request.validate({ schema: imageSchema })

        const filename = `${string.generateRandom(32)}.${payload.fileImage.extname}`



        if (payload.fileImage) {

            await payload.fileImage.moveToDisk(", {

                name: `gallery/${filename}`

            }, 's3')



            const galleryString = await Redis.get('gallery')



            let gallery:string[] = []



            if (galleryString) {

                gallery = JSON.parse(galleryString)

            }



            gallery.push(filename)

            await Redis.set('gallery', JSON.stringify(gallery))

        }



        return response.redirect().toPath("https://www.vultr.com/")

    }

}

                      
                    

Añadir rutas

Abre el start/routes.ts expediente:

                      
                        $ nano start/routes.ts

                      
                    

Agrega el siguiente código:

                      
                        Route.get("https://www.vultr.com/", 'GalleryController.index')

Route.post("https://www.vultr.com/", 'GalleryController.upload')

                      
                    

Probar la aplicación

  1. Para probar su aplicación, debe deshabilitar el firewall de Ubuntu .

                              
                                $ sudo ufw disable
    
                              
                            

    Puede volver a habilitarlo cuando cree su aplicación para producción.

  2. Inicie un servidor de desarrollo:

                              
                                $ node ace serve --encore-args="--host [VULTR_VPS_IP_ADDRESS]"
    
                              
                            
  3. Abre el https://[VULTR_VPS_IP_ADDRESS]:3333 en un navegador.

  4. Debería ver el formulario de carga.

  5. Subir una imagen.

  6. Compruebe si la imagen aparece en su almacenamiento de objetos Vultr y en la galería debajo del formulario de carga.

  7. Presione CTRL+C para detener el servidor de desarrollo.

Utilice varias ubicaciones de almacenamiento de objetos

Vultr Object Storage está disponible en varias ubicaciones. Las siguientes son las ubicaciones que admite Vultr:

  • Ámsterdam: ams1.vultrobjects.com

  • New Jersey: ewr1.vultrobjects.com

  • Silicon Valley: sjc1.vultrobjects.com

  • Singapur: sgp1.vultrobjects.com

Puede usar varias ubicaciones de almacenamiento de objetos en la aplicación AdonisJS para agregar redundancia a sus archivos. Para hacer eso, agrega una configuración de disco para cada ubicación en el config/drive.ts expediente. El disco en AdonisJS Drive representa una ubicación y un controlador de almacenamiento en particular.

Crear nuevo almacenamiento de objetos

  1. Ir a Portal de clientes Vultr .

  2. Navegar a Productos -> Objetos .

  3. Agregar almacenamiento de objetos . Elija una región diferente, para exampleÁmsterdam.

  4. Cree un depósito en el nuevo almacenamiento de objetos.

  5. Toma nota de la Hostname la Secret Key la Access Key y el Bucket Name .

Configurar nuevo disco

Abre el config/drive.ts expediente:

                      
                        $ nano config/drive.ts

                      
                    

Agregue una nueva configuración de disco en la sección S3. Puede establecer el nombre del disco en cualquier cosa, por example, s3ams . Agregue un sufijo a los nombres de las variables de entorno para diferenciarlos del primer disco.

                      
                        s3ams: {

  driver: 's3',

  visibility: 'public',

  key: Env.get('S3_KEY_AMS'),

  secret: Env.get('S3_SECRET_AMS'),

  region: Env.get('S3_REGION_AMS'),

  bucket: Env.get('S3_BUCKET_AMS'),

  endpoint: Env.get('S3_ENDPOINT_AMS'),

},

                      
                    

Abre el .env expediente:

                      
                        $ nano .env

                      
                    

Agregue las credenciales de su almacenamiento de objetos:

                      
                        S3_KEY_AMS=

S3_SECRET_AMS=

S3_BUCKET_AMS=adonis-drive

S3_REGION_AMS=ams1

S3_ENDPOINT_AMS=https://ams1.vultrobjects.com

                      
                    
  • S3_KEY_AMS es su clave de acceso al almacenamiento de objetos Vultr.

  • S3_SECRET_AMS es su clave secreta de almacenamiento de objetos Vultr.

  • S3_BUCKET_AMS es su nombre de depósito de almacenamiento de objetos Vultr.

  • S3_ENDPOINT_AMS es su nombre de host de almacenamiento de objetos Vultr.

  • S3_REGION_AMS es su región de almacenamiento de objetos Vultr.

Abre el env.ts expediente:

                      
                        $ nano env.ts

                      
                    

Añade el s3ams nombre del disco al DRIVE_DISK valor de enumeración:

                      
                        DRIVE_DISK: Env.schema.enum(['local','s3','s3ams'] as const),

                      
                    

Agregar reglas de validación para s3ams variables de entorno del disco:

                      
                        S3_KEY_AMS: Env.schema.string(),

S3_SECRET_AMS: Env.schema.string(),

S3_BUCKET_AMS: Env.schema.string(),

S3_REGION_AMS: Env.schema.string(),

S3_ENDPOINT_AMS: Env.schema.string.optional(),

                      
                    

Guarda el archivo y cierra.

Actualizar controlador

Abre el GalleryController.ts expediente:

                      
                        $ nano app/Controllers/Http/GalleryController.ts

                      
                    

En el upload acción, busque el código responsable de cargar el archivo en el almacenamiento de objetos.

                      
                        await payload.fileImage.moveToDisk(", {

    name: `gallery/${filename}`

}, 's3')

                      
                    

Duplique el código y cambie el nombre del disco a s3ams .

                      
                        await payload.fileImage.moveToDisk(", {

    name: `gallery/${filename}`

}, 's3')



await payload.fileImage.moveToDisk(", {

    name: `gallery/${filename}`

}, 's3ams')

                      
                    

Carga el archivo en ambas ubicaciones de almacenamiento de objetos.

Establecer el disco predeterminado

El index acción en el GalleryController.ts utiliza el disco predeterminado para obtener la URL de la imagen. Para cambiar el disco predeterminado, abra el .env expediente:

                      
                        $ nano .env

                      
                    

Actualizar el DRIVE_DISK valor al disco que desea:

                      
                        DRIVE_DISK=s3ams

                      
                    

Guarda el archivo y cierra.

Prueba la aplicación

  1. Inicie un servidor de desarrollo:

                              
                                $ node ace serve --encore-args="--host [VULTR_VPS_IP_ADDRESS]"
    
                              
                            
  2. Abre el https://[VULTR_VPS_IP_ADDRESS]:3333 en un navegador.

  3. Subir una imagen.

  4. Compruebe si la imagen aparece tanto en las ubicaciones de Vultr Object Storage como en la galería debajo del formulario de carga.

  5. Presione CTRL+C para detener el servidor de desarrollo.

Construir para producción

La aplicación AdonisJS usa TypeScript. Debe compilarlo en JavaScript antes de ejecutarlo en producción.

Ve a la website carpeta:

                      
                        $ cd ~/app/website

                      
                    

Compílalo usando el build dominio. El resultado está en el build carpeta.

                      
                        $ node ace build --production --ignore-ts-errors

                      
                    

Copia el .env archivo a la build carpeta y abrirlo:

                      
                        $ cp .env build/.env

$ nano build/.env

                      
                    

Selecciona el NODE_ENV variables en el .env archivo a production :

                      
                        NODE_ENV=production

                      
                    

Instale dependencias solo de producción en el build carpeta:

                      
                        $ cd build

$ npm ci --production

                      
                    

Ejecute la aplicación en producción usando PM2

PM2 es un administrador de procesos demonio. Le ayuda a ejecutar y administrar su aplicación AdonisJS en producción.

Instale el paquete PM2 más reciente:

                      
                        $ npm install [email protected] -g

                      
                    

Cree el archivo del ecosistema PM2 para administrar su aplicación:

                      
                        $ cd ~/app

$ nano ecosystem.config.js

                      
                    

Agregue estas configuraciones a la ecosystem.config.js expediente:

                      
                        module.exports = {

    apps : [

        {

            name   : "website",

            script : "./website/build/server.js"

        }

    ]

}

                      
                    
  • Pon el nombre de tu aplicación en el name parámetro.

  • Ponga la ruta de producción de script.js en el script parámetro.

Ejecute su aplicación:

                      
                        $ pm2 start ecosystem.config.js

$ pm2 list

                      
                    

En este punto, su aplicación se está ejecutando. Pero, el proceso no es persistente todavía. Significa que debe ejecutar su aplicación manualmente nuevamente después de reiniciar su servidor.

Para ejecutar una aplicación persistente, debe generar un script de inicio para PM2.

                      
                        $ pm2 startup

                      
                    

Copie y pegue el comando que se muestra en la terminal:

                      
                        $ sudo env PATH=$PATH:/home/ubuntu/.nvm/versions/node/v19.3.0/bin /home/ubuntu/.nvm/versions/node/v19.3.0/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu

                      
                    

Guarde su lista de aplicaciones PM2 usando este comando:

                      
                        $ pm2 save

                      
                    

Proxy de reserva de Nginx

Debe configurar un proxy de reserva de Nginx para conectar su dominio a su aplicación. Pones tus aplicaciones detrás del servidor web Nginx. Acepta todas las solicitudes entrantes y las reenvía a sus aplicaciones.

Agregue los repositorios ondrej para obtener la última versión de Nginx.

                      
                        $ sudo add-apt-repository -y ppa:ondrej/nginx-mainline

$ sudo apt update

                      
                    

Instalar Nginx:

                      
                        $ sudo apt install nginx

                      
                    

Deshabilite la configuración predeterminada de Nginx:

                      
                        $ sudo unlink /etc/nginx/sites-enabled/default

                      
                    

Cree un nuevo archivo de configuración de Nginx:

                      
                        $ sudo nano /etc/nginx/sites-available/website

                      
                    

Agregue las siguientes configuraciones. Asegúrate de cambiar el nombre de dominio googlesyndication.com a tu dominio. Guarda el archivo y cierra.

                      
                        server {

    listen 80;



    server_name googlesyndication.com;



    location / {

        proxy_pass https://localhost:3333;

        proxy_http_version 1.1;

        proxy_set_header Upgrade $http_upgrade;

        proxy_set_header Connection 'upgrade';

        proxy_set_header Host $host;

        proxy_set_header X-Real-IP $remote_addr;

        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_cache_bypass $http_upgrade;

    }

}

                      
                    

Habilitar la configuración de Nginx:

                      
                        $ sudo ln -s /etc/nginx/sites-available/website /etc/nginx/sites-enabled/

                      
                    

Pruebe sus configuraciones a partir de errores de sintaxis:

                      
                        $ sudo nginx -t

                      
                    

Si no hay errores, puede volver a cargar el proceso Nginx:

                      
                        $ sudo systemctl reload nginx

                      
                    

Apunte sus dominios a su dirección IP Vultr VPS.

Configurar cortafuegos

Configure el firewall para permitir el puerto ssh:

                      
                        $ sudo ufw allow 'OpenSSH'

                      
                    

Permitir puertos HTTP y HTTPS:

                      
                        $ sudo ufw allow 'Nginx Full'

                      
                    

Habilite el cortafuegos:

                      
                        $ sudo ufw enable

                      
                    

Comprobar el estado del cortafuegos:

                      
                        $ sudo ufw status

                      
                    

Aplicaciones seguras con el certificado SSL de Let’s Encrypt

Let’s Encrypt proporciona un certificado SSL gratuito para su sitio web. Para generar el certificado, debe utilizar la herramienta de software Certbot.

Instalar Certbot:

                      
                        $ sudo snap install core; sudo snap refresh core

$ sudo snap install --classic certbot

$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

                      
                    

Genere el certificado SSL:

                      
                        $ sudo certbot --nginx

                      
                    

Visite su dominio en el navegador y confirme que tiene una conexión HTTPS.

El certificado de Let’s Encrypt caduca después de 90 días. Certbot agrega el comando de renovación al temporizador systemd o Cron Job para renovar el certificado automáticamente antes de que caduque. Puedes verificarlo con el siguiente comando:

                      
                        $ systemctl list-timers | grep 'certbot|ACTIVATES'

                      
                    

Conclusión

Esta guía muestra ejemplos del uso de Vultr Object Storage en AdonisJS con una o varias ubicaciones de almacenamiento de objetos, incluidos los pasos para que la aplicación esté lista para la producción.

Otras lecturas

Almacenamiento de objetos Vultr.

Documentación de AdonisJS Drive.

Implemente múltiples aplicaciones de Adonis.js con PM2 y Nginx.

Documentación PM2.

Proxy inverso NGINX.

Título del artículo Nombre (opcional) Correo electrónico (opcional) Descripción

Enviar sugerencia

Related Posts