package main import ( "encoding/json" "fmt" "reflect" "sort" "sync" "github.com/peterbourgon/diskv" ) type AlbumDatabase struct { dv *diskv.Diskv consolidated bool byID map[AlbumID]*Album byPurchaseDate []AlbumID byCollection map[string][]AlbumID byAuthor map[string][]AlbumID lock *sync.RWMutex } func OpenAlbumDatabase(basepath string) *AlbumDatabase { return &AlbumDatabase{ dv: diskv.New(diskv.Options{ BasePath: basepath, CacheSizeMax: 50 * 1024 * 1024, // 50 MB Compression: diskv.NewGzipCompression(), }), consolidated: false, byID: make(map[AlbumID]*Album), byPurchaseDate: make([]AlbumID, 0, 10), byCollection: make(map[string][]AlbumID), byAuthor: make(map[string][]AlbumID), lock: &sync.RWMutex{}, } } func (db *AlbumDatabase) AddOrUpdate(a *Album) error { aInDb, err := db.Get(a.ID) dbNeedUpdate := true if err == nil { dbNeedUpdate = (a.PurchaseDate != aInDb.PurchaseDate || !reflect.DeepEqual(a.Designers, aInDb.Designers) || !reflect.DeepEqual(a.Scenarists, aInDb.Scenarists) || !reflect.DeepEqual(a.Colorists, aInDb.Colorists) || a.Collection != aInDb.Collection) } data, err := json.Marshal(a) if err != nil { return fmt.Errorf("json encoding: %s", err) } db.lock.Lock() err = db.dv.Write(AlbumIDString(a.ID), data) if err == nil { db.consolidated = !dbNeedUpdate db.byID[a.ID] = a } db.lock.Unlock() return err } func deleteFromList(list []AlbumID, idToDelete AlbumID) []AlbumID { toDeleteIdx := len(list) for i, aID := range list { if aID == idToDelete { toDeleteIdx = i break } } if toDeleteIdx == len(list) { return list } return append(list[:toDeleteIdx], list[toDeleteIdx+1:]...) } func (db *AlbumDatabase) Delete(ID AlbumID) error { inDB, err := db.Get(ID) if err != nil { return fmt.Errorf("Album %d not found", ID) } db.lock.Lock() defer db.lock.Unlock() delete(db.byID, ID) db.byPurchaseDate = deleteFromList(db.byPurchaseDate, ID) for _, a := range inDB.Authors() { db.byAuthor[a] = deleteFromList(db.byAuthor[a], ID) } if len(inDB.Collection) > 0 { db.byCollection[inDB.Collection] = deleteFromList(db.byCollection[inDB.Collection], ID) } return db.dv.Erase(AlbumIDString(ID)) } func (db *AlbumDatabase) get(ID string) (*Album, error) { res := &Album{} r, err := db.dv.ReadStream(ID, false) if err != nil { return nil, fmt.Errorf("Album %s not found: %s", ID, err) } dec := json.NewDecoder(r) defer closeOrPanic(r, "db record "+ID) return res, dec.Decode(res) } func (db *AlbumDatabase) Get(ID AlbumID) (*Album, error) { db.lock.RLock() defer db.lock.RUnlock() a, ok := db.byID[ID] if ok == true { return a, nil } return db.get(AlbumIDString(ID)) } type ListOfAlbum []*Album func (l ListOfAlbum) Len() int { return len(l) } func (l ListOfAlbum) Less(i, j int) bool { return l[i].PurchaseDate.Before(l[j].PurchaseDate) } func (l ListOfAlbum) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func maxOfInt(a, b int) int { if a < b { return b } return a } func (db *AlbumDatabase) ensureConsolidated() error { db.lock.RLock() if db.consolidated == true { db.lock.RUnlock() return nil } db.lock.RUnlock() db.lock.Lock() defer db.lock.Unlock() allAlbums := make([]*Album, 0, maxOfInt(len(db.byID), len(db.byPurchaseDate))) db.byPurchaseDate = make([]AlbumID, 0, maxOfInt(len(db.byID), len(db.byPurchaseDate))) db.byID = make(map[AlbumID]*Album) db.byAuthor = make(map[string][]AlbumID) db.byCollection = make(map[string][]AlbumID) for aIDStr := range db.dv.Keys(nil) { a, err := db.get(aIDStr) if err != nil { return err } allAlbums = append(allAlbums, a) db.byID[a.ID] = a } sort.Sort(sort.Reverse(ListOfAlbum(allAlbums))) for _, album := range allAlbums { db.byPurchaseDate = append(db.byPurchaseDate, album.ID) } for aID, album := range db.byID { if len(album.Collection) > 0 { db.byCollection[album.Collection] = append(db.byCollection[album.Collection], aID) } for _, author := range album.Authors() { db.byAuthor[author] = append(db.byAuthor[author], aID) } } db.consolidated = true return nil } func (db *AlbumDatabase) ByPurchaseDate() ([]AlbumID, error) { err := db.ensureConsolidated() if err != nil { return nil, err } db.lock.RLock() defer db.lock.RUnlock() res := make([]AlbumID, len(db.byPurchaseDate)) copy(res, db.byPurchaseDate) return res, nil }