diff --git a/api.go b/api.go index 7d9c2cd..fd6df93 100644 --- a/api.go +++ b/api.go @@ -29,7 +29,7 @@ type BucketRequest struct { http *http.Request } -func handleAPIGarageBucket(w http.ResponseWriter, r *http.Request) { +func handleAPIWebsite(w http.ResponseWriter, r *http.Request) { br, err := buildBucketRequest(w, r) if err != nil { diff --git a/garage.go b/garage.go index 4586e26..8c8633f 100644 --- a/garage.go +++ b/garage.go @@ -49,7 +49,7 @@ func grgGetKey(accessKey string) (*garage.KeyInfo, error) { -func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInfo, error) { +func grgCreateBucket(bucket string) (*garage.BucketInfo, error) { client, ctx := gadmin() br := garage.NewCreateBucketRequest() @@ -61,32 +61,40 @@ func grgCreateWebsite(gkey, bucket string, quotas *UserQuota) (*garage.BucketInf fmt.Printf("%+v\n", err) return nil, err } + return binfo, nil +} + +func grgAllowKeyOnBucket(bid, gkey string) (*garage.BucketInfo, error) { + client, ctx := gadmin() // Allow user's key ar := garage.AllowBucketKeyRequest{ - BucketId: *binfo.Id, + BucketId: bid, AccessKeyId: gkey, Permissions: *garage.NewAllowBucketKeyRequestPermissions(true, true, true), } - binfo, _, err = client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() + binfo, _, err := client.BucketApi.AllowBucketKey(ctx).AllowBucketKeyRequest(ar).Execute() if err != nil { fmt.Printf("%+v\n", err) return nil, err } - // Expose website and set quota + return binfo, nil +} + +func allowWebsiteDefault() *garage.UpdateBucketRequestWebsiteAccess { wr := garage.NewUpdateBucketRequestWebsiteAccess() wr.SetEnabled(true) wr.SetIndexDocument("index.html") wr.SetErrorDocument("error.html") - qr := quotas.DefaultWebsiteQuota() + return wr +} - ur := garage.NewUpdateBucketRequest() - ur.SetWebsiteAccess(*wr) - ur.SetQuotas(*qr) +func grgUpdateBucket(bid string, ur *garage.UpdateBucketRequest) (*garage.BucketInfo, error) { + client, ctx := gadmin() - binfo, _, err = client.BucketApi.UpdateBucket(ctx, *binfo.Id).UpdateBucketRequest(*ur).Execute() + binfo, _, err := client.BucketApi.UpdateBucket(ctx, bid).UpdateBucketRequest(*ur).Execute() if err != nil { fmt.Printf("%+v\n", err) return nil, err @@ -154,7 +162,7 @@ func grgGetBucket(bid string) (*garage.BucketInfo, error) { // --- Start page rendering functions -func handleGarageKey(w http.ResponseWriter, r *http.Request) { +func handleWebsiteConfigure(w http.ResponseWriter, r *http.Request) { user := RequireUserHtml(w, r) if user == nil { return @@ -164,22 +172,48 @@ func handleGarageKey(w http.ResponseWriter, r *http.Request) { tKey.Execute(w, user) } -func handleGarageWebsiteList(w http.ResponseWriter, r *http.Request) { +func handleWebsiteList(w http.ResponseWriter, r *http.Request) { user := RequireUserHtml(w, r) if user == nil { return } - tWebsiteList := getTemplate("garage_website_list.html") - tWebsiteList.Execute(w, user) + ctrl, err := NewWebsiteController(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + list := ctrl.List() + if len(list) > 0 { + http.Redirect(w, r, "/website/inspect/"+list[0].Pretty, http.StatusFound) + } else { + http.Redirect(w, r, "/website/new", http.StatusFound) + } } -func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { +type WebsiteNewTpl struct { + ctrl *WebsiteController + err error +} + +func handleWebsiteNew(w http.ResponseWriter, r *http.Request) { user := RequireUserHtml(w, r) if user == nil { return } + ctrl, err := NewWebsiteController(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + tpl := &WebsiteNewTpl{ + ctrl: ctrl, + err: nil, + } + tWebsiteNew := getTemplate("garage_website_new.html") if r.Method == "POST" { r.ParseForm() @@ -188,73 +222,47 @@ func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { if bucket == "" { bucket = strings.Join(r.Form["bucket2"], "") } - if bucket == "" { - log.Println("Form empty") - // @FIXME we need to return the error to the user - tWebsiteNew.Execute(w, nil) - return - } - keyInfo, err := user.S3KeyInfo() + view, err := ctrl.Create(bucket) if err != nil { - log.Println(err) - // @FIXME we need to return the error to the user - tWebsiteNew.Execute(w, nil) - return + tpl.err = err + tWebsiteNew.Execute(w, tpl) } - binfo, err := grgCreateWebsite(*keyInfo.AccessKeyId, bucket, user.Quota) - if err != nil { - log.Println(err) - // @FIXME we need to return the error to the user - tWebsiteNew.Execute(w, nil) - return - } - - http.Redirect(w, r, "/garage/website/b/"+*binfo.Id, http.StatusFound) + http.Redirect(w, r, "/website/inspect/"+view.Name.Pretty, http.StatusFound) return } tWebsiteNew.Execute(w, nil) } -type webInspectView struct { - User *LoggedUser - Bucket *garage.BucketInfo - IndexDoc string - ErrorDoc string - MaxObjects int64 - MaxSize int64 - UsedSizePct float64 +type WebsiteInspectTpl struct { + Ctrl *WebsiteController + View *WebsiteView } -func handleGarageWebsiteInspect(w http.ResponseWriter, r *http.Request) { +func handleWebsiteInspect(w http.ResponseWriter, r *http.Request) { user := RequireUserHtml(w, r) if user == nil { return } - bucketId := mux.Vars(r)["bucket"] - // @FIXME check that user owns the bucket.... + ctrl, err := NewWebsiteController(user) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } - binfo, err := grgGetBucket(bucketId) + bucketName := mux.Vars(r)["bucket"] + + view, err := ctrl.Inspect(bucketName) if err != nil { - log.Println(err) - return + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - wc := binfo.GetWebsiteConfig() - q := binfo.GetQuotas() - - view := webInspectView{ - User: user, - Bucket: binfo, - IndexDoc: (&wc).GetIndexDocument(), - ErrorDoc: (&wc).GetErrorDocument(), - MaxObjects: (&q).GetMaxObjects(), - MaxSize: (&q).GetMaxSize(), - } + tpl := &WebsiteInspectTpl{ ctrl, view } tWebsiteInspect := getTemplate("garage_website_inspect.html") - tWebsiteInspect.Execute(w, &view) + tWebsiteInspect.Execute(w, &tpl) } diff --git a/main.go b/main.go index ee1863c..34a1630 100644 --- a/main.go +++ b/main.go @@ -147,7 +147,7 @@ func server(args []string) { r.HandleFunc("/login", handleLogin) r.HandleFunc("/logout", handleLogout) - r.HandleFunc("/api/unstable/garage/bucket/{bucket}", handleAPIGarageBucket) + r.HandleFunc("/api/unstable/website/{bucket}", handleAPIWebsite) r.HandleFunc("/profile", handleProfile) r.HandleFunc("/passwd", handlePasswd) @@ -156,10 +156,10 @@ func server(args []string) { r.HandleFunc("/directory/search", handleDirectorySearch) r.HandleFunc("/directory", handleDirectory) - r.HandleFunc("/garage/key", handleGarageKey) - r.HandleFunc("/garage/website", handleGarageWebsiteList) - r.HandleFunc("/garage/website/new", handleGarageWebsiteNew) - r.HandleFunc("/garage/website/b/{bucket}", handleGarageWebsiteInspect) + r.HandleFunc("/website", handleWebsiteList) + r.HandleFunc("/website/new", handleWebsiteNew) + r.HandleFunc("/website/configure", handleWebsiteConfigure) + r.HandleFunc("/website/inspect/{bucket}", handleWebsiteInspect) r.HandleFunc("/invite/new_account", handleInviteNewAccount) r.HandleFunc("/invite/send_code", handleInviteSendCode) diff --git a/templates/garage_key.html b/templates/garage_key.html index e1a9019..cf56822 100644 --- a/templates/garage_key.html +++ b/templates/garage_key.html @@ -3,7 +3,7 @@ {{define "body"}}

Mes identifiants

- Mes sites webs + Mes sites webs Menu principal
diff --git a/templates/garage_website_inspect.html b/templates/garage_website_inspect.html index bc60711..d5f48c2 100644 --- a/templates/garage_website_inspect.html +++ b/templates/garage_website_inspect.html @@ -2,57 +2,54 @@ {{define "body"}}
-

Inspecter le site web

- Mes identifiants - Nouveau site web - Mes sites webs + Nouveau site web + + Mes identifiants + Menu principal
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID{{ .Bucket.Id }}
URLs - {{ range $alias := .Bucket.GlobalAliases }} - {{ if contains $alias "." }} - https://{{ $alias }} - {{ else }} - https://{{ $alias }}.web.deuxfleurs.fr - {{ end }} - {{ end }} -
Document d'index {{ .IndexDoc }}
Document d'erreur{{ .ErrorDoc }}
Nombre de fichiers{{ .Bucket.Objects }} / {{ .MaxObjects }}
Espace utilisé{{ .Bucket.Bytes }} / {{ .MaxSize }} octets
+
+
+
+ {{ $view := .View }} + {{ range $wid := .Ctrl.List }} + {{ if eq $wid.Internal $view.Name.Internal }} + + {{ $wid.Url }} + + {{ else }} + + {{ $wid.Url }} + + {{ end }} + {{ end }} +
+
+
+

{{ .View.Name.Url }}

-

Configurer le nom de domaine

+
Quotas
+
+
+ {{ .View.Size.Ratio }}% +
+
-{{ range $alias := .Bucket.GlobalAliases }} -{{ if contains $alias "." }} -

Le nom de domaine {{ $alias }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée CNAME garage.deuxfleurs.fr ou ALIAS garage.deuxfleurs.fr auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).

-{{ else }} -

Le nom de domaine https://{{ $alias }}.web.deuxfleurs.fr est fourni par Deuxfleurs, il n'y a pas de configuration à faire.

-{{ end }} +

+ {{ .View.Size.PrettyCurrent }} utilisé sur un maximum de {{ .View.Size.PrettyMax }} + {{ if gt .View.Files.Ratio 0.5 }} +
{{ .View.Files.Current }} fichiers sur un maximum de {{ .View.Files.Max }} + {{ end }} +

+ + + {{ if .View.Name.Expanded }} +
Vous ne savez pas comment configurer votre nom de domaine ?
+

Le nom de domaine {{ .View.Name.Url }} n'est pas géré par Deuxfleurs, il vous revient donc de configurer la zone DNS. Vous devez ajouter une entrée CNAME garage.deuxfleurs.fr ou ALIAS garage.deuxfleurs.fr auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).

+ {{ end }} + + +
+
{{ end }} -{{end}} diff --git a/templates/garage_website_list.html b/templates/garage_website_list.html deleted file mode 100644 index 0f4a3b3..0000000 --- a/templates/garage_website_list.html +++ /dev/null @@ -1,38 +0,0 @@ -{{define "title"}}Sites webs |{{end}} - -{{define "body"}} - -
-

Sites webs

- Mes identifiants - Nouveau site web - Menu principal -
- - - - - - - - {{ range $buck := .S3KeyInfo.Buckets }} - {{ if $buck.GlobalAliases }} - - - - - {{ end }} - {{ end }} - -
IDURLs
- {{$buck.Id}} - - {{ range $alias := $buck.GlobalAliases }} - {{ if contains $alias "." }} - https://{{ $alias }} - {{ else }} - https://{{ $alias }}.web.deuxfleurs.fr - {{ end }} - {{ end }} -
-{{end}} diff --git a/templates/home.html b/templates/home.html index 3dad6b6..3475795 100644 --- a/templates/home.html +++ b/templates/home.html @@ -27,8 +27,8 @@ Garage
- Mes identifiants - Mes sites webs + Mes identifiants + Mes sites webs
diff --git a/website.go b/website.go new file mode 100644 index 0000000..c06ccbc --- /dev/null +++ b/website.go @@ -0,0 +1,211 @@ +package main + +import ( + "fmt" + "sort" + "strings" + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" +) + +var ( + ErrWebsiteNotFound = fmt.Errorf("Website not found") + ErrFetchBucketInfo = fmt.Errorf("Failed to fetch bucket information") + ErrWebsiteQuotaReached = fmt.Errorf("Can't create additional websites, quota reached") + ErrEmptyBucketName = fmt.Errorf("You can't create a website with an empty name") + ErrCantCreateBucket = fmt.Errorf("Can't create this bucket. Maybe another bucket already exists with this name or you have an invalid character") + ErrCantAllowKey = fmt.Errorf("Can't allow given key on the target bucket") + ErrCantConfigureBucket = fmt.Errorf("Unable to configure the bucket (activating website, adding quotas, etc.)") +) + +type QuotaStat struct { + Current int64 + Max int64 + Ratio float64 + Burstable bool +} +func NewQuotaStat(current, max int64, burstable bool) QuotaStat { + return QuotaStat { + Current: current, + Max: max, + Ratio: float64(current) / float64(max), + Burstable: burstable, + } +} +func (q *QuotaStat) IsFull() bool { + return q.Current >= q.Max +} +func (q *QuotaStat) Percent() int64 { + return int64(q.Ratio * 100) +} + +func (q *QuotaStat) PrettyCurrent() string { + return prettyValue(q.Current) +} +func (q *QuotaStat) PrettyMax() string { + return prettyValue(q.Max) +} + +func prettyValue(v int64) string { + if v < 1024 { + return fmt.Sprintf("%d octets", v) + } + v = v / 1024 + if v < 1024 { + return fmt.Sprintf("%d kio", v) + } + v = v / 1024 + if v < 1024 { + return fmt.Sprintf("%d Mio", v) + } + v = v / 1024 + if v < 1024 { + return fmt.Sprintf("%d Gio", v) + } + v = v / 1024 + return fmt.Sprintf("%d Tio", v) +} + +type WebsiteId struct { + Pretty string + Internal string + Alt []string + Expanded bool + Url string + +} +func NewWebsiteId(id string, aliases []string) *WebsiteId { + pretty := id + var alt []string + if len(aliases) > 0 { + pretty = aliases[0] + alt = aliases[1:] + } + expanded := strings.Contains(pretty, ".") + + url := pretty + if !expanded { + url = fmt.Sprintf("%s.web.deuxfleurs.fr", pretty) + } + + return &WebsiteId { pretty, id, alt, expanded, url } +} +func NewWebsiteIdFromBucketInfo(binfo *garage.BucketInfo) *WebsiteId { + return NewWebsiteId(*binfo.Id, binfo.GlobalAliases) +} + +type WebsiteController struct { + User *LoggedUser + WebsiteIdx map[string]*WebsiteId + PrettyList []string + WebsiteCount QuotaStat +} + +func NewWebsiteController(user *LoggedUser) (*WebsiteController, error) { + idx := map[string]*WebsiteId{} + var wlist []string + + keyInfo, err := user.S3KeyInfo() + if err != nil { + return nil, err + } + + for _, bckt := range(keyInfo.Buckets) { + if len(bckt.GlobalAliases) > 0 { + wid := NewWebsiteId(*bckt.Id, bckt.GlobalAliases) + idx[wid.Pretty] = wid + wlist = append(wlist, wid.Pretty) + } + } + sort.Strings(wlist) + + maxW := user.Quota.WebsiteCount + quota := NewQuotaStat(int64(len(wlist)), maxW, true) + + return &WebsiteController { user, idx, wlist, quota }, nil +} + +func (w *WebsiteController) List() []*WebsiteId { + r := make([]*WebsiteId, 0, len(w.PrettyList)) + for _, k := range w.PrettyList { + r = append(r, w.WebsiteIdx[k]) + } + return r +} + +func (w *WebsiteController) Inspect(pretty string) (*WebsiteView, error) { + website, ok := w.WebsiteIdx[pretty] + if !ok { + return nil, ErrWebsiteNotFound + } + + binfo, err := grgGetBucket(website.Internal) + if err != nil { + return nil, ErrFetchBucketInfo + } + + return NewWebsiteView(binfo), nil +} + +func (w *WebsiteController) Patch(patch *WebsitePatch) (*WebsiteView, error) { + return nil, nil +} + +func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { + if pretty == "" { + return nil, ErrEmptyBucketName + } + + if w.WebsiteCount.IsFull() { + return nil, ErrWebsiteQuotaReached + } + + binfo, err := grgCreateBucket(pretty) + if err != nil { + return nil, ErrCantCreateBucket + } + + s3key, err := w.User.S3KeyInfo() + if err != nil { + return nil, err + } + + binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId) + if err != nil { + return nil, ErrCantAllowKey + } + + qr := w.User.Quota.DefaultWebsiteQuota() + wr := allowWebsiteDefault() + + ur := garage.NewUpdateBucketRequest() + ur.SetWebsiteAccess(*wr) + ur.SetQuotas(*qr) + + + binfo, err = grgUpdateBucket(*binfo.Id, ur) + if err != nil { + return nil, ErrCantConfigureBucket + } + + return NewWebsiteView(binfo), nil +} + + +type WebsiteView struct { + Name *WebsiteId + Size QuotaStat + Files QuotaStat +} + +func NewWebsiteView(binfo *garage.BucketInfo) *WebsiteView { + q := binfo.GetQuotas() + + wid := NewWebsiteIdFromBucketInfo(binfo) + size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true) + objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false) + return &WebsiteView { wid, size, objects } +} + +type WebsitePatch struct { + size int64 +}