From 3b65138337b795875b6ca40292c2bd971c0c9499 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 18 Dec 2023 17:39:26 -0500 Subject: [PATCH 01/12] feat: ajouter `V1DocumentsPOST` request et response --- apirequest/document.go | 63 +++++++++++++++++++++++++++++++++++++++++ apiresponse/document.go | 11 +++++++ 2 files changed, 74 insertions(+) diff --git a/apirequest/document.go b/apirequest/document.go index e4cfa3c..92f317d 100644 --- a/apirequest/document.go +++ b/apirequest/document.go @@ -12,6 +12,69 @@ import ( "git.agecem.com/agecem/agecem-org/apiresponse" ) +var _ request.Requester[apiresponse.V1DocumentsPOST] = V1DocumentsPOST{} + +type V1DocumentsPOST struct { + Data struct { + Documents []*multipart.FileHeader `json:"documents"` + } + Params struct { + Bucket string `json:"bucket"` + } +} + +func NewV1DocumentsPOST(bucket string, documents ...*multipart.FileHeader) (request V1DocumentsPOST, err error) { + if bucket == "" { + err = fmt.Errorf("NewV1DocumentsPOST requires non-nil bucket name") + return + } + + request.Params.Bucket = bucket + + if documents == nil { + err = fmt.Errorf("NewV1DocumentsPOST requires non-nil documents") + return + } + + for _, document := range documents { + if document == nil { + err = fmt.Errorf("NewV1DocumentsPOST requires non-nil documents") + return + } + } + + request.Data.Documents = documents + + return +} + +func (request V1DocumentsPOST) Complete() bool { + if request.Data.Documents == nil { + return false + } + for _, document := range request.Data.Documents { + if document == nil { + return false + } + } + + return request.Params.Bucket != "" +} + +func (request V1DocumentsPOST) Request(v *voki.Voki) (response apiresponse.V1DocumentsPOST, err error) { + if !request.Complete() { + err = fmt.Errorf("Incomplete V1DocumentsPOST request") + return + } + + var buf bytes.Buffer + if err = json.NewEncoder(&buf).Encode(request.Data); err != nil { + return + } + + return response, v.UnmarshalIfComplete(http.MethodPost, fmt.Sprintf("/v1/bucket/%s/many", request.Params.Bucket), &buf, true, &response) +} + var _ request.Requester[apiresponse.V1DocumentPOST] = V1DocumentPOST{} type V1DocumentPOST struct { diff --git a/apiresponse/document.go b/apiresponse/document.go index beab0ae..d02fe83 100644 --- a/apiresponse/document.go +++ b/apiresponse/document.go @@ -1,5 +1,16 @@ package apiresponse +type V1DocumentsPOST struct { + Response + Data struct { + Bucket string + Documents struct { + Key string + Size int64 + } + } +} + type V1DocumentPOST struct { Response Data struct { From 24855a41151d1dff635e07a24550ceeec33a34a6 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 18 Dec 2023 17:40:37 -0500 Subject: [PATCH 02/12] wip: ajouter `MediaClient.UploadFormFiles` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Une fois terminé, permettra de téléverser plusieurs documents à la fois dans un même bucket. Pour l'instant, est capable de recevoir des `[]*multipart.FileHeader` et de valider leur `Content-Type` selon une liste prédéfinie (pdf, markdown et plain text). Valide aussi que les pointeurs mémoires sont non-nil. Retourne `http.StatusCreated` ainsi que la liste des documents validés qui doivent être insérés. Le travail restant est écrit en commentaires TODO --- media/media.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/media/media.go b/media/media.go index b5cb765..7f295d4 100644 --- a/media/media.go +++ b/media/media.go @@ -3,6 +3,10 @@ package media import ( "context" "errors" + "fmt" + "mime" + "mime/multipart" + "net/http" "git.agecem.com/agecem/agecem-org/config" "github.com/minio/minio-go/v7" @@ -76,3 +80,57 @@ func (m *MediaClient) Seed() ([]string, error) { return new_buckets, nil } + +func (m *MediaClient) UploadFormFiles(fileHeaders []*multipart.FileHeader) (statusCode int, result string) { + switch count := len(fileHeaders); count { + case 0: + return http.StatusBadRequest, "Veuillez sélectionner au moins 1 document à téléverser" + case 1: + result = "Téléversement de 1 fichier\n" + default: + result = fmt.Sprintf("Téléversement de %d fichiers\n", count) + } + + var allowedMediaTypes = []string{"application/pdf", "text/markdown", "text/plain"} + + var fileNames []string + for _, fileHeader := range fileHeaders { + fileNames = append(fileNames, fileHeader.Filename) + } + + for i, fileHeader := range fileHeaders { + // Check for conflicting file names in upload + for j, fileName := range fileNames { + if fileName == fileHeader.Filename && i != j { + return http.StatusBadRequest, fmt.Sprintf("Doublon de nom de fichier '%s' trouvé, les noms de fichiers doivent être uniques", fileName) + } + } + + //TODO check for conflicting fileNames with existing files + + // Check media type + mediaType, _, err := mime.ParseMediaType(fileHeader.Header.Get("Content-Type")) + if err != nil { + return http.StatusBadRequest, fmt.Sprintf("Impossible de déterminer le type de fichier pour %d '%s'.\nPlus de détails: %s", i, fileHeader.Filename, err.Error()) + } + + var isAllowedMediaType bool + + for _, allowedMediaType := range allowedMediaTypes { + if allowedMediaType == mediaType { + isAllowedMediaType = true + } + } + + if !isAllowedMediaType { + return http.StatusBadRequest, fmt.Sprintf("Type de fichier interdit '%s' pour '%s'.\nTypes de fichiers permis: %s", mediaType, fileHeader.Filename, allowedMediaTypes) + } + + result = fmt.Sprintf("%sDocument %d '%s' est de type '%s'\n", + result, i, fileHeader.Filename, mediaType) + + //TODO Upload file + } + + return http.StatusCreated, result +} From d3ad8f4a91db616dfa92aa360da83fe56b3b4074 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 18 Dec 2023 17:44:57 -0500 Subject: [PATCH 03/12] wip: ajouter handler `V1DocumentsPOST` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contient déjà la plupart de la logique autour de `mediaclient.(*MediaClient).UploadFormFiles` --- apihandler/document.go | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/apihandler/document.go b/apihandler/document.go index 844a114..08b2e52 100644 --- a/apihandler/document.go +++ b/apihandler/document.go @@ -2,6 +2,7 @@ package apihandler import ( "context" + "fmt" "net/http" "git.agecem.com/agecem/agecem-org/apirequest" @@ -10,6 +11,51 @@ import ( "github.com/minio/minio-go/v7" ) +/* +V1DocumentPOST permet d'ajouter un object dans un bucket, par multipart/form-data + +WIP +*/ +func (h *V1Handler) V1DocumentsPOST(c echo.Context) (err error) { + var request apirequest.V1DocumentsPOST + var response apiresponse.V1DocumentsPOST + + request.Params.Bucket = c.Param("bucket") + + form, err := c.MultipartForm() + if err != nil { + response.StatusCode = http.StatusBadRequest + response.Message = fmt.Sprintf("Téléversement invalide: %s", err) + + return c.JSON(response.StatusCode, response) + } + if form == nil { + return c.JSON(http.StatusBadRequest, "MultipartForm pointer vers une addresse mémoire nil") + } + + if len(form.File) == 0 { + return c.JSON(http.StatusBadRequest, "Veuillez sélectionner au moins 1 document à téléverser") + } + + for inputName, inputFileHeaders := range form.File { + if inputName == "documents" { + request.Data.Documents = inputFileHeaders + } + } + + if request.Data.Documents == nil { + return c.JSON(http.StatusBadRequest, "Impossible d'obtenir les documents depuis le formulaire") + } + + if !request.Complete() { + return c.JSON(http.StatusBadRequest, "Requête V1DocumentsPOST incomplète reçue") + } + + response.StatusCode, response.Message = h.MediaClient.UploadFormFiles(request.Data.Documents) + + return c.JSON(response.StatusCode, response.Message) +} + // V1DocumentPOST permet d'ajouter un object dans un bucket, par multipart/form-data func (h *V1Handler) V1DocumentPOST(c echo.Context) (err error) { var request apirequest.V1DocumentPOST From ff3e81145749b8b1db06e861f44a728e21ed6195 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 18 Dec 2023 17:46:31 -0500 Subject: [PATCH 04/12] feat: Ajouter route `/bucket/:bucket/many` pour `V1DocumentsPOST` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nécessite plus de travail sur les fonctions sous-jacentes, mais la route devrait au moins maintenant exister. Sera éventuellement utilisée pour remplacer `/bucket/:bucket`. TODO Tests nécessaires. --- cmd/server.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/server.go b/cmd/server.go index 5f61cd0..fc7f54b 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -242,6 +242,12 @@ func RunServer() { log.Fatal(err) } + if err := pave.EchoRegister[ + apirequest.V1DocumentsPOST, + apiresponse.V1DocumentsPOST](groupV1, &p, "/v1", http.MethodPost, "/bucket/:bucket/many", "Upload documents to specified bucket", "V1DocumentsPOST", v1Handler.V1DocumentsPOST); err != nil { + log.Fatal(err) + } + if err := pave.EchoRegister[ apirequest.V1DocumentPOST, apiresponse.V1DocumentPOST](groupV1, &p, "/v1", http.MethodPost, "/bucket/:bucket", "Upload document to specified bucket", "V1DocumentPOST", v1Handler.V1DocumentPOST); err != nil { From 3d911f3a3711cc6f8c9110d0e566dc85a01716b5 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:32:03 -0500 Subject: [PATCH 05/12] refactor(apirequest): deprecate V1DocumentPOST et son constructeur --- apirequest/document.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apirequest/document.go b/apirequest/document.go index 92f317d..b5c1191 100644 --- a/apirequest/document.go +++ b/apirequest/document.go @@ -77,6 +77,7 @@ func (request V1DocumentsPOST) Request(v *voki.Voki) (response apiresponse.V1Doc var _ request.Requester[apiresponse.V1DocumentPOST] = V1DocumentPOST{} +// Deprecated: Use V1DocumentsPOST instead type V1DocumentPOST struct { Data struct { Document *multipart.FileHeader `document` @@ -86,6 +87,7 @@ type V1DocumentPOST struct { } } +// Deprecated: Use NewV1DocumentsPOST instead func NewV1DocumentPOST(bucket string, document *multipart.FileHeader) (request V1DocumentPOST, err error) { if bucket == "" { err = fmt.Errorf("NewV1DocumentPOST requires non-nil bucket name") From becd5659f3850ca5039da32ad25d67f2005222ce Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:36:58 -0500 Subject: [PATCH 06/12] refactor(apiresponse): deprecate V1DocumentPOST --- apiresponse/document.go | 1 + 1 file changed, 1 insertion(+) diff --git a/apiresponse/document.go b/apiresponse/document.go index d02fe83..c775da8 100644 --- a/apiresponse/document.go +++ b/apiresponse/document.go @@ -11,6 +11,7 @@ type V1DocumentsPOST struct { } } +// Deprecated: Use V1DocumentsPOST instead type V1DocumentPOST struct { Response Data struct { From 8a35f96999d7dda410ecd9e8b8475b363475e239 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:37:36 -0500 Subject: [PATCH 07/12] =?UTF-8?q?refactor(apiresponse):=20d=C3=A9placer=20?= =?UTF-8?q?d=C3=A9finition=20de=20document=20=C3=A0=20`DataDocument`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apiresponse/document.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apiresponse/document.go b/apiresponse/document.go index c775da8..b6518b9 100644 --- a/apiresponse/document.go +++ b/apiresponse/document.go @@ -1,13 +1,15 @@ package apiresponse +type DataDocument struct { + Key string + Size int64 +} + type V1DocumentsPOST struct { Response Data struct { Bucket string - Documents struct { - Key string - Size int64 - } + Documents []DataDocument } } @@ -16,8 +18,7 @@ type V1DocumentPOST struct { Response Data struct { Bucket string - Key string - Size int64 + DataDocument } } From 220da4d695ee909f27b7ef4aae3d7c31f2757080 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:38:35 -0500 Subject: [PATCH 08/12] =?UTF-8?q?fix(media):=20finaliser=20impl=C3=A9menta?= =?UTF-8?q?tion=20de=20`UploadFormFiles`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- media/media.go | 63 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/media/media.go b/media/media.go index 7f295d4..f023612 100644 --- a/media/media.go +++ b/media/media.go @@ -81,7 +81,18 @@ func (m *MediaClient) Seed() ([]string, error) { return new_buckets, nil } -func (m *MediaClient) UploadFormFiles(fileHeaders []*multipart.FileHeader) (statusCode int, result string) { +func (m *MediaClient) UploadFormFiles(bucketName string, fileHeaders []*multipart.FileHeader) (statusCode int, result string) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ok, err := m.MinioClient.BucketExists(ctx, bucketName) + if err != nil { + return http.StatusInternalServerError, fmt.Sprintf("Erreur lors de vérification d'existence de bucket '%s': %s", bucketName, err) + } + if !ok { + return http.StatusBadRequest, fmt.Sprintf("Bucket '%s' n'existe pas", bucketName) + } + switch count := len(fileHeaders); count { case 0: return http.StatusBadRequest, "Veuillez sélectionner au moins 1 document à téléverser" @@ -98,6 +109,8 @@ func (m *MediaClient) UploadFormFiles(fileHeaders []*multipart.FileHeader) (stat fileNames = append(fileNames, fileHeader.Filename) } + var validFileHeaders []*multipart.FileHeader + for i, fileHeader := range fileHeaders { // Check for conflicting file names in upload for j, fileName := range fileNames { @@ -106,8 +119,6 @@ func (m *MediaClient) UploadFormFiles(fileHeaders []*multipart.FileHeader) (stat } } - //TODO check for conflicting fileNames with existing files - // Check media type mediaType, _, err := mime.ParseMediaType(fileHeader.Header.Get("Content-Type")) if err != nil { @@ -123,13 +134,51 @@ func (m *MediaClient) UploadFormFiles(fileHeaders []*multipart.FileHeader) (stat } if !isAllowedMediaType { - return http.StatusBadRequest, fmt.Sprintf("Type de fichier interdit '%s' pour '%s'.\nTypes de fichiers permis: %s", mediaType, fileHeader.Filename, allowedMediaTypes) + return http.StatusUnsupportedMediaType, fmt.Sprintf("Type de fichier interdit '%s' pour '%s'.\nTypes de fichiers permis: %s", mediaType, fileHeader.Filename, allowedMediaTypes) } - result = fmt.Sprintf("%sDocument %d '%s' est de type '%s'\n", - result, i, fileHeader.Filename, mediaType) + // Check for conflicting fileNames with existing files + objectInfo, err := m.MinioClient.StatObject(ctx, bucketName, fileHeader.Filename, minio.StatObjectOptions{}) + if err == nil && objectInfo.Key == fileHeader.Filename { + return http.StatusConflict, fmt.Sprintf("Un document au nom '%s' de catégorie '%s' existe déjà et ne peut pas être inséré de cette façon.", fileHeader.Filename, bucketName) + } - //TODO Upload file + switch msg := err.Error(); msg { + case "The specified key does not exist.": + default: + return http.StatusInternalServerError, fmt.Sprintf("Erreur inattendue lors de vérification de conflit de nom de fichier avec la base de données: %s", err) + } + + validFileHeaders = append(validFileHeaders, fileHeader) + } + + if len(validFileHeaders) == 0 { + return http.StatusOK, "Aucun fichier valide envoyé au serveur, rien à faire." + } + + for i, fileHeader := range validFileHeaders { + mediaType, _, err := mime.ParseMediaType(fileHeader.Header.Get("Content-Type")) + if err != nil { + return http.StatusBadRequest, fmt.Sprintf("Impossible de déterminer le type de fichier pour %d '%s'.\nPlus de détails: %s", i, fileHeader.Filename, err.Error()) + } + + // Get file content + fileContent, err := fileHeader.Open() + if err != nil { + return http.StatusBadRequest, fmt.Sprintf("Impossible de lire le contenu de '%s': %s", fileHeader.Filename, err) + } + defer fileContent.Close() + + // Upload file + info, err := m.MinioClient.PutObject(ctx, bucketName, fileHeader.Filename, fileContent, fileHeader.Size, minio.PutObjectOptions{ + ContentType: mediaType, + }) + if err != nil { + return http.StatusInternalServerError, fmt.Sprintf("Impossible d'ajouter '%s' à la base de donnée: %s", fileHeader.Filename, err) + } + + result = fmt.Sprintf("%sDocument %d '%s' de type '%s' et de taille '%d' téléversé à '%s' avec succès\n", + result, i, info.Key, mediaType, info.Size, info.Bucket) } return http.StatusCreated, result From 92886a8b8b7e6189a7e8bfe6877cccee3e2f7283 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:40:58 -0500 Subject: [PATCH 09/12] =?UTF-8?q?fix(apihandler):=20finaliser=20impl=C3=A9?= =?UTF-8?q?mentation=20de=20`V1DocumentsPOST`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apihandler/document.go | 45 +++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/apihandler/document.go b/apihandler/document.go index 08b2e52..3f4a2ae 100644 --- a/apihandler/document.go +++ b/apihandler/document.go @@ -12,9 +12,13 @@ import ( ) /* -V1DocumentPOST permet d'ajouter un object dans un bucket, par multipart/form-data +V1DocumentsPOST permet d'ajouter un object dans un bucket, par multipart/form-data -WIP +Example: + +Téléverser plusieurs fichiers à cette route avec `curl`: + + curl -F 'documents=@example.pdf' -F 'documents=@example.md;type=text/markdown' */ func (h *V1Handler) V1DocumentsPOST(c echo.Context) (err error) { var request apirequest.V1DocumentsPOST @@ -22,19 +26,36 @@ func (h *V1Handler) V1DocumentsPOST(c echo.Context) (err error) { request.Params.Bucket = c.Param("bucket") + var allowed bool + for allowedBucket := range h.Config.Server.Documents.Buckets { + if request.Params.Bucket == allowedBucket { + allowed = true + } + } + + if !allowed { + return c.JSON(apiresponse.NotFoundResponse()) + } + form, err := c.MultipartForm() if err != nil { - response.StatusCode = http.StatusBadRequest response.Message = fmt.Sprintf("Téléversement invalide: %s", err) + response.StatusCode = http.StatusBadRequest return c.JSON(response.StatusCode, response) } if form == nil { - return c.JSON(http.StatusBadRequest, "MultipartForm pointer vers une addresse mémoire nil") + response.Message = "MultipartForm pointe vers une addresse mémoire nil" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) } if len(form.File) == 0 { - return c.JSON(http.StatusBadRequest, "Veuillez sélectionner au moins 1 document à téléverser") + response.Message = "Veuillez sélectionner au moins 1 document à téléverser" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) } for inputName, inputFileHeaders := range form.File { @@ -44,16 +65,22 @@ func (h *V1Handler) V1DocumentsPOST(c echo.Context) (err error) { } if request.Data.Documents == nil { - return c.JSON(http.StatusBadRequest, "Impossible d'obtenir les documents depuis le formulaire") + response.Message = "Impossible d'obtenir les documents depuis le formulaire" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) } if !request.Complete() { - return c.JSON(http.StatusBadRequest, "Requête V1DocumentsPOST incomplète reçue") + response.Message = "Requête V1DocumentsPOST incomplète reçue" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) } - response.StatusCode, response.Message = h.MediaClient.UploadFormFiles(request.Data.Documents) + response.StatusCode, response.Message = h.MediaClient.UploadFormFiles(request.Params.Bucket, request.Data.Documents) - return c.JSON(response.StatusCode, response.Message) + return c.JSON(response.StatusCode, response) } // V1DocumentPOST permet d'ajouter un object dans un bucket, par multipart/form-data From 8f185c8e8508083b8412dd2b40e3bf56883d11fc Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:42:08 -0500 Subject: [PATCH 10/12] feat(api): ajouter `UploadDocuments` pour `V1DocumentsPOST` --- api/api.go | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/api/api.go b/api/api.go index c1e49e3..06bda1a 100644 --- a/api/api.go +++ b/api/api.go @@ -36,6 +36,80 @@ func New(client *http.Client, host, key string, port int, protocol string) (*API return &API{Voki: voki.New(client, host, key, port, protocol)}, nil } +func (a *API) UploadDocuments(bucketName string, fileHeaders ...*multipart.FileHeader) (response apiresponse.V1DocumentsPOST, err error) { + if count := len(fileHeaders); count == 0 { + err = fmt.Errorf("api.(*API).UploadDocuments requiert au moins 1 fichier") + return + } + + endpoint := fmt.Sprintf("%s://%s:%d/v1/bucket/%s/many", + a.Voki.Protocol, + a.Voki.Host, + a.Voki.Port, + bucketName, + ) + + // Create new multipart writer + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Add files to the request + for i, fileHeader := range fileHeaders { + if fileHeader == nil { + return response, fmt.Errorf("Fichier %d pointe vers une addresse mémoire nulle", i) + } + + file, err := fileHeader.Open() + if err != nil { + return response, fmt.Errorf("Impossible de lire le contenu du fichier %d '%s': %s", i, fileHeader.Filename, err) + } + defer file.Close() + + fileName, err := url.QueryUnescape(fileHeader.Filename) + if err != nil { + return response, fmt.Errorf("Fichier %d '%s' a un nom invalide et impossible à convertir: %s", i, fileHeader.Filename, err) + } + + part, err := writer.CreatePart(fileHeader.Header) + if err != nil { + return response, fmt.Errorf("Impossible d'ajouter %d '%s' au formulaire de téléversement: %s", i, fileName, err) + } + + _, err = io.Copy(part, file) + if err != nil { + return response, fmt.Errorf("Impossible d'ajouter le contenu de %d '%s' au formulaire de téléversement: %s", i, fileName, err) + } + } + + if err := writer.Close(); err != nil { + return response, fmt.Errorf("Impossible de fermer le io.Writer: %s", err) + } + + req, err := http.NewRequest(http.MethodPost, endpoint, body) + if err != nil { + return response, fmt.Errorf("Impossible de produire une requête: %s", err) + } + + if err := req.ParseForm(); err != nil { + return response, fmt.Errorf("Impossible de parse le formulaire: %s", err) + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + + if a.Voki.Key != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.Voki.Key)) + } + + // Send the HTTP request + httpResponse, err := a.Voki.Client.Do(req) + if err != nil { + return response, fmt.Errorf("Impossible d'exécuter la requête http: %s", err) + } + defer httpResponse.Body.Close() + + return response, json.NewDecoder(httpResponse.Body).Decode(&response) +} + func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (apiresponse.V1DocumentPOST, error) { var response apiresponse.V1DocumentPOST endpoint := fmt.Sprintf("%s://%s:%d", From f46dc54aafcb648e557b15c1d3ba032c67d78055 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:44:25 -0500 Subject: [PATCH 11/12] =?UTF-8?q?feat(web):=20permettre=20=C3=A0=20`admin-?= =?UTF-8?q?upload`=20de=20t=C3=A9l=C3=A9verser=20plusieurs=20fichiers=20?= =?UTF-8?q?=C3=A0=20la=20fois?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seuls les fichiers pdf, markdown et plain text sont permis. --- templates/html/admin-upload.html | 7 +++- webhandler/webhandler.go | 66 +++++++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/templates/html/admin-upload.html b/templates/html/admin-upload.html index aee3a6c..11b45a7 100644 --- a/templates/html/admin-upload.html +++ b/templates/html/admin-upload.html @@ -15,7 +15,12 @@

Document à téléverser

- +
diff --git a/webhandler/webhandler.go b/webhandler/webhandler.go index 6d945c7..bf6ba3b 100644 --- a/webhandler/webhandler.go +++ b/webhandler/webhandler.go @@ -8,6 +8,7 @@ import ( "sort" "git.agecem.com/agecem/agecem-org/api" + "git.agecem.com/agecem/agecem-org/apirequest" "git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/models" "git.agecem.com/agecem/agecem-org/webresponse" @@ -155,6 +156,7 @@ func (h *WebHandler) HandleAdminDocumentsUpload(c echo.Context) error { } func (h *WebHandler) HandleAdminDocumentsUploadPOST(c echo.Context) error { + var request apirequest.V1DocumentsPOST var response webresponse.HandleAdminDocumentsUploadResponse v1BucketsGET, err := h.ApiClient.ListBuckets() @@ -173,35 +175,79 @@ func (h *WebHandler) HandleAdminDocumentsUploadPOST(c echo.Context) error { }) } - bucket := c.FormValue("bucket") + request.Params.Bucket = c.FormValue("bucket") - document, err := c.FormFile("document") + form, err := c.MultipartForm() if err != nil { response.StatusCode = http.StatusBadRequest - response.Message = "Formulaire invalide" + response.Message = "Impossible de téléverser" response.Error = err.Error() return c.Render(response.StatusCode, "admin-upload-html", response) } + if form == nil { + response.StatusCode = http.StatusInternalServerError + response.Message = "Formulaire pointe vers une addresse mémoire nulle" + response.Error = "Formulaire pointe vers une addresse mémoire nulle" - uploadDocumentResponse, err := h.ApiClient.UploadDocument(bucket, document) + return c.Render(response.StatusCode, "admin-upload-html", response) + } + + if len(form.File) == 0 { + response.StatusCode = http.StatusBadRequest + response.Message = "Veuillez sélectionner au moins 1 fichier à téléverser" + response.Error = "Input 'documents' ne contient aucun fichier" + + return c.Render(response.StatusCode, "admin-upload-html", response) + } + + for inputName, inputFileHeaders := range form.File { + if inputName == "documents" { + request.Data.Documents = inputFileHeaders + break + } + } + + if request.Data.Documents == nil { + response.StatusCode = http.StatusBadRequest + response.Message = "Impossible d'obtenir les documents depuis le formulaire" + response.Error = "Impossible d'obtenir les documents depuis le formulaire" + + return c.Render(response.StatusCode, "admin-upload-html", response) + } + + uploadDocumentsResponse, err := h.ApiClient.UploadDocuments(request.Params.Bucket, request.Data.Documents...) if err != nil { - response.StatusCode = uploadDocumentResponse.StatusCode - response.Message = uploadDocumentResponse.Message - response.Error = err.Error() + //TODO figure out pourquoi `err` n'est jamais `nil` + response.StatusCode = uploadDocumentsResponse.StatusCode + response.Message = uploadDocumentsResponse.Message + response.Error = fmt.Sprintf("%s. Détails: %s", err.Error(), uploadDocumentsResponse.Error) + /* + response.StatusCode = http.StatusInternalServerError + response.Message = fmt.Sprintf("api.(*API).UploadDocuments: %s", err) + response.Error = err.Error() + */ return c.Render(response.StatusCode, "admin-upload-html", response) } + //TODO figure out pourquoi on se rend jamais ici + // Format response var info, status string - info = fmt.Sprintf("[%d] /public/documentation/%s/%s", uploadDocumentResponse.Data.Size, uploadDocumentResponse.Data.Bucket, uploadDocumentResponse.Data.Key) + for i, document := range uploadDocumentsResponse.Data.Documents { + info = fmt.Sprintf("%s[%d] /public/documentation/%s/%s (%dk) ok\n", + info, i, uploadDocumentsResponse.Data.Bucket, document.Key, document.Size) + } - status = uploadDocumentResponse.Message + status = uploadDocumentsResponse.Message + if errMsg := uploadDocumentsResponse.Error; errMsg != "" { + status = fmt.Sprintf("%s. Erreur: %s", status, errMsg) + } response.StatusCode = http.StatusOK - response.Message = fmt.Sprintf("%s - %s", status, info) + response.Message = fmt.Sprintf("%s \n %s", status, info) return c.Render(response.StatusCode, "admin-upload-html", response) } From c5443403244054ec400f67db2f70fda74eafdf5b Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 19 Dec 2023 19:49:37 -0500 Subject: [PATCH 12/12] fix(web): pluraliser module d'ajout de documents --- templates/html/admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/html/admin.html b/templates/html/admin.html index c424ee4..3a5e0c8 100644 --- a/templates/html/admin.html +++ b/templates/html/admin.html @@ -12,7 +12,7 @@

Admin

-