master
go 793 lines 21.9 KB
Raw
1 package fsrepo
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "net"
9 "os"
10 "path/filepath"
11 "strings"
12 "sync"
13 "time"
14
15 filestore "github.com/ipfs/boxo/filestore"
16 keystore "github.com/ipfs/boxo/keystore"
17 version "github.com/ipfs/kubo"
18 repo "github.com/ipfs/kubo/repo"
19 "github.com/ipfs/kubo/repo/common"
20 rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
21
22 ds "github.com/ipfs/go-datastore"
23 measure "github.com/ipfs/go-ds-measure"
24 lockfile "github.com/ipfs/go-fs-lock"
25 logging "github.com/ipfs/go-log/v2"
26 config "github.com/ipfs/kubo/config"
27 serialize "github.com/ipfs/kubo/config/serialize"
28 "github.com/ipfs/kubo/misc/fsutil"
29 "github.com/ipfs/kubo/repo/fsrepo/migrations"
30 ma "github.com/multiformats/go-multiaddr"
31 )
32
33 // LockFile is the filename of the repo lock, relative to config dir
34 // TODO rename repo lock and hide name.
35 const LockFile = "repo.lock"
36
37 var log = logging.Logger("fsrepo")
38
39 // RepoVersion is the version number that we are currently expecting to see.
40 var RepoVersion = version.RepoVersion
41
42 var migrationInstructions = `See https://github.com/ipfs/fs-repo-migrations/blob/master/run.md
43 Sorry for the inconvenience. In the future, these will run automatically.`
44
45 var programTooLowMessage = `Your programs version (%d) is lower than your repos (%d).
46 Please update ipfs to a version that supports the existing repo, or run
47 a migration in reverse.
48
49 See https://github.com/ipfs/fs-repo-migrations/blob/master/run.md for details.`
50
51 var (
52 ErrNoVersion = errors.New("no version file found, please run 0-to-1 migration tool.\n" + migrationInstructions)
53 ErrOldRepo = errors.New("ipfs repo found in old '~/.go-ipfs' location, please run migration tool.\n" + migrationInstructions)
54 ErrNeedMigration = errors.New("ipfs repo needs migration, please run migration tool.\n" + migrationInstructions)
55 )
56
57 type NoRepoError struct {
58 Path string
59 }
60
61 var _ error = NoRepoError{}
62
63 func (err NoRepoError) Error() string {
64 return fmt.Sprintf("no IPFS repo found in %s.\nplease run: 'ipfs init'", err.Path)
65 }
66
67 const (
68 apiFile = "api"
69 gatewayFile = "gateway"
70 swarmKeyFile = "swarm.key"
71 )
72
73 const specFn = "datastore_spec"
74
75 var (
76
77 // packageLock must be held to while performing any operation that modifies an
78 // FSRepo's state field. This includes Init, Open, Close, and Remove.
79 packageLock sync.Mutex
80
81 // onlyOne keeps track of open FSRepo instances.
82 //
83 // TODO: once command Context / Repo integration is cleaned up,
84 // this can be removed. Right now, this makes ConfigCmd.Run
85 // function try to open the repo twice:
86 //
87 // $ ipfs daemon &
88 // $ ipfs config foo
89 //
90 // The reason for the above is that in standalone mode without the
91 // daemon, `ipfs config` tries to save work by not building the
92 // full IpfsNode, but accessing the Repo directly.
93 onlyOne repo.OnlyOne
94 )
95
96 // FSRepo represents an IPFS FileSystem Repo. It is safe for use by multiple
97 // callers.
98 type FSRepo struct {
99 // has Close been called already
100 closed bool
101 // path is the file-system path
102 path string
103 // Path to the configuration file that may or may not be inside the FSRepo
104 // path (see config.Filename for more details).
105 configFilePath string
106 // lockfile is the file system lock to prevent others from opening
107 // the same fsrepo path concurrently
108 lockfile io.Closer
109 config *config.Config
110 userResourceOverrides rcmgr.PartialLimitConfig
111 ds repo.Datastore
112 keystore keystore.Keystore
113 filemgr *filestore.FileManager
114 }
115
116 var _ repo.Repo = (*FSRepo)(nil)
117
118 // Open the FSRepo at path. Returns an error if the repo is not
119 // initialized.
120 func Open(repoPath string) (repo.Repo, error) {
121 fn := func() (repo.Repo, error) {
122 return open(repoPath, "")
123 }
124 return onlyOne.Open(repoPath, fn)
125 }
126
127 // OpenWithUserConfig is the equivalent to the Open function above but with the
128 // option to set the configuration file path instead of using the default.
129 func OpenWithUserConfig(repoPath string, userConfigFilePath string) (repo.Repo, error) {
130 fn := func() (repo.Repo, error) {
131 return open(repoPath, userConfigFilePath)
132 }
133 return onlyOne.Open(repoPath, fn)
134 }
135
136 func open(repoPath string, userConfigFilePath string) (repo.Repo, error) {
137 packageLock.Lock()
138 defer packageLock.Unlock()
139
140 r, err := newFSRepo(repoPath, userConfigFilePath)
141 if err != nil {
142 return nil, err
143 }
144
145 // Check if its initialized
146 if err := checkInitialized(r.path); err != nil {
147 return nil, err
148 }
149
150 text := os.Getenv("IPFS_WAIT_REPO_LOCK")
151 if text != "" {
152 var lockWaitTime time.Duration
153 lockWaitTime, err = time.ParseDuration(text)
154 if err != nil {
155 log.Errorw("Cannot parse value of IPFS_WAIT_REPO_LOCK as duration, not waiting for repo lock", "err", err, "value", text)
156 r.lockfile, err = lockfile.Lock(r.path, LockFile)
157 } else if lockWaitTime <= 0 {
158 r.lockfile, err = lockfile.WaitLock(context.Background(), r.path, LockFile)
159 } else {
160 ctx, cancel := context.WithTimeout(context.Background(), lockWaitTime)
161 r.lockfile, err = lockfile.WaitLock(ctx, r.path, LockFile)
162 cancel()
163 }
164 } else {
165 r.lockfile, err = lockfile.Lock(r.path, LockFile)
166 }
167 if err != nil {
168 return nil, err
169 }
170 keepLocked := false
171 defer func() {
172 // unlock on error, leave it locked on success
173 if !keepLocked {
174 r.lockfile.Close()
175 }
176 }()
177
178 // Check version, and error out if not matching
179 ver, err := migrations.RepoVersion(r.path)
180 if err != nil {
181 if os.IsNotExist(err) {
182 return nil, ErrNoVersion
183 }
184 return nil, err
185 }
186
187 if RepoVersion > ver {
188 return nil, ErrNeedMigration
189 } else if ver > RepoVersion {
190 // program version too low for existing repo
191 return nil, fmt.Errorf(programTooLowMessage, RepoVersion, ver)
192 }
193
194 // check repo path, then check all constituent parts.
195 if err := fsutil.DirWritable(r.path); err != nil {
196 return nil, err
197 }
198
199 if err := r.openConfig(); err != nil {
200 return nil, err
201 }
202
203 if err := r.openUserResourceOverrides(); err != nil {
204 return nil, err
205 }
206
207 if err := r.openDatastore(); err != nil {
208 return nil, err
209 }
210
211 if err := r.openKeystore(); err != nil {
212 return nil, err
213 }
214
215 if r.config.Experimental.FilestoreEnabled || r.config.Experimental.UrlstoreEnabled {
216 r.filemgr = filestore.NewFileManager(r.ds, filepath.Dir(r.path))
217 r.filemgr.AllowFiles = r.config.Experimental.FilestoreEnabled
218 r.filemgr.AllowUrls = r.config.Experimental.UrlstoreEnabled
219 }
220
221 keepLocked = true
222 return r, nil
223 }
224
225 func newFSRepo(rpath string, userConfigFilePath string) (*FSRepo, error) {
226 expPath, err := fsutil.ExpandHome(filepath.Clean(rpath))
227 if err != nil {
228 return nil, err
229 }
230
231 configFilePath, err := config.Filename(rpath, userConfigFilePath)
232 if err != nil {
233 // FIXME: Personalize this when the user config path is "".
234 return nil, fmt.Errorf("finding config filepath from repo %s and user config %s: %w",
235 rpath, userConfigFilePath, err)
236 }
237 return &FSRepo{path: expPath, configFilePath: configFilePath}, nil
238 }
239
240 func checkInitialized(path string) error {
241 if !isInitializedUnsynced(path) {
242 alt := strings.Replace(path, ".ipfs", ".go-ipfs", 1)
243 if isInitializedUnsynced(alt) {
244 return ErrOldRepo
245 }
246 return NoRepoError{Path: path}
247 }
248 return nil
249 }
250
251 // configIsInitialized returns true if the repo is initialized at
252 // provided |path|.
253 func configIsInitialized(path string) bool {
254 configFilename, err := config.Filename(path, "")
255 if err != nil {
256 return false
257 }
258 if !fsutil.FileExists(configFilename) {
259 return false
260 }
261 return true
262 }
263
264 func initConfig(path string, conf *config.Config) error {
265 if configIsInitialized(path) {
266 return nil
267 }
268 configFilename, err := config.Filename(path, "")
269 if err != nil {
270 return err
271 }
272 // initialization is the one time when it's okay to write to the config
273 // without reading the config from disk and merging any user-provided keys
274 // that may exist.
275 if err := serialize.WriteConfigFile(configFilename, conf); err != nil {
276 return err
277 }
278
279 return nil
280 }
281
282 func initSpec(path string, conf map[string]any) error {
283 fn, err := config.Path(path, specFn)
284 if err != nil {
285 return err
286 }
287
288 if fsutil.FileExists(fn) {
289 return nil
290 }
291
292 dsc, err := AnyDatastoreConfig(conf)
293 if err != nil {
294 return err
295 }
296 bytes := dsc.DiskSpec().Bytes()
297
298 return os.WriteFile(fn, bytes, 0o600)
299 }
300
301 // Init initializes a new FSRepo at the given path with the provided config.
302 // TODO add support for custom datastores.
303 func Init(repoPath string, conf *config.Config) error {
304 // packageLock must be held to ensure that the repo is not initialized more
305 // than once.
306 packageLock.Lock()
307 defer packageLock.Unlock()
308
309 if isInitializedUnsynced(repoPath) {
310 return nil
311 }
312
313 if err := initConfig(repoPath, conf); err != nil {
314 return err
315 }
316
317 if err := initSpec(repoPath, conf.Datastore.Spec); err != nil {
318 return err
319 }
320
321 if err := migrations.WriteRepoVersion(repoPath, RepoVersion); err != nil {
322 return err
323 }
324
325 return nil
326 }
327
328 // LockedByOtherProcess returns true if the FSRepo is locked by another
329 // process. If true, then the repo cannot be opened by this process.
330 func LockedByOtherProcess(repoPath string) (bool, error) {
331 repoPath = filepath.Clean(repoPath)
332 locked, err := lockfile.Locked(repoPath, LockFile)
333 if locked {
334 log.Debugf("(%t)<->Lock is held at %s", locked, repoPath)
335 }
336 return locked, err
337 }
338
339 // APIAddr returns the registered API addr, according to the api file
340 // in the fsrepo. This is a concurrent operation, meaning that any
341 // process may read this file. modifying this file, therefore, should
342 // use "mv" to replace the whole file and avoid interleaved read/writes.
343 func APIAddr(repoPath string) (ma.Multiaddr, error) {
344 repoPath = filepath.Clean(repoPath)
345 apiFilePath := filepath.Join(repoPath, apiFile)
346
347 // if there is no file, assume there is no api addr.
348 f, err := os.Open(apiFilePath)
349 if err != nil {
350 if os.IsNotExist(err) {
351 return nil, repo.ErrApiNotRunning
352 }
353 return nil, err
354 }
355 defer f.Close()
356
357 // read up to 2048 bytes. io.ReadAll is a vulnerability, as
358 // someone could hose the process by putting a massive file there.
359 //
360 // NOTE(@stebalien): @jbenet probably wasn't thinking straight when he
361 // wrote that comment but I'm leaving the limit here in case there was
362 // some hidden wisdom. However, I'm fixing it such that:
363 // 1. We don't read too little.
364 // 2. We don't truncate and succeed.
365 buf, err := io.ReadAll(io.LimitReader(f, 2048))
366 if err != nil {
367 return nil, err
368 }
369 if len(buf) == 2048 {
370 return nil, fmt.Errorf("API file too large, must be <2048 bytes long: %s", apiFilePath)
371 }
372
373 s := string(buf)
374 s = strings.TrimSpace(s)
375 return ma.NewMultiaddr(s)
376 }
377
378 func (r *FSRepo) Keystore() keystore.Keystore {
379 return r.keystore
380 }
381
382 func (r *FSRepo) Path() string {
383 return r.path
384 }
385
386 // SetAPIAddr writes the API Addr to the /api file.
387 func (r *FSRepo) SetAPIAddr(addr ma.Multiaddr) error {
388 // Create a temp file to write the address, so that we don't leave empty file when the
389 // program crashes after creating the file.
390 f, err := os.Create(filepath.Join(r.path, "."+apiFile+".tmp"))
391 if err != nil {
392 return err
393 }
394
395 if _, err = f.WriteString(addr.String()); err != nil {
396 f.Close()
397 return err
398 }
399 if err = f.Close(); err != nil {
400 return err
401 }
402
403 // Atomically rename the temp file to the correct file name.
404 if err = os.Rename(filepath.Join(r.path, "."+apiFile+".tmp"), filepath.Join(r.path,
405 apiFile)); err == nil {
406 return nil
407 }
408 // Remove the temp file when rename return error
409 if err1 := os.Remove(filepath.Join(r.path, "."+apiFile+".tmp")); err1 != nil {
410 return fmt.Errorf("file Rename error: %s, file remove error: %s", err.Error(),
411 err1.Error())
412 }
413 return err
414 }
415
416 // SetGatewayAddr writes the Gateway Addr to the /gateway file.
417 func (r *FSRepo) SetGatewayAddr(addr net.Addr) error {
418 // Create a temp file to write the address, so that we don't leave empty file when the
419 // program crashes after creating the file.
420 tmpPath := filepath.Join(r.path, "."+gatewayFile+".tmp")
421 f, err := os.Create(tmpPath)
422 if err != nil {
423 return err
424 }
425 var good bool
426 // Silently remove as worst last case with defers.
427 defer func() {
428 if !good {
429 os.Remove(tmpPath)
430 }
431 }()
432 defer f.Close()
433
434 if _, err := fmt.Fprintf(f, "http://%s", addr.String()); err != nil {
435 return err
436 }
437 if err := f.Close(); err != nil {
438 return err
439 }
440
441 // Atomically rename the temp file to the correct file name.
442 err = os.Rename(tmpPath, filepath.Join(r.path, gatewayFile))
443 good = err == nil
444 if good {
445 return nil
446 }
447 // Remove the temp file when rename return error
448 if err1 := os.Remove(tmpPath); err1 != nil {
449 return fmt.Errorf("file Rename error: %w, file remove error: %s", err, err1.Error())
450 }
451 return err
452 }
453
454 // openConfig returns an error if the config file is not present.
455 func (r *FSRepo) openConfig() error {
456 conf, err := serialize.Load(r.configFilePath)
457 if err != nil {
458 return err
459 }
460 r.config = conf
461 return nil
462 }
463
464 // openUserResourceOverrides will remove all overrides if the file is not present.
465 // It will error if the decoding fails.
466 func (r *FSRepo) openUserResourceOverrides() error {
467 // This filepath is documented in docs/libp2p-resource-management.md and be kept in sync.
468 err := serialize.ReadConfigFile(filepath.Join(r.path, "libp2p-resource-limit-overrides.json"), &r.userResourceOverrides)
469 if errors.Is(err, serialize.ErrNotInitialized) {
470 err = nil
471 }
472 return err
473 }
474
475 func (r *FSRepo) openKeystore() error {
476 ksp := filepath.Join(r.path, "keystore")
477 ks, err := keystore.NewFSKeystore(ksp)
478 if err != nil {
479 return err
480 }
481
482 r.keystore = ks
483
484 return nil
485 }
486
487 // openDatastore returns an error if the config file is not present.
488 func (r *FSRepo) openDatastore() error {
489 if r.config.Datastore.Type != "" || r.config.Datastore.Path != "" {
490 return fmt.Errorf("old style datatstore config detected")
491 } else if r.config.Datastore.Spec == nil {
492 return fmt.Errorf("required Datastore.Spec entry missing from config file")
493 }
494 if r.config.Datastore.NoSync {
495 log.Warn("NoSync is now deprecated in favor of datastore specific settings. If you want to disable fsync on flatfs set 'sync' to false. See https://github.com/ipfs/kubo/blob/master/docs/datastores.md#flatfs.")
496 }
497
498 dsc, err := AnyDatastoreConfig(r.config.Datastore.Spec)
499 if err != nil {
500 return err
501 }
502 spec := dsc.DiskSpec()
503
504 oldSpec, err := r.readSpec()
505 if err != nil {
506 return err
507 }
508 if oldSpec != spec.String() {
509 return fmt.Errorf("datastore configuration of '%s' does not match what is on disk '%s'",
510 oldSpec, spec.String())
511 }
512
513 d, err := dsc.Create(r.path)
514 if err != nil {
515 return err
516 }
517 r.ds = d
518
519 // Wrap it with metrics gathering
520 prefix := "ipfs.fsrepo.datastore"
521 r.ds = measure.New(prefix, r.ds)
522
523 return nil
524 }
525
526 func (r *FSRepo) readSpec() (string, error) {
527 fn, err := config.Path(r.path, specFn)
528 if err != nil {
529 return "", err
530 }
531 b, err := os.ReadFile(fn)
532 if err != nil {
533 return "", err
534 }
535 return strings.TrimSpace(string(b)), nil
536 }
537
538 // Close closes the FSRepo, releasing held resources.
539 func (r *FSRepo) Close() error {
540 packageLock.Lock()
541 defer packageLock.Unlock()
542
543 if r.closed {
544 return errors.New("repo is closed")
545 }
546
547 err := os.Remove(filepath.Join(r.path, apiFile))
548 if err != nil && !os.IsNotExist(err) {
549 log.Warn("error removing api file: ", err)
550 }
551
552 err = os.Remove(filepath.Join(r.path, gatewayFile))
553 if err != nil && !os.IsNotExist(err) {
554 log.Warn("error removing gateway file: ", err)
555 }
556
557 if err := r.ds.Close(); err != nil {
558 return err
559 }
560
561 // This code existed in the previous versions, but
562 // EventlogComponent.Close was never called. Preserving here
563 // pending further discussion.
564 //
565 // TODO It isn't part of the current contract, but callers may like for us
566 // to disable logging once the component is closed.
567 // logging.Configure(logging.Output(os.Stderr))
568
569 r.closed = true
570 return r.lockfile.Close()
571 }
572
573 // Config the current config. This function DOES NOT copy the config. The caller
574 // MUST NOT modify it without first calling `Clone`.
575 //
576 // Result when not Open is undefined. The method may panic if it pleases.
577 func (r *FSRepo) Config() (*config.Config, error) {
578 // It is not necessary to hold the package lock since the repo is in an
579 // opened state. The package lock is _not_ meant to ensure that the repo is
580 // thread-safe. The package lock is only meant to guard against removal and
581 // coordinate the lockfile. However, we provide thread-safety to keep
582 // things simple.
583 packageLock.Lock()
584 defer packageLock.Unlock()
585
586 if r.closed {
587 return nil, errors.New("cannot access config, repo not open")
588 }
589 return r.config, nil
590 }
591
592 func (r *FSRepo) UserResourceOverrides() (rcmgr.PartialLimitConfig, error) {
593 // It is not necessary to hold the package lock since the repo is in an
594 // opened state. The package lock is _not_ meant to ensure that the repo is
595 // thread-safe. The package lock is only meant to guard against removal and
596 // coordinate the lockfile. However, we provide thread-safety to keep
597 // things simple.
598 packageLock.Lock()
599 defer packageLock.Unlock()
600
601 if r.closed {
602 return rcmgr.PartialLimitConfig{}, errors.New("cannot access config, repo not open")
603 }
604 return r.userResourceOverrides, nil
605 }
606
607 func (r *FSRepo) FileManager() *filestore.FileManager {
608 return r.filemgr
609 }
610
611 func (r *FSRepo) BackupConfig(prefix string) (string, error) {
612 temp, err := os.CreateTemp(r.path, "config-"+prefix)
613 if err != nil {
614 return "", err
615 }
616 defer temp.Close()
617
618 orig, err := os.OpenFile(r.configFilePath, os.O_RDONLY, 0o600)
619 if err != nil {
620 return "", err
621 }
622 defer orig.Close()
623
624 _, err = io.Copy(temp, orig)
625 if err != nil {
626 return "", err
627 }
628
629 return orig.Name(), nil
630 }
631
632 // SetConfig updates the FSRepo's config. The user must not modify the config
633 // object after calling this method.
634 // FIXME: There is an inherent contradiction with storing non-user-generated
635 // Go config.Config structures as user-generated JSON nested maps. This is
636 // evidenced by the issue of `omitempty` property of fields that aren't defined
637 // by the user and Go still needs to initialize them to its default (which
638 // is not reflected in the repo's config file, see
639 // https://github.com/ipfs/kubo/issues/8088 for more details).
640 // In general we should call this API with a JSON nested maps as argument
641 // (`map[string]interface{}`). Many calls to this function are forced to
642 // synthesize the config.Config struct from their available JSON map just to
643 // satisfy this (causing incompatibilities like the `omitempty` one above).
644 // We need to comb SetConfig calls and replace them when possible with a
645 // JSON map variant.
646 func (r *FSRepo) SetConfig(updated *config.Config) error {
647 // packageLock is held to provide thread-safety.
648 packageLock.Lock()
649 defer packageLock.Unlock()
650
651 // to avoid clobbering user-provided keys, must read the config from disk
652 // as a map, write the updated struct values to the map and write the map
653 // to disk.
654 var mapconf map[string]any
655 if err := serialize.ReadConfigFile(r.configFilePath, &mapconf); err != nil {
656 return err
657 }
658 m, err := config.ToMap(updated)
659 if err != nil {
660 return err
661 }
662 mergedMap := common.MapMergeDeep(mapconf, m)
663 if err := serialize.WriteConfigFile(r.configFilePath, mergedMap); err != nil {
664 return err
665 }
666 // Do not use `*r.config = ...`. This will modify the *shared* config
667 // returned by `r.Config`.
668 r.config = updated
669 return nil
670 }
671
672 // GetConfigKey retrieves only the value of a particular key.
673 func (r *FSRepo) GetConfigKey(key string) (any, error) {
674 packageLock.Lock()
675 defer packageLock.Unlock()
676
677 if r.closed {
678 return nil, errors.New("repo is closed")
679 }
680
681 var cfg map[string]any
682 if err := serialize.ReadConfigFile(r.configFilePath, &cfg); err != nil {
683 return nil, err
684 }
685 return common.MapGetKV(cfg, key)
686 }
687
688 // SetConfigKey writes the value of a particular key.
689 func (r *FSRepo) SetConfigKey(key string, value any) error {
690 packageLock.Lock()
691 defer packageLock.Unlock()
692
693 if r.closed {
694 return errors.New("repo is closed")
695 }
696
697 // Validate the key's presence in the config structure.
698 err := config.CheckKey(key)
699 if err != nil {
700 return err
701 }
702
703 // Load into a map so we don't end up writing any additional defaults to the config file.
704 var mapconf map[string]any
705 if err := serialize.ReadConfigFile(r.configFilePath, &mapconf); err != nil {
706 return err
707 }
708
709 // Load private key to guard against it being overwritten.
710 // NOTE: this is a temporary measure to secure this field until we move
711 // keys out of the config file.
712 pkval, err := common.MapGetKV(mapconf, config.PrivKeySelector)
713 if err != nil {
714 return err
715 }
716
717 // Set the key in the map.
718 if err := common.MapSetKV(mapconf, key, value); err != nil {
719 return err
720 }
721
722 // replace private key, in case it was overwritten.
723 if err := common.MapSetKV(mapconf, config.PrivKeySelector, pkval); err != nil {
724 return err
725 }
726
727 // This step doubles as to validate the map against the struct
728 // before serialization
729 conf, err := config.FromMap(mapconf)
730 if err != nil {
731 return err
732 }
733 r.config = conf
734
735 if err := serialize.WriteConfigFile(r.configFilePath, mapconf); err != nil {
736 return err
737 }
738
739 return nil
740 }
741
742 // Datastore returns a repo-owned datastore. If FSRepo is Closed, return value
743 // is undefined.
744 func (r *FSRepo) Datastore() repo.Datastore {
745 packageLock.Lock()
746 d := r.ds
747 packageLock.Unlock()
748 return d
749 }
750
751 // GetStorageUsage computes the storage space taken by the repo in bytes.
752 func (r *FSRepo) GetStorageUsage(ctx context.Context) (uint64, error) {
753 return ds.DiskUsage(ctx, r.Datastore())
754 }
755
756 func (r *FSRepo) SwarmKey() ([]byte, error) {
757 repoPath := filepath.Clean(r.path)
758 spath := filepath.Join(repoPath, swarmKeyFile)
759
760 f, err := os.Open(spath)
761 if err != nil {
762 if os.IsNotExist(err) {
763 err = nil
764 }
765 return nil, err
766 }
767 defer f.Close()
768
769 return io.ReadAll(f)
770 }
771
772 var (
773 _ io.Closer = &FSRepo{}
774 _ repo.Repo = &FSRepo{}
775 )
776
777 // IsInitialized returns true if the repo is initialized at provided |path|.
778 func IsInitialized(path string) bool {
779 // packageLock is held to ensure that another caller doesn't attempt to
780 // Init or Remove the repo while this call is in progress.
781 packageLock.Lock()
782 defer packageLock.Unlock()
783
784 return isInitializedUnsynced(path)
785 }
786
787 // private methods below this point. NB: packageLock must held by caller.
788
789 // isInitializedUnsynced reports whether the repo is initialized. Caller must
790 // hold the packageLock.
791 func isInitializedUnsynced(repoPath string) bool {
792 return configIsInitialized(repoPath)
793 }