diff --git a/Dockerfile b/Dockerfile index 3b53c4a..6210ca7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,8 @@ ADD api/ api/ ADD apihandler/ apihandler/ +ADD apirequest/ apirequest/ + ADD apiresponse/ apiresponse/ ADD config/ config/ diff --git a/api/api.go b/api/api.go index 9e8fbf7..c1e49e3 100644 --- a/api/api.go +++ b/api/api.go @@ -10,6 +10,7 @@ import ( "net/url" "codeberg.org/vlbeaudoin/voki" + "git.agecem.com/agecem/agecem-org/apirequest" "git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/config" "github.com/spf13/viper" @@ -35,8 +36,8 @@ 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) UploadDocument(bucket string, file_header *multipart.FileHeader) (apiresponse.V1DocumentCreateResponse, error) { - var response apiresponse.V1DocumentCreateResponse +func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (apiresponse.V1DocumentPOST, error) { + var response apiresponse.V1DocumentPOST endpoint := fmt.Sprintf("%s://%s:%d", a.Voki.Protocol, a.Voki.Host, @@ -100,6 +101,15 @@ func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) ( return response, err } -func (a *API) ListBuckets() (response apiresponse.V1BucketListResponse, err error) { +func (a *API) ListBuckets() (response apiresponse.V1BucketsGET, err error) { return response, a.Voki.Unmarshal(http.MethodGet, "/v1/bucket", nil, true, &response) } + +func (a *API) Seed() (response apiresponse.V1SeedPOST, err error) { + request, err := apirequest.NewV1SeedPOST() + if err != nil { + return + } + + return request.Request(a.Voki) +} diff --git a/apihandler/apihandler.go b/apihandler/apihandler.go index d3d170f..0dac7db 100644 --- a/apihandler/apihandler.go +++ b/apihandler/apihandler.go @@ -1,313 +1,27 @@ package apihandler import ( - "context" "net/http" "sort" - "git.agecem.com/agecem/agecem-org/apiresponse" + "codeberg.org/vlbeaudoin/pave" "git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/media" "github.com/labstack/echo/v4" - "github.com/minio/minio-go/v7" ) type V1Handler struct { Config config.Config MediaClient *media.MediaClient + Pave *pave.Pave } // API Handlers -// HandleV1 affiche les routes accessibles. +// V1GET affiche les routes accessibles. // Les routes sont triées selon .Path, pour les rendre plus facilement navigables. -func (h *V1Handler) HandleV1(c echo.Context) error { +func (h *V1Handler) V1GET(c echo.Context) error { routes := c.Echo().Routes() sort.Slice(routes, func(i, j int) bool { return routes[i].Path < routes[j].Path }) return c.JSON(http.StatusOK, routes) } - -// HandleV1Seed créé des buckets dans minio selon la liste de buckets dans server.documents.buckets -// Les buckets sont créés avec paramètres par défaut, et sont ensuite visible dans /v1/bucket. -func (h *V1Handler) HandleV1Seed(c echo.Context) error { - var response apiresponse.V1SeedResponse - - new_buckets, err := h.MediaClient.Seed() - response.Data.Buckets = new_buckets - if err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Error during mediaClient.Seed()" - response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - - if len(new_buckets) == 0 { - response.Message = "All buckets already exist" - - } else { - response.Message = "Buckets successfully created" - } - - response.StatusCode = http.StatusOK - - return c.JSON(response.StatusCode, response) -} - -// HandleV1BucketList affiche les buckets permis par server.documents.buckets, qui existent. -func (h *V1Handler) HandleV1BucketList(c echo.Context) error { - var response apiresponse.V1BucketListResponse - - var buckets = make(map[string]string) - - for bucket_name, bucket_display_name := range h.Config.Server.Documents.Buckets { - exists, err := h.MediaClient.MinioClient.BucketExists(context.Background(), bucket_name) - if err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Error during minio#BucketExists" - // response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - - if exists { - buckets[bucket_name] = bucket_display_name - } - } - - response.StatusCode = http.StatusOK - response.Message = "Buckets list successful" - response.Data.Buckets = buckets - - return c.JSON(response.StatusCode, response) -} - -func (h *V1Handler) HandleV1BucketRead(c echo.Context) error { - var response apiresponse.V1BucketReadResponse - - bucket := c.Param("bucket") - - allowed := false - for bucket_allowed := range h.Config.Server.Documents.Buckets { - if bucket == bucket_allowed { - allowed = true - } - } - - if !allowed { - return c.JSON(apiresponse.NotFoundResponse()) - } - - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - exists, err := h.MediaClient.MinioClient.BucketExists(ctx, bucket) - if err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Error during minio#BucketExists" - response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - - if !exists { - return c.JSON(apiresponse.NotFoundResponse()) - } - - objectCh := h.MediaClient.MinioClient.ListObjects(ctx, bucket, minio.ListObjectsOptions{}) - for object := range objectCh { - if object.Err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Error during minio#ListObjects" - //TODO make sure this is safe - //response.Error = object.Err.Error() - - return c.JSON(response.StatusCode, response) - } - - response.Data.Keys = append(response.Data.Keys, object.Key) - } - - response.StatusCode = http.StatusOK - response.Message = "V1BucketRead ok" - - return c.JSON(response.StatusCode, response) -} - -// HandleV1DocumentCreate permet d'ajouter un object dans un bucket, par multipart/form-data -func (h *V1Handler) HandleV1DocumentCreate(c echo.Context) error { - var response apiresponse.V1DocumentCreateResponse - - bucket := c.Param("bucket") - - form_file, err := c.FormFile("document") - if err != nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Error during HandleV1DocumentCreate's echo#Context.FormFile" - response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - - allowed := false - for bucket_allowed := range h.Config.Server.Documents.Buckets { - if bucket == bucket_allowed { - allowed = true - } - } - - if !allowed { - return c.JSON(apiresponse.NotFoundResponse()) - } - - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - src, err := form_file.Open() - if err != nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Error during form_file.Open()" - response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - defer src.Close() - - info, err := h.MediaClient.MinioClient.PutObject(ctx, bucket, form_file.Filename, src, form_file.Size, minio.PutObjectOptions{ - ContentType: form_file.Header.Get("Content-Type"), - }) - if err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Error during minio#PutObject" - //response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - - response.StatusCode = http.StatusOK - response.Message = "ok" - response.Data.Bucket = info.Bucket - response.Data.Key = info.Key - response.Data.Size = info.Size - - return c.JSON(response.StatusCode, response) -} - -// HandleV1DocumentRead permet de lire le contenu d'un fichier et protentiellement de le télécharger -func (h *V1Handler) HandleV1DocumentRead(c echo.Context) error { - bucket := c.Param("bucket") - document := c.Param("document") - - allowed := false - for bucket_allowed := range h.Config.Server.Documents.Buckets { - if bucket == bucket_allowed { - allowed = true - } - } - - if !allowed { - return c.JSON(apiresponse.NotFoundResponse()) - } - - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - bucket_exists, err := h.MediaClient.MinioClient.BucketExists(ctx, bucket) - if err != nil { - return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists") - } - - if !bucket_exists { - return c.JSON(apiresponse.NotFoundResponse()) - } - - document_info, err := h.MediaClient.MinioClient.StatObject(ctx, bucket, document, minio.StatObjectOptions{}) - - if err != nil { - if err.Error() == "The specified key does not exist." { - - return c.JSON(apiresponse.NotFoundResponse()) - } - - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "message": "Error during minio#StatObject", - }) - } - - _ = document_info - - document_object, err := h.MediaClient.MinioClient.GetObject(ctx, bucket, document, minio.GetObjectOptions{}) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{ - "message": "Error during minio#GetObject", - }) - } - - defer document_object.Close() - - return c.Stream(http.StatusOK, document_info.ContentType, document_object) -} - -// HandleV1DocumentUpdate permet de mettre à jour certains champs d'un object, comme le Content-Type ou le Filename -func (h *V1Handler) HandleV1DocumentUpdate(c echo.Context) error { - return c.JSON(apiresponse.NotImplementedResponse()) -} - -// HandleV1DocumentDelete permet de supprimer un object -func (h *V1Handler) HandleV1DocumentDelete(c echo.Context) error { - bucket := c.Param("bucket") - document := c.Param("document") - - allowed := false - for bucket_allowed := range h.Config.Server.Documents.Buckets { - if bucket == bucket_allowed { - allowed = true - } - } - - if !allowed { - return c.JSON(apiresponse.NotFoundResponse()) - } - - ctx, cancel := context.WithCancel(context.Background()) - - defer cancel() - - bucket_exists, err := h.MediaClient.MinioClient.BucketExists(ctx, bucket) - if err != nil { - return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists") - } - - if !bucket_exists { - return c.JSON(apiresponse.NotFoundResponse()) - } - - document_info, err := h.MediaClient.MinioClient.StatObject(ctx, bucket, document, minio.StatObjectOptions{}) - if err != nil { - if err.Error() == "The specified key does not exist." { - - return c.JSON(apiresponse.NotFoundResponse()) - } - - return c.JSON(http.StatusInternalServerError, map[string]interface{}{ - "message": "Error during minio#StatObject", - }) - } - - //TODO Add error validation - _ = document_info - - err = h.MediaClient.MinioClient.RemoveObject(ctx, bucket, document, minio.RemoveObjectOptions{}) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{ - "message": "Error during minio#RemoveObject", - }) - } - - return c.JSON(http.StatusOK, map[string]string{ - "message": "Document deleted", - }) -} diff --git a/apihandler/bucket.go b/apihandler/bucket.go new file mode 100644 index 0000000..04818f6 --- /dev/null +++ b/apihandler/bucket.go @@ -0,0 +1,108 @@ +package apihandler + +import ( + "context" + "net/http" + + "git.agecem.com/agecem/agecem-org/apirequest" + "git.agecem.com/agecem/agecem-org/apiresponse" + "github.com/labstack/echo/v4" + "github.com/minio/minio-go/v7" +) + +// V1BucketsGET affiche les buckets permis par server.documents.buckets, qui existent. +func (h *V1Handler) V1BucketsGET(c echo.Context) error { + var request apirequest.V1BucketsGET + var response apiresponse.V1BucketsGET + + if !request.Complete() { + response.Message = "Incomplete V1BucketsGET request received" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) + } + + var buckets = make(map[string]string) + + for bucket_name, bucket_display_name := range h.Config.Server.Documents.Buckets { + exists, err := h.MediaClient.MinioClient.BucketExists(context.Background(), bucket_name) + if err != nil { + response.StatusCode = http.StatusInternalServerError + response.Message = "Error during minio#BucketExists" + // response.Error = err.Error() + + return c.JSON(response.StatusCode, response) + } + + if exists { + buckets[bucket_name] = bucket_display_name + } + } + + response.StatusCode = http.StatusOK + response.Message = "Buckets list successful" + response.Data.Buckets = buckets + + return c.JSON(response.StatusCode, response) +} + +func (h *V1Handler) V1BucketGET(c echo.Context) error { + var request apirequest.V1BucketGET + var response apiresponse.V1BucketGET + + request.Params.Bucket = c.Param("bucket") + + if !request.Complete() { + response.Message = "Incomplete V1BucketGET request received" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) + } + + allowed := false + for bucket_allowed := range h.Config.Server.Documents.Buckets { + if request.Params.Bucket == bucket_allowed { + allowed = true + } + } + + if !allowed { + return c.JSON(apiresponse.NotFoundResponse()) + } + + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + exists, err := h.MediaClient.MinioClient.BucketExists(ctx, request.Params.Bucket) + if err != nil { + response.StatusCode = http.StatusInternalServerError + response.Message = "Error during minio#BucketExists" + response.Error = err.Error() + + return c.JSON(response.StatusCode, response) + } + + if !exists { + return c.JSON(apiresponse.NotFoundResponse()) + } + + objectCh := h.MediaClient.MinioClient.ListObjects(ctx, request.Params.Bucket, minio.ListObjectsOptions{}) + for object := range objectCh { + if object.Err != nil { + response.StatusCode = http.StatusInternalServerError + response.Message = "Error during minio#ListObjects" + //TODO make sure this is safe + //response.Error = object.Err.Error() + + return c.JSON(response.StatusCode, response) + } + + response.Data.Keys = append(response.Data.Keys, object.Key) + } + + response.StatusCode = http.StatusOK + response.Message = "V1BucketRead ok" + + return c.JSON(response.StatusCode, response) +} diff --git a/apihandler/document.go b/apihandler/document.go new file mode 100644 index 0000000..29b70b5 --- /dev/null +++ b/apihandler/document.go @@ -0,0 +1,206 @@ +package apihandler + +import ( + "context" + "net/http" + + "git.agecem.com/agecem/agecem-org/apirequest" + "git.agecem.com/agecem/agecem-org/apiresponse" + "github.com/labstack/echo/v4" + "github.com/minio/minio-go/v7" +) + +// 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 + var response apiresponse.V1DocumentPOST + + request.Data.Bucket = c.Param("bucket") + + request.Data.Document, err = c.FormFile("document") + if err != nil { + response.StatusCode = http.StatusBadRequest + response.Message = "Error during HandleV1DocumentCreate's echo#Context.FormFile" + response.Error = err.Error() + + return c.JSON(response.StatusCode, response) + } + + allowed := false + for bucket_allowed := range h.Config.Server.Documents.Buckets { + if request.Data.Bucket == bucket_allowed { + allowed = true + } + } + + if !allowed { + return c.JSON(apiresponse.NotFoundResponse()) + } + + if !request.Complete() { + response.Message = "Incomplete V1DocumentPOST request received" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) + } + + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + src, err := request.Data.Document.Open() + if err != nil { + response.StatusCode = http.StatusBadRequest + response.Message = "Error during form_file.Open()" + response.Error = err.Error() + + return c.JSON(response.StatusCode, response) + } + defer src.Close() + + info, err := h.MediaClient.MinioClient.PutObject(ctx, request.Data.Bucket, request.Data.Document.Filename, src, request.Data.Document.Size, minio.PutObjectOptions{ + ContentType: request.Data.Document.Header.Get("Content-Type"), + }) + if err != nil { + response.StatusCode = http.StatusInternalServerError + response.Message = "Error during minio#PutObject" + //response.Error = err.Error() + + return c.JSON(response.StatusCode, response) + } + + response.StatusCode = http.StatusOK + response.Message = "ok" + response.Data.Bucket = info.Bucket + response.Data.Key = info.Key + response.Data.Size = info.Size + + return c.JSON(response.StatusCode, response) +} + +// V1DocumentGET permet de lire le contenu d'un fichier et protentiellement de le télécharger +func (h *V1Handler) V1DocumentGET(c echo.Context) error { + bucket := c.Param("bucket") + document := c.Param("document") + + allowed := false + for bucket_allowed := range h.Config.Server.Documents.Buckets { + if bucket == bucket_allowed { + allowed = true + } + } + + if !allowed { + return c.JSON(apiresponse.NotFoundResponse()) + } + + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + bucket_exists, err := h.MediaClient.MinioClient.BucketExists(ctx, bucket) + if err != nil { + return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists") + } + + if !bucket_exists { + return c.JSON(apiresponse.NotFoundResponse()) + } + + document_info, err := h.MediaClient.MinioClient.StatObject(ctx, bucket, document, minio.StatObjectOptions{}) + + if err != nil { + if err.Error() == "The specified key does not exist." { + + return c.JSON(apiresponse.NotFoundResponse()) + } + + return c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "message": "Error during minio#StatObject", + }) + } + + _ = document_info + + document_object, err := h.MediaClient.MinioClient.GetObject(ctx, bucket, document, minio.GetObjectOptions{}) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "message": "Error during minio#GetObject", + }) + } + + defer document_object.Close() + + return c.Stream(http.StatusOK, document_info.ContentType, document_object) +} + +// V1DocumentDELETE permet de supprimer un object +func (h *V1Handler) V1DocumentDELETE(c echo.Context) error { + var request apirequest.V1DocumentDELETE + var response apiresponse.V1DocumentDELETE + + request.Params.Bucket = c.Param("bucket") + request.Params.Document = c.Param("document") + + allowed := false + for bucket_allowed := range h.Config.Server.Documents.Buckets { + if request.Params.Bucket == bucket_allowed { + allowed = true + } + } + + if !allowed { + return c.JSON(apiresponse.NotFoundResponse()) + } + + if !request.Complete() { + response.Message = "Incomplete V1DocumentDELETE request received" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) + } + + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + + bucket_exists, err := h.MediaClient.MinioClient.BucketExists(ctx, request.Params.Bucket) + if err != nil { + return c.JSON(http.StatusInternalServerError, "Error during minio#BucketExists") + } + + if !bucket_exists { + return c.JSON(apiresponse.NotFoundResponse()) + } + + document_info, err := h.MediaClient.MinioClient.StatObject(ctx, request.Params.Bucket, request.Params.Document, minio.StatObjectOptions{}) + if err != nil { + if err.Error() == "The specified key does not exist." { + + return c.JSON(apiresponse.NotFoundResponse()) + } + + //response.Error = err.Error() + response.Message = "Error during minio#StatObject" + response.StatusCode = http.StatusInternalServerError + + return c.JSON(response.StatusCode, response) + } + + //TODO Add error validation + _ = document_info + + err = h.MediaClient.MinioClient.RemoveObject(ctx, request.Params.Bucket, request.Params.Document, minio.RemoveObjectOptions{}) + if err != nil { + //response.Error = err.Error() + response.Message = "Error during minio#RemoveObject" + response.StatusCode = http.StatusInternalServerError + + return c.JSON(response.StatusCode, response) + } + + response.Message = "Document deleted" + response.StatusCode = http.StatusOK + + return c.JSON(response.StatusCode, response) +} diff --git a/apihandler/seed.go b/apihandler/seed.go new file mode 100644 index 0000000..77f93c6 --- /dev/null +++ b/apihandler/seed.go @@ -0,0 +1,35 @@ +package apihandler + +import ( + "net/http" + + "git.agecem.com/agecem/agecem-org/apiresponse" + "github.com/labstack/echo/v4" +) + +// V1SeedPOST créé des buckets dans minio selon la liste de buckets dans server.documents.buckets +// Les buckets sont créés avec paramètres par défaut, et sont ensuite visible dans /v1/bucket. +func (h *V1Handler) V1SeedPOST(c echo.Context) error { + var response apiresponse.V1SeedPOST + + new_buckets, err := h.MediaClient.Seed() + response.Data.Buckets = new_buckets + if err != nil { + response.StatusCode = http.StatusInternalServerError + response.Message = "Error during mediaClient.Seed()" + response.Error = err.Error() + + return c.JSON(response.StatusCode, response) + } + + if len(new_buckets) == 0 { + response.Message = "All buckets already exist" + + } else { + response.Message = "Buckets successfully created" + } + + response.StatusCode = http.StatusOK + + return c.JSON(response.StatusCode, response) +} diff --git a/apihandler/spec.go b/apihandler/spec.go new file mode 100644 index 0000000..20985fb --- /dev/null +++ b/apihandler/spec.go @@ -0,0 +1,36 @@ +package apihandler + +import ( + "fmt" + "net/http" + + "git.agecem.com/agecem/agecem-org/apirequest" + "git.agecem.com/agecem/agecem-org/apiresponse" + "git.agecem.com/agecem/agecem-org/version" + "github.com/labstack/echo/v4" +) + +const DescriptionV1SpecGET string = "Afficher le API spec en format pave" + +func (h *V1Handler) V1SpecGET(c echo.Context) error { + var request apirequest.V1SpecGET + var response apiresponse.V1SpecGET + + if !request.Complete() { + response.Message = "Incomplete V1SpecGET request received" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) + } + + response.Data.Spec = fmt.Sprintf("# pave spec for agecem-org %s\n", version.Version()) + + for _, route := range h.Pave.SortedRouteStrings() { + response.Data.Spec = fmt.Sprintf("%s%s", response.Data.Spec, route) + } + + response.Message = "ok" + response.StatusCode = http.StatusOK + + return c.JSON(response.StatusCode, response) +} diff --git a/apirequest/bucket.go b/apirequest/bucket.go new file mode 100644 index 0000000..e4ba60a --- /dev/null +++ b/apirequest/bucket.go @@ -0,0 +1,58 @@ +package apirequest + +import ( + "fmt" + "net/http" + + "codeberg.org/vlbeaudoin/voki" + "codeberg.org/vlbeaudoin/voki/request" + "git.agecem.com/agecem/agecem-org/apiresponse" +) + +var _ request.Requester[apiresponse.V1BucketsGET] = V1BucketsGET{} + +type V1BucketsGET struct{} + +func NewV1BucketsGET() (request V1BucketsGET, err error) { + return +} + +func (request V1BucketsGET) Complete() bool { return true } + +func (request V1BucketsGET) Request(v *voki.Voki) (response apiresponse.V1BucketsGET, err error) { + if !request.Complete() { + err = fmt.Errorf("Incomplete V1BucketsGET request") + return + } + + return response, v.UnmarshalIfComplete(http.MethodGet, "/v1/bucket", nil, true, &response) +} + +var _ request.Requester[apiresponse.V1BucketGET] = V1BucketGET{} + +type V1BucketGET struct { + Params struct { + Bucket string `json:"bucket"` + } +} + +func NewV1BucketGET(bucket string) (request V1BucketGET, err error) { + if bucket == "" { + err = fmt.Errorf("NewV1BucketGET requires non-nil bucket name") + } + + request.Params.Bucket = bucket + + return +} + +func (request V1BucketGET) Complete() bool { return request.Params.Bucket != "" } + +func (request V1BucketGET) Request(v *voki.Voki) (response apiresponse.V1BucketGET, err error) { + if !request.Complete() { + err = fmt.Errorf("Incomplete V1BucketGET request") + return + } + + return response, v.UnmarshalIfComplete(http.MethodGet, fmt.Sprintf("/v1/bucket/%s", request.Params.Bucket), nil, true, &response) +} diff --git a/apirequest/document.go b/apirequest/document.go new file mode 100644 index 0000000..c5c1f9a --- /dev/null +++ b/apirequest/document.go @@ -0,0 +1,98 @@ +package apirequest + +import ( + "bytes" + "encoding/json" + "fmt" + "mime/multipart" + "net/http" + + "codeberg.org/vlbeaudoin/voki" + "codeberg.org/vlbeaudoin/voki/request" + "git.agecem.com/agecem/agecem-org/apiresponse" +) + +var _ request.Requester[apiresponse.V1DocumentPOST] = V1DocumentPOST{} + +type V1DocumentPOST struct { + Data struct { + Bucket string + Document *multipart.FileHeader + } +} + +func NewV1DocumentPOST(bucket string, document *multipart.FileHeader) (request V1DocumentPOST, err error) { + if bucket == "" { + err = fmt.Errorf("NewV1DocumentPOST requires non-nil bucket name") + return + } + + request.Data.Bucket = bucket + + if document == nil { + err = fmt.Errorf("NewV1DocumentPOST requires non-nil document") + return + } + + request.Data.Document = document + + return +} + +func (request V1DocumentPOST) Complete() bool { + return request.Data.Bucket != "" && request.Data.Document != nil +} + +func (request V1DocumentPOST) Request(v *voki.Voki) (response apiresponse.V1DocumentPOST, err error) { + if !request.Complete() { + err = fmt.Errorf("Incomplete V1DocumentPOST 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", request.Data.Bucket), &buf, true, &response) +} + +var _ request.Requester[apiresponse.V1DocumentDELETE] = V1DocumentDELETE{} + +type V1DocumentDELETE struct { + Params struct { + Bucket string + Document string + } +} + +func NewV1DocumentDELETE(bucket, document string) (request V1DocumentDELETE, err error) { + if bucket == "" { + err = fmt.Errorf("NewV1DocumentDELETE requires non-nil bucket name") + return + } + + request.Params.Bucket = bucket + + if document == "" { + err = fmt.Errorf("NewV1DocumentDELETE requires non-nil document name") + return + } + + request.Params.Document = document + + return +} + +func (request V1DocumentDELETE) Complete() bool { + return request.Params.Bucket != "" && request.Params.Document != "" +} + +func (request V1DocumentDELETE) Request(v *voki.Voki) (response apiresponse.V1DocumentDELETE, err error) { + if !request.Complete() { + err = fmt.Errorf("Incomplete V1DocumentDELETE request") + return + } + + return response, v.UnmarshalIfComplete(http.MethodDelete, fmt.Sprintf("/v1/bucket/%s/%s", request.Params.Bucket, request.Params.Document), nil, true, &response) +} diff --git a/apirequest/seed.go b/apirequest/seed.go new file mode 100644 index 0000000..89187d7 --- /dev/null +++ b/apirequest/seed.go @@ -0,0 +1,29 @@ +package apirequest + +import ( + "fmt" + "net/http" + + "codeberg.org/vlbeaudoin/voki" + "codeberg.org/vlbeaudoin/voki/request" + "git.agecem.com/agecem/agecem-org/apiresponse" +) + +var _ request.Requester[apiresponse.V1SeedPOST] = V1SeedPOST{} + +type V1SeedPOST struct{} + +func NewV1SeedPOST() (request V1SeedPOST, err error) { + return +} + +func (r V1SeedPOST) Complete() bool { return true } + +func (r V1SeedPOST) Request(v *voki.Voki) (response apiresponse.V1SeedPOST, err error) { + if !r.Complete() { + err = fmt.Errorf("Incomplete V1SeedPOST") + return + } + + return response, v.UnmarshalIfComplete(http.MethodPost, "/v1/seed", nil, true, &response) +} diff --git a/apirequest/spec.go b/apirequest/spec.go new file mode 100644 index 0000000..b2aaa4e --- /dev/null +++ b/apirequest/spec.go @@ -0,0 +1,29 @@ +package apirequest + +import ( + "fmt" + "net/http" + + "codeberg.org/vlbeaudoin/voki" + "codeberg.org/vlbeaudoin/voki/request" + "git.agecem.com/agecem/agecem-org/apiresponse" +) + +var _ request.Requester[apiresponse.V1SpecGET] = V1SpecGET{} + +type V1SpecGET struct{} + +func NewV1SpecGET() (request V1SpecGET, err error) { + return +} + +func (request V1SpecGET) Complete() bool { return true } + +func (request V1SpecGET) Request(v *voki.Voki) (response apiresponse.V1SpecGET, err error) { + if !request.Complete() { + err = fmt.Errorf("Incomplete V1SpecGET") + return + } + + return response, v.UnmarshalIfComplete(http.MethodGet, "/v1/spec", nil, true, &response) +} diff --git a/apiresponse/apiresponse.go b/apiresponse/apiresponse.go index d4e98da..89adb8b 100644 --- a/apiresponse/apiresponse.go +++ b/apiresponse/apiresponse.go @@ -2,27 +2,19 @@ package apiresponse import ( "net/http" + + "codeberg.org/vlbeaudoin/voki/response" ) -type Responder interface { - Respond() Responder -} - type Response struct { - StatusCode int `json:"status_code"` - Message string - Error string -} - -func (r Response) Respond() Responder { - return r + response.ResponseWithError } type SimpleResponse struct { Message string } -func (r SimpleResponse) Respond() Responder { +func (r SimpleResponse) Respond() response.Responder { return r } diff --git a/apiresponse/bucket.go b/apiresponse/bucket.go index 02c26a3..3f4008f 100644 --- a/apiresponse/bucket.go +++ b/apiresponse/bucket.go @@ -1,13 +1,13 @@ package apiresponse -type V1BucketListResponse struct { +type V1BucketsGET struct { Response Data struct { Buckets map[string]string } } -type V1BucketReadResponse struct { +type V1BucketGET struct { Response Data struct { Keys []string diff --git a/apiresponse/document.go b/apiresponse/document.go index 9e1677b..beab0ae 100644 --- a/apiresponse/document.go +++ b/apiresponse/document.go @@ -1,6 +1,6 @@ package apiresponse -type V1DocumentCreateResponse struct { +type V1DocumentPOST struct { Response Data struct { Bucket string @@ -8,3 +8,7 @@ type V1DocumentCreateResponse struct { Size int64 } } + +type V1DocumentDELETE struct { + Response +} diff --git a/apiresponse/seed.go b/apiresponse/seed.go index 858beab..b266c5a 100644 --- a/apiresponse/seed.go +++ b/apiresponse/seed.go @@ -1,6 +1,6 @@ package apiresponse -type V1SeedResponse struct { +type V1SeedPOST struct { Response Data struct { Buckets []string diff --git a/apiresponse/spec.go b/apiresponse/spec.go new file mode 100644 index 0000000..d9e87c7 --- /dev/null +++ b/apiresponse/spec.go @@ -0,0 +1,8 @@ +package apiresponse + +type V1SpecGET struct { + Response + Data struct { + Spec string + } +} diff --git a/cmd/server.go b/cmd/server.go index 1380154..a5a9fbf 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -13,12 +13,15 @@ import ( "io" "net/http" + "codeberg.org/vlbeaudoin/pave" "codeberg.org/vlbeaudoin/serpents" "github.com/spf13/cobra" "github.com/spf13/viper" "git.agecem.com/agecem/agecem-org/api" "git.agecem.com/agecem/agecem-org/apihandler" + "git.agecem.com/agecem/agecem-org/apirequest" + "git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/media" "git.agecem.com/agecem/agecem-org/public" @@ -205,26 +208,53 @@ func RunServer() { log.Fatal("Error during NewMediaClientFromViper for API handlers") } + p := pave.New() + v1Handler := apihandler.V1Handler{ Config: cfg, MediaClient: mediaClient, + Pave: &p, } - groupV1.GET("", v1Handler.HandleV1) + groupV1.GET("", v1Handler.V1GET) - groupV1.POST("/seed", v1Handler.HandleV1Seed) + if err := pave.EchoRegister[ + apirequest.V1SeedPOST, + apiresponse.V1SeedPOST](groupV1, &p, "/v1", http.MethodPost, "/seed", "Créer buckets manquants définis dans `server.documents.buckets`", "V1SeedPOST", v1Handler.V1SeedPOST); err != nil { + log.Fatal(err) + } - groupV1.GET("/bucket", v1Handler.HandleV1BucketList) + if err := pave.EchoRegister[ + apirequest.V1SpecGET, + apiresponse.V1SpecGET](groupV1, &p, "/v1", http.MethodGet, "/spec", apihandler.DescriptionV1SpecGET, "V1SpecGET", v1Handler.V1SpecGET); err != nil { + log.Fatal(err) + } - groupV1.GET("/bucket/:bucket", v1Handler.HandleV1BucketRead) + if err := pave.EchoRegister[ + apirequest.V1BucketsGET, + apiresponse.V1BucketsGET](groupV1, &p, "/v1", http.MethodGet, "/bucket", "List buckets", "V1BucketsGET", v1Handler.V1BucketsGET); err != nil { + log.Fatal(err) + } - groupV1.POST("/bucket/:bucket", v1Handler.HandleV1DocumentCreate) + if err := pave.EchoRegister[ + apirequest.V1BucketGET, + apiresponse.V1BucketGET](groupV1, &p, "/v1", http.MethodGet, "/bucket/:bucket", "Read bucket content", "V1BucketGET", v1Handler.V1BucketGET); err != nil { + log.Fatal(err) + } - groupV1.GET("/bucket/:bucket/:document", v1Handler.HandleV1DocumentRead) + 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 { + log.Fatal(err) + } - groupV1.PUT("/bucket/:bucket/:document", v1Handler.HandleV1DocumentUpdate) + groupV1.GET("/bucket/:bucket/:document", v1Handler.V1DocumentGET) - groupV1.DELETE("/bucket/:bucket/:document", v1Handler.HandleV1DocumentDelete) + if err := pave.EchoRegister[ + apirequest.V1DocumentDELETE, + apiresponse.V1DocumentDELETE](groupV1, &p, "/v1", http.MethodDelete, "/bucket/:bucket/:document", "Delete document in specified bucket", "V1DocumentDELETE", v1Handler.V1DocumentDELETE); err != nil { + log.Fatal(err) + } // HTML Routes client := http.DefaultClient diff --git a/go.mod b/go.mod index 999b68e..40534c0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.agecem.com/agecem/agecem-org go 1.21.1 require ( + codeberg.org/vlbeaudoin/pave v1.0.1 codeberg.org/vlbeaudoin/serpents v1.1.0 codeberg.org/vlbeaudoin/voki v1.7.1 github.com/labstack/echo/v4 v4.11.3 diff --git a/go.sum b/go.sum index 27815d3..6826586 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +codeberg.org/vlbeaudoin/pave v1.0.1 h1:N7/TIb615By1nds5h+iKSZtPAFeuT3ceuUH/VG6t7Rw= +codeberg.org/vlbeaudoin/pave v1.0.1/go.mod h1:D/Lb/EmfJzl066A+2g4wc42e1Pb/l4nmXjIGouYBviM= codeberg.org/vlbeaudoin/serpents v1.1.0 h1:U9f2+2D1yUVHx90yePi2ZOLRLG/Wkoob4JXDIVyoBwA= codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ= codeberg.org/vlbeaudoin/voki v1.7.1 h1:Eywgk2A8NQmg4vucJjtheUpB0S2RYlDS8A7VwP+wFHU= diff --git a/webhandler/webhandler.go b/webhandler/webhandler.go index cec2667..6d945c7 100644 --- a/webhandler/webhandler.go +++ b/webhandler/webhandler.go @@ -53,20 +53,20 @@ func HandleVieEtudianteOrganisme(c echo.Context) error { func (h *WebHandler) HandleDocumentation(c echo.Context) error { var response webresponse.HandleDocumentationResponse - v1BucketListResponse, err := h.ApiClient.ListBuckets() + v1BucketsGET, err := h.ApiClient.ListBuckets() if err != nil { response.Error = err.Error() - response.Message = v1BucketListResponse.Message - response.StatusCode = v1BucketListResponse.StatusCode + response.Message = v1BucketsGET.Message + response.StatusCode = v1BucketsGET.StatusCode return c.Render(response.StatusCode, "documentation-html", response) } - //TODO check v1BucketListRespone StatusCode and Error + //TODO check v1BucketsGET StatusCode and Error - for bucket, displayName := range v1BucketListResponse.Data.Buckets { + for bucket, displayName := range v1BucketsGET.Data.Buckets { // TODO move call to dedicated API client method - var v1BucketReadResponse apiresponse.V1BucketReadResponse + var v1BucketReadResponse apiresponse.V1BucketGET if err = h.ApiClient.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v1/bucket/%s", bucket), nil, true, &v1BucketReadResponse); err != nil { response.Error = err.Error() @@ -134,16 +134,16 @@ func HandleAdmin(c echo.Context) error { func (h *WebHandler) HandleAdminDocumentsUpload(c echo.Context) error { var response webresponse.HandleAdminDocumentsUploadResponse - v1BucketListResponse, err := h.ApiClient.ListBuckets() + v1BucketsGET, err := h.ApiClient.ListBuckets() if err != nil { - response.StatusCode = v1BucketListResponse.StatusCode + response.StatusCode = v1BucketsGET.StatusCode response.Error = err.Error() - response.Message = v1BucketListResponse.Message + response.Message = v1BucketsGET.Message return c.Render(response.StatusCode, "admin-upload-html", nil) } - for bucketName, displayName := range v1BucketListResponse.Data.Buckets { + for bucketName, displayName := range v1BucketsGET.Data.Buckets { response.Data.Buckets = append(response.Data.Buckets, models.Bucket{ Name: bucketName, DisplayName: displayName, @@ -157,16 +157,16 @@ func (h *WebHandler) HandleAdminDocumentsUpload(c echo.Context) error { func (h *WebHandler) HandleAdminDocumentsUploadPOST(c echo.Context) error { var response webresponse.HandleAdminDocumentsUploadResponse - v1BucketListResponse, err := h.ApiClient.ListBuckets() + v1BucketsGET, err := h.ApiClient.ListBuckets() if err != nil { - response.StatusCode = v1BucketListResponse.StatusCode - response.Message = v1BucketListResponse.Message + response.StatusCode = v1BucketsGET.StatusCode + response.Message = v1BucketsGET.Message response.Error = err.Error() return c.Render(response.StatusCode, "admin-upload-html", response) } - for bucketName, displayName := range v1BucketListResponse.Data.Buckets { + for bucketName, displayName := range v1BucketsGET.Data.Buckets { response.Data.Buckets = append(response.Data.Buckets, models.Bucket{ Name: bucketName, DisplayName: displayName,