Skip to content

Commit

Permalink
Aggiunto salvataggio degli allegati caricati
Browse files Browse the repository at this point in the history
La backend adesso salva gli allegati ricevuti su (POST) /attachments

Modificato fetch di api.js per poter essere utilizzato con content-type
multipart invece di avere un secondo metodo praticamente identico

Aggiunta validazione lato backend (dimensione file < 20MB) e avvisi lato
frontend per validazione degli allegati
  • Loading branch information
Fran314 committed Aug 14, 2023
1 parent 55e314d commit c87440e
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 32 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ api/webroot/js/ver
api/webroot/css/style.min.css.map
api/webroot/node_modules
api/node_modules
db
db
attachments-db
1 change: 1 addition & 0 deletions api/controllers/AttachmentController.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const AttachmentController = {

post: async req => {
console.log(req.files)
console.log(req.body)
return
// TODO: il contenuto dell'allegato deve essere gestito all'interno
// di questo metodo, probabilmente utlizzando il middleware `multer`
Expand Down
3 changes: 3 additions & 0 deletions api/models/Attachment.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const mongoose = require('mongoose');

const Attachment = mongoose.model('Attachment', {
filename: {
type: String
},
mimetype: {
type: String
},
Expand Down
26 changes: 24 additions & 2 deletions api/router.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
let express = require('express')
const multer = require('multer')
let router = new express.Router()
const { BadRequestError, NotFoundError } = require('./exceptions/ApiException')
const { BadRequestError, ValidationError, NotFoundError } = require('./exceptions/ApiException')
const Exams = require('./controllers/ExamsController')
const Users = require('./controllers/UsersController')
const Degrees = require('./controllers/DegreesController')
Expand Down Expand Up @@ -34,6 +34,28 @@ function test_error(req, res) {
throw new BadRequestError("fake error!");
}

const attachmentHandler = multer({
// TODO: cambia la destinazione una volta stabilito dove vadano conservati
// gli allegati (se si decide di non salvarli su disco ma utilizzare
// qualche servizio tipo S3 bisogna sostituire multer)
dest: '../attachments-db',
limits: {
fileSize: 20 * 1000 * 1000 // 20MB
}
}).any()

function attachmentPost(req, res, next) {
attachmentHandler(req, res, (err) => {
if (err instanceof multer.MulterError && err.code === "LIMIT_FILE_SIZE") {
next(new ValidationError({ message: 'Gli allegati possono avere come dimensione massima 20MB', location: err.field}))
} else if (err) {
next(err)
} else {
response_envelope(Attachments.post)(req, res, next)
}
})
}

router.get('/', response_envelope(req => "Hello there!"))
router.get('/proposals', response_envelope(Proposals.index))
router.get('/proposals/:id', response_envelope(Proposals.view))
Expand All @@ -59,7 +81,7 @@ router.get('/comments/:id', response_envelope(Comments.view))
router.post('/comments', response_envelope(Comments.post))
router.get('/attachments', response_envelope(Attachments.index))
router.get('/attachments/:id', response_envelope(Attachments.view))
router.post('/attachments', multer().any(), response_envelope(Attachments.post))
router.post('/attachments', attachmentPost)

router.all(/.*/, response_envelope((req) => {throw new NotFoundError()}))

Expand Down
50 changes: 31 additions & 19 deletions frontend/src/components/CommentWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import React, { useState } from "react";

import { useEngine } from "../modules/engine";

import Attachment from "../models/Attachment";

import { default as BootStrapCard } from 'react-bootstrap/Card';

/**
Expand All @@ -16,42 +18,49 @@ import { default as BootStrapCard } from 'react-bootstrap/Card';
*/
export default function CommentWidget({ afterCommentPost }) {
const engine = useEngine()
// const inserter = engine.useInsert(Comment)
const [status, setStatus] = useState("input")
const attachmentInserter = engine.useMultipartInsert(Attachment)
const [files, setFiles] = useState([])
const [error, setError] = useState({})

function addFile() {
setError({})
let newId = 0
while (files.includes(newId)) {
newId = newId + 1
}
setFiles(files.concat([newId]))
}
function deleteFile(id) {
setError({})
setFiles(files.filter(e => e !== id))
}

async function send() {
setStatus("uploading")
setError({})
const data = new FormData();
for(const id of files) {
for (const id of files) {
// TODO: vedi se c'è un modo migliore di recuperare l'input che non
// sia tramite document.getElementById
const file = document.getElementById(`file-input-${id}`).files[0]
if (file !== undefined) data.append('allegati', file, file.name)
const file = document.getElementById(`allegato-${id}`).files[0]
if (file !== undefined) data.append(`allegato-${id}`, file, file.name)
}
data.append('uploader_id', engine.user.id)

attachmentInserter.mutate(
data,
{
onSuccess: (attachmentIds) => {
engine.flashSuccess("Allegati aggiunti con successo")

// Soluzione temporanea per testare: non verrà utilizzato "fetch"
// così in questo modo nella soluzione finale, probabilmente verrà
// messo un metodo apposito in engine (se non sarà possibile
// utilizzare il già esistente post, ma forse è possibile usare post)
fetch('/api/v0/attachments', {
method: 'POST',
body: data,
})
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => console.error(err));
// TODO: fai insert del commento oltre che degli allegati
},
onError: (err) => {
if (err.code === 403) {
setError(err.issues)
}
}
}
)
}

return <BootStrapCard className="shadow my-2">
Expand All @@ -68,15 +77,18 @@ export default function CommentWidget({ afterCommentPost }) {
<div className="d-flex flex-column">
{
files.map(e => {
return <div key={e} className="d-flex justify-content-between">
<input key={e} id={`file-input-${e}`} className="mb-2" type="file" disabled={status !== "input"} />
return <div key={e} className={`d-flex justify-content-between mb-2 ${error.location === `allegato-${e}` ? "pl-2 border-left-danger" : ""}`}>
<input id={`allegato-${e}`} type="file" onChange={() => setError({})}/>
<span role="button" onClick={() => deleteFile(e)}>
<i className="fas fa-times"></i>
</span>
</div>
})
}
</div>
{error &&
<div className="text-danger">{error.message}</div>
}
<div className="d-flex justify-content-between">
<button className="btn btn-primary" onClick={addFile}>
<i className="fas fa-plus"></i>
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/models/Attachment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Model from './Model'

export default class Attachment extends Model {
static api_url = 'attachments/'
static table_headers = [
{
field: 'filename',
label: "nome file",
enable_sort: true,
enable_link: true
}, {
field: 'size',
label: "dimensione",
enable_sort: true,
}, {
field: 'mimetype',
label: "tipo file",
enable_sort: true,
}]
static sort_default = 'name'
static sort_default_direction = 1
}

25 changes: 15 additions & 10 deletions frontend/src/modules/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@ class BaseRestClient {
}
}

async fetch(uri, method = 'GET', data = null) {
async fetch(uri, method = 'GET', data = null, multipart = false) {
let response = {}
let headers = {'X-CSRF-Token': this.csrf}
let body = null
if (!multipart) {
headers['Content-Type'] = 'application/json'
body = data ? JSON.stringify(data) : null
} else {
body = data
}

try {
const res = await fetch(api_root + uri, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.csrf
},
body: data ? JSON.stringify(data) : null
headers: headers,
body: body
});
response = await res.json();
} catch(err) {
Expand All @@ -49,8 +54,8 @@ class BaseRestClient {
return await this.fetch(uri + '?' + params.toString(), 'GET');
}

async post(uri, data = null) {
return await this.fetch(uri, 'POST', data);
async post(uri, data = null, multipart = false) {
return await this.fetch(uri, 'POST', data, multipart);
}

async patch(uri, data) {
Expand Down Expand Up @@ -96,11 +101,11 @@ class RestClient extends BaseRestClient {
throw new Error("non usare la funzione status(), chiama get(\"status\")");
}

async fetch(uri, method = 'GET', data = null) {
async fetch(uri, method = 'GET', data = null, multipart) {
// genera errori casuali per testarne la gestione
if (false && Math.random()>0.8) throw new ApiError({code:500, message: `fake random error! [${method} ${uri}]`});

const res = await super.fetch(uri, method, data);
const res = await super.fetch(uri, method, data, multipart);
if (res.code < 200 || res.code >= 300) {
throw new ApiError(res, uri, method, data);
}
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/modules/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ export function useCreateEngine() {
},
onError: onPossibleValidationError,
}),

useMultipartInsert: (Model) => useMutation({
mutationFn: async (data) => {
return await api.post(`${Model.api_url}`, data, true)
},
onSuccess: async () => {
//
},
onError: onPossibleValidationError,
}),

useDelete: (Model, id) => useMutation({
mutationFn: async () => {
Expand Down

0 comments on commit c87440e

Please sign in to comment.