Files
anydb/app/db.go

420 lines
8.5 KiB
Go
Raw Permalink Normal View History

2024-12-21 00:08:16 +01:00
/*
Copyright © 2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2024-12-16 19:11:36 +01:00
package app
import (
2024-12-17 14:23:56 +01:00
"encoding/json"
"errors"
2024-12-17 14:23:56 +01:00
"fmt"
2024-12-16 19:11:36 +01:00
"os"
"path/filepath"
2024-12-17 14:23:56 +01:00
"regexp"
"strings"
2024-12-16 19:11:36 +01:00
"time"
2024-12-17 14:23:56 +01:00
bolt "go.etcd.io/bbolt"
2024-12-16 19:11:36 +01:00
)
const MaxValueWidth int = 60
2024-12-16 19:11:36 +01:00
type DB struct {
2024-12-17 14:23:56 +01:00
Debug bool
Dbfile string
2024-12-21 22:49:22 +01:00
Bucket string
2024-12-17 14:23:56 +01:00
DB *bolt.DB
2024-12-16 19:11:36 +01:00
}
2024-12-17 14:23:56 +01:00
type DbEntry struct {
2024-12-19 10:37:21 +01:00
Id string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
Encrypted bool `json:"encrypted"`
Bin []byte `json:"bin"`
Tags []string `json:"tags"`
Created time.Time `json:"created"`
Size int
}
2024-12-22 00:07:33 +01:00
type BucketInfo struct {
Name string
Keys int
Size int
Sequence uint64
Stats bolt.BucketStats
2024-12-22 00:07:33 +01:00
}
type DbInfo struct {
Buckets []BucketInfo
Path string
}
// Post process an entry for list output.
// Do NOT call it during write processing!
func (entry *DbEntry) Normalize() {
entry.Size = len(entry.Value)
if entry.Encrypted {
entry.Value = "<encrypted-content>"
}
if len(entry.Bin) > 0 {
entry.Value = "<binary-content>"
entry.Size = len(entry.Bin)
}
2024-12-22 17:46:44 +01:00
if strings.Contains(entry.Value, "\n") {
parts := strings.Split(entry.Value, "\n")
if len(parts) > 0 {
entry.Value = parts[0]
}
}
if len(entry.Value) > MaxValueWidth {
entry.Value = entry.Value[0:MaxValueWidth] + "..."
}
2024-12-16 19:11:36 +01:00
}
2024-12-17 14:23:56 +01:00
type DbEntries []DbEntry
type DbTag struct {
Keys []string `json:"key"`
2024-12-16 19:11:36 +01:00
}
2024-12-17 14:23:56 +01:00
const BucketData string = "data"
2024-12-21 22:49:22 +01:00
func New(file string, bucket string, debug bool) (*DB, error) {
2024-12-22 00:07:33 +01:00
return &DB{Debug: debug, Dbfile: file, Bucket: bucket}, nil
2024-12-17 14:23:56 +01:00
}
func (db *DB) Open() error {
2024-12-18 20:51:29 +01:00
if _, err := os.Stat(filepath.Dir(db.Dbfile)); os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(db.Dbfile), 0700); err != nil {
return err
}
}
2024-12-17 14:23:56 +01:00
b, err := bolt.Open(db.Dbfile, 0600, nil)
2024-12-16 19:11:36 +01:00
if err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to open DB %s: %w", db.Dbfile, err)
2024-12-16 19:11:36 +01:00
}
2024-12-17 14:23:56 +01:00
db.DB = b
return nil
2024-12-16 19:11:36 +01:00
}
func (db *DB) Close() error {
return db.DB.Close()
}
2024-12-17 14:23:56 +01:00
func (db *DB) List(attr *DbAttr) (DbEntries, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
var entries DbEntries
var filter *regexp.Regexp
2024-12-16 19:11:36 +01:00
if len(attr.Args) > 0 {
2024-12-17 14:23:56 +01:00
filter = regexp.MustCompile(attr.Args[0])
2024-12-16 19:11:36 +01:00
}
2024-12-17 14:23:56 +01:00
err := db.DB.View(func(tx *bolt.Tx) error {
2024-12-21 22:49:22 +01:00
bucket := tx.Bucket([]byte(db.Bucket))
2024-12-17 14:23:56 +01:00
if bucket == nil {
return nil
}
err := bucket.ForEach(func(key, jsonentry []byte) error {
var entry DbEntry
if err := json.Unmarshal(jsonentry, &entry); err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to unmarshal from json: %w", err)
2024-12-17 14:23:56 +01:00
}
var include bool
switch {
case filter != nil:
if filter.MatchString(entry.Value) ||
filter.MatchString(entry.Key) ||
filter.MatchString(strings.Join(entry.Tags, " ")) {
include = true
}
case len(attr.Tags) > 0:
for _, search := range attr.Tags {
for _, tag := range entry.Tags {
if tag == search {
include = true
break
}
}
if include {
break
}
}
default:
include = true
}
if include {
entries = append(entries, entry)
}
return nil
})
2024-12-19 11:05:26 +01:00
return err
2024-12-17 14:23:56 +01:00
})
return entries, err
}
func (db *DB) Set(attr *DbAttr) error {
if err := db.Open(); err != nil {
return err
}
defer db.Close()
entry := DbEntry{
2024-12-19 10:37:21 +01:00
Key: attr.Key,
Value: attr.Val,
Bin: attr.Bin,
Tags: attr.Tags,
Encrypted: attr.Encrypted,
Created: time.Now(),
2024-12-17 14:23:56 +01:00
}
// check if the entry already exists and if yes, check if it has
// any tags. if so, we initialize our update struct with these
// tags unless it has new tags configured.
err := db.DB.View(func(tx *bolt.Tx) error {
2024-12-21 22:49:22 +01:00
bucket := tx.Bucket([]byte(db.Bucket))
if bucket == nil {
return nil
}
jsonentry := bucket.Get([]byte(entry.Key))
if jsonentry == nil {
return nil
}
var oldentry DbEntry
if err := json.Unmarshal(jsonentry, &oldentry); err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to unmarshal from json: %w", err)
}
if len(oldentry.Tags) > 0 && len(entry.Tags) == 0 {
// initialize update entry with tags from old entry
entry.Tags = oldentry.Tags
}
return nil
})
if err != nil {
return err
}
err = db.DB.Update(func(tx *bolt.Tx) error {
2024-12-17 14:23:56 +01:00
// insert data
2024-12-21 22:49:22 +01:00
bucket, err := tx.CreateBucketIfNotExists([]byte(db.Bucket))
2024-12-17 14:23:56 +01:00
if err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to create DB bucket: %w", err)
2024-12-17 14:23:56 +01:00
}
jsonentry, err := json.Marshal(entry)
if err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to marshall json: %w", err)
2024-12-17 14:23:56 +01:00
}
err = bucket.Put([]byte(entry.Key), []byte(jsonentry))
if err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to insert data: %w", err)
2024-12-17 14:23:56 +01:00
}
return nil
})
2024-12-17 14:23:56 +01:00
if err != nil {
return err
}
2024-12-17 14:23:56 +01:00
return nil
}
2024-12-17 14:23:56 +01:00
func (db *DB) Get(attr *DbAttr) (*DbEntry, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
2024-12-17 14:23:56 +01:00
entry := DbEntry{}
err := db.DB.View(func(tx *bolt.Tx) error {
2024-12-21 22:49:22 +01:00
bucket := tx.Bucket([]byte(db.Bucket))
if bucket == nil {
return nil
}
jsonentry := bucket.Get([]byte(attr.Key))
if jsonentry == nil {
// FIXME: shall we return a key not found error?
return nil
}
if err := json.Unmarshal(jsonentry, &entry); err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to unmarshal from json: %w", err)
2024-12-17 14:23:56 +01:00
}
return nil
})
2024-12-17 14:23:56 +01:00
if err != nil {
2024-12-19 10:55:39 +01:00
return nil, fmt.Errorf("failed to read from DB: %w", err)
}
return &entry, nil
}
func (db *DB) Del(attr *DbAttr) error {
// FIXME: check if it exists prior to just call bucket.Delete()?
if err := db.Open(); err != nil {
2024-12-17 14:23:56 +01:00
return err
}
defer db.Close()
err := db.DB.Update(func(tx *bolt.Tx) error {
2024-12-21 22:49:22 +01:00
bucket := tx.Bucket([]byte(db.Bucket))
if bucket == nil {
return nil
}
return bucket.Delete([]byte(attr.Key))
})
return err
}
2024-12-22 11:29:12 +01:00
func (db *DB) Import(attr *DbAttr) (string, error) {
// open json file into attr.Val
if err := attr.GetFileValue(); err != nil {
2024-12-22 11:29:12 +01:00
return "", err
}
if attr.Val == "" {
2024-12-22 11:29:12 +01:00
return "", errors.New("empty json file")
}
var entries DbEntries
now := time.Now()
newfile := db.Dbfile + now.Format("-02.01.2006T03:04.05")
if err := json.Unmarshal([]byte(attr.Val), &entries); err != nil {
2024-12-22 11:29:12 +01:00
return "", cleanError(newfile, fmt.Errorf("failed to unmarshal json: %w", err))
}
if fileExists(db.Dbfile) {
// backup the old file
err := os.Rename(db.Dbfile, newfile)
if err != nil {
2024-12-22 11:29:12 +01:00
return "", fmt.Errorf("failed to rename file %s to %s: %w", db.Dbfile, newfile, err)
}
}
// should now be a new db file
if err := db.Open(); err != nil {
2024-12-22 11:29:12 +01:00
return "", cleanError(newfile, err)
}
defer db.Close()
err := db.DB.Update(func(tx *bolt.Tx) error {
// insert data
2024-12-21 22:49:22 +01:00
bucket, err := tx.CreateBucketIfNotExists([]byte(db.Bucket))
if err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to create bucket: %w", err)
}
for _, entry := range entries {
jsonentry, err := json.Marshal(entry)
if err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to marshall json: %w", err)
}
err = bucket.Put([]byte(entry.Key), []byte(jsonentry))
if err != nil {
2024-12-19 10:55:39 +01:00
return fmt.Errorf("failed to insert data into DB: %w", err)
}
}
return nil
})
if err != nil {
2024-12-22 11:29:12 +01:00
return "", cleanError(newfile, err)
}
2024-12-22 11:29:12 +01:00
return fmt.Sprintf("backed up database file to %s\nimported %d database entries\n",
newfile, len(entries)), nil
}
2024-12-22 00:07:33 +01:00
func (db *DB) Info() (*DbInfo, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
info := &DbInfo{Path: db.Dbfile}
err := db.DB.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
stats := bucket.Stats()
binfo := BucketInfo{
Name: string(name),
Sequence: bucket.Sequence(),
Keys: stats.KeyN,
Stats: bucket.Stats(),
}
2024-12-22 00:07:33 +01:00
err := bucket.ForEach(func(key, entry []byte) error {
binfo.Size += len(entry) + len(key)
2024-12-22 00:07:33 +01:00
return nil
})
if err != nil {
return fmt.Errorf("failed to read keys: %w", err)
2024-12-22 00:07:33 +01:00
}
info.Buckets = append(info.Buckets, binfo)
return nil
})
2024-12-22 00:26:55 +01:00
if err != nil {
return fmt.Errorf("failed to read from DB: %w", err)
2024-12-22 00:26:55 +01:00
}
2024-12-22 00:07:33 +01:00
return nil
})
2024-12-22 00:07:33 +01:00
return info, err
}