216 lines
4.7 KiB
Go
216 lines
4.7 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
|
|
"aego/asset"
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/project"
|
|
)
|
|
|
|
type MetaMode uint8
|
|
|
|
const (
|
|
MetaEnsure MetaMode = iota
|
|
MetaReadOnly
|
|
)
|
|
|
|
type MetaIssueKind uint8
|
|
|
|
const (
|
|
MetaMissing MetaIssueKind = iota
|
|
MetaOrphan
|
|
MetaDuplicate
|
|
MetaStale
|
|
)
|
|
|
|
var metaCodes = [...]string{"META001", "META002", "META003", "META004"}
|
|
|
|
type MetaIssue struct {
|
|
Kind MetaIssueKind
|
|
Path string
|
|
Other string
|
|
ID ids.AssetID
|
|
}
|
|
|
|
func (i MetaIssue) Code() string {
|
|
return metaCodes[i.Kind]
|
|
}
|
|
|
|
func (i MetaIssue) Message() string {
|
|
switch i.Kind {
|
|
case MetaMissing:
|
|
return i.Path + " has no .meta file; its asset id exists only in memory"
|
|
case MetaOrphan:
|
|
return i.Path + " has no asset next to it"
|
|
case MetaDuplicate:
|
|
return i.Path + " uses the same asset id as " + i.Other + ", it was probably copied together with its .meta"
|
|
}
|
|
return "the asset index lists " + i.Path + ", which no longer exists"
|
|
}
|
|
|
|
func (i MetaIssue) Action() string {
|
|
switch i.Kind {
|
|
case MetaMissing:
|
|
return "create " + i.Path + MetaSuffix
|
|
case MetaOrphan:
|
|
return "delete " + i.Path
|
|
case MetaDuplicate:
|
|
return "give " + i.Path + " a new asset id"
|
|
}
|
|
return "drop " + i.Path + " from the asset index"
|
|
}
|
|
|
|
type walk struct {
|
|
seen map[string]bool
|
|
files map[string]bool
|
|
metas map[string]bool
|
|
}
|
|
|
|
func newWalk() *walk {
|
|
return &walk{seen: map[string]bool{}, files: map[string]bool{}, metas: map[string]bool{}}
|
|
}
|
|
|
|
func derivedID(rel string) ids.AssetID {
|
|
sum := sha256.Sum256([]byte("aego.meta/" + rel))
|
|
return ids.AssetID{Hi: binary.BigEndian.Uint64(sum[0:8]), Lo: binary.BigEndian.Uint64(sum[8:16])}
|
|
}
|
|
|
|
func (p *Pipeline) SetMetaMode(m MetaMode) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if !p.shared {
|
|
p.mode = m
|
|
}
|
|
}
|
|
|
|
func (p *Pipeline) MetaMode() MetaMode {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
return p.mode
|
|
}
|
|
|
|
func (p *Pipeline) Shared() bool {
|
|
return p.shared
|
|
}
|
|
|
|
func (p *Pipeline) MetaIssues() ([]MetaIssue, uint64) {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
return p.issues, p.issueRev
|
|
}
|
|
|
|
func (p *Pipeline) settle(r, other *record, added *[]ids.AssetID) {
|
|
e, ok := p.index.Lookup(other.id)
|
|
if !ok || e.Source != r.path || e.Source == other.path {
|
|
r.dup, r.derived = other.path, true
|
|
r.meta.ID = derivedID(r.path)
|
|
return
|
|
}
|
|
old := other.id
|
|
if other.group != "" {
|
|
p.dropFromGroup(other.group, old)
|
|
}
|
|
delete(p.byID, old)
|
|
other.id = derivedID(other.path)
|
|
other.meta.ID = other.id
|
|
other.dup, other.derived, other.key = r.path, true, ""
|
|
p.byID[other.id] = other
|
|
for i, id := range *added {
|
|
if id == old {
|
|
(*added)[i] = other.id
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Pipeline) audit() {
|
|
var out []MetaIssue
|
|
for _, r := range p.byID {
|
|
switch {
|
|
case r.dup != "":
|
|
out = append(out, MetaIssue{Kind: MetaDuplicate, Path: r.path, Other: r.dup, ID: r.id})
|
|
case r.derived:
|
|
out = append(out, MetaIssue{Kind: MetaMissing, Path: r.path, ID: r.id})
|
|
}
|
|
}
|
|
for m := range p.metas {
|
|
if !p.files[strings.TrimSuffix(m, MetaSuffix)] {
|
|
out = append(out, MetaIssue{Kind: MetaOrphan, Path: m})
|
|
}
|
|
}
|
|
p.index.Each(func(e *asset.Entry) {
|
|
if e.Missing || strings.Contains(e.Source, ":") || p.byID[e.ID] != nil {
|
|
return
|
|
}
|
|
out = append(out, MetaIssue{Kind: MetaStale, Path: e.Source, ID: e.ID})
|
|
})
|
|
slices.SortFunc(out, func(a, b MetaIssue) int {
|
|
if a.Kind != b.Kind {
|
|
return int(a.Kind) - int(b.Kind)
|
|
}
|
|
return strings.Compare(a.Path, b.Path)
|
|
})
|
|
if !slices.Equal(out, p.issues) {
|
|
p.issues = out
|
|
p.issueRev++
|
|
}
|
|
}
|
|
|
|
func (p *Pipeline) Repair(list []MetaIssue) (int, error) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.shared {
|
|
return 0, errs.New(errs.AssetBadMeta, "another process holds the asset index, repair the meta files there")
|
|
}
|
|
n := 0
|
|
var all []error
|
|
for _, is := range list {
|
|
var err error
|
|
switch is.Kind {
|
|
case MetaMissing, MetaDuplicate:
|
|
r := p.byPath[is.Path]
|
|
if r == nil || !r.derived {
|
|
continue
|
|
}
|
|
if err = os.WriteFile(p.abs(r.path)+MetaSuffix, WriteMeta(r.meta), 0o644); err == nil {
|
|
r.derived, r.dup = false, ""
|
|
p.metas[r.path+MetaSuffix] = true
|
|
}
|
|
case MetaOrphan:
|
|
if err = os.Remove(p.abs(is.Path)); err == nil || os.IsNotExist(err) {
|
|
err = nil
|
|
delete(p.metas, is.Path)
|
|
}
|
|
case MetaStale:
|
|
p.index.Delete(is.ID)
|
|
}
|
|
if err != nil {
|
|
all = append(all, errs.Wrap(err, errs.VFSIO, "cannot repair "+is.Path).With(log.F("path", is.Path)))
|
|
continue
|
|
}
|
|
n++
|
|
}
|
|
p.audit()
|
|
p.rev++
|
|
if err := p.saveIndex(); err != nil {
|
|
all = append(all, err)
|
|
}
|
|
return n, errors.Join(all...)
|
|
}
|
|
|
|
func Audit(pr *project.Project, imps *ImporterRegistry, mode MetaMode) ([]MetaIssue, error) {
|
|
p := newPipeline(pr, imps, nil)
|
|
p.mode, p.shared = mode, true
|
|
if _, _, err := p.scan(); err != nil {
|
|
return nil, err
|
|
}
|
|
return p.issues, nil
|
|
}
|