diff options
-rw-r--r-- | api.go | 2 | ||||
-rw-r--r-- | garage.go | 128 | ||||
-rw-r--r-- | main.go | 10 | ||||
-rw-r--r-- | templates/garage_key.html | 2 | ||||
-rw-r--r-- | templates/garage_website_inspect.html | 97 | ||||
-rw-r--r-- | templates/garage_website_list.html | 38 | ||||
-rw-r--r-- | templates/home.html | 4 | ||||
-rw-r--r-- | website.go | 211 |
8 files changed, 335 insertions, 157 deletions
@@ -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 { @@ -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) + } +} + +type WebsiteNewTpl struct { + ctrl *WebsiteController + err error } -func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) { +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() - if err != nil { - log.Println(err) - // @FIXME we need to return the error to the user - tWebsiteNew.Execute(w, nil) - return - } - binfo, err := grgCreateWebsite(*keyInfo.AccessKeyId, bucket, user.Quota) + 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) } - 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) } @@ -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"}} <div class="d-flex"> <h4>Mes identifiants</h4> - <a class="ml-auto btn btn-link" href="/garage/website">Mes sites webs</a> + <a class="ml-auto btn btn-link" href="/website">Mes sites webs</a> <a class="ml-4 btn btn-info" href="/">Menu principal</a> </div> 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"}} <div class="d-flex"> - <h4>Inspecter le site web</h4> - <a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a> - <a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a> - <a class="ml-4 btn btn-info" href="/garage/website">Mes sites webs</a> + <a class="ml-4 btn btn-primary" href="/website/new">Nouveau site web</a> + <!--<h4>Inspecter les sites webs</h4>--> + <a class="ml-auto btn btn-link" href="/website/configure">Mes identifiants</a> + <a class="ml-4 btn btn-info" href="/">Menu principal</a> </div> -<table class="table mt-4"> - <tbody> - <tr> - <th scope="row">ID</th> - <td>{{ .Bucket.Id }}</td> - </tr> - <tr> - <th scope="row">URLs</th> - <td> - {{ range $alias := .Bucket.GlobalAliases }} - {{ if contains $alias "." }} - https://{{ $alias }} - {{ else }} - https://{{ $alias }}.web.deuxfleurs.fr - {{ end }} - {{ end }} - </td> - </tr> - <tr> - <th scope="row">Document d'index</th> - <td> {{ .IndexDoc }}</td> - </tr> - <tr> - <th scope="row">Document d'erreur</th> - <td>{{ .ErrorDoc }}</td> - </tr> - <tr> - <th scope="row">Nombre de fichiers</th> - <td>{{ .Bucket.Objects }} / {{ .MaxObjects }}</td> - </tr> - <tr> - <th scope="row">Espace utilisé</th> - <td>{{ .Bucket.Bytes }} / {{ .MaxSize }} octets</td> - </tr> - </tbody> -</table> - -<h4>Configurer le nom de domaine</h4> - -{{ range $alias := .Bucket.GlobalAliases }} -{{ if contains $alias "." }} -<p> 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 <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p> -{{ else }} -<p> Le nom de domaine https://{{ $alias }}.web.deuxfleurs.fr est fourni par Deuxfleurs, il n'y a pas de configuration à faire.</p> -{{ end }} +<div class="row mt-3" > + <div class="col-md-3"> + <div class="list-group"> + {{ $view := .View }} + {{ range $wid := .Ctrl.List }} + {{ if eq $wid.Internal $view.Name.Internal }} + <a href="/website/inspect/{{ $wid.Pretty }}" class="list-group-item list-group-item-action active"> + {{ $wid.Url }} + </a> + {{ else }} + <a href="/website/inspect/{{ $wid.Pretty }}" class="list-group-item list-group-item-action"> + {{ $wid.Url }} + </a> + {{ end }} + {{ end }} + </div> + </div> + <div class="col-md-9"> + <h2>{{ .View.Name.Url }}</h2> + + <h5 class="mt-3">Quotas</h5> + <div class="progress mt-3"> + <div class="progress-bar" role="progressbar" aria-valuenow="{{ .View.Size.Current }}" aria-valuemin="0" aria-valuemax="{{ .View.Size.Max }}" style="width: {{ .View.Size.Percent }}%; min-width: 2em;"> + {{ .View.Size.Ratio }}% + </div> + </div> + + <p class="text-center"> + {{ .View.Size.PrettyCurrent }} utilisé sur un maximum de {{ .View.Size.PrettyMax }} + {{ if gt .View.Files.Ratio 0.5 }} + <br>{{ .View.Files.Current }} fichiers sur un maximum de {{ .View.Files.Max }} + {{ end }} + </p> + + + {{ if .View.Name.Expanded }} + <h5 class="mt-5">Vous ne savez pas comment configurer votre nom de domaine ?</h5> + <p> 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 <code>CNAME garage.deuxfleurs.fr</code> ou <code>ALIAS garage.deuxfleurs.fr</code> auprès de votre hébergeur DNS, qui est souvent aussi le bureau d'enregistrement (eg. Gandi, GoDaddy, BookMyName, etc.).</p> + {{ end }} + + + </div> +</div> {{ 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"}} - -<div class="d-flex"> - <h4>Sites webs</h4> - <a class="ml-auto btn btn-link" href="/garage/key">Mes identifiants</a> - <a class="ml-4 btn btn-success" href="/garage/website/new">Nouveau site web</a> - <a class="ml-4 btn btn-info" href="/">Menu principal</a> -</div> - -<table class="table mt-4"> - <thead> - <th scope="col">ID</th> - <th scope="col">URLs</th> - </thead> - <tbody> - {{ range $buck := .S3KeyInfo.Buckets }} - {{ if $buck.GlobalAliases }} - <tr> - <td> - <a href="/garage/website/b/{{$buck.Id}}">{{$buck.Id}}</a> - </td> - <td> - {{ range $alias := $buck.GlobalAliases }} - {{ if contains $alias "." }} - https://{{ $alias }} - {{ else }} - https://{{ $alias }}.web.deuxfleurs.fr - {{ end }} - {{ end }} - </td> - </tr> - {{ end }} - {{ end }} - </tbody> -</table> -{{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 </div> <div class="list-group list-group-flush"> - <a class="list-group-item list-group-item-action" href="/garage/key">Mes identifiants</a> - <a class="list-group-item list-group-item-action" href="/garage/website">Mes sites webs</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> </div> </div> </div> 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 +} |