diff --git a/database/database.go b/database/database.go index 991e74f..451c7fa 100644 --- a/database/database.go +++ b/database/database.go @@ -1,15 +1,7 @@ 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 { @@ -53,391 +45,3 @@ type Database interface { 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 -} diff --git a/database/jsondb.go b/database/jsondb.go new file mode 100644 index 0000000..300a146 --- /dev/null +++ b/database/jsondb.go @@ -0,0 +1,379 @@ +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 + }) +}