fsrepo.go 8.04 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 310 311 312

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