fsrepo.go 10.3 KB
Newer Older
1 2 3
package fsrepo

import (
4
	"errors"
5
	"fmt"
6
	"io"
7
	"os"
8
	"path"
9
	"sync"
10

11
	ds "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-datastore"
12
	repo "github.com/jbenet/go-ipfs/repo"
13
	config "github.com/jbenet/go-ipfs/repo/config"
14
	component "github.com/jbenet/go-ipfs/repo/fsrepo/component"
15
	counter "github.com/jbenet/go-ipfs/repo/fsrepo/counter"
16
	lockfile "github.com/jbenet/go-ipfs/repo/fsrepo/lock"
17
	serialize "github.com/jbenet/go-ipfs/repo/fsrepo/serialize"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
18
	dir "github.com/jbenet/go-ipfs/thirdparty/dir"
Jeromy's avatar
Jeromy committed
19
	u "github.com/jbenet/go-ipfs/util"
20
	debugerror "github.com/jbenet/go-ipfs/util/debugerror"
21 22
)

23
var (
24 25 26

	// packageLock must be held to while performing any operation that modifies an
	// FSRepo's state field. This includes Init, Open, Close, and Remove.
27
	packageLock sync.Mutex // protects openersCounter and lockfiles
28 29 30
	// lockfiles holds references to the Closers that ensure that repos are
	// only accessed by one process at a time.
	lockfiles map[string]io.Closer
31
	// openersCounter prevents the fsrepo from being removed while there exist open
32 33 34 35 36 37
	// FSRepo handles. It also ensures that the Init is atomic.
	//
	// packageLock also protects numOpenedRepos
	//
	// If an operation is used when repo is Open and the operation does not
	// change the repo's state, the package lock does not need to be acquired.
38
	openersCounter *counter.Openers
39 40 41
)

func init() {
42
	openersCounter = counter.NewOpenersCounter()
43
	lockfiles = make(map[string]io.Closer)
44 45
}

46 47
// FSRepo represents an IPFS FileSystem Repo. It is safe for use by multiple
// callers.
48
type FSRepo struct {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
49 50 51 52
	// state is the FSRepo's state (unopened, opened, closed)
	state state
	// path is the file-system path
	path string
53 54
	// configComponent is loaded when FSRepo is opened and kept up to date when
	// the FSRepo is modified.
Brian Tiger Chow's avatar
Brian Tiger Chow committed
55
	// TODO test
56
	configComponent    component.ConfigComponent
57
	datastoreComponent component.DatastoreComponent
58
	eventlogComponent  component.EventlogComponent
59 60
}

Tommi Virtanen's avatar
Tommi Virtanen committed
61 62
var _ repo.Repo = (*FSRepo)(nil)

63 64 65 66
type componentBuilder struct {
	Init          component.Initializer
	IsInitialized component.InitializationChecker
	OpenHandler   func(*FSRepo) error
67 68
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
69
// At returns a handle to an FSRepo at the provided |path|.
70
func At(repoPath string) *FSRepo {
71
	// This method must not have side-effects.
72
	return &FSRepo{
73 74
		path:  path.Clean(repoPath),
		state: unopened, // explicitly set for clarity
75 76 77
	}
}

78 79 80
// ConfigAt returns an error if the FSRepo at the given path is not
// initialized. This function allows callers to read the config file even when
// another process is running and holding the lock.
Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
81
func ConfigAt(repoPath string) (*config.Config, error) {
82 83 84 85 86

	// packageLock must be held to ensure that the Read is atomic.
	packageLock.Lock()
	defer packageLock.Unlock()

Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
87 88 89 90
	configFilename, err := config.Filename(repoPath)
	if err != nil {
		return nil, err
	}
91
	return serialize.Load(configFilename)
Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
92 93
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
94
// Init initializes a new FSRepo at the given path with the provided config.
95
// TODO add support for custom datastores.
96
func Init(path string, conf *config.Config) error {
97 98 99 100

	// packageLock must be held to ensure that the repo is not initialized more
	// than once.
	packageLock.Lock()
101
	defer packageLock.Unlock()
102 103

	if isInitializedUnsynced(path) {
104 105
		return nil
	}
106 107 108 109
	for _, b := range componentBuilders() {
		if err := b.Init(path, conf); err != nil {
			return err
		}
110 111 112 113
	}
	return nil
}

114
// Remove recursively removes the FSRepo at |path|.
115 116 117 118 119
func Remove(repoPath string) error {
	repoPath = path.Clean(repoPath)

	// packageLock must be held to ensure that the repo is not removed while
	// being accessed by others.
120 121
	packageLock.Lock()
	defer packageLock.Unlock()
122

123
	if openersCounter.NumOpeners(repoPath) != 0 {
124 125
		return errors.New("repo in use")
	}
126
	return os.RemoveAll(repoPath)
127 128
}

129 130 131
// LockedByOtherProcess returns true if the FSRepo is locked by another
// process. If true, then the repo cannot be opened by this process.
func LockedByOtherProcess(repoPath string) bool {
132 133 134
	repoPath = path.Clean(repoPath)

	// packageLock must be held to check the number of openers.
135 136
	packageLock.Lock()
	defer packageLock.Unlock()
137

138
	// NB: the lock is only held when repos are Open
139
	return lockfile.Locked(repoPath) && openersCounter.NumOpeners(repoPath) == 0
140 141
}

142
// Open returns an error if the repo is not initialized.
143
func (r *FSRepo) Open() error {
144 145 146 147

	// packageLock must be held to make sure that the repo is not destroyed by
	// another caller. It must not be released until initialization is complete
	// and the number of openers is incremeneted.
148 149
	packageLock.Lock()
	defer packageLock.Unlock()
150

Jeromy's avatar
Jeromy committed
151 152 153 154 155 156
	expPath, err := u.TildeExpansion(r.path)
	if err != nil {
		return err
	}
	r.path = expPath

157 158 159
	if r.state != unopened {
		return debugerror.Errorf("repo is %s", r.state)
	}
160
	if !isInitializedUnsynced(r.path) {
161
		return debugerror.New("ipfs not initialized, please run 'ipfs init'")
162
	}
163
	// check repo path, then check all constituent parts.
164
	// TODO acquire repo lock
165
	// TODO if err := initCheckDir(logpath); err != nil { // }
166
	if err := dir.Writable(r.path); err != nil {
167 168 169
		return err
	}

170 171
	for _, b := range componentBuilders() {
		if err := b.OpenHandler(r); err != nil {
172 173
			return err
		}
174 175
	}

176
	return r.transitionToOpened()
177 178
}

179 180 181 182 183 184 185 186
// Close closes the FSRepo, releasing held resources.
func (r *FSRepo) Close() error {
	packageLock.Lock()
	defer packageLock.Unlock()

	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
187 188 189 190 191 192

	for _, closer := range r.components() {
		if err := closer.Close(); err != nil {
			return err
		}
	}
193
	return r.transitionToClosed()
194 195
}

196 197 198 199
// Config returns the FSRepo's config. This method must not be called if the
// repo is not open.
//
// Result when not Open is undefined. The method may panic if it pleases.
200
func (r *FSRepo) Config() *config.Config {
201 202 203 204 205 206 207 208 209

	// It is not necessary to hold the package lock since the repo is in an
	// opened state. The package lock is _not_ meant to ensure that the repo is
	// thread-safe. The package lock is only meant to guard againt removal and
	// coordinate the lockfile. However, we provide thread-safety to keep
	// things simple.
	packageLock.Lock()
	defer packageLock.Unlock()

210 211 212
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
213
	return r.configComponent.Config()
214 215
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
216
// SetConfig updates the FSRepo's config.
217
func (r *FSRepo) SetConfig(updated *config.Config) error {
218 219 220 221 222

	// packageLock is held to provide thread-safety.
	packageLock.Lock()
	defer packageLock.Unlock()

223
	return r.configComponent.SetConfig(updated)
224 225
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
226
// GetConfigKey retrieves only the value of a particular key.
227
func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
228 229 230
	packageLock.Lock()
	defer packageLock.Unlock()

231 232 233
	if r.state != opened {
		return nil, debugerror.Errorf("repo is %s", r.state)
	}
234
	return r.configComponent.GetConfigKey(key)
235 236
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
237
// SetConfigKey writes the value of a particular key.
238
func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
239 240 241
	packageLock.Lock()
	defer packageLock.Unlock()

242 243 244
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
245
	return r.configComponent.SetConfigKey(key, value)
246 247
}

248 249 250 251 252 253 254 255 256
// Datastore returns a repo-owned datastore. If FSRepo is Closed, return value
// is undefined.
func (r *FSRepo) Datastore() ds.ThreadSafeDatastore {
	packageLock.Lock()
	d := r.datastoreComponent.Datastore()
	packageLock.Unlock()
	return d
}

257
var _ io.Closer = &FSRepo{}
258
var _ repo.Repo = &FSRepo{}
259

260 261
// IsInitialized returns true if the repo is initialized at provided |path|.
func IsInitialized(path string) bool {
262 263
	// packageLock is held to ensure that another caller doesn't attempt to
	// Init or Remove the repo while this call is in progress.
264 265
	packageLock.Lock()
	defer packageLock.Unlock()
266

267
	return isInitializedUnsynced(path)
268 269
}

270 271
// private methods below this point. NB: packageLock must held by caller.

272
// isInitializedUnsynced reports whether the repo is initialized. Caller must
273
// hold the packageLock.
274
func isInitializedUnsynced(path string) bool {
275 276 277 278 279 280
	for _, b := range componentBuilders() {
		if !b.IsInitialized(path) {
			return false
		}
	}
	return true
281
}
282

283
// transitionToOpened manages the state transition to |opened|. Caller must hold
284
// the package mutex.
285
func (r *FSRepo) transitionToOpened() error {
286
	r.state = opened
287
	if countBefore := openersCounter.NumOpeners(r.path); countBefore == 0 { // #first
288 289 290 291 292 293
		closer, err := lockfile.Lock(r.path)
		if err != nil {
			return err
		}
		lockfiles[r.path] = closer
	}
294
	return openersCounter.AddOpener(r.path)
295 296 297
}

// transitionToClosed manages the state transition to |closed|. Caller must
298
// hold the package mutex.
299
func (r *FSRepo) transitionToClosed() error {
300
	r.state = closed
301
	if err := openersCounter.RemoveOpener(r.path); err != nil {
302 303
		return err
	}
304
	if countAfter := openersCounter.NumOpeners(r.path); countAfter == 0 {
305 306 307 308 309 310 311
		closer, ok := lockfiles[r.path]
		if !ok {
			return errors.New("package error: lockfile is not held")
		}
		if err := closer.Close(); err != nil {
			return err
		}
312
		delete(lockfiles, r.path)
313 314 315
	}
	return nil
}
316

317
// components returns the FSRepo's constituent components
318 319
func (r *FSRepo) components() []component.Component {
	return []component.Component{
320
		&r.configComponent,
321
		&r.datastoreComponent,
322 323
	}
}
324 325 326 327

func componentBuilders() []componentBuilder {
	return []componentBuilder{

328 329
		// ConfigComponent must be initialized first because other components
		// depend on it.
330 331 332 333
		componentBuilder{
			Init:          component.InitConfigComponent,
			IsInitialized: component.ConfigComponentIsInitialized,
			OpenHandler: func(r *FSRepo) error {
334 335
				c := component.ConfigComponent{}
				c.SetPath(r.path)
336
				if err := c.Open(nil); err != nil {
337 338
					return err
				}
339
				r.configComponent = c
340 341 342 343
				return nil
			},
		},

344 345 346 347 348 349 350
		// DatastoreComponent
		componentBuilder{
			Init:          component.InitDatastoreComponent,
			IsInitialized: component.DatastoreComponentIsInitialized,
			OpenHandler: func(r *FSRepo) error {
				c := component.DatastoreComponent{}
				c.SetPath(r.path)
351
				if err := c.Open(r.configComponent.Config()); err != nil {
352 353 354 355 356 357
					return err
				}
				r.datastoreComponent = c
				return nil
			},
		},
358 359 360 361 362 363 364 365

		// EventlogComponent
		componentBuilder{
			Init:          component.InitEventlogComponent,
			IsInitialized: component.EventlogComponentIsInitialized,
			OpenHandler: func(r *FSRepo) error {
				c := component.EventlogComponent{}
				c.SetPath(r.path)
366
				if err := c.Open(r.configComponent.Config()); err != nil {
367 368 369 370 371 372
					return err
				}
				r.eventlogComponent = c
				return nil
			},
		},
373 374
	}
}