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 AllAPIKeys() ([]string, error) CreateAPIKey() (string, error) CheckAPIKey(key string) error RemoveAPIKey(key string) error // Bucket Operations AllBuckets() ([]string, error) CreateBucket(bucket string, settings any) error GetBucketSettings(bucket string) (any, error) SetBucketSettings(bucket string, settings any) error DeleteBucket(bucket string) error // Bucket Object Operations AllBucketObjects(bucket string) ([]string, error) CreateBucketObject(bucket string, r io.Reader) (string, error) GetBucketObject(bucket, id string, w io.Writer) error SetBucketObject(bucket, id string, r io.Reader) error DeleteBucketObject(bucket, id string) error } type JsonBucketSettings struct { Path string `json:"path"` } type jsonBucket struct { Name string `json:"name"` Objects []string `json:"objects"` Settings *JsonBucketSettings `json:"settings"` } 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 } // // Bucket Methods // func (db *jsonDB) CreateBucket(bucket string, settings 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, Settings: &JsonBucketSettings{ Path: bucket + "/", }, Objects: []string{}, } if settings != nil { bi.Settings = settings.(*JsonBucketSettings) } db.Buckets[bucket] = bi if err := db.store(); err != nil { return err } return nil } func (db *jsonDB) GetBucketSettings(bucket string) (any, error) { if err := db.load(); err != nil { return nil, err } if _, found := db.Buckets[bucket]; !found { return nil, fmt.Errorf("bucket named %q not found", bucket) } return db.Buckets[bucket].Settings, nil } func (db *jsonDB) SetBucketSettings(bucket string, settings any) 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) } b.Settings = settings.(*JsonBucketSettings) log.Printf("[Warning] Bucket %q settings changed but bucket migration is not yet implemented (move the folder yourself)", bucket) 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.Settings.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.Settings.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.Settings.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.Settings.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.Settings.Path) if err != nil { return nil, err } b.Objects = objects if err := db.store(); err != nil { return nil, err } return b.Objects, nil }