Upload di un file su feathers

Come caricare un file su feathers senza diventare matti

Il caricamento di un file, solitamente, è un'operazione abbastanza semplice. Se però stiamo utilizzando feathers come framework backend dobbiamo seguire qualche passaggio in più per far funzionare tutto correttamente.

Sulla documentazione ufficiale troviamo una guida all'upload dei file, ma sinceramente la trovo eccessivamente complessa e poco flessibile.

# Servizio sul backend

Come prima cosa dobbiamo generare un servizio dove andremo ad aggiungere il supporto all'upload dei file multipart. Questo servizio portà sia essere un servizio dedicato per gestire dei multimedia sia un servizio può accettare in input un file oltre ad altri dati.

Utilizzando il comando feathers generate service generiamo un nuovo servizio:

feathers generate service
? What kind of service is it? Mongoose
? What is the name of the service? posts
? Which path should the service be registered on? /posts
? Does the service require authentication? Yes

Modifichiamo il modello in src/models/posts.model.js aggiungendo le proprietà title, body e image:

// posts-model.js - A mongoose model
//
// See http://mongoosejs.com/docs/models.html
// for more of what you can do here.

export default function(app) {
  const modelName = "posts"
  const mongooseClient = app.get("mongooseClient")
  const { Schema } = mongooseClient
  const schema = new Schema(
    {
      title: { type: String },
      body: { type: String },
      image: { type: String }
    },
    {
      timestamps: true
    }
  )

  // This is necessary to avoid model compilation errors in watch mode
  // see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel
  if (mongooseClient.modelNames().includes(modelName)) {
    mongooseClient.deleteModel(modelName)
  }

  return mongooseClient.model(modelName, schema)
}

Installiamo multer con npm i multer e all'interno di src/services/posts/posts.service.js aggiungiamo il supporto alla codifica multipart/form-data:

// Initializes the `posts` service on path `/posts`
import { Posts } from "./posts.class"
import createModel from "../../models/posts.model"
import hooks from "./posts.hooks"
import Multer from "multer"

const multer = Multer()

export default function(app) {
  const options = {
    Model: createModel(app),
    paginate: app.get("paginate")
  }

  // Initialize our service with any options it requires
  app.use(
    "/posts",
    // Accettiamo un singolo file nel campo file del form
    multer.single("file"),
    (req, res, next) => {
      if (req.file && req.feathers && !req.feathers.file) {
        /* Aggiungiamo il riferimento del file su req.feathers
           in modo tale da averlo disponibile all'interno degli hooks
           sotto context.params.file
        */
        req.feathers.file = req.file
      }

      next()
    },
    new Posts(options, app)
  )

  // Get our initialized service so that we can register hooks
  const service = app.service("posts")

  service.hooks(hooks)
}

Possiamo ora creare un nuovo before hook sul metodo create in src/services/posts/posts.hooks.js che si occuperà di salvare il file da qualche parte e aggiungere un riferimento nel database:

import { authenticate } from "@feathersjs/authentication"
import dauria from "dauria"
// Don't remove this comment. It's needed to format import lines nicely.

export default {
  before: {
    all: [authenticate("jwt")],
    find: [],
    get: [],
    create: [
      async (context) => {
        const file = context.params.file
        /*
          La constante file possiede le seguenti proprietà:

          fieldname: nome del campo utilizzato all'interno nel form per questo file
          originalname: nome del file originale sul dispositivo dell'utente
          encoding: tipo della codifica
          mimetype: tipo del file (es: image/png)
          size: dimensione in bytes
          stream: stream del file
          destination: percorso di salvataggio
          filename: nome del file
          path: percorso di caricamento
          buffer: buffer contente l'intero file
        */

        if (file) {
          /* In questo caso stiamo utilizzando dauria per
             convertire il file in base64 e salvarlo
             direttamente nel database
          */
          context.data.image = dauria.getBase64DataURI(
            file.buffer,
            file.mimetype
          )
        }

        return context
      }
    ],
    update: [],
    patch: [],
    remove: []
  },

  after: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  },

  error: {
    all: [],
    find: [],
    get: [],
    create: [],
    update: [],
    patch: [],
    remove: []
  }
}

# Caricamento lato client

Per caricare il file dal frontend possiamo utilizzare un semplice form html:

<form
  action="http://localhost:3030/posts"
  enctype="multipart/form-data"
  method="POST"
>
  <input placeholder="Titolo" type="text" name="title" />
  <textarea placeholder="Contenuto" name="body"></textarea>
  <input type="file" name="file" accept="image/*" />
  <button type="submit">Invia</button>
</form>

Oppure in alternativa utilizzando javascript:

function createPost(title, body, image) {
  const formData = new FormData()

  formData.append("title", title)
  formData.append("body", body)
  formData.append("file", image)

  return fetch("http://localhost:3030/posts", {
    method: "POST",
    body: formData,
    headers: {
      authorization: "Bearer ..."
    }
  }).then((r) => {
    if (!r.ok) {
      throw new Error(r.statusText)
    }

    return r.json()
  })
}