|
|
|
@ -200,6 +200,11 @@ func ChonkDir(dir string) (*FS, error) {
|
|
|
|
|
if !stat.IsDir() {
|
|
|
|
|
return nil, fmt.Errorf("chonk directory %q is a file", dir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO(tom): *FS marks AUMs as deleted but does not actually
|
|
|
|
|
// delete them, to avoid data loss in the event of a bug.
|
|
|
|
|
// Implement deletion after we are fairly sure in the implementation.
|
|
|
|
|
|
|
|
|
|
return &FS{base: dir}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -213,8 +218,17 @@ func ChonkDir(dir string) (*FS, error) {
|
|
|
|
|
// much smaller than JSON for AUMs. The 'keyasint' thing isn't essential
|
|
|
|
|
// but again it saves a bunch of bytes.
|
|
|
|
|
type fsHashInfo struct {
|
|
|
|
|
Children []AUMHash `cbor:"1,keyasint"`
|
|
|
|
|
AUM *AUM `cbor:"2,keyasint"`
|
|
|
|
|
Children []AUMHash `cbor:"1,keyasint"`
|
|
|
|
|
AUM *AUM `cbor:"2,keyasint"`
|
|
|
|
|
CreatedUnix int64 `cbor:"3,keyasint,omitempty"`
|
|
|
|
|
|
|
|
|
|
// PurgedUnix is set when the AUM is deleted. The value is
|
|
|
|
|
// the unix epoch at the time it was deleted.
|
|
|
|
|
//
|
|
|
|
|
// While a non-zero PurgedUnix symbolizes the AUM is deleted,
|
|
|
|
|
// the fsHashInfo entry can continue to exist to track children
|
|
|
|
|
// of this AUMHash.
|
|
|
|
|
PurgedUnix int64 `cbor:"4,keyasint,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// aumDir returns the directory an AUM is stored in, and its filename
|
|
|
|
@ -238,12 +252,45 @@ func (c *FS) AUM(hash AUMHash) (AUM, error) {
|
|
|
|
|
}
|
|
|
|
|
return AUM{}, err
|
|
|
|
|
}
|
|
|
|
|
if info.AUM == nil {
|
|
|
|
|
if info.AUM == nil || info.PurgedUnix > 0 {
|
|
|
|
|
return AUM{}, os.ErrNotExist
|
|
|
|
|
}
|
|
|
|
|
return *info.AUM, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CommitTime returns the time at which the AUM was committed.
|
|
|
|
|
//
|
|
|
|
|
// If the AUM does not exist, then os.ErrNotExist is returned.
|
|
|
|
|
func (c *FS) CommitTime(h AUMHash) (time.Time, error) {
|
|
|
|
|
c.mu.RLock()
|
|
|
|
|
defer c.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
info, err := c.get(h)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
return time.Time{}, os.ErrNotExist
|
|
|
|
|
}
|
|
|
|
|
return time.Time{}, err
|
|
|
|
|
}
|
|
|
|
|
if info.PurgedUnix > 0 {
|
|
|
|
|
return time.Time{}, os.ErrNotExist
|
|
|
|
|
}
|
|
|
|
|
if info.CreatedUnix > 0 {
|
|
|
|
|
return time.Unix(info.CreatedUnix, 0), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we got this far, the AUM exists but CreatedUnix is not
|
|
|
|
|
// set, presumably because this AUM was committed using a version
|
|
|
|
|
// of tailscaled that pre-dates the introduction of CreatedUnix.
|
|
|
|
|
// As such, we use the file modification time as a suitable analog.
|
|
|
|
|
dir, base := c.aumDir(h)
|
|
|
|
|
s, err := os.Stat(filepath.Join(dir, base))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return time.Time{}, nil
|
|
|
|
|
}
|
|
|
|
|
return s.ModTime(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AUM returns any known AUMs with a specific parent hash.
|
|
|
|
|
func (c *FS) ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) {
|
|
|
|
|
c.mu.RLock()
|
|
|
|
@ -257,6 +304,9 @@ func (c *FS) ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) {
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
// NOTE(tom): We don't check PurgedUnix here because 'purged'
|
|
|
|
|
// only applies to that specific AUM (i.e. info.AUM) and not to
|
|
|
|
|
// any information about children stored against that hash.
|
|
|
|
|
|
|
|
|
|
out := make([]AUM, len(info.Children))
|
|
|
|
|
for i, h := range info.Children {
|
|
|
|
@ -265,7 +315,7 @@ func (c *FS) ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) {
|
|
|
|
|
// We expect any AUM recorded as a child on its parent to exist.
|
|
|
|
|
return nil, fmt.Errorf("reading child %d of %x: %v", i, h, err)
|
|
|
|
|
}
|
|
|
|
|
if c.AUM == nil {
|
|
|
|
|
if c.AUM == nil || c.PurgedUnix > 0 {
|
|
|
|
|
return nil, fmt.Errorf("child %d of %x: AUM not stored", i, h)
|
|
|
|
|
}
|
|
|
|
|
out[i] = *c.AUM
|
|
|
|
@ -309,13 +359,27 @@ func (c *FS) Heads() ([]AUM, error) {
|
|
|
|
|
|
|
|
|
|
out := make([]AUM, 0, 6) // 6 is arbitrary.
|
|
|
|
|
err := c.scanHashes(func(info *fsHashInfo) {
|
|
|
|
|
if len(info.Children) == 0 && info.AUM != nil {
|
|
|
|
|
if len(info.Children) == 0 && info.AUM != nil && info.PurgedUnix == 0 {
|
|
|
|
|
out = append(out, *info.AUM)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AllAUMs returns all AUMs stored in the chonk.
|
|
|
|
|
func (c *FS) AllAUMs() ([]AUMHash, error) {
|
|
|
|
|
c.mu.RLock()
|
|
|
|
|
defer c.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
out := make([]AUMHash, 0, 6) // 6 is arbitrary.
|
|
|
|
|
err := c.scanHashes(func(info *fsHashInfo) {
|
|
|
|
|
if info.AUM != nil && info.PurgedUnix == 0 {
|
|
|
|
|
out = append(out, info.AUM.Hash())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FS) scanHashes(eachHashInfo func(*fsHashInfo)) error {
|
|
|
|
|
prefixDirs, err := os.ReadDir(c.base)
|
|
|
|
|
if err != nil {
|
|
|
|
@ -411,6 +475,7 @@ func (c *FS) CommitVerifiedAUMs(updates []AUM) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := c.commit(h, func(info *fsHashInfo) {
|
|
|
|
|
info.PurgedUnix = 0 // just in-case it was set for some reason
|
|
|
|
|
info.AUM = &aum
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
@ -421,6 +486,31 @@ func (c *FS) CommitVerifiedAUMs(updates []AUM) error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PurgeAUMs marks the specified AUMs for deletion from storage.
|
|
|
|
|
func (c *FS) PurgeAUMs(hashes []AUMHash) error {
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
for i, h := range hashes {
|
|
|
|
|
stored, err := c.get(h)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("reading %d (%x): %w", i, h, err)
|
|
|
|
|
}
|
|
|
|
|
if stored.AUM == nil || stored.PurgedUnix > 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = c.commit(h, func(info *fsHashInfo) {
|
|
|
|
|
info.PurgedUnix = now.Unix()
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("committing purge[%d] (%x): %w", i, h, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// commit calls the provided updater function to record changes relevant
|
|
|
|
|
// to the given hash. The caller is expected to update the AUM and
|
|
|
|
|
// Children fields, as relevant.
|
|
|
|
@ -430,6 +520,7 @@ func (c *FS) commit(h AUMHash, updater func(*fsHashInfo)) error {
|
|
|
|
|
existing, err := c.get(h)
|
|
|
|
|
switch {
|
|
|
|
|
case os.IsNotExist(err):
|
|
|
|
|
toCommit.CreatedUnix = time.Now().Unix()
|
|
|
|
|
case err != nil:
|
|
|
|
|
return err
|
|
|
|
|
default:
|
|
|
|
@ -754,5 +845,8 @@ func Compact(storage CompactableChonk, head AUMHash, opts CompactionOptions) (la
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := storage.SetLastActiveAncestor(lastActiveAncestor); err != nil {
|
|
|
|
|
return AUMHash{}, err
|
|
|
|
|
}
|
|
|
|
|
return lastActiveAncestor, storage.PurgeAUMs(toDelete)
|
|
|
|
|
}
|
|
|
|
|