package main
import (
"fmt"
garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang"
"sort"
"strings"
)
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.)")
ErrBucketDeleteNotEmpty = fmt.Errorf("You must remove all the files before deleting a bucket")
ErrBucketDeleteUnfinishedUpload = fmt.Errorf("You must remove all the unfinished multipart uploads before deleting a bucket")
ErrCantChangeVhost = fmt.Errorf("Can't change the vhost to the desired value. Maybe it's already used by someone else or an internal error occured")
ErrCantRemoveOldVhost = fmt.Errorf("The new vhost is bound to the bucket but the old one can't be removed, this is an internal error")
)
type WebsiteId struct {
Pretty string `json:"name"`
Internal string `json:"-"`
Alt []string `json:"alt_name"`
Expanded bool `json:"expanded"`
Url string `json:"domain"`
}
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
}
type WebsiteDescribe struct {
AccessKeyId string `json:"access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
AllowedWebsites *QuotaStat `json:"quota_website_count"`
BurstBucketQuotaSize string `json:"burst_bucket_quota_size"`
Websites []*WebsiteId `json:"vhosts"`
}
func (w *WebsiteController) Describe() (*WebsiteDescribe, error) {
s3key, err := w.User.S3KeyInfo()
if err != nil {
return nil, err
}
r := make([]*WebsiteId, 0, len(w.PrettyList))
for _, k := range w.PrettyList {
r = append(r, w.WebsiteIdx[k])
}
return &WebsiteDescribe{
*s3key.AccessKeyId,
*s3key.SecretAccessKey,
&w.WebsiteCount,
w.User.Quota.WebsiteSizeBurstedPretty(),
r}, nil
}
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(pretty string, patch *WebsitePatch) (*WebsiteView, error) {
website, ok := w.WebsiteIdx[pretty]
if !ok {
return nil, ErrWebsiteNotFound
}
binfo, err := grgGetBucket(website.Internal)
if err != nil {
return nil, ErrFetchBucketInfo
}
// Patch the max size
urQuota := garage.NewUpdateBucketRequestQuotas()
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(binfo.Quotas.GetMaxSize()))
urQuota.SetMaxObjects(w.User.Quota.WebsiteObjectAdjust(binfo.Quotas.GetMaxObjects()))
if patch.Size != nil {
urQuota.SetMaxSize(w.User.Quota.WebsiteSizeAdjust(*patch.Size))
}
// Build the update
ur := garage.NewUpdateBucketRequest()
ur.SetQuotas(*urQuota)
// Call garage "update bucket" function
binfo, err = grgUpdateBucket(website.Internal, ur)
if err != nil {
return nil, ErrCantConfigureBucket
}
// Update the alias if the vhost field is set and different
if patch.Vhost != nil && *patch.Vhost != "" && *patch.Vhost != pretty {
binfo, err = grgAddGlobalAlias(website.Internal, *patch.Vhost)
if err != nil {
return nil, ErrCantChangeVhost
}
binfo, err = grgDelGlobalAlias(website.Internal, pretty)
if err != nil {
return nil, ErrCantRemoveOldVhost
}
}
return NewWebsiteView(binfo), 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
}
func (w *WebsiteController) Delete(pretty string) error {
if pretty == "" {
return ErrEmptyBucketName
}
website, ok := w.WebsiteIdx[pretty]
if !ok {
return ErrWebsiteNotFound
}
binfo, err := grgGetBucket(website.Internal)
if err != nil {
return ErrFetchBucketInfo
}
if *binfo.Objects > int64(0) {
return ErrBucketDeleteNotEmpty
}
if *binfo.UnfinishedUploads > int32(0) {
return ErrBucketDeleteUnfinishedUpload
}
err = grgDeleteBucket(website.Internal)
return err
}
type WebsiteView struct {
Name *WebsiteId `json:"vhost"`
Size QuotaStat `json:"quota_size"`
Files QuotaStat `json:"quota_files"`
}
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 `json:"quota_size"`
Vhost *string `json:"vhost"`
}