An API for Guichet #23
6 changed files with 116 additions and 168 deletions
226
api.go
226
api.go
|
@ -1,48 +1,29 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
//"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApiQuotaView struct {
|
func handleAPIWebsiteList(w http.ResponseWriter, r *http.Request) {
|
||||||
files *uint64
|
user := RequireUserApi(w, r)
|
||||||
size *uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ApiBucketView struct {
|
if user == nil {
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodPatch {
|
ctrl, err := NewWebsiteController(user)
|
||||||
patchGarageBucket(w, br)
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
getGarageBucket(w, br)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(ctrl.Describe())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,131 +31,92 @@ func handleAPIWebsite(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBucketRequest(w http.ResponseWriter, r *http.Request) (*BucketRequest, error) {
|
func handleAPIWebsiteInspect(w http.ResponseWriter, r *http.Request) {
|
||||||
user := RequireUserApi(w, r)
|
user := RequireUserApi(w, r)
|
||||||
|
|
||||||
if user == nil {
|
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"]
|
bucketName := mux.Vars(r)["bucket"]
|
||||||
var bucketId *string
|
ctrl, err := NewWebsiteController(user)
|
||||||
var global *bool
|
|
||||||
|
|
||||||
s3key, err := user.S3KeyInfo()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SET THE GLOBAL FLAG
|
if r.Method == http.MethodGet {
|
||||||
if queuedChange.global != nil {
|
view, err := ctrl.Inspect(bucketName)
|
||||||
if *queuedChange.global && !br.global {
|
if errors.Is(err, ErrWebsiteNotFound) {
|
||||||
_, err = grgAddGlobalAlias(br.bucketId, br.bucketName)
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
if err != nil {
|
return
|
||||||
http.Error(w, "Unable to add the requested name as global alias for this bucket", http.StatusInternalServerError)
|
} else if err != nil {
|
||||||
return
|
http.Error(w, err.Error(), 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(view)
|
||||||
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUILD A VIEW
|
if r.Method == http.MethodPost {
|
||||||
log.Println(bucket)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,9 +194,9 @@ func handleWebsiteList(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
list := ctrl.List()
|
desc := ctrl.Describe()
|
||||||
if len(list) > 0 {
|
if len(desc.Websites) > 0 {
|
||||||
http.Redirect(w, r, "/website/inspect/"+list[0].Pretty, http.StatusFound)
|
http.Redirect(w, r, "/website/inspect/"+desc.Websites[0].Pretty, http.StatusFound)
|
||||||
} else {
|
} else {
|
||||||
http.Redirect(w, r, "/website/new", http.StatusFound)
|
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"],"")
|
action := strings.Join(r.Form["action"],"")
|
||||||
switch action {
|
switch action {
|
||||||
case "increase_quota":
|
case "increase_quota":
|
||||||
_, processErr = ctrl.Patch(bucketName, &WebsitePatch { size: &user.Quota.WebsiteSizeBursted })
|
_, processErr = ctrl.Patch(bucketName, &WebsitePatch { Size: &user.Quota.WebsiteSizeBursted })
|
||||||
case "delete_bucket":
|
case "delete_bucket":
|
||||||
processErr = ctrl.Delete(bucketName)
|
processErr = ctrl.Delete(bucketName)
|
||||||
http.Redirect(w, r, "/website", http.StatusFound)
|
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("/login", handleLogin)
|
||||||
r.HandleFunc("/logout", handleLogout)
|
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("/profile", handleProfile)
|
||||||
r.HandleFunc("/passwd", handlePasswd)
|
r.HandleFunc("/passwd", handlePasswd)
|
||||||
|
|
|
@ -102,10 +102,10 @@ func (q *UserQuota) WebsiteSizeBurstedPretty() string {
|
||||||
|
|
||||||
// --- A quota stat we can use
|
// --- A quota stat we can use
|
||||||
type QuotaStat struct {
|
type QuotaStat struct {
|
||||||
Current int64
|
Current int64 `json:"current"`
|
||||||
Max int64
|
Max int64 `json:"max"`
|
||||||
Ratio float64
|
Ratio float64 `json:"ratio"`
|
||||||
Burstable bool
|
Burstable bool `json:"burstable"`
|
||||||
}
|
}
|
||||||
func NewQuotaStat(current, max int64, burstable bool) QuotaStat {
|
func NewQuotaStat(current, max int64, burstable bool) QuotaStat {
|
||||||
return QuotaStat {
|
return QuotaStat {
|
||||||
|
|
|
@ -24,11 +24,11 @@
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Garage
|
Mon espace sur la toile
|
||||||
</div>
|
</div>
|
||||||
<div class="list-group list-group-flush">
|
<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/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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
31
website.go
31
website.go
|
@ -22,11 +22,11 @@ var (
|
||||||
|
|
||||||
|
|
||||||
type WebsiteId struct {
|
type WebsiteId struct {
|
||||||
Pretty string
|
Pretty string `json:"name"`
|
||||||
Internal string
|
Internal string `json:"-"`
|
||||||
Alt []string
|
Alt []string `json:"alt_name"`
|
||||||
Expanded bool
|
Expanded bool `json:"expanded"`
|
||||||
Url string
|
Url string `json:"domain"`
|
||||||
|
|
||||||
}
|
}
|
||||||
func NewWebsiteId(id string, aliases []string) *WebsiteId {
|
func NewWebsiteId(id string, aliases []string) *WebsiteId {
|
||||||
|
@ -80,12 +80,17 @@ func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) {
|
||||||
return &WebsiteController { user, idx, wlist, quota }, nil
|
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))
|
r := make([]*WebsiteId, 0, len(w.PrettyList))
|
||||||
for _, k := range w.PrettyList {
|
for _, k := range w.PrettyList {
|
||||||
r = append(r, w.WebsiteIdx[k])
|
r = append(r, w.WebsiteIdx[k])
|
||||||
}
|
}
|
||||||
return r
|
return &WebsiteDescribe { &w.WebsiteCount, r }
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) {
|
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 := garage.NewUpdateBucketRequestQuotas()
|
||||||
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(binfo.Quotas.GetMaxSize()))
|
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(binfo.Quotas.GetMaxSize()))
|
||||||
urQuota.SetMaxObjects(w.User.Quota.WebsiteObjectAdjust(binfo.Quotas.GetMaxObjects()))
|
urQuota.SetMaxObjects(w.User.Quota.WebsiteObjectAdjust(binfo.Quotas.GetMaxObjects()))
|
||||||
if patch.size != nil {
|
if patch.Size != nil {
|
||||||
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.size))
|
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.Size))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the update
|
// Build the update
|
||||||
|
@ -205,9 +210,9 @@ func (w *WebsiteController) Delete(pretty string) error {
|
||||||
|
|
||||||
|
|
||||||
type WebsiteView struct {
|
type WebsiteView struct {
|
||||||
Name *WebsiteId
|
Name *WebsiteId `json:"identified_as"`
|
||||||
Size QuotaStat
|
Size QuotaStat `json:"quota_size"`
|
||||||
Files QuotaStat
|
Files QuotaStat `json:"quota_files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView {
|
func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView {
|
||||||
|
@ -220,5 +225,5 @@ func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView {
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsitePatch struct {
|
type WebsitePatch struct {
|
||||||
size *int64
|
Size *int64 `json:"quota_size"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue