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, it's an internal error") ErrFetchDedicatedKey = fmt.Errorf("Bucket has no dedicated key while it's required, it's 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 { AllowedWebsites *QuotaStat `json:"quota_website_count"` BurstBucketQuotaSize string `json:"burst_bucket_quota_size"` Websites []*WebsiteId `json:"vhosts"` } func (w *WebsiteController) Describe() (*WebsiteDescribe, error) { r := make([]*WebsiteId, 0, len(w.PrettyList)) for _, k := range w.PrettyList { r = append(r, w.WebsiteIdx[k]) } return &WebsiteDescribe{ &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 } // @TODO: fetch the associated key 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 } } if patch.RotateKey != nil && *patch.RotateKey { // @TODO: rotate key } return NewWebsiteView(binfo, nil) } func (w *WebsiteController) Create(pretty string) (*WebsiteView, error) { if pretty == "" { return nil, ErrEmptyBucketName } if w.WebsiteCount.IsFull() { return nil, ErrWebsiteQuotaReached } // Create bucket binfo, err := grgCreateBucket(pretty) if err != nil { return nil, ErrCantCreateBucket } // Allow user's global key on bucket s3key, err := w.User.S3KeyInfo() if err != nil { return nil, err } binfo, err = grgAllowKeyOnBucket(*binfo.Id, *s3key.AccessKeyId) if err != nil { return nil, ErrCantAllowKey } // Set quota 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 } // Create a dedicated key // @TODO return NewWebsiteView(binfo, nil) } func (w *WebsiteController) Delete(pretty string) error { if pretty == "" { return ErrEmptyBucketName } website, ok := w.WebsiteIdx[pretty] if !ok { return ErrWebsiteNotFound } // Error checking binfo, err := grgGetBucket(website.Internal) if err != nil { return ErrFetchBucketInfo } if *binfo.Objects > int64(0) { return ErrBucketDeleteNotEmpty } if *binfo.UnfinishedUploads > int32(0) { return ErrBucketDeleteUnfinishedUpload } // Delete dedicated key // @TODO // Actually delete bucket err = grgDeleteBucket(website.Internal) return err } type WebsiteView struct { Name *WebsiteId `json:"vhost"` AccessKeyId string `json:"access_key_id"` SecretAccessKey string `json:"secret_access_key"` Size QuotaStat `json:"quota_size"` Files QuotaStat `json:"quota_files"` } func NewWebsiteView(binfo *garage.BucketInfo, s3key *garage.KeyInfo) (*WebsiteView, error) { if binfo == nil { return nil, ErrFetchBucketInfo } if s3key == nil { return nil, ErrFetchDedicatedKey } q := binfo.GetQuotas() wid := NewWebsiteIdFromBucketInfo(binfo) size := NewQuotaStat(*binfo.Bytes, (&q).GetMaxSize(), true) objects := NewQuotaStat(*binfo.Objects, (&q).GetMaxObjects(), false) return &WebsiteView{ wid, *s3key.AccessKeyId, *s3key.SecretAccessKey.Get(), size, objects, }, nil } type WebsitePatch struct { Size *int64 `json:"quota_size"` Vhost *string `json:"vhost"` RotateKey *bool `json:"rotate_key"` }