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

import (
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
4 5
	"errors"
	"fmt"
6
	"io"
7
	"os"
Jeromy's avatar
Jeromy committed
8
	"path/filepath"
9
	"strconv"
10
	"strings"
11
	"sync"
12

13
	"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/mitchellh/go-homedir"
14 15 16 17
	repo "github.com/ipfs/go-ipfs/repo"
	"github.com/ipfs/go-ipfs/repo/common"
	config "github.com/ipfs/go-ipfs/repo/config"
	lockfile "github.com/ipfs/go-ipfs/repo/fsrepo/lock"
18
	mfsr "github.com/ipfs/go-ipfs/repo/fsrepo/migrations"
19 20
	serialize "github.com/ipfs/go-ipfs/repo/fsrepo/serialize"
	dir "github.com/ipfs/go-ipfs/thirdparty/dir"
21
	logging "gx/ipfs/QmNQynaz7qfriSUJkiEZUrm2Wen1u3Kj9goZzWtrPyu7XR/go-log"
22
	util "gx/ipfs/QmZNVWh8LLjAavuQ2JXuFmuYH3C11xo988vSgp7UQrTRj1/go-ipfs-util"
Jeromy's avatar
Jeromy committed
23
	"gx/ipfs/QmfQzVugPq1w5shWRcLWSeiHF4a2meBX7yVD8Vw7GWJM9o/go-datastore/measure"
24 25
)

Jeromy's avatar
Jeromy committed
26 27
var log = logging.Logger("fsrepo")

28
// version number that we are currently expecting to see
29
var RepoVersion = 4
30

31 32
var migrationInstructions = `See https://github.com/ipfs/fs-repo-migrations/blob/master/run.md
Sorry for the inconvenience. In the future, these will run automatically.`
33

34 35 36 37 38
var errIncorrectRepoFmt = `Repo has incorrect version: %s
Program version is: %s
Please run the ipfs migration tool before continuing.
` + migrationInstructions

39 40 41 42 43 44
var programTooLowMessage = `Your programs version (%d) is lower than your repos (%d).
Please update ipfs to a version that supports the existing repo, or run
a migration in reverse.

See https://github.com/ipfs/fs-repo-migrations/blob/master/run.md for details.`

45 46 47 48
var (
	ErrNoVersion = errors.New("no version file found, please run 0-to-1 migration tool.\n" + migrationInstructions)
	ErrOldRepo   = errors.New("ipfs repo found in old '~/.go-ipfs' location, please run migration tool.\n" + migrationInstructions)
)
49

50 51 52 53 54 55 56
type NoRepoError struct {
	Path string
}

var _ error = NoRepoError{}

func (err NoRepoError) Error() string {
rht's avatar
rht committed
57
	return fmt.Sprintf("no ipfs repo found in %s.\nplease run: ipfs init", err.Path)
58 59
}

60
const apiFile = "api"
61

62
var (
63 64 65

	// packageLock must be held to while performing any operation that modifies an
	// FSRepo's state field. This includes Init, Open, Close, and Remove.
Tommi Virtanen's avatar
Tommi Virtanen committed
66
	packageLock sync.Mutex
67

68 69 70 71 72 73 74 75 76 77 78 79 80
	// onlyOne keeps track of open FSRepo instances.
	//
	// TODO: once command Context / Repo integration is cleaned up,
	// this can be removed. Right now, this makes ConfigCmd.Run
	// function try to open the repo twice:
	//
	//     $ ipfs daemon &
	//     $ ipfs config foo
	//
	// The reason for the above is that in standalone mode without the
	// daemon, `ipfs config` tries to save work by not building the
	// full IpfsNode, but accessing the Repo directly.
	onlyOne repo.OnlyOne
81 82
)

83 84
// FSRepo represents an IPFS FileSystem Repo. It is safe for use by multiple
// callers.
85
type FSRepo struct {
86 87
	// has Close been called already
	closed bool
Brian Tiger Chow's avatar
Brian Tiger Chow committed
88 89
	// path is the file-system path
	path string
90 91 92
	// lockfile is the file system lock to prevent others from opening
	// the same fsrepo path concurrently
	lockfile io.Closer
93
	config   *config.Config
94
	ds       repo.Datastore
95 96
}

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

