package database import ( "encoding/json" "fmt" "io" "log" "os" "sync" "git.phc.dm.unipi.it/phc/storage/store" "git.phc.dm.unipi.it/phc/storage/utils" "golang.org/x/exp/slices" ) 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 { mu sync.Mutex 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 (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 withValue[T any]( transactorFunc func(func() error) error, workFunc func() (T, error)) (T, error) { var value T if err := transactorFunc(func() error { var err error value, err = workFunc() return err }); err != nil { var zero T return zero, err } return value, nil } func (db *jsonDB) writeTransaction(workFunc func() error) error { db.mu.Lock() defer db.mu.Unlock() if err := db.load(); err != nil { return err } if err := workFunc(); err != nil { return err } if err := db.store(); err != nil { return err } return nil } func (db *jsonDB) readTransaction(transactionFunc func() error) error { db.mu.Lock() defer db.mu.Unlock() if err := db.load(); err != nil { return err } if err := transactionFunc(); err != nil { return err } return nil } func (db *jsonDB) GetDashboardState() (DashboardState, error) { return withValue(db.readTransaction, func() (DashboardState, error) { 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) { return withValue(db.writeTransaction, func() (string, error) { key := utils.GenerateRandomString(32) db.APIKeys = append(db.APIKeys, key) return key, nil }) } func (db *jsonDB) AllAPIKeys() ([]string, error) { return withValue(db.readTransaction, func() ([]string, error) { return db.APIKeys, nil }) } func (db *jsonDB) CheckAPIKey(key string) error { return db.readTransaction(func() error { 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 { return db.writeTransaction(func() error { 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:]...) return nil }) } // // Bucket Methods // func (db *jsonDB) CreateBucket(bucket string, settings any) error { return db.writeTransaction(func() error { 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 return nil }) } func (db *jsonDB) GetBucketSettings(bucket string) (any, error) { return withValue(db.readTransaction, func() (any, error) { 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 { return db.writeTransaction(func() error { 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) return nil }) } func (db *jsonDB) DeleteBucket(bucket string) error { return db.writeTransaction(func() error { 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) return nil }) } func (db *jsonDB) AllBuckets() ([]string, error) { return withValue(db.readTransaction, func() ([]string, error) { 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) { return withValue(db.writeTransaction, func() (string, error) { 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) return id, err }) } func (db *jsonDB) SetBucketObject(bucket, id string, r io.Reader) error { return db.writeTransaction(func() error { b, found := db.Buckets[bucket] if !found { return fmt.Errorf("bucket named %q not found", bucket) } return store.Update(b.Settings.Path, db.PrefixSize, id, r) }) } func (db *jsonDB) GetBucketObject(bucket, id string, w io.Writer) error { return db.readTransaction(func() error { 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 { return db.writeTransaction(func() error { 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:]...) return nil }) } func (db *jsonDB) AllBucketObjects(bucket string) ([]string, error) { return withValue(db.writeTransaction, func() ([]string, error) { 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 return b.Objects, nil }) }