package noodle import ( "errors" "fmt" "log" "sync" "time" "github.com/google/uuid" "go.mills.io/bitcask/v2" ) var ErrUserNotFound = errors.New("user not found") var ErrLastAdminRemoval = errors.New("cannot remove the last admin") type Database struct { connection *bitcask.Bitcask Handle *bitcask.Collection Users *bitcask.Collection mu sync.Mutex } func NewDatabase(path string) *Database { db, err := bitcask.Open(path) if err != nil { log.Fatal(err) } return &Database{ connection: db, Handle: db.Collection("noodles"), Users: db.Collection("users"), } } func (db *Database) MakeID() string { id, err := uuid.NewUUID() if err != nil { log.Print(err) return "" } return id.String() } func (db *Database) GetAll() []Noodle { db.mu.Lock() defer db.mu.Unlock() var data []Noodle if err := db.Handle.List(&data); err != nil { log.Print(err) return nil } return data } func (db *Database) GetAllGeneric() []interface{} { db.mu.Lock() defer db.mu.Unlock() var data []interface{} if err := db.Handle.List(&data); err != nil { log.Print(err) return nil } return data } func (db *Database) Get(id string) Noodle { db.mu.Lock() defer db.mu.Unlock() var item Noodle log.Printf("Looking up noodle key='%s'", id) log.Printf("key='%s' exists=%t", id, db.Handle.Has(id)) if err := db.Handle.Get(id, &item); err != nil { log.Print(err) return Noodle{} } return item } func (db *Database) Add(item Noodle) error { db.mu.Lock() defer db.mu.Unlock() if err := db.Handle.Add(item.Id, item); err != nil { log.Print(err) return err } return nil } func (db *Database) Update(item Noodle) error { db.mu.Lock() defer db.mu.Unlock() if err := db.Handle.Add(item.Id, item); err != nil { log.Print(err) return err } return nil } func (db *Database) Delete(id string) error { db.mu.Lock() defer db.mu.Unlock() if err := db.Handle.Delete(id); err != nil { log.Print(err) return err } return nil } func (db *Database) Close() error { return db.connection.Close() } func (db *Database) GetAllUsers() []User { db.mu.Lock() defer db.mu.Unlock() var data []User if err := db.Users.List(&data); err != nil { log.Print(err) return nil } return data } func (db *Database) GetUserByUsername(username string) (User, error) { db.mu.Lock() defer db.mu.Unlock() var data []User if err := db.Users.List(&data); err != nil { log.Print(err) return User{}, err } for _, user := range data { if user.Username == username { return user, nil } } return User{}, ErrUserNotFound } func (db *Database) AddUser(user User) error { db.mu.Lock() defer db.mu.Unlock() var data []User if err := db.Users.List(&data); err != nil { log.Print(err) return err } for _, existing := range data { if existing.Username == user.Username { return fmt.Errorf("user %q already exists", user.Username) } } if err := db.Users.Add(user.Id, user); err != nil { log.Print(err) return err } return nil } func (db *Database) UpdateUser(user User) error { db.mu.Lock() defer db.mu.Unlock() if err := db.Users.Add(user.Id, user); err != nil { log.Print(err) return err } return nil } func (db *Database) SetUserRole(username, role string) (User, error) { db.mu.Lock() defer db.mu.Unlock() var data []User if err := db.Users.List(&data); err != nil { log.Print(err) return User{}, err } var target User found := false adminCount := 0 for _, user := range data { if user.Role == UserRoleAdmin { adminCount++ } if user.Username == username { target = user found = true } } if !found { return User{}, ErrUserNotFound } if target.Role == UserRoleAdmin && role != UserRoleAdmin && adminCount <= 1 { return User{}, ErrLastAdminRemoval } target.Role = role if err := db.Users.Add(target.Id, target); err != nil { log.Print(err) return User{}, err } return target, nil } func (db *Database) SetUserPassword(username, passwordHash string) (User, error) { db.mu.Lock() defer db.mu.Unlock() var data []User if err := db.Users.List(&data); err != nil { log.Print(err) return User{}, err } for _, user := range data { if user.Username != username { continue } user.PasswordHash = passwordHash if err := db.Users.Add(user.Id, user); err != nil { log.Print(err) return User{}, err } return user, nil } return User{}, ErrUserNotFound } func (db *Database) DeleteUser(username string) (User, error) { db.mu.Lock() defer db.mu.Unlock() var data []User if err := db.Users.List(&data); err != nil { log.Print(err) return User{}, err } var target User found := false adminCount := 0 for _, user := range data { if user.Role == UserRoleAdmin { adminCount++ } if user.Username == username { target = user found = true } } if !found { return User{}, ErrUserNotFound } if target.Role == UserRoleAdmin && adminCount <= 1 { return User{}, ErrLastAdminRemoval } if err := db.Users.Delete(target.Id); err != nil { log.Print(err) return User{}, err } return target, nil } func (db *Database) SetIsUp(id string, isUp bool) (Noodle, error) { db.mu.Lock() defer db.mu.Unlock() var item Noodle if err := db.Handle.Get(id, &item); err != nil { log.Print(err) return Noodle{}, err } item.IsUp = isUp if err := db.Handle.Add(item.Id, item); err != nil { log.Print(err) return Noodle{}, err } return item, nil } func (db *Database) DeleteByID(id string) (Noodle, error) { db.mu.Lock() defer db.mu.Unlock() var item Noodle if err := db.Handle.Get(id, &item); err != nil { log.Print(err) return Noodle{}, err } if err := db.Handle.Delete(id); err != nil { log.Print(err) return Noodle{}, err } return item, nil } func (db *Database) TickExpirations() []Noodle { db.mu.Lock() defer db.mu.Unlock() var noodles []Noodle if err := db.Handle.List(&noodles); err != nil { log.Print(err) return nil } var stopped []Noodle for _, item := range noodles { if !item.IsUp { continue } if item.Expiration <= 0 { item.IsUp = false stopped = append(stopped, item) if err := db.Handle.Delete(item.Id); err != nil { log.Print(err) } continue } item.Expiration -= time.Second if item.Expiration <= 0 { item.Expiration = 0 item.IsUp = false stopped = append(stopped, item) if err := db.Handle.Delete(item.Id); err != nil { log.Print(err) } continue } if err := db.Handle.Add(item.Id, item); err != nil { log.Print(err) } } return stopped }