package repo import ( "encoding/json" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "sync" "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/namespace" badger "github.com/ipfs/go-ds-badger2" fslock "github.com/ipfs/go-fs-lock" logging "github.com/ipfs/go-log/v2" "github.com/mitchellh/go-homedir" "github.com/multiformats/go-base32" "github.com/multiformats/go-multiaddr" "golang.org/x/xerrors" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/node/config" ) const ( fsAPI = "api" fsAPIToken = "token" fsConfig = "config.toml" fsStorageConfig = "storage.json" fsDatastore = "datastore" fsLock = "repo.lock" fsKeystore = "keystore" ) type RepoType int const ( _ = iota // Default is invalid FullNode RepoType = iota StorageMiner ) func defConfForType(t RepoType) interface{} { switch t { case FullNode: return config.DefaultFullNode() case StorageMiner: return config.DefaultStorageMiner() default: panic(fmt.Sprintf("unknown RepoType(%d)", int(t))) } } var log = logging.Logger("repo") var ErrRepoExists = xerrors.New("repo exists") // FsRepo is struct for repo, use NewFS to create type FsRepo struct { path string repoType RepoType } var _ Repo = &FsRepo{} // NewFS creates a repo instance based on a path on file system func NewFS(path string) (*FsRepo, error) { path, err := homedir.Expand(path) if err != nil { return nil, err } return &FsRepo{ path: path, }, nil } func (fsr *FsRepo) Exists() (bool, error) { _, err := os.Stat(filepath.Join(fsr.path, fsDatastore)) notexist := os.IsNotExist(err) if notexist { err = nil } return !notexist, err } func (fsr *FsRepo) Init(t RepoType) error { exist, err := fsr.Exists() if err != nil { return err } if exist { return nil } log.Infof("Initializing repo at '%s'", fsr.path) err = os.Mkdir(fsr.path, 0755) //nolint: gosec if err != nil && !os.IsExist(err) { return err } if err := fsr.initConfig(t); err != nil { return xerrors.Errorf("init config: %w", err) } return fsr.initKeystore() } func (fsr *FsRepo) initConfig(t RepoType) error { cfgP := filepath.Join(fsr.path, fsConfig) _, err := os.Stat(cfgP) if err == nil { // exists return nil } else if !os.IsNotExist(err) { return err } c, err := os.Create(cfgP) if err != nil { return err } comm, err := config.ConfigComment(defConfForType(t)) if err != nil { return xerrors.Errorf("comment: %w", err) } _, err = c.Write(comm) if err != nil { return xerrors.Errorf("write config: %w", err) } if err := c.Close(); err != nil { return xerrors.Errorf("close config: %w", err) } return nil } func (fsr *FsRepo) initKeystore() error { kstorePath := filepath.Join(fsr.path, fsKeystore) if _, err := os.Stat(kstorePath); err == nil { return ErrRepoExists } else if !os.IsNotExist(err) { return err } return os.Mkdir(kstorePath, 0700) } // APIEndpoint returns endpoint of API in this repo func (fsr *FsRepo) APIEndpoint() (multiaddr.Multiaddr, error) { p := filepath.Join(fsr.path, fsAPI) f, err := os.Open(p) if os.IsNotExist(err) { return nil, ErrNoAPIEndpoint } else if err != nil { return nil, err } defer f.Close() //nolint: errcheck // Read only op data, err := ioutil.ReadAll(f) if err != nil { return nil, xerrors.Errorf("failed to read %q: %w", p, err) } strma := string(data) strma = strings.TrimSpace(strma) apima, err := multiaddr.NewMultiaddr(strma) if err != nil { return nil, err } return apima, nil } func (fsr *FsRepo) APIToken() ([]byte, error) { p := filepath.Join(fsr.path, fsAPIToken) f, err := os.Open(p) if os.IsNotExist(err) { return nil, ErrNoAPIEndpoint } else if err != nil { return nil, err } defer f.Close() //nolint: errcheck // Read only op return ioutil.ReadAll(f) } // Lock acquires exclusive lock on this repo func (fsr *FsRepo) Lock(repoType RepoType) (LockedRepo, error) { locked, err := fslock.Locked(fsr.path, fsLock) if err != nil { return nil, xerrors.Errorf("could not check lock status: %w", err) } if locked { return nil, ErrRepoAlreadyLocked } closer, err := fslock.Lock(fsr.path, fsLock) if err != nil { return nil, xerrors.Errorf("could not lock the repo: %w", err) } return &fsLockedRepo{ path: fsr.path, repoType: repoType, closer: closer, }, nil } type fsLockedRepo struct { path string repoType RepoType closer io.Closer ds datastore.Batching dsErr error dsOnce sync.Once } func (fsr *fsLockedRepo) Path() string { return fsr.path } func (fsr *fsLockedRepo) Close() error { err := os.Remove(fsr.join(fsAPI)) if err != nil && !os.IsNotExist(err) { return xerrors.Errorf("could not remove API file: %w", err) } if fsr.ds != nil { if err := fsr.ds.Close(); err != nil { return xerrors.Errorf("could not close datastore: %w", err) } } err = fsr.closer.Close() fsr.closer = nil return err } // join joins path elements with fsr.path func (fsr *fsLockedRepo) join(paths ...string) string { return filepath.Join(append([]string{fsr.path}, paths...)...) } func (fsr *fsLockedRepo) stillValid() error { if fsr.closer == nil { return ErrClosedRepo } return nil } func (fsr *fsLockedRepo) Datastore(ns string) (datastore.Batching, error) { fsr.dsOnce.Do(func() { opts := badger.DefaultOptions opts.Truncate = true fsr.ds, fsr.dsErr = badger.NewDatastore(fsr.join(fsDatastore), &opts) /*if fsr.dsErr == nil { fsr.ds = datastore.NewLogDatastore(fsr.ds, "fsrepo") }*/ }) if fsr.dsErr != nil { return nil, fsr.dsErr } return namespace.Wrap(fsr.ds, datastore.NewKey(ns)), nil } func (fsr *fsLockedRepo) Config() (interface{}, error) { if err := fsr.stillValid(); err != nil { return nil, err } return config.FromFile(fsr.join(fsConfig), defConfForType(fsr.repoType)) } func (fsr *fsLockedRepo) GetStorage() (config.StorageConfig, error) { c, err := config.StorageFromFile(fsr.join(fsStorageConfig), nil) if err != nil { return config.StorageConfig{}, err } return *c, nil } func (fsr *fsLockedRepo) SetStorage(c config.StorageConfig) error { return config.WriteStorageFile(fsr.join(fsStorageConfig), c) } func (fsr *fsLockedRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error { if err := fsr.stillValid(); err != nil { return err } return ioutil.WriteFile(fsr.join(fsAPI), []byte(ma.String()), 0644) } func (fsr *fsLockedRepo) SetAPIToken(token []byte) error { if err := fsr.stillValid(); err != nil { return err } return ioutil.WriteFile(fsr.join(fsAPIToken), token, 0600) } func (fsr *fsLockedRepo) KeyStore() (types.KeyStore, error) { if err := fsr.stillValid(); err != nil { return nil, err } return fsr, nil } var kstrPermissionMsg = "permissions of key: '%s' are too relaxed, " + "required: 0600, got: %#o" // List lists all the keys stored in the KeyStore func (fsr *fsLockedRepo) List() ([]string, error) { if err := fsr.stillValid(); err != nil { return nil, err } kstorePath := fsr.join(fsKeystore) dir, err := os.Open(kstorePath) if err != nil { return nil, xerrors.Errorf("opening dir to list keystore: %w", err) } files, err := dir.Readdir(-1) if err != nil { return nil, xerrors.Errorf("reading keystore dir: %w", err) } keys := make([]string, 0, len(files)) for _, f := range files { if f.Mode()&0077 != 0 { return nil, xerrors.Errorf(kstrPermissionMsg, f.Name(), f.Mode()) } name, err := base32.RawStdEncoding.DecodeString(f.Name()) if err != nil { return nil, xerrors.Errorf("decoding key: '%s': %w", f.Name(), err) } keys = append(keys, string(name)) } return keys, nil } // Get gets a key out of keystore and returns types.KeyInfo coresponding to named key func (fsr *fsLockedRepo) Get(name string) (types.KeyInfo, error) { if err := fsr.stillValid(); err != nil { return types.KeyInfo{}, err } encName := base32.RawStdEncoding.EncodeToString([]byte(name)) keyPath := fsr.join(fsKeystore, encName) fstat, err := os.Stat(keyPath) if os.IsNotExist(err) { return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, types.ErrKeyInfoNotFound) } else if err != nil { return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, err) } if fstat.Mode()&0077 != 0 { return types.KeyInfo{}, xerrors.Errorf(kstrPermissionMsg, name, fstat.Mode()) } file, err := os.Open(keyPath) if err != nil { return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, err) } defer file.Close() //nolint: errcheck // read only op data, err := ioutil.ReadAll(file) if err != nil { return types.KeyInfo{}, xerrors.Errorf("reading key '%s': %w", name, err) } var res types.KeyInfo err = json.Unmarshal(data, &res) if err != nil { return types.KeyInfo{}, xerrors.Errorf("decoding key '%s': %w", name, err) } return res, nil } // Put saves key info under given name func (fsr *fsLockedRepo) Put(name string, info types.KeyInfo) error { if err := fsr.stillValid(); err != nil { return err } encName := base32.RawStdEncoding.EncodeToString([]byte(name)) keyPath := fsr.join(fsKeystore, encName) _, err := os.Stat(keyPath) if err == nil { return xerrors.Errorf("checking key before put '%s': %w", name, types.ErrKeyExists) } else if !os.IsNotExist(err) { return xerrors.Errorf("checking key before put '%s': %w", name, err) } keyData, err := json.Marshal(info) if err != nil { return xerrors.Errorf("encoding key '%s': %w", name, err) } err = ioutil.WriteFile(keyPath, keyData, 0600) if err != nil { return xerrors.Errorf("writing key '%s': %w", name, err) } return nil } func (fsr *fsLockedRepo) Delete(name string) error { if err := fsr.stillValid(); err != nil { return err } encName := base32.RawStdEncoding.EncodeToString([]byte(name)) keyPath := fsr.join(fsKeystore, encName) _, err := os.Stat(keyPath) if os.IsNotExist(err) { return xerrors.Errorf("checking key before delete '%s': %w", name, types.ErrKeyInfoNotFound) } else if err != nil { return xerrors.Errorf("checking key before delete '%s': %w", name, err) } err = os.Remove(keyPath) if err != nil { return xerrors.Errorf("deleting key '%s': %w", name, err) } return nil }