fsrepo.go 8.07 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
huh  
Brian Tiger Chow committed
54 55 56 57 58 59 60 61
func ConfigAt(repoPath string) (*config.Config, error) {
	configFilename, err := config.Filename(repoPath)
	if err != nil {
		return nil, err
	}
	return load(configFilename)
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
62
// Init initializes a new FSRepo at the given path with the provided config.
63
func Init(path string, conf *config.Config) error {
64 65
	openerCounter.Lock() // lock must be held to ensure atomicity (prevent Removal)
	defer openerCounter.Unlock()
66 67

	if isInitializedUnsynced(path) {
68 69 70 71 72 73 74 75 76 77 78 79
		return nil
	}
	configFilename, err := config.Filename(path)
	if err != nil {
		return err
	}
	if err := writeConfigFile(configFilename, conf); err != nil {
		return err
	}
	return nil
}

80 81
// Remove recursively removes the FSRepo at |path|.
func Remove(path string) error {
82 83 84
	openerCounter.Lock()
	defer openerCounter.Unlock()
	if openerCounter.NumOpeners(path) != 0 {
85 86 87 88 89
		return errors.New("repo in use")
	}
	return os.RemoveAll(path)
}

90 91 92 93 94 95 96 97 98
// 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
}

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

116 117 118 119
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
120
	conf, err := load(configFilename)
121 122 123 124 125
	if err != nil {
		return err
	}
	r.config = conf

126 127 128 129 130 131 132 133 134
	// datastore
	dspath, err := config.DataStorePath("")
	if err != nil {
		return err
	}
	if err := initCheckDir(dspath); err != nil {
		return debugerror.Errorf("datastore: %s", err)
	}

135 136 137 138 139 140 141 142
	logpath, err := config.LogsPath("")
	if err != nil {
		return debugerror.Wrap(err)
	}
	if err := initCheckDir(logpath); err != nil {
		return debugerror.Errorf("logs: %s", err)
	}

143
	return transitionToOpened(r)
144 145
}

146 147 148 149
// 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.
150
func (r *FSRepo) Config() *config.Config {
151 152
	// no lock necessary because repo is either Open (and thus protected from
	// Removal) or has no side-effect
153 154 155 156 157 158
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
	return r.config
}

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

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

Brian Tiger Chow's avatar
Brian Tiger Chow committed
233
// Close closes the FSRepo, releasing held resources.
234
func (r *FSRepo) Close() error {
235 236
	openerCounter.Lock()
	defer openerCounter.Unlock()
237 238 239
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
240
	return transitionToClosed(r)
241 242 243
}

var _ io.Closer = &FSRepo{}
244
var _ repo.Repo = &FSRepo{}
245

246 247
// IsInitialized returns true if the repo is initialized at provided |path|.
func IsInitialized(path string) bool {
248 249
	openerCounter.Lock()
	defer openerCounter.Unlock()
250 251 252 253
	return isInitializedUnsynced(path)
}

// isInitializedUnsynced reports whether the repo is initialized. Caller must
254
// hold openerCounter lock.
255
func isInitializedUnsynced(path string) bool {
256 257 258 259 260 261 262 263 264
	configFilename, err := config.Filename(path)
	if err != nil {
		return false
	}
	if !util.FileExists(configFilename) {
		return false
	}
	return true
}
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279

// 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
}
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 305 306 307 308 309

// 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
		}
310
		delete(lockfiles, r.path)
311 312 313
	}
	return nil
}