@cryptotaxi247 / kubo / commits / 4aba28237

feat(fsrepo): handle safe Removal

Brian Tiger Chow committed Jan 12, 2015 at 20:03 UTC 4aba282379c961e91dd2bd6622e21334af911dc0
6 files changed +180 -5
cmd/ipfs/init.go
+3
@@ -101,6 +101,9 @@ func doInit(repoRoot string, force bool, nBitsForKeypair int) (interface{}, erro
101 return nil, err
102 }
103 } else {
104 + if err := fsrepo.Remove(repoRoot); err != nil {
105 + return nil, err
106 + }
107 r := fsrepo.At(repoRoot)
108 if err := r.Open(); err != nil {
109 return nil, err
repo/fsrepo/fsrepo.go
+55 -5
@@ -1,6 +1,7 @@
1 package fsrepo
2
3 import (
4 + "errors"
5 "fmt"
6 "io"
7 "os"
@@ -13,7 +14,22 @@ import (
14 debugerror "github.com/jbenet/go-ipfs/util/debugerror"
15 )
16
16 -// FSRepo represents an IPFS FileSystem Repo
17 +var (
18 + // pkgLock prevents the fsrepo from being removed while there exist open
19 + // FSRepo handles. It also ensures that the Init is atomic.
20 + //
21 + // packageLock also protects numOpenedRepos
22 + //
23 + // If an operation is used when repo is Open and the operation does not
24 + // change the repo's state, the package lock does not need to be acquired.
25 + pkgLock *packageLock
26 +)
27 +
28 +func init() {
29 + pkgLock = makePackageLock()
30 +}
31 +
32 +// FSRepo represents an IPFS FileSystem Repo. It is not thread-safe.
33 type FSRepo struct {
34 state state
35 path string
@@ -22,6 +38,7 @@ type FSRepo struct {
38
39 // At returns a handle to an FSRepo at the provided |path|.
40 func At(path string) *FSRepo {
41 + // This method must not have side-effects.
42 return &FSRepo{
43 path: path,
44 state: unopened, // explicitly set for clarity
@@ -30,7 +47,10 @@ func At(path string) *FSRepo {
47
48 // Init initializes a new FSRepo at the given path with the provided config.
49 func Init(path string, conf *config.Config) error {
33 - if IsInitialized(path) {
50 + pkgLock.Lock() // lock must be held to ensure atomicity (prevent Removal)
51 + defer pkgLock.Unlock()
52 +
53 + if isInitializedUnsynced(path) {
54 return nil
55 }
56 configFilename, err := config.Filename(path)
@@ -43,12 +63,24 @@ func Init(path string, conf *config.Config) error {
63 return nil
64 }
65
66 +// Remove recursively removes the FSRepo at |path|.
67 +func Remove(path string) error {
68 + pkgLock.Lock()
69 + defer pkgLock.Unlock()
70 + if pkgLock.NumOpeners(path) != 0 {
71 + return errors.New("repo in use")
72 + }
73 + return os.RemoveAll(path)
74 +}
75 +
76 // Open returns an error if the repo is not initialized.
77 func (r *FSRepo) Open() error {
78 + pkgLock.Lock()
79 + defer pkgLock.Unlock()
80 if r.state != unopened {
81 return debugerror.Errorf("repo is %s", r.state)
82 }
51 - if !IsInitialized(r.path) {
83 + if !isInitializedUnsynced(r.path) {
84 return debugerror.New("ipfs not initialized, please run 'ipfs init'")
85 }
86 // check repo path, then check all constituent parts.
@@ -86,12 +118,17 @@ func (r *FSRepo) Open() error {
118 }
119
120 r.state = opened
121 + pkgLock.AddOpener(r.path)
122 return nil
123 }
124
92 -// Config returns the FSRepo's config. Result is undefined if the Repo is not
93 -// Open.
125 +// Config returns the FSRepo's config. This method must not be called if the
126 +// repo is not open.
127 +//
128 +// Result when not Open is undefined. The method may panic if it pleases.
129 func (r *FSRepo) Config() *config.Config {
130 + // no lock necessary because repo is either Open (and thus protected from
131 + // Removal) or has no side-effect
132 if r.state != opened {
133 panic(fmt.Sprintln("repo is", r.state))
134 }
@@ -100,6 +137,7 @@ func (r *FSRepo) Config() *config.Config {
137
138 // SetConfig updates the FSRepo's config.
139 func (r *FSRepo) SetConfig(updated *config.Config) error {
140 + // no lock required because repo should be Open
141 if r.state != opened {
142 panic(fmt.Sprintln("repo is", r.state))
143 }
@@ -146,6 +184,7 @@ func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
184
185 // SetConfigKey writes the value of a particular key.
186 func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
187 + // no lock required because repo should be Open
188 if r.state != opened {
189 return debugerror.Errorf("repo is %s", r.state)
190 }
@@ -172,9 +211,12 @@ func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
211
212 // Close closes the FSRepo, releasing held resources.
213 func (r *FSRepo) Close() error {
214 + pkgLock.Lock()
215 + defer pkgLock.Unlock()
216 if r.state != opened {
217 return debugerror.Errorf("repo is %s", r.state)
218 }
219 + pkgLock.RemoveOpener(r.path)
220 return nil // TODO release repo lock
221 }
222
@@ -183,6 +225,14 @@ var _ repo.Interface = &FSRepo{}
225
226 // IsInitialized returns true if the repo is initialized at provided |path|.
227 func IsInitialized(path string) bool {
228 + pkgLock.Lock()
229 + defer pkgLock.Unlock()
230 + return isInitializedUnsynced(path)
231 +}
232 +
233 +// isInitializedUnsynced reports whether the repo is initialized. Caller must
234 +// hold pkgLock.
235 +func isInitializedUnsynced(path string) bool {
236 configFilename, err := config.Filename(path)
237 if err != nil {
238 return false
repo/fsrepo/fsrepo_test.go new
+67
@@ -0,0 +1,67 @@
1 +package fsrepo
2 +
3 +import (
4 + "os"
5 + "path"
6 + "testing"
7 +
8 + "github.com/jbenet/go-ipfs/repo/config"
9 +)
10 +
11 +// NB: These tests cannot be run in parallel
12 +
13 +func init() {
14 + // ensure tests begin in clean state
15 + os.RemoveAll(testRepoDir)
16 +}
17 +
18 +const testRepoDir = "./fsrepo_test/repos"
19 +
20 +func testRepoPath(p string) string {
21 + return path.Join(testRepoDir, p)
22 +}
23 +
24 +func TestCannotRemoveIfOpen(t *testing.T) {
25 + path := testRepoPath("TestCannotRemoveIfOpen")
26 + AssertNil(Init(path, &config.Config{}), t, "should initialize successfully")
27 + r := At(path)
28 + AssertNil(r.Open(), t)
29 + AssertErr(Remove(path), t, "should not be able to remove while open")
30 + AssertNil(r.Close(), t)
31 + AssertNil(Remove(path), t, "should be able to remove after closed")
32 +}
33 +
34 +func TestCanManageReposIndependently(t *testing.T) {
35 + pathA := testRepoPath("a")
36 + pathB := testRepoPath("b")
37 +
38 + t.Log("initialize two repos")
39 + AssertNil(Init(pathA, &config.Config{}), t, "should initialize successfully")
40 + AssertNil(Init(pathB, &config.Config{}), t, "should initialize successfully")
41 +
42 + t.Log("open the two repos")
43 + repoA := At(pathA)
44 + repoB := At(pathB)
45 + AssertNil(repoA.Open(), t)
46 + AssertNil(repoB.Open(), t)
47 +
48 + t.Log("close and remove b while a is open")
49 + AssertNil(repoB.Close(), t, "close b")
50 + AssertNil(Remove(pathB), t, "remove b")
51 +
52 + t.Log("close and remove a")
53 + AssertNil(repoA.Close(), t)
54 + AssertNil(Remove(pathA), t)
55 +}
56 +
57 +func AssertNil(err error, t *testing.T, msgs ...string) {
58 + if err != nil {
59 + t.Error(msgs, "error:", err)
60 + }
61 +}
62 +
63 +func AssertErr(err error, t *testing.T, msgs ...string) {
64 + if err == nil {
65 + t.Error(msgs, "error:", err)
66 + }
67 +}
repo/fsrepo/fsrepo_test/.gitkeep
repo/fsrepo/fsrepo_test/README.md new
+1
@@ -0,0 +1 @@
1 +This directory is used to store FSRepos generated during go tests.
repo/fsrepo/lock.go new
+54
@@ -0,0 +1,54 @@
1 +package fsrepo
2 +
3 +import (
4 + "path"
5 + "sync"
6 +)
7 +
8 +type packageLock struct {
9 + // lock protects repos
10 + lock sync.Mutex
11 + // repos maps repo paths to the number of openers holding an FSRepo handle
12 + // to it
13 + repos map[string]int
14 +}
15 +
16 +func makePackageLock() *packageLock {
17 + return &packageLock{
18 + repos: make(map[string]int),
19 + }
20 +}
21 +
22 +// Lock must be held to while performing any operation that modifies an
23 +// FSRepo's state field. This includes Init, Open, Close, and Remove.
24 +func (l *packageLock) Lock() {
25 + l.lock.Lock()
26 +}
27 +
28 +func (l *packageLock) Unlock() {
29 + l.lock.Unlock()
30 +}
31 +
32 +// NumOpeners returns the number of FSRepos holding a handle to the repo at
33 +// this path. This method is not thread-safe. The caller must have this object
34 +// locked.
35 +func (l *packageLock) NumOpeners(repoPath string) int {
36 + return l.repos[key(repoPath)]
37 +}
38 +
39 +// AddOpener messages that an FSRepo holds a handle to the repo at this path.
40 +// This method is not thread-safe. The caller must have this object locked.
41 +func (l *packageLock) AddOpener(repoPath string) {
42 + l.repos[key(repoPath)]++
43 +}
44 +
45 +// RemoveOpener messgaes that an FSRepo no longer holds a handle to the repo at
46 +// this path. This method is not thread-safe. The caller must have this object
47 +// locked.
48 +func (l *packageLock) RemoveOpener(repoPath string) {
49 + l.repos[key(repoPath)]--
50 +}
51 +
52 +func key(repoPath string) string {
53 + return path.Clean(repoPath)
54 +}