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

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

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

27
var (
28 29 30

	// packageLock must be held to while performing any operation that modifies an
	// FSRepo's state field. This includes Init, Open, Close, and Remove.
31
	packageLock sync.Mutex // protects openersCounter and lockfiles
32 33 34
	// lockfiles holds references to the Closers that ensure that repos are
	// only accessed by one process at a time.
	lockfiles map[string]io.Closer
35
	// openersCounter prevents the fsrepo from being removed while there exist open
36 37 38 39 40 41
	// 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.
42
	openersCounter *counter.Openers
43 44 45
)

func init() {
46
	openersCounter = counter.NewOpenersCounter()
47
	lockfiles = make(map[string]io.Closer)
48 49
}

50 51
// FSRepo represents an IPFS FileSystem Repo. It is safe for use by multiple
// callers.
52
type FSRepo struct {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
53 54 55 56
	// state is the FSRepo's state (unopened, opened, closed)
	state state
	// path is the file-system path
	path string
57 58 59
	// config is set on Open, guarded by packageLock
	config *config.Config

Brian Tiger Chow's avatar
Brian Tiger Chow committed
60
	// TODO test
61
	datastoreComponent component.DatastoreComponent
62 63
}

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

66 67 68 69
type componentBuilder struct {
	Init          component.Initializer
	IsInitialized component.InitializationChecker
	OpenHandler   func(*FSRepo) error
70 71
}

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

81 82 83
// 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
84
func ConfigAt(repoPath string) (*config.Config, error) {
85 86 87 88 89

	// 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
90 91 92 93
	configFilename, err := config.Filename(repoPath)
	if err != nil {
		return nil, err
	}
94
	return serialize.Load(configFilename)
Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
95 96
}

97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
// configIsInitialized returns true if the repo is initialized at
// provided |path|.
func configIsInitialized(path string) bool {
	configFilename, err := config.Filename(path)
	if err != nil {
		return false
	}
	if !util.FileExists(configFilename) {
		return false
	}
	return true
}

func initConfig(path string, conf *config.Config) error {
	if configIsInitialized(path) {
		return nil
	}
	configFilename, err := config.Filename(path)
	if err != nil {
		return err
	}
	// initialization is the one time when it's okay to write to the config
	// without reading the config from disk and merging any user-provided keys
	// that may exist.
	if err := serialize.WriteConfigFile(configFilename, conf); err != nil {
		return err
	}
	return nil
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
127
// Init initializes a new FSRepo at the given path with the provided config.
128
// TODO add support for custom datastores.
129
func Init(repoPath string, conf *config.Config) error {
130 131 132 133

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

136
	if isInitializedUnsynced(repoPath) {
137 138
		return nil
	}
139

140
	if err := initConfig(repoPath, conf); err != nil {
141 142 143
		return err
	}

144
	for _, b := range componentBuilders() {
145
		if err := b.Init(repoPath, conf); err != nil {
146 147
			return err
		}
148
	}
149 150 151 152 153

	if err := dir.Writable(path.Join(repoPath, "logs")); err != nil {
		return err
	}

154 155 156
	return nil
}

157
// Remove recursively removes the FSRepo at |path|.
158 159 160 161 162
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.
163 164
	packageLock.Lock()
	defer packageLock.Unlock()
165

166
	if openersCounter.NumOpeners(repoPath) != 0 {
167 168
		return errors.New("repo in use")
	}
169
	return os.RemoveAll(repoPath)
170 171
}

172 173 174
// 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 {
175 176 177
	repoPath = path.Clean(repoPath)

	// packageLock must be held to check the number of openers.
178 179
	packageLock.Lock()
	defer packageLock.Unlock()
180

181
	// NB: the lock is only held when repos are Open
182
	return lockfile.Locked(repoPath) && openersCounter.NumOpeners(repoPath) == 0
183 184
}

185 186 187 188 189 190 191 192 193 194 195 196 197 198
// openConfig returns an error if the config file is not present.
func (r *FSRepo) openConfig() error {
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
	conf, err := serialize.Load(configFilename)
	if err != nil {
		return err
	}
	r.config = conf
	return nil
}

