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

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

12 13
	repo "github.com/jbenet/go-ipfs/repo"
	common "github.com/jbenet/go-ipfs/repo/common"
14
	config "github.com/jbenet/go-ipfs/repo/config"
15
	lockfile "github.com/jbenet/go-ipfs/repo/fsrepo/lock"
16
	opener "github.com/jbenet/go-ipfs/repo/fsrepo/opener"
17
	util "github.com/jbenet/go-ipfs/util"
18
	debugerror "github.com/jbenet/go-ipfs/util/debugerror"
19 20
)

21
var (
22 23 24 25 26 27 28

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

func init() {
40
	openerCounter = opener.NewCounter()
41
	lockfiles = make(map[string]io.Closer)
42 43
}

44 45
// FSRepo represents an IPFS FileSystem Repo. It is safe for use by multiple
// callers.
46
type FSRepo struct {
47
	state  state
48
	path   string
49
	config *config.Config
50 51
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
52
// At returns a handle to an FSRepo at the provided |path|.
53
func At(repoPath string) *FSRepo {
54
	// This method must not have side-effects.
55
	return &FSRepo{
56
		path:  path.Clean(repoPath),
57
		state: unopened, // explicitly set for clarity
58 59 60
	}
}

Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
61
func ConfigAt(repoPath string) (*config.Config, error) {
62 63 64 65 66

	// 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
67 68 69 70 71 72 73
	configFilename, err := config.Filename(repoPath)
	if err != nil {
		return nil, err
	}
	return load(configFilename)
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
74
// Init initializes a new FSRepo at the given path with the provided config.
75
func Init(path string, conf *config.Config) error {
76 77 78 79

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

	if isInitializedUnsynced(path) {
83 84 85 86 87 88 89 90 91 92 93 94
		return nil
	}
	configFilename, err := config.Filename(path)
	if err != nil {
		return err
	}
	if err := writeConfigFile(configFilename, conf); err != nil {
		return err
	}
	return nil
}

95
// Remove recursively removes the FSRepo at |path|.
96 97 98 99 100
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.
101 102
	packageLock.Lock()
	defer packageLock.Unlock()
103 104

	if openerCounter.NumOpeners(repoPath) != 0 {
105 106
		return errors.New("repo in use")
	}
107
	return os.RemoveAll(repoPath)
108 109
}

110 111 112
// 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 {
113 114 115
	repoPath = path.Clean(repoPath)

	// packageLock must be held to check the number of openers.
116 117
	packageLock.Lock()
	defer packageLock.Unlock()
118

119 120 121 122
	// NB: the lock is only held when repos are Open
	return lockfile.Locked(repoPath) && openerCounter.NumOpeners(repoPath) == 0
}

123
// Open returns an error if the repo is not initialized.
124
func (r *FSRepo) Open() error {
125 126 127 128

	// 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.
129 130
	packageLock.Lock()
	defer packageLock.Unlock()
131

132 133 134
	if r.state != unopened {
		return debugerror.Errorf("repo is %s", r.state)
	}
135
	if !isInitializedUnsynced(r.path) {
136
		return debugerror.New("ipfs not initialized, please run 'ipfs init'")
137
	}
138
	// check repo path, then check all constituent parts.
139
	// TODO acquire repo lock
140 141 142 143 144
	// TODO if err := initCheckDir(logpath); err != nil { // }
	if err := initCheckDir(r.path); err != nil {
		return err
	}

145 146 147 148
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
149
	conf, err := load(configFilename)
150 151 152 153 154
	if err != nil {
		return err
	}
	r.config = conf

155 156 157 158 159 160 161 162 163
	// datastore
	dspath, err := config.DataStorePath("")
	if err != nil {
		return err
	}
	if err := initCheckDir(dspath); err != nil {
		return debugerror.Errorf("datastore: %s", err)
	}

164 165 166 167 168 169 170 171
	logpath, err := config.LogsPath("")
	if err != nil {
		return debugerror.Wrap(err)
	}
	if err := initCheckDir(logpath); err != nil {
		return debugerror.Errorf("logs: %s", err)
	}

172
	return transitionToOpened(r)
173 174
}

175 176 177 178
// 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.
179
func (r *FSRepo) Config() *config.Config {
180 181 182 183 184 185 186 187 188

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

189 190 191 192 193 194
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
	return r.config
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
195
// SetConfig updates the FSRepo's config.
196
func (r *FSRepo) SetConfig(updated *config.Config) error {
197 198 199 200 201 202

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

	return r.setConfigUnsynced(updated)
203 204
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
205
// GetConfigKey retrieves only the value of a particular key.
206
func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
207 208 209
	packageLock.Lock()
	defer packageLock.Unlock()

210 211 212
	if r.state != opened {
		return nil, debugerror.Errorf("repo is %s", r.state)
	}
213 214 215 216 217 218 219 220 221 222 223
	filename, err := config.Filename(r.path)
	if err != nil {
		return nil, err
	}
	var cfg map[string]interface{}
	if err := readConfigFile(filename, &cfg); err != nil {
		return nil, err
	}
	return common.MapGetKV(cfg, key)
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
224
// SetConfigKey writes the value of a particular key.
225
func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
226 227 228
	packageLock.Lock()
	defer packageLock.Unlock()

229 230 231
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
232 233 234 235 236 237 238 239 240 241 242 243 244 245
	filename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
	var mapconf map[string]interface{}
	if err := readConfigFile(filename, &mapconf); err != nil {
		return err
	}
	if err := common.MapSetKV(mapconf, key, value); err != nil {
		return err
	}
	if err := writeConfigFile(filename, mapconf); err != nil {
		return err
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
246
	conf, err := config.FromMap(mapconf)
247 248 249
	if err != nil {
		return err
	}
250
	return r.setConfigUnsynced(conf)
251 252
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
253
// Close closes the FSRepo, releasing held resources.
254
func (r *FSRepo) Close() error {
255 256
	packageLock.Lock()
	defer packageLock.Unlock()
257

258 259 260
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
261
	return transitionToClosed(r)
262 263 264
}

var _ io.Closer = &FSRepo{}
265
var _ repo.Repo = &FSRepo{}
266

267 268
// IsInitialized returns true if the repo is initialized at provided |path|.
func IsInitialized(path string) bool {
269 270
	// packageLock is held to ensure that another caller doesn't attempt to
	// Init or Remove the repo while this call is in progress.
271 272
	packageLock.Lock()
	defer packageLock.Unlock()
273 274 275
	return isInitializedUnsynced(path)
}

276 277
// private methods below this point. NB: packageLock must held by caller.

278
// isInitializedUnsynced reports whether the repo is initialized. Caller must
279
// hold openerCounter lock.
280
func isInitializedUnsynced(path string) bool {
281 282 283 284 285 286 287 288 289
	configFilename, err := config.Filename(path)
	if err != nil {
		return false
	}
	if !util.FileExists(configFilename) {
		return false
	}
	return true
}
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304

// initCheckDir ensures the directory exists and is writable
func initCheckDir(path string) error {
	// Construct the path if missing
	if err := os.MkdirAll(path, os.ModePerm); err != nil {
		return err
	}
	// Check the directory is writeable
	if f, err := os.Create(filepath.Join(path, "._check_writeable")); err == nil {
		os.Remove(f.Name())
	} else {
		return debugerror.New("'" + path + "' is not writeable")
	}
	return nil
}
305 306

// transitionToOpened manages the state transition to |opened|. Caller must hold
307
// the package mutex.
308 309 310 311 312 313 314 315 316 317 318 319 320
func transitionToOpened(r *FSRepo) error {
	r.state = opened
	if countBefore := openerCounter.NumOpeners(r.path); countBefore == 0 { // #first
		closer, err := lockfile.Lock(r.path)
		if err != nil {
			return err
		}
		lockfiles[r.path] = closer
	}
	return openerCounter.AddOpener(r.path)
}

// transitionToClosed manages the state transition to |closed|. Caller must
321
// hold the package mutex.
322 323 324 325 326 327 328 329 330 331 332 333 334
func transitionToClosed(r *FSRepo) error {
	r.state = closed
	if err := openerCounter.RemoveOpener(r.path); err != nil {
		return err
	}
	if countAfter := openerCounter.NumOpeners(r.path); countAfter == 0 {
		closer, ok := lockfiles[r.path]
		if !ok {
			return errors.New("package error: lockfile is not held")
		}
		if err := closer.Close(); err != nil {
			return err
		}
335
		delete(lockfiles, r.path)
336 337 338
	}
	return nil
}
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368

// setConfigUnsynced is for private use. Callers must hold the packageLock.
func (r *FSRepo) setConfigUnsynced(updated *config.Config) error {
	if r.state != opened {
		return fmt.Errorf("repo is", r.state)
	}
	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 := 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 := writeConfigFile(configFilename, mapconf); err != nil {
		return err
	}
	*r.config = *updated // copy so caller cannot modify this private config
	return nil
}