package database import ( "encoding/json" "fmt" "io" "log" "os" "git.phc.dm.unipi.it/phc/storage/store" "git.phc.dm.unipi.it/phc/storage/utils" "golang.org/x/exp/slices" ) type MonitorWidget struct { Type string `json:"type"` Value any `json:"value"` } type DashboardState struct { Widgets []MonitorWidget `json:"widgets"` } type Database interface { // Dashboard DashboardState GetDashboardState() (DashboardState, error) SetDashboardState(DashboardState) error // API Keys CreateAPIKey() (string, error) CheckAPIKey(key string) error RemoveAPIKey(key string) error AllAPIKeys() ([]string, error) // Bucket Operations CreateBucket(bucket string, options ...any) error DeleteBucket(bucket string) error AllBuckets() ([]string, error) // Bucket Object Operations CreateBucketObject(bucket string, r io.Reader) (string, error) DeleteBucketObject(bucket, id string) error GetBucketObject(bucket, id string, w io.Writer) error SetBucketObject(bucket, id string, r io.Reader) error AllBucketObjects(bucket string) ([]string, error) } type jsonBucket struct { Name string `json:"name"` Path string `json:"path"` Objects []string `json:"objects"` } type jsonDB struct { file string PrefixSize int `json:"prefixSize"` DashboardState DashboardState `json:"dashboardState"` Buckets map[string]*jsonBucket `json:"buckets"` APIKeys []string `json:"api-keys"` } func NewJSON(file string) Database { db := &jsonDB{ file: file, PrefixSize: 2, DashboardState: DashboardState{ Widgets: []MonitorWidget{}, }, Buckets: map[string]*jsonBucket{}, APIKeys: []string{}, } err := db.load() if err != nil { panic(err) } return db } // func writeTransaction[T any](db *jsonDB, transactionFunc func(db *jsonDB) (T, error)) (T, error) { // var zero T // if err := db.load(); err != nil { // return zero, err // } // v, err := transactionFunc(db) // if err := db.store(); err != nil { // return zero, err // } // return v, err // } func (db *jsonDB) setup() error { if _, err := os.Stat(db.file); !os.IsNotExist(err) { return nil } log.Printf("missing %q, creating empty database file", db.file) f, err := os.Create(db.file) defer f.Close() if err != nil { return err } enc := json.NewEncoder(f) enc.SetIndent("", " ") if err := enc.Encode(&db); err != nil { return err } return nil } func (db *jsonDB) store() error { f, err := os.Create(db.file) defer f.Close() if err != nil { return err } enc := json.NewEncoder(f) enc.SetIndent("", " ") if err := enc.Encode(&db); err != nil { return err } return nil } func (db *jsonDB) load() error { if err := db.setup(); err != nil { return err } f, err := os.Open(db.file) defer f.Close() if err != nil { return err } if err := json.NewDecoder(f).Decode(&db); err != nil { return err } return nil } func (db *jsonDB) GetDashboardState() (DashboardState, error) { if err := db.load(); err != nil { return DashboardState{}, err } return db.DashboardState, nil } func (db *jsonDB) SetDashboardState(state DashboardState) error { if err := db.load(); err != nil { return err } db.DashboardState = state if err := db.store(); err != nil { return err } return nil } func (db *jsonDB) CreateAPIKey() (string, error) { if err := db.load(); err != nil { return "", err } key := utils.GenerateRandomString(32) db.APIKeys = append(db.APIKeys, key) if err := db.store(); err != nil { return "", err } return key, nil } func (db *jsonDB) AllAPIKeys() ([]string, error) { if err := db.load(); err != nil { return nil, err } return db.APIKeys, nil } func (db *jsonDB) CheckAPIKey(key string) error { if err := db.load(); err != nil { return err } if !slices.Contains(db.APIKeys, key) { return fmt.Errorf("the given API key %q is invalid", key) } return nil } func (db *jsonDB) RemoveAPIKey(key string) error { if err := db.load(); err != nil { return err } i := slices.Index(db.APIKeys, key) if i == -1 { return fmt.Errorf("the given API key %q is invalid", key) } db.APIKeys = append(db.APIKeys[:i], db.APIKeys[i+1:]...) if err := db.store(); err != nil { return err } return nil } func (db *jsonDB) CreateBucket(bucket string, options ...any) error { if err := db.load(); err != nil { return err } if _, found := db.Buckets[bucket]; found { return fmt.Errorf("bucket named %q already present", bucket) } bi := &jsonBucket{ Name: bucket, Path: bucket + "/", Objects: []string{}, } if len(options) > 0 { bi.Path = options[0].(string) } db.Buckets[bucket] = bi if err := db.store(); err != nil { return err } return nil } func (db *jsonDB) DeleteBucket(bucket string) error { if err := db.load(); err != nil { return err } if _, found := db.Buckets[bucket]; !found { return fmt.Errorf("bucket named %q not found", bucket) } delete(db.Buckets, bucket) log.Printf("The bucket named %q was removed but its files are still on disk", bucket) if err := db.store(); err != nil { return err } return nil } func (db *jsonDB) AllBuckets() ([]string, error) { if err := db.load(); err != nil { return nil, err } buckets := make([]string, 0, len(db.Buckets)) for _, b := range db.Buckets { buckets = append(buckets, b.Name) } return buckets, nil } func (db *jsonDB) CreateBucketObject(bucket string, r io.Reader) (string, error) { if err := db.load(); err != nil { return "", err } b, found := db.Buckets[bucket] if !found { return "", fmt.Errorf("bucket named %q not found", bucket) } id, err := store.Create(b.Path, db.PrefixSize, r) if err != nil { return "", err } b.Objects = append(b.Objects, id) if err := db.store(); err != nil { return "", err } return id, err } func (db *jsonDB) SetBucketObject(bucket, id string, r io.Reader) error { if err := db.load(); err != nil { return err } b, found := db.Buckets[bucket] if !found { return fmt.Errorf("bucket named %q not found", bucket) } if err := db.store(); err != nil { return err } return store.Update(b.Path, db.PrefixSize, id, r) } func (db *jsonDB) GetBucketObject(bucket, id string, w io.Writer) error { if err := db.load(); err != nil { return err } b, found := db.Buckets[bucket] if !found { return fmt.Errorf("bucket named %q not found", bucket) } return store.Read(b.Path, db.PrefixSize, id, w) } func (db *jsonDB) DeleteBucketObject(bucket, id string) error { if err := db.load(); err != nil { return err } b, found := db.Buckets[bucket] if !found { return fmt.Errorf("bucket named %q not found", bucket) } if err := store.Delete(b.Path, db.PrefixSize, id); err != nil { return err } i := slices.Index(b.Objects, id) b.Objects = append(b.Objects[:i], b.Objects[i+1:]...) if err := db.store(); err != nil { return err } return nil } func (db *jsonDB) AllBucketObjects(bucket string) ([]string, error) { if err := db.load(); err != nil { return nil, err } b, found := db.Buckets[bucket] if !found { return nil, fmt.Errorf("bucket named %q not found", bucket) } objects, err := store.All(b.Path) if err != nil { return nil, err } b.Objects = objects if err := db.store(); err != nil { return nil, err } return b.Objects, nil }