199 200 201 202 203 204 205 206 207 208 209 210
func configureEventLoggerAtRepoPath(c *config.Config, repoPath string) {
	eventlog.Configure(eventlog.LevelInfo)
	eventlog.Configure(eventlog.LdJSONFormatter)
	rotateConf := eventlog.LogRotatorConfig{
		Filename:   path.Join(repoPath, "logs", "events.log"),
		MaxSizeMB:  c.Log.MaxSizeMB,
		MaxBackups: c.Log.MaxBackups,
		MaxAgeDays: c.Log.MaxAgeDays,
	}
	eventlog.Configure(eventlog.OutputRotatingLogFile(rotateConf))
}

211
// Open returns an error if the repo is not initialized.
212
func (r *FSRepo) Open() error {
213 214 215 216

	// 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.
217 218
	packageLock.Lock()
	defer packageLock.Unlock()
219

Jeromy's avatar
Jeromy committed
220 221 222 223 224 225
	expPath, err := u.TildeExpansion(r.path)
	if err != nil {
		return err
	}
	r.path = expPath

226 227 228
	if r.state != unopened {
		return debugerror.Errorf("repo is %s", r.state)
	}
229
	if !isInitializedUnsynced(r.path) {
230
		return debugerror.New("ipfs not initialized, please run 'ipfs init'")
231
	}
232
	// check repo path, then check all constituent parts.
233
	// TODO acquire repo lock
234
	// TODO if err := initCheckDir(logpath); err != nil { // }
235
	if err := dir.Writable(r.path); err != nil {
236 237 238
		return err
	}

239 240 241 242
	if err := r.openConfig(); err != nil {
		return err
	}

243 244
	for _, b := range componentBuilders() {
		if err := b.OpenHandler(r); err != nil {
245 246
			return err
		}
247 248
	}

249 250 251
	// log.Debugf("writing eventlogs to ...", c.path)
	configureEventLoggerAtRepoPath(r.config, r.path)

252
	return r.transitionToOpened()
253 254
}

255 256 257 258 259 260 261 262
// 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)
	}
263 264 265 266 267 268

	for _, closer := range r.components() {
		if err := closer.Close(); err != nil {
			return err
		}
	}
269 270 271 272 273 274 275 276 277

	// This code existed in the previous versions, but
	// EventlogComponent.Close was never called. Preserving here
	// pending further discussion.
	//
	// TODO It isn't part of the current contract, but callers may like for us
	// to disable logging once the component is closed.
	// eventlog.Configure(eventlog.Output(os.Stderr))

278
	return r.transitionToClosed()
279 280
}

281 282 283 284
// 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.
285
func (r *FSRepo) Config() *config.Config {
286 287 288 289 290 291 292 293 294

	// 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()

295 296 297
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
	return r.config
}

// setConfigUnsynced is for private use.
func (r *FSRepo) setConfigUnsynced(updated *config.Config) error {
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
	// to avoid clobbering user-provided keys, must read the config from disk
	// as a map, write the updated struct values to the map and write the map
	// to disk.
	var mapconf map[string]interface{}
	if err := serialize.ReadConfigFile(configFilename, &mapconf); err != nil {
		return err
	}
	m, err := config.ToMap(updated)
	if err != nil {
		return err
	}
	for k, v := range m {
		mapconf[k] = v
	}
	if err := serialize.WriteConfigFile(configFilename, mapconf); err != nil {
		return err
	}
	*r.config = *updated // copy so caller cannot modify this private config
	return nil
326 327
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
328
// SetConfig updates the FSRepo's config.
329
func (r *FSRepo) SetConfig(updated *config.Config) error {
330 331 332 333 334

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

335
	return r.setConfigUnsynced(updated)
336 337
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
338
// GetConfigKey retrieves only the value of a particular key.
339
func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
340 341 342
	packageLock.Lock()
	defer packageLock.Unlock()

343 344 345
	if r.state != opened {
		return nil, debugerror.Errorf("repo is %s", r.state)
	}
346 347 348 349 350 351 352 353 354 355

	filename, err := config.Filename(r.path)
	if err != nil {
		return nil, err
	}
	var cfg map[string]interface{}
	if err := serialize.ReadConfigFile(filename, &cfg); err != nil {
		return nil, err
	}
	return common.MapGetKV(cfg, key)
356 357
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
358
// SetConfigKey writes the value of a particular key.
359
func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
360 361 362
	packageLock.Lock()
	defer packageLock.Unlock()

363 364 365
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391

	filename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
	switch v := value.(type) {
	case string:
		if i, err := strconv.Atoi(v); err == nil {
			value = i
		}
	}
	var mapconf map[string]interface{}
	if err := serialize.ReadConfigFile(filename, &mapconf); err != nil {
		return err
	}
	if err := common.MapSetKV(mapconf, key, value); err != nil {
		return err
	}
	conf, err := config.FromMap(mapconf)
	if err != nil {
		return err
	}
	if err := serialize.WriteConfigFile(filename, mapconf); err != nil {
		return err
	}
	return r.setConfigUnsynced(conf) // TODO roll this into this method
392 393
}

394 395 396 397 398 399 400 401 402
// 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
}

