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

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

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

19
var (
20
	// openerCounter prevents the fsrepo from being removed while there exist open
21 22 23 24 25 26
	// 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.
27
	openerCounter *opener.Counter
28 29

	lockfiles map[string]io.Closer
30 31 32
)

func init() {
33
	openerCounter = opener.NewCounter()
34
	lockfiles = make(map[string]io.Closer)
35 36 37
}

// FSRepo represents an IPFS FileSystem Repo. It is not thread-safe.
38
type FSRepo struct {
39
	state  state
40
	path   string
41
	config *config.Config
42 43
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
44
// At returns a handle to an FSRepo at the provided |path|.
45
func At(path string) *FSRepo {
46
	// This method must not have side-effects.
47
	return &FSRepo{
48 49
		path:  path,
		state: unopened, // explicitly set for clarity
50 51 52
	}
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
53
// Init initializes a new FSRepo at the given path with the provided config.
54
func Init(path string, conf *config.Config) error {
55 56
	openerCounter.Lock() // lock must be held to ensure atomicity (prevent Removal)
	defer openerCounter.Unlock()
57 58

	if isInitializedUnsynced(path) {
59 60 61 62 63 64 65 66 67 68 69 70
		return nil
	}
	configFilename, err := config.Filename(path)
	if err != nil {
		return err
	}
	if err := writeConfigFile(configFilename, conf); err != nil {
		return err
	}
	return nil
}

71 72
// Remove recursively removes the FSRepo at |path|.
func Remove(path string) error {
73 74 75
	openerCounter.Lock()
	defer openerCounter.Unlock()
	if openerCounter.NumOpeners(path) != 0 {
76 77 78 79 80
		return errors.New("repo in use")
	}
	return os.RemoveAll(path)
}

81 82 83 84 85 86 87 88 89
// 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 {
	openerCounter.Lock()
	defer openerCounter.Unlock()
	// NB: the lock is only held when repos are Open
	return lockfile.Locked(repoPath) && openerCounter.NumOpeners(repoPath) == 0
}

90
// Open returns an error if the repo is not initialized.
91
func (r *FSRepo) Open() error {
92 93
	openerCounter.Lock()
	defer openerCounter.Unlock()
94 95 96
	if r.state != unopened {
		return debugerror.Errorf("repo is %s", r.state)
	}
97
	if !isInitializedUnsynced(r.path) {
98
		return debugerror.New("ipfs not initialized, please run 'ipfs init'")
99
	}
100
	// check repo path, then check all constituent parts.
101
	// TODO acquire repo lock
102 103 104 105 106
	// TODO if err := initCheckDir(logpath); err != nil { // }
	if err := initCheckDir(r.path); err != nil {
		return err
	}

107 108 109 110
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
111
	conf, err := load(configFilename)
112 113 114 115 116
	if err != nil {
		return err
	}
	r.config = conf

117 118 119 120 121 122 123 124 125
	// datastore
	dspath, err := config.DataStorePath("")
	if err != nil {
		return err
	}
	if err := initCheckDir(dspath); err != nil {
		return debugerror.Errorf("datastore: %s", err)
	}

126 127 128 129 130 131 132 133
	logpath, err := config.LogsPath("")
	if err != nil {
		return debugerror.Wrap(err)
	}
	if err := initCheckDir(logpath); err != nil {
		return debugerror.Errorf("logs: %s", err)
	}

134
	return transitionToOpened(r)
135 136
}

137 138 139 140
// 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.
141
func (r *FSRepo) Config() *config.Config {
142 143
	// no lock necessary because repo is either Open (and thus protected from
	// Removal) or has no side-effect
144 145 146 147 148 149
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
	return r.config
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
150
// SetConfig updates the FSRepo's config.
151
func (r *FSRepo) SetConfig(updated *config.Config) error {
152
	// no lock required because repo should be Open
153 154 155
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
156 157 158 159
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
	// 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 {
175 176
		return err
	}
177
	*r.config = *updated // copy so caller cannot modify this private config
178 179 180
	return nil
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
181
// GetConfigKey retrieves only the value of a particular key.
182
func (r *FSRepo) GetConfigKey(key string) (interface{}, error) {
183 184 185
	if r.state != opened {
		return nil, debugerror.Errorf("repo is %s", r.state)
	}
186 187 188 189 190 191 192 193 194 195 196
	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
197
// SetConfigKey writes the value of a particular key.
198
func (r *FSRepo) SetConfigKey(key string, value interface{}) error {
199
	// no lock required because repo should be Open
200 201 202
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
203 204 205 206 207 208 209 210 211 212 213 214 215 216
	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
217
	conf, err := config.FromMap(mapconf)
218 219 220 221 222 223
	if err != nil {
		return err
	}
	return r.SetConfig(conf)
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
224
// Close closes the FSRepo, releasing held resources.
225
func (r *FSRepo) Close() error {
226 227
	openerCounter.Lock()
	defer openerCounter.Unlock()
228 229 230
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
231
	return transitionToClosed(r)
232 233 234
}

var _ io.Closer = &FSRepo{}
235
var _ repo.Repo = &FSRepo{}
236

237 238
// IsInitialized returns true if the repo is initialized at provided |path|.
func IsInitialized(path string) bool {
239 240
	openerCounter.Lock()
	defer openerCounter.Unlock()
241 242 243 244
	return isInitializedUnsynced(path)
}

// isInitializedUnsynced reports whether the repo is initialized. Caller must
245
// hold openerCounter lock.
246
func isInitializedUnsynced(path string) bool {
247 248 249 250 251 252 253 254 255
	configFilename, err := config.Filename(path)
	if err != nil {
		return false
	}
	if !util.FileExists(configFilename) {
		return false
	}
	return true
}
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270

// 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
}
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303

// transitionToOpened manages the state transition to |opened|. Caller must hold
// openerCounter lock.
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
// hold openerCounter lock.
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
		}
	}
	return nil
}