fsrepo.go 6.56 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
	opener "github.com/jbenet/go-ipfs/repo/fsrepo/opener"
14
	util "github.com/jbenet/go-ipfs/util"
15
	debugerror "github.com/jbenet/go-ipfs/util/debugerror"
16 17
)

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

func init() {
30
	openerCounter = opener.NewCounter()
31 32 33
}

// FSRepo represents an IPFS FileSystem Repo. It is not thread-safe.
34
type FSRepo struct {
35
	state  state
36
	path   string
37
	config *config.Config
38 39
}

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

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

	if isInitializedUnsynced(path) {
55 56 57 58 59 60 61 62 63 64 65 66
		return nil
	}
	configFilename, err := config.Filename(path)
	if err != nil {
		return err
	}
	if err := writeConfigFile(configFilename, conf); err != nil {
		return err
	}
	return nil
}

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

77
// Open returns an error if the repo is not initialized.
78
func (r *FSRepo) Open() error {
79 80
	openerCounter.Lock()
	defer openerCounter.Unlock()
81 82 83
	if r.state != unopened {
		return debugerror.Errorf("repo is %s", r.state)
	}
84
	if !isInitializedUnsynced(r.path) {
85
		return debugerror.New("ipfs not initialized, please run 'ipfs init'")
86
	}
87
	// check repo path, then check all constituent parts.
88
	// TODO acquire repo lock
89 90 91 92 93
	// TODO if err := initCheckDir(logpath); err != nil { // }
	if err := initCheckDir(r.path); err != nil {
		return err
	}

94 95 96 97
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
98
	conf, err := load(configFilename)
99 100 101 102 103
	if err != nil {
		return err
	}
	r.config = conf

104 105 106 107 108 109 110 111 112
	// datastore
	dspath, err := config.DataStorePath("")
	if err != nil {
		return err
	}
	if err := initCheckDir(dspath); err != nil {
		return debugerror.Errorf("datastore: %s", err)
	}

113 114 115 116 117 118 119 120
	logpath, err := config.LogsPath("")
	if err != nil {
		return debugerror.Wrap(err)
	}
	if err := initCheckDir(logpath); err != nil {
		return debugerror.Errorf("logs: %s", err)
	}

121
	r.state = opened
122
	openerCounter.AddOpener(r.path)
123 124 125
	return nil
}

126 127 128 129
// 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.
130
func (r *FSRepo) Config() *config.Config {
131 132
	// no lock necessary because repo is either Open (and thus protected from
	// Removal) or has no side-effect
133 134 135 136 137 138
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
	return r.config
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
139
// SetConfig updates the FSRepo's config.
140
func (r *FSRepo) SetConfig(updated *config.Config) error {
141
	// no lock required because repo should be Open
142 143 144
	if r.state != opened {
		panic(fmt.Sprintln("repo is", r.state))
	}
145 146 147 148
	configFilename, err := config.Filename(r.path)
	if err != nil {
		return err
	}
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
	// 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 {
164 165
		return err
	}
166
	*r.config = *updated // copy so caller cannot modify this private config
167 168 169
	return nil
}

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

Brian Tiger Chow's avatar
Brian Tiger Chow committed
213
// Close closes the FSRepo, releasing held resources.
214
func (r *FSRepo) Close() error {
215 216
	openerCounter.Lock()
	defer openerCounter.Unlock()
217 218 219
	if r.state != opened {
		return debugerror.Errorf("repo is %s", r.state)
	}
220
	openerCounter.RemoveOpener(r.path)
221 222 223 224
	return nil // TODO release repo lock
}

var _ io.Closer = &FSRepo{}
225
var _ repo.Interface = &FSRepo{}
226

227 228
// IsInitialized returns true if the repo is initialized at provided |path|.
func IsInitialized(path string) bool {
229 230
	openerCounter.Lock()
	defer openerCounter.Unlock()
231 232 233 234
	return isInitializedUnsynced(path)
}

// isInitializedUnsynced reports whether the repo is initialized. Caller must
235
// hold openerCounter lock.
236
func isInitializedUnsynced(path string) bool {
237 238 239 240 241 242 243 244 245
	configFilename, err := config.Filename(path)
	if err != nil {
		return false
	}
	if !util.FileExists(configFilename) {
		return false
	}
	return true
}
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260

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