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.
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: []
}
}
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()
})
}