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

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

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

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

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

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

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

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

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

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

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

82 83 84 85 86 87 88 89 90
// 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
}

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

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

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

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

135
	return transitionToOpened(r)
136 137
}

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

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

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

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

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

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

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

// 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
}
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 304

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