diff --git a/pkg/fs/mut.go b/pkg/fs/mut.go index eb00688b9..fbcc53a21 100644 --- a/pkg/fs/mut.go +++ b/pkg/fs/mut.go @@ -42,6 +42,10 @@ import ( // How often to refresh directory nodes by reading from the blobstore. const populateInterval = 30 * time.Second +// How long an item that was created locally will be present +// regardless of its presence in the indexing server. +const deletionRefreshWindow = time.Minute + type nodeType int const ( @@ -58,6 +62,8 @@ type mutDir struct { parent *mutDir // or nil, if the root within its roots.go root. name string // ent name (base name within parent) + localCreateTime time.Time // time this node was created locally (iff it was) + mu sync.Mutex lastPop time.Time children map[string]mutFileOrDir @@ -133,6 +139,7 @@ func (n *mutDir) populate() error { if n.children == nil { n.children = make(map[string]mutFileOrDir) } + currentChildren := map[string]bool{} for k, v := range db.Permanode.Attr { const p = "camliPath:" if !strings.HasPrefix(k, p) || len(v) < 1 { @@ -147,22 +154,22 @@ func (n *mutDir) populate() error { } if target := child.Permanode.Attr.Get("camliSymlinkTarget"); target != "" { // This is a symlink. - n.children[name] = &mutFile{ + n.maybeAddChild(name, child.Permanode, &mutFile{ fs: n.fs, permanode: blob.ParseOrZero(childRef), parent: n, name: name, symLink: true, target: target, - } + }) } else if isDir(child.Permanode) { // This is a directory. - n.children[name] = &mutDir{ + n.maybeAddChild(name, child.Permanode, &mutDir{ fs: n.fs, permanode: blob.ParseOrZero(childRef), parent: n, name: name, - } + }) } else if contentRef := child.Permanode.Attr.Get("camliContent"); contentRef != "" { // This is a file. content := res.Meta[contentRef] @@ -174,23 +181,43 @@ func (n *mutDir) populate() error { log.Printf("child not a file: %v", childRef) continue } - n.children[name] = &mutFile{ + n.maybeAddChild(name, child.Permanode, &mutFile{ fs: n.fs, permanode: blob.ParseOrZero(childRef), parent: n, name: name, content: blob.ParseOrZero(contentRef), size: content.File.Size, - } + }) } else { // unhandled type... continue } - n.children[name].xattr().load(child.Permanode) + currentChildren[name] = true + } + // Remove unreferenced children + for name, oldchild := range n.children { + if _, ok := currentChildren[name]; !ok { + if oldchild.eligibleToDelete() { + delete(n.children, name) + } + } } return nil } +// maybeAddChild adds a child directory to this mutable directory +// unless it already has one with this name and permanode. +func (m *mutDir) maybeAddChild(name string, permanode *search.DescribedPermanode, + child mutFileOrDir) { + if current, ok := m.children[name]; !ok || + current.permanodeString() != child.permanodeString() { + + child.xattr().load(permanode) + m.children[name] = child + } +} + func isDir(d *search.DescribedPermanode) bool { // Explicit if d.Attr.Get("camliNodeType") == "directory" { @@ -354,19 +381,21 @@ func (n *mutDir) creat(name string, typ nodeType) (fuse.Node, error) { switch typ { case dirType: child = &mutDir{ - fs: n.fs, - permanode: pr.BlobRef, - parent: n, - name: name, - xattrs: map[string][]byte{}, + fs: n.fs, + permanode: pr.BlobRef, + parent: n, + name: name, + xattrs: map[string][]byte{}, + localCreateTime: time.Now(), } case fileType, symlinkType: child = &mutFile{ - fs: n.fs, - permanode: pr.BlobRef, - parent: n, - name: name, - xattrs: map[string][]byte{}, + fs: n.fs, + permanode: pr.BlobRef, + parent: n, + name: name, + xattrs: map[string][]byte{}, + localCreateTime: time.Now(), } default: panic("bogus creat type") @@ -472,6 +501,8 @@ type mutFile struct { parent *mutDir name string // ent name (base name within parent) + localCreateTime time.Time // time this node was created locally (iff it was) + mu sync.Mutex // protects all following fields symLink bool // if true, is a symlink target string // if a symlink @@ -816,6 +847,7 @@ type mutFileOrDir interface { invalidate() permanodeString() string xattr() *xattr + eligibleToDelete() bool } func (n *mutFile) permanodeString() string { @@ -837,3 +869,11 @@ func (n *mutDir) invalidate() { n.deleted = true n.mu.Unlock() } + +func (n *mutFile) eligibleToDelete() bool { + return n.localCreateTime.Before(time.Now().Add(-deletionRefreshWindow)) +} + +func (n *mutDir) eligibleToDelete() bool { + return n.localCreateTime.Before(time.Now().Add(-deletionRefreshWindow)) +} diff --git a/pkg/fs/mut_test.go b/pkg/fs/mut_test.go new file mode 100644 index 000000000..b20b53708 --- /dev/null +++ b/pkg/fs/mut_test.go @@ -0,0 +1,49 @@ +// +build linux darwin + +/* +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fs + +import ( + "testing" + "time" +) + +func TestDeleteEligibility(t *testing.T) { + tests := []struct { + name string + ts time.Time + exp bool + }{ + {"zero", time.Time{}, true}, + {"now", time.Now(), false}, + {"future", time.Now().Add(time.Hour), false}, + {"recent", time.Now().Add(-(deletionRefreshWindow / 2)), false}, + {"past", time.Now().Add(-(deletionRefreshWindow * 2)), true}, + } + + for _, test := range tests { + d := &mutDir{localCreateTime: test.ts} + if d.eligibleToDelete() != test.exp { + t.Errorf("Expected %v %T/%v", test.exp, d, test.name) + } + f := &mutFile{localCreateTime: test.ts} + if f.eligibleToDelete() != test.exp { + t.Errorf("Expected %v for %T/%v", test.exp, f, test.name) + } + } +}