An API for Guichet #23
6 changed files with 116 additions and 168 deletions
224
api.go
224
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 {
|
||||
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
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(view)
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
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
|
||||
}
|
||||
|
||||
return &BucketRequest{
|
||||
s3key: s3key,
|
||||
bucketName: bucketName,
|
||||
bucketId: *bucketId,
|
||||
global: *global,
|
||||
http: r,
|
||||
}, nil
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(view)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
||||
// BUILD A VIEW
|
||||
log.Println(bucket)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
3
main.go
3
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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -24,11 +24,11 @@
|
|||
<div class="mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Garage
|
||||
Mon espace sur la toile
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="/website/configure">Mes identifiants</a>
|
||||
<a class="list-group-item list-group-item-action" href="/website">Mes sites webs</a>
|
||||
<a class="list-group-item list-group-item-action" href="/website">Mes sites Web</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
31
website.go
31
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 {
|
||||
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue