An API for Guichet #23

Merged
quentin merged 14 commits from api into main 2023-09-26 06:44:36 +00:00
8 changed files with 333 additions and 155 deletions
Showing only changes of commit bc368943a4 - Show all commits

2
api.go
View file

@ -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 {

124
garage.go
View file

@ -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
}
func handleGarageWebsiteNew(w http.ResponseWriter, r *http.Request) {
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 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....
binfo, err := grgGetBucket(bucketId)
ctrl, err := NewWebsiteController(user)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
wc := binfo.GetWebsiteConfig()
q := binfo.GetQuotas()
bucketName := mux.Vars(r)["bucket"]
view := webInspectView{
User: user,
Bucket: binfo,
IndexDoc: (&wc).GetIndexDocument(),
ErrorDoc: (&wc).GetErrorDocument(),
MaxObjects: (&q).GetMaxObjects(),
MaxSize: (&q).GetMaxSize(),
view, err := ctrl.Inspect(bucketName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tpl := &WebsiteInspectTpl{ ctrl, view }
tWebsiteInspect := getTemplate("garage_website_inspect.html")
tWebsiteInspect.Execute(w, &view)
tWebsiteInspect.Execute(w, &tpl)
}

10
main.go
View file

@ -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)

View file

@ -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>

View file

@ -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 }}
<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 }}
https://{{ $alias }}.web.deuxfleurs.fr
<a href="/website/inspect/{{ $wid.Pretty }}" class="list-group-item list-group-item-action">
{{ $wid.Url }}
</a>
{{ 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>
</div>
</div>
<div class="col-md-9">
<h2>{{ .View.Name.Url }}</h2>
<h4>Configurer le nom de domaine</h4>
<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>
{{ 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>
<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 }}

View file

@ -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}}

View file

@ -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>

211
website.go Normal file
View file

@ -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
}