aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api.go2
-rw-r--r--garage.go128
-rw-r--r--main.go10
-rw-r--r--templates/garage_key.html2
-rw-r--r--templates/garage_website_inspect.html97
-rw-r--r--templates/garage_website_list.html38
-rw-r--r--templates/home.html4
-rw-r--r--website.go211
8 files changed, 335 insertions, 157 deletions
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)
+ }
+}
+
+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)
}
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"}}
<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
+}