From 982bd8a43c50bb5845b694dbd0b3e0ffbf43dad7 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Mon, 25 Sep 2023 23:00:57 +0200 Subject: [PATCH] done with API --- api.go | 228 +++++++++++++++++--------------------------- garage.go | 8 +- main.go | 3 +- quotas.go | 8 +- templates/home.html | 4 +- website.go | 33 ++++--- 6 files changed, 116 insertions(+), 168 deletions(-) diff --git a/api.go b/api.go index fd6df93..c804276 100644 --- a/api.go +++ b/api.go @@ -1,48 +1,29 @@ package main import ( - //"context" "errors" "encoding/json" - garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "fmt" "github.com/gorilla/mux" - "log" "net/http" ) -type ApiQuotaView struct { - files *uint64 - size *uint64 -} +func handleAPIWebsiteList(w http.ResponseWriter, r *http.Request) { + user := RequireUserApi(w, r) -type ApiBucketView struct { - global *bool - max *ApiQuotaView - used *ApiQuotaView -} - -type BucketRequest struct { - s3key *garage.KeyInfo - bucketName string - bucketId string - global bool - http *http.Request -} - -func handleAPIWebsite(w http.ResponseWriter, r *http.Request) { - - br, err := buildBucketRequest(w, r) - if err != nil { - return + if user == nil { + return } - if r.Method == http.MethodPatch { - patchGarageBucket(w, br) + ctrl, err := NewWebsiteController(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) return } if r.Method == http.MethodGet { - getGarageBucket(w, br) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ctrl.Describe()) return } @@ -50,131 +31,92 @@ func handleAPIWebsite(w http.ResponseWriter, r *http.Request) { return } -func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) { +func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) { user := RequireUserApi(w, r) + if user == nil { - return nil, errors.New("Unable to fetch user") + return } - // FETCH BUCKET ID by iterating over buckets owned by this key bucketName := mux.Vars(r)["bucket"] - var bucketId *string - var global *bool - - s3key, err := user.S3KeyInfo() + ctrl, err := NewWebsiteController(user) if err != nil { - return nil, err - } - -findBucketIdLoop: - for _, bucket := range s3key.Buckets { - for _, localAlias := range bucket.LocalAliases { - if localAlias == bucketName { - bucketId = bucket.Id - *global = false - break findBucketIdLoop - } - } - for _, globalAlias := range bucket.GlobalAliases { - if globalAlias == bucketName { - bucketId = bucket.Id - *global = true - break findBucketIdLoop - } - } - } - - if bucketId == nil || global == nil { - http.Error(w, "Bucket not found in this account", http.StatusNotFound) - return nil, errors.New("Unable to fetch bucket ID") - } - - return &BucketRequest{ - s3key: s3key, - bucketName: bucketName, - bucketId: *bucketId, - global: *global, - http: r, - }, nil - -} - -func patchGarageBucket(w http.ResponseWriter, br *BucketRequest) { - var err error - - // DECODE BODY - var queuedChange ApiBucketView - decoder := json.NewDecoder(br.http.Body) - err = decoder.Decode(&queuedChange) - if err != nil { - log.Println(err) - http.Error(w, "Unable to decode the body", http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusInternalServerError) return } - // SET THE GLOBAL FLAG - if queuedChange.global != nil { - if *queuedChange.global && !br.global { - _, err = grgAddGlobalAlias(br.bucketId, br.bucketName) - if err != nil { - http.Error(w, "Unable to add the requested name as global alias for this bucket", http.StatusInternalServerError) - return - } - _, err = grgDelLocalAlias(br.bucketId, *br.s3key.AccessKeyId, br.bucketName) - if err != nil { - http.Error(w, "Unable to remove the local alias for this bucket", http.StatusInternalServerError) - return - } - } else if !*queuedChange.global && br.global { - grgAddLocalAlias(br.bucketId, *br.s3key.AccessKeyId, br.bucketName) - if err != nil { - http.Error(w, "Unable to add the requested name as local alias for this bucket", http.StatusInternalServerError) - return - } - grgDelGlobalAlias(br.bucketId, br.bucketName) - if err != nil { - http.Error(w, "Unable to remove the global alias for this bucket", http.StatusInternalServerError) - return - } + if r.Method == http.MethodGet { + view, err := ctrl.Inspect(bucketName) + if errors.Is(err, ErrWebsiteNotFound) { + http.Error(w, err.Error(), http.StatusNotFound) + return + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - } - - // CHECK IF QUOTA MUST BE ADDED TO THIS BUCKET - - // VALIDATE IT - // --- global --- - // 1. can be true, false, or nil (use pointers) - // 2. if nil do nothing - // 3. if false, throw "not yet implemented" (501) - // 4. if true, check that the bucket name does not exist yet in the global namespace, throw "forbidden" (403) - // --- quota.size --- - // 1. if no quota on the bucket + this field is none, set to 50MB - // 2. if lower than 50MB, set to 50MB. If higher than 200MB, set to 200MB - // --- quota.files --- - // 1. if no quota on the bucket + this field is none, set to 10k - // 2. if lower than 10k, set to 10k. If higher than 40k, set to 40k - // READ BODY JSON - - // IF BODY.GLOBAL is not NONE - // DO: Add an alias - - // IF BODY.QUOTA.SIZE is not NONE - // DO: Change quota - - // IF BODY.QUOTA.FILE is not NONE - // DO: Change quota - - getGarageBucket(w, br) -} - -func getGarageBucket(w http.ResponseWriter, br *BucketRequest) { - // FETCH AN UPDATED BUCKET VIEW - bucket, err := grgGetBucket(br.bucketId) - if err != nil { - http.Error(w, "Unable to fetch bucket details", http.StatusInternalServerError) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(view) return } - // BUILD A VIEW - log.Println(bucket) + if r.Method == http.MethodPost { + view, err := ctrl.Create(bucketName) + if errors.Is(err, ErrEmptyBucketName) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } else if errors.Is(err, ErrWebsiteQuotaReached) { + http.Error(w, err.Error(), http.StatusForbidden) + return + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(view) + return + } + + if r.Method == http.MethodPatch { + var patch WebsitePatch + err := json.NewDecoder(r.Body).Decode(&patch) + if err != nil { + http.Error(w, errors.Join(fmt.Errorf("Can't parse the request body as a website patch JSON"), err).Error(), http.StatusBadRequest) + return + } + + view, err := ctrl.Patch(bucketName, &patch) + if errors.Is(err, ErrWebsiteNotFound) { + http.Error(w, err.Error(), http.StatusNotFound) + return + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(view) + return + } + + if r.Method == http.MethodDelete { + err := ctrl.Delete(bucketName) + if errors.Is(err, ErrEmptyBucketName) || errors.Is(err, ErrBucketDeleteNotEmpty) || errors.Is(err, ErrBucketDeleteUnfinishedUpload) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } else if errors.Is(err, ErrWebsiteNotFound) { + http.Error(w, err.Error(), http.StatusNotFound) + return + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + return + + } + + http.Error(w, "This method is not implemented for this endpoint", http.StatusNotImplemented) + return } diff --git a/garage.go b/garage.go index d3d4d38..b3ca836 100644 --- a/garage.go +++ b/garage.go @@ -194,9 +194,9 @@ func handleWebsiteList(w http.ResponseWriter, r *http.Request) { return } - list := ctrl.List() - if len(list) > 0 { - http.Redirect(w, r, "/website/inspect/"+list[0].Pretty, http.StatusFound) + desc := ctrl.Describe() + if len(desc.Websites) > 0 { + http.Redirect(w, r, "/website/inspect/"+desc.Websites[0].Pretty, http.StatusFound) } else { http.Redirect(w, r, "/website/new", http.StatusFound) } @@ -271,7 +271,7 @@ func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { action := strings.Join(r.Form["action"],"") switch action { case "increase_quota": - _, processErr = ctrl.Patch(bucketName, &WebsitePatch { size: &user.Quota.WebsiteSizeBursted }) + _, processErr = ctrl.Patch(bucketName, &WebsitePatch { Size: &user.Quota.WebsiteSizeBursted }) case "delete_bucket": processErr = ctrl.Delete(bucketName) http.Redirect(w, r, "/website", http.StatusFound) diff --git a/main.go b/main.go index 34a1630..9763f53 100644 --- a/main.go +++ b/main.go @@ -147,7 +147,8 @@ func server(args []string) { r.HandleFunc("/login", handleLogin) r.HandleFunc("/logout", handleLogout) - r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsite) + r.HandleFunc("/api/unstable/website", handleAPIWebsiteList) + r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsiteInspect) r.HandleFunc("/profile", handleProfile) r.HandleFunc("/passwd", handlePasswd) diff --git a/quotas.go b/quotas.go index 9a2e426..e520f5c 100644 --- a/quotas.go +++ b/quotas.go @@ -102,10 +102,10 @@ func (q *UserQuota) WebsiteSizeBurstedPretty() string { // --- A quota stat we can use type QuotaStat struct { - Current int64 - Max int64 - Ratio float64 - Burstable bool + Current int64 `json:"current"` + Max int64 `json:"max"` + Ratio float64 `json:"ratio"` + Burstable bool `json:"burstable"` } func NewQuotaStat(current, max int64, burstable bool) QuotaStat { return QuotaStat { diff --git a/templates/home.html b/templates/home.html index 3475795..dd88d13 100644 --- a/templates/home.html +++ b/templates/home.html @@ -24,11 +24,11 @@
- Garage + Mon espace sur la toile
diff --git a/website.go b/website.go index 6e86b19..7e89a41 100644 --- a/website.go +++ b/website.go @@ -22,11 +22,11 @@ var ( type WebsiteId struct { - Pretty string - Internal string - Alt []string - Expanded bool - Url string + Pretty string `json:"name"` + Internal string `json:"-"` + Alt []string `json:"alt_name"` + Expanded bool `json:"expanded"` + Url string `json:"domain"` } func NewWebsiteId(id string, aliases []string) *WebsiteId { @@ -50,7 +50,7 @@ func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId { } type WebsiteController struct { - User *LoggedUser + User *LoggedUser WebsiteIdx map[string]*WebsiteId PrettyList []string WebsiteCount QuotaStat @@ -80,12 +80,17 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) { return &WebsiteController { user, idx, wlist, quota }, nil } -func (w *WebsiteController) List() []*WebsiteId { +type WebsiteDescribe struct { + AllowedWebsites *QuotaStat `json:"quota"` + Websites []*WebsiteId `json:"vhosts"` +} + +func (w *WebsiteController) Describe() *WebsiteDescribe { r := make([]*WebsiteId, 0, len(w.PrettyList)) for _, k := range w.PrettyList { r = append(r, w.WebsiteIdx[k]) } - return r + return &WebsiteDescribe { &w.WebsiteCount, r } } func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { @@ -117,8 +122,8 @@ func (w *WebsiteController) Patch(pretty string, patch *WebsitePatch) (*WebsiteV urQuota := garage.NewUpdateBucketRequestQuotas() urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(binfo.Quotas.GetMaxSize())) urQuota.SetMaxObjects(w.User.Quota.WebsiteObjectAdjust(binfo.Quotas.GetMaxObjects())) - if patch.size != nil { - urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.size)) + if patch.Size != nil { + urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.Size)) } // Build the update @@ -205,9 +210,9 @@ func (w *WebsiteController) Delete(pretty string) error { type WebsiteView struct { - Name *WebsiteId - Size QuotaStat - Files QuotaStat + Name *WebsiteId `json:"identified_as"` + Size QuotaStat `json:"quota_size"` + Files QuotaStat `json:"quota_files"` } func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { @@ -220,5 +225,5 @@ func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { } type WebsitePatch struct { - size *int64 + Size *int64 `json:"quota_size"` }