198 lines
2.9 KiB
Go
198 lines
2.9 KiB
Go
package data
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
diskWriteInterval = 100 * time.Millisecond
|
|
fileExtension = ".dat"
|
|
)
|
|
|
|
type File[T any] struct {
|
|
collection storageData
|
|
dirty atomic.Uint32
|
|
sync chan struct{}
|
|
}
|
|
|
|
func (fs *File[T]) Init(c storageData) error {
|
|
fs.collection = c
|
|
fs.sync = make(chan struct{})
|
|
|
|
go fs.flushWorker()
|
|
|
|
fileName := filepath.Join(c.Root(), c.Name()+fileExtension)
|
|
file, err := os.Open(fileName)
|
|
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer file.Close()
|
|
return fs.readFrom(file)
|
|
}
|
|
|
|
func (fs *File[T]) Delete(key string) error {
|
|
fs.dirty.Store(1)
|
|
return nil
|
|
}
|
|
|
|
func (fs *File[T]) Set(key string, value *T) error {
|
|
fs.dirty.Store(1)
|
|
return nil
|
|
}
|
|
|
|
func (fs *File[T]) Sync() {
|
|
<-fs.sync
|
|
}
|
|
|
|
func (fs *File[T]) flushWorker() {
|
|
for {
|
|
time.Sleep(diskWriteInterval)
|
|
|
|
if fs.dirty.Swap(0) == 0 {
|
|
select {
|
|
case fs.sync <- struct{}{}:
|
|
default:
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
err := fs.flush()
|
|
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fs *File[T]) flush() error {
|
|
oldPath := filepath.Join(fs.collection.Root(), fs.collection.Name()+fileExtension)
|
|
newPath := oldPath + ".tmp"
|
|
file, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bufferedWriter := bufio.NewWriter(file)
|
|
err = fs.writeTo(bufferedWriter)
|
|
|
|
if err != nil {
|
|
file.Close()
|
|
return err
|
|
}
|
|
|
|
err = bufferedWriter.Flush()
|
|
|
|
if err != nil {
|
|
file.Close()
|
|
return err
|
|
}
|
|
|
|
err = file.Sync()
|
|
|
|
if err != nil {
|
|
file.Close()
|
|
return err
|
|
}
|
|
|
|
err = file.Close()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.Rename(newPath, oldPath)
|
|
}
|
|
|
|
// readFrom reads the entire collection.
|
|
func (fs *File[T]) readFrom(stream io.Reader) error {
|
|
var (
|
|
key string
|
|
value []byte
|
|
)
|
|
|
|
scanner := bufio.NewScanner(stream)
|
|
|
|
for scanner.Scan() {
|
|
if key == "" {
|
|
key = scanner.Text()
|
|
continue
|
|
}
|
|
|
|
value = scanner.Bytes()
|
|
object := new(T)
|
|
err := json.Unmarshal(value, object)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fs.collection.Data().Store(key, object)
|
|
key = ""
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeTo writes the entire collection.
|
|
func (fs *File[T]) writeTo(writer io.Writer) error {
|
|
stringWriter, ok := writer.(io.StringWriter)
|
|
|
|
if !ok {
|
|
panic("The given io.Writer is not an io.StringWriter")
|
|
}
|
|
|
|
records := []keyValue{}
|
|
|
|
fs.collection.Data().Range(func(key, value any) bool {
|
|
records = append(records, keyValue{
|
|
key: key.(string),
|
|
value: value,
|
|
})
|
|
return true
|
|
})
|
|
|
|
sort.Slice(records, func(i, j int) bool {
|
|
return records[i].key < records[j].key
|
|
})
|
|
|
|
encoder := json.NewEncoder(writer)
|
|
|
|
for _, record := range records {
|
|
_, err := stringWriter.WriteString(record.key)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = stringWriter.WriteString("\n")
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = encoder.Encode(record.value)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|