132 lines
4.2 KiB
Go
132 lines
4.2 KiB
Go
package resource
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5" // nolint: gas
|
|
"encoding/binary"
|
|
"io"
|
|
"sync"
|
|
)
|
|
|
|
type resourceHash []byte
|
|
type resourceHashes map[ID]resourceHash
|
|
type resourceHashSnapshot map[Language]resourceHashes
|
|
|
|
// ModificationCallback is a callback function to notify a change in resources.
|
|
type ModificationCallback func(modifiedIDs []ID, failedIDs []ID)
|
|
|
|
// Localizer provides selectors to resources for specific languages.
|
|
type Localizer interface {
|
|
LocalizedResources(lang Language) Selector
|
|
}
|
|
|
|
// ChangeNotifier is a utility that assists in detecting changes in modifying resources.
|
|
// A callback is called for any resource identifier that refers to resources that are different after a modification.
|
|
//
|
|
// Use this utility in combination to resource lists where resources can be overwritten by other entries.
|
|
// Changes in order, or content, affects how a resource is resolved.
|
|
type ChangeNotifier struct {
|
|
Localizer Localizer
|
|
Callback ModificationCallback
|
|
}
|
|
|
|
// ModifyAndNotify must be called with a range of affected IDs that will change during the call of the modifier.
|
|
// A hash snapshot is taken before and after the modifier, considering the affected IDs.
|
|
// Any change is then reported to the callback, listing all IDs that have different hashes.
|
|
//
|
|
// Hashing the resources considers all languages, as well as the meta-information of the resources.
|
|
func (notifier ChangeNotifier) ModifyAndNotify(modifier func(), affectedIDs ...[]ID) {
|
|
var flattenedIDs []ID
|
|
for _, idList := range affectedIDs {
|
|
flattenedIDs = append(flattenedIDs, idList...)
|
|
}
|
|
oldHashes, _ := notifier.hashAll(flattenedIDs)
|
|
modifier()
|
|
newHashes, failedIDs := notifier.hashAll(flattenedIDs)
|
|
modifiedResourceIDs := notifier.modifiedIDs(oldHashes, newHashes)
|
|
notifier.Callback(modifiedResourceIDs, failedIDs)
|
|
}
|
|
|
|
func (notifier ChangeNotifier) hashAll(ids []ID) (hashes resourceHashSnapshot, failedIDs []ID) {
|
|
var failedMap IDMarkerMap
|
|
hashes = make(resourceHashSnapshot)
|
|
var selectMutex sync.Mutex
|
|
var mapMutex sync.Mutex
|
|
for _, lang := range Languages() {
|
|
hashByResourceID := make(resourceHashes)
|
|
hashes[lang] = hashByResourceID
|
|
selector := notifier.Localizer.LocalizedResources(lang)
|
|
selectorFunc := func(id ID) (View, error) {
|
|
selectMutex.Lock()
|
|
defer selectMutex.Unlock()
|
|
return selector.Select(id)
|
|
}
|
|
var wg sync.WaitGroup
|
|
|
|
for _, id := range ids {
|
|
wg.Add(1)
|
|
go func(id ID) {
|
|
defer wg.Done()
|
|
hash, hashErr := notifier.hashResource(selectorFunc, id)
|
|
mapMutex.Lock()
|
|
defer mapMutex.Unlock()
|
|
if hashErr == nil {
|
|
hashByResourceID[id] = hash
|
|
} else {
|
|
failedMap.Add(id)
|
|
}
|
|
}(id)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
failedIDs = failedMap.ToList()
|
|
return
|
|
}
|
|
|
|
func (notifier ChangeNotifier) hashResource(selector func(ID) (View, error), id ID) (resourceHash, error) {
|
|
view, viewErr := selector(id)
|
|
if viewErr != nil {
|
|
return nil, viewErr
|
|
}
|
|
hasher := md5.New() // nolint: gas
|
|
blocks := view.BlockCount()
|
|
for index := 0; index < blocks; index++ {
|
|
blockReader, blockErr := view.Block(index)
|
|
if blockErr != nil {
|
|
return nil, blockErr
|
|
}
|
|
written, hashErr := io.Copy(hasher, blockReader)
|
|
if hashErr != nil {
|
|
return nil, hashErr
|
|
}
|
|
binary.Write(hasher, binary.LittleEndian, &written) // nolint: errcheck
|
|
}
|
|
binary.Write(hasher, binary.LittleEndian, int64(blocks)) // nolint: errcheck
|
|
binary.Write(hasher, binary.LittleEndian, view.Compound()) // nolint: errcheck
|
|
binary.Write(hasher, binary.LittleEndian, view.ContentType()) // nolint: errcheck
|
|
binary.Write(hasher, binary.LittleEndian, view.Compressed()) // nolint: errcheck
|
|
|
|
return hasher.Sum(nil), nil
|
|
}
|
|
|
|
func (notifier ChangeNotifier) modifiedIDs(oldHashes resourceHashSnapshot, newHashes resourceHashSnapshot) []ID {
|
|
var modifiedIDs IDMarkerMap
|
|
for _, lang := range Languages() {
|
|
oldHashesByResourceID := oldHashes[lang]
|
|
newHashesByResourceID := newHashes[lang]
|
|
|
|
for newID, newHash := range newHashesByResourceID {
|
|
oldHash, oldExisting := oldHashesByResourceID[newID]
|
|
if !oldExisting || !bytes.Equal(newHash, oldHash) {
|
|
modifiedIDs.Add(newID)
|
|
}
|
|
}
|
|
for oldID := range oldHashesByResourceID {
|
|
if _, newExisting := newHashesByResourceID[oldID]; !newExisting {
|
|
modifiedIDs.Add(oldID)
|
|
}
|
|
}
|
|
}
|
|
return modifiedIDs.ToList()
|
|
}
|