99 100
// Open the FSRepo at path. Returns an error if the repo is not
// initialized.
101 102 103 104 105 106 107 108
func Open(repoPath string) (repo.Repo, error) {
	fn := func() (repo.Repo, error) {
		return open(repoPath)
	}
	return onlyOne.Open(repoPath, fn)
}

func open(repoPath string) (repo.Repo, error) {
109 110 111
	packageLock.Lock()
	defer packageLock.Unlock()

112
	r, err := newFSRepo(repoPath)
113 114 115 116
	if err != nil {
		return nil, err
	}

117 118 119
	// Check if its initialized
	if err := checkInitialized(r.path); err != nil {
		return nil, err
120
	}
Tommi Virtanen's avatar
Tommi Virtanen committed
121

Tommi Virtanen's avatar
Tommi Virtanen committed
122 123 124 125 126 127 128 129 130 131 132 133
	r.lockfile, err = lockfile.Lock(r.path)
	if err != nil {
		return nil, err
	}
	keepLocked := false
	defer func() {
		// unlock on error, leave it locked on success
		if !keepLocked {
			r.lockfile.Close()
		}
	}()

134
	// Check version, and error out if not matching
135
	ver, err := mfsr.RepoPath(r.path).Version()
136 137
	if err != nil {
		if os.IsNotExist(err) {
138
			return nil, ErrNoVersion
139 140 141 142
		}
		return nil, err
	}

143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
	if RepoVersion > ver {
		r.lockfile.Close()

		err := mfsr.TryMigrating(RepoVersion)
		if err != nil {
			return nil, err
		}

		r.lockfile, err = lockfile.Lock(r.path)
		if err != nil {
			return nil, fmt.Errorf("reacquiring lock: %s", err)
		}

	} else if ver > RepoVersion {
		// program version too low for existing repo
		return nil, fmt.Errorf(programTooLowMessage, RepoVersion, ver)
159 160
	}

161 162 163 164 165 166 167 168 169 170 171 172 173
	// check repo path, then check all constituent parts.
	if err := dir.Writable(r.path); err != nil {
		return nil, err
	}

	if err := r.openConfig(); err != nil {
		return nil, err
	}

	if err := r.openDatastore(); err != nil {
		return nil, err
	}

Tommi Virtanen's avatar
Tommi Virtanen committed
174
	keepLocked = true
175
	return r, nil
176 177
}

178
func newFSRepo(rpath string) (*FSRepo, error) {
179
	expPath, err := homedir.Expand(filepath.Clean(rpath))
180 181 182 183 184 185 186 187 188 189 190
	if err != nil {
		return nil, err
	}

	return &FSRepo{path: expPath}, nil
}

func checkInitialized(path string) error {
	if !isInitializedUnsynced(path) {
		alt := strings.Replace(path, ".ipfs", ".go-ipfs", 1)
		if isInitializedUnsynced(alt) {
191
			return ErrOldRepo
192
		}
193
		return NoRepoError{Path: path}
194 195 196 197
	}
	return nil
}

198 199 200
// 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
201
func ConfigAt(repoPath string) (*config.Config, error) {
202 203 204 205 206

	// 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
207 208 209 210
	configFilename, err := config.Filename(repoPath)
	if err != nil {
		return nil, err
	}
211
	return serialize.Load(configFilename)
Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
212 213
}

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
// 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
244
// Init initializes a new FSRepo at the given path with the provided config.
245
// TODO add support for custom datastores.
246
func Init(repoPath string, conf *config.Config) error {
247 248 249 250

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

253
	if isInitializedUnsynced(repoPath) {
254 255
		return nil
	}
256

257
	if err := initConfig(repoPath, conf); err != nil {
258 259 260
		return err
	}

261 262
	if err := initDefaultDatastore(repoPath, conf); err != nil {
		return err
263 264
	}

265 266 267 268
	if err := mfsr.RepoPath(repoPath).WriteVersion(RepoVersion); err != nil {
		return err
	}

269 270 271
	return nil
}