403
var _ io.Closer = &FSRepo{}
404
var _ repo.Repo = &FSRepo{}
405

406 407
// IsInitialized returns true if the repo is initialized at provided |path|.
func IsInitialized(path string) bool {
408 409
	// packageLock is held to ensure that another caller doesn't attempt to
	// Init or Remove the repo while this call is in progress.
410 411
	packageLock.Lock()
	defer packageLock.Unlock()
412

413
	return isInitializedUnsynced(path)
414 415
}

416 417
// private methods below this point. NB: packageLock must held by caller.

418
// isInitializedUnsynced reports whether the repo is initialized. Caller must
419
// hold the packageLock.
420 421
func isInitializedUnsynced(repoPath string) bool {
	if !configIsInitialized(repoPath) {
422 423
		return false
	}
424
	for _, b := range componentBuilders() {
425
		if !b.IsInitialized(repoPath) {
426 427 428 429
			return false
		}
	}
	return true
430
}
431

432
// transitionToOpened manages the state transition to |opened|. Caller must hold
433
// the package mutex.
434
func (r *FSRepo) transitionToOpened() error {
435
	r.state = opened
436
	if countBefore := openersCounter.NumOpeners(r.path); countBefore == 0 { // #first
437 438 439 440 441 442
		closer, err := lockfile.Lock(r.path)
		if err != nil {
			return err
		}
		lockfiles[r.path] = closer
	}
443
	return openersCounter.AddOpener(r.path)
444 445 446
}

// transitionToClosed manages the state transition to |closed|. Caller must
447
// hold the package mutex.
448
func (r *FSRepo) transitionToClosed() error {
449
	r.state = closed
450
	if err := openersCounter.RemoveOpener(r.path); err != nil {
451 452
		return err
	}
453
	if countAfter := openersCounter.NumOpeners(r.path); countAfter == 0 {
454 455 456 457 458 459 460
		closer, ok := lockfiles[r.path]
		if !ok {
			return errors.New("package error: lockfile is not held")
		}
		if err := closer.Close(); err != nil {
			return err
		}
461
		delete(lockfiles, r.path)
462 463 464
	}
	return nil
}
465

466
// components returns the FSRepo's constituent components
467 468
func (r *FSRepo) components() []component.Component {
	return []component.Component{
469
		&r.datastoreComponent,
470 471
	}
}
472 473 474 475

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

476 477 478 479 480 481 482
		// DatastoreComponent
		componentBuilder{
			Init:          component.InitDatastoreComponent,
			IsInitialized: component.DatastoreComponentIsInitialized,
			OpenHandler: func(r *FSRepo) error {
				c := component.DatastoreComponent{}
				c.SetPath(r.path)
483
				if err := c.Open(r.config); err != nil {
484 485 486 487 488 489
					return err
				}
				r.datastoreComponent = c
				return nil
			},
		},
490 491
	}
}