272 273
// LockedByOtherProcess returns true if the FSRepo is locked by another
// process. If true, then the repo cannot be opened by this process.
274
func LockedByOtherProcess(repoPath string) (bool, error) {
Jeromy's avatar
Jeromy committed
275
	repoPath = filepath.Clean(repoPath)
michael's avatar
michael committed
276 277 278 279 280
	locked, err := lockfile.Locked(repoPath)
	if locked {
		log.Debugf("(%t)<->Lock is held at %s", locked, repoPath)
	}
	return locked, err
281 282
}

283 284 285 286 287
// APIAddr returns the registered API addr, according to the api file
// in the fsrepo. This is a concurrent operation, meaning that any
// process may read this file. modifying this file, therefore, should
// use "mv" to replace the whole file and avoid interleaved read/writes.
func APIAddr(repoPath string) (string, error) {
Jeromy's avatar
Jeromy committed
288 289
	repoPath = filepath.Clean(repoPath)
	apiFilePath := filepath.Join(repoPath, apiFile)
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315

	// if there is no file, assume there is no api addr.
	f, err := os.Open(apiFilePath)
	if err != nil {
		if os.IsNotExist(err) {
			return "", repo.ErrApiNotRunning
		}
		return "", err
	}
	defer f.Close()

	// read up to 2048 bytes. io.ReadAll is a vulnerability, as
	// someone could hose the process by putting a massive file there.
	buf := make([]byte, 2048)
	n, err := f.Read(buf)
	if err != nil && err != io.EOF {
		return "", err
	}

	s := string(buf[:n])
	s = strings.TrimSpace(s)
	return s, nil
}

// SetAPIAddr writes the API Addr to the /api file.
func (r *FSRepo) SetAPIAddr(addr string) error {
Jeromy's avatar
Jeromy committed
316
	f, err := os.Create(filepath.Join(r.path, apiFile))
317 318 319 320 321 322 323 324 325
	if err != nil {
		return err
	}
	defer f.Close()

	_, err = f.WriteString(addr)
	return err
}

326 327 328 329 330 331 332 333 334 335 336 337 338 339
// 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
}

340 341
// openDatastore returns an error if the config file is not present.
func (r *FSRepo) openDatastore() error {
342 343 344 345 346 347 348 349 350
	switch r.config.Datastore.Type {
	case "default", "leveldb", "":
		d, err := openDefaultDatastore(r)
		if err != nil {
			return err
		}
		r.ds = d
	default:
		return fmt.Errorf("unknown datastore type: %s", r.config.Datastore.Type)
351
	}
352

353 354 355 356 357 358 359 360 361 362 363 364 365 366
	// Wrap it with metrics gathering
	//
	// Add our PeerID to metrics paths to keep them unique
	//
	// As some tests just pass a zero-value Config to fsrepo.Init,
	// cope with missing PeerID.
	id := r.config.Identity.PeerID
	if id == "" {
		// the tests pass in a zero Config; cope with it
		id = fmt.Sprintf("uninitialized_%p", r)
	}
	prefix := "fsrepo." + id + ".datastore"
	r.ds = measure.New(prefix, r.ds)

367 368 369
	return nil
}

370 371 372 373 374
// Close closes the FSRepo, releasing held resources.
func (r *FSRepo) Close() error {
	packageLock.Lock()
	defer packageLock.Unlock()

375
	if r.closed {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
376
		return errors.New("repo is closed")
377
	}
378

Jeromy's avatar
Jeromy committed
379
	err := os.Remove(filepath.Join(r.path, apiFile))
michael's avatar
michael committed
380
	if err != nil && !os.IsNotExist(err) {
Jeromy's avatar
Jeromy committed
381 382 383
		log.Warning("error removing api file: ", err)
	}

384 385 386 387
	if err := r.ds.Close(); err != nil {
		return err
	}

388 389 390 391 392 393
	// 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.
Jeromy's avatar
Jeromy committed
394
	// logging.Configure(logging.Output(os.Stderr))
395

396 397 398 399 400
	r.closed = true
	if err := r.lockfile.Close(); err != nil {
		return err
	}
	return nil
401 402
}

403
// Result when not Open is undefined. The method may panic if it pleases.
404
func (r *FSRepo) Config() (*config.Config, error) {
405 406 407 408 409 410 411 412 413

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

414
	if r.closed {
415
		return nil, errors.New("cannot access config, repo not open")
416
	}
417
	return r.config, nil
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
}

// 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
445 446
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
447
// SetConfig updates the FSRepo's config.
448
func (r *FSRepo) SetConfig(updated *config.Config) error {
449 450 451 452 453

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

454
	return r.setConfigUnsynced(updated)
455 456
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
457
// GetConfigKey retrieves only the value of a particular key.
458
func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
459 460 461
	packageLock.Lock()
	defer packageLock.Unlock()

462
	if r.closed {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
463
		return nil, errors.New("repo is closed")
464
	}
465 466 467 468 469 470 471 472 473 474

	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)
475 476
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
477
// SetConfigKey writes the value of a particular key.
478
func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
479 480 481
	packageLock.Lock()
	defer packageLock.Unlock()

482
	if r.closed {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
483
		return errors.New("repo is closed")
484
	}
485 486 487 488 489 490 491 492 493

	filename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
	var mapconf map[string]interface{}
	if err := serialize.ReadConfigFile(filename, &mapconf); err != nil {
		return err
	}
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531

	// Get the type of the value associated with the key
	oldValue, err := common.MapGetKV(mapconf, key)
	ok := true
	if err != nil {
		// key-value does not exist yet
		switch v := value.(type) {
		case string:
			value, err = strconv.ParseBool(v)
			if err != nil {
				value, err = strconv.Atoi(v)
				if err != nil {
					value, err = strconv.ParseFloat(v, 32)
					if err != nil {
						value = v
					}
				}
			}
		default:
		}
	} else {
		switch oldValue.(type) {
		case bool:
			value, ok = value.(bool)
		case int:
			value, ok = value.(int)
		case float32:
			value, ok = value.(float32)
		case string:
			value, ok = value.(string)
		default:
			value = value
		}
		if !ok {
			return fmt.Errorf("Wrong config type, expected %T", oldValue)
		}
	}

532 533 534
	if err := common.MapSetKV(mapconf, key, value); err != nil {
		return err
	}
535 536 537

	// This step doubles as to validate the map against the struct
	// before serialization
538 539 540 541 542 543 544 545
	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
546 547
}

548 549
// Datastore returns a repo-owned datastore. If FSRepo is Closed, return value
// is undefined.
550
func (r *FSRepo) Datastore() repo.Datastore {
551
	packageLock.Lock()
552
	d := r.ds
553 554 555 556
	packageLock.Unlock()
	return d
}

rht's avatar
rht committed
557 558 559 560 561 562 563 564 565
// GetStorageUsage computes the storage space taken by the repo in bytes
func (r *FSRepo) GetStorageUsage() (uint64, error) {
	pth, err := config.PathRoot()
	if err != nil {
		return 0, err
	}

	var du uint64
	err = filepath.Walk(pth, func(p string, f os.FileInfo, err error) error {
rht's avatar
rht committed
566 567 568 569 570 571 572
		if err != nil {
			log.Debugf("filepath.Walk error: %s", err)
			return nil
		}
		if f != nil {
			du += uint64(f.Size())
		}
rht's avatar
rht committed
573 574 575 576 577
		return nil
	})
	return du, err
}

578
var _ io.Closer = &FSRepo{}
579
var _ repo.Repo = &FSRepo{}
580

581 582
// IsInitialized returns true if the repo is initialized at provided |path|.
func IsInitialized(path string) bool {
583 584
	// packageLock is held to ensure that another caller doesn't attempt to
	// Init or Remove the repo while this call is in progress.
585 586
	packageLock.Lock()
	defer packageLock.Unlock()
587

588
	return isInitializedUnsynced(path)
589 590
}

591 592
// private methods below this point. NB: packageLock must held by caller.

593
// isInitializedUnsynced reports whether the repo is initialized. Caller must
594
// hold the packageLock.
595
func isInitializedUnsynced(repoPath string) bool {
596
	return configIsInitialized(repoPath)
597
}