Skip to content
This repository was archived by the owner on May 15, 2024. It is now read-only.

Commit f9281a2

Browse files
authored
Fail upload if it uses more then available space (#220)
Before upload, check that more then the disk has more than the minimum free space. Upload up to the size limit that would exceed the available space. Fixes #179
1 parent c504ff1 commit f9281a2

File tree

10 files changed

+170
-21
lines changed

10 files changed

+170
-21
lines changed

blob/blob.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
var (
15-
ErrBlobTooLarge = errors.New("blob size exceeds the maximum allowed")
16-
ErrBlobNotFound = errors.New("no blob is found with given ID")
15+
ErrBlobNotFound = errors.New("no blob is found with given ID")
16+
ErrBlobTooLarge = errors.New("blob size exceeds the maximum allowed")
17+
ErrNotEnoughSpace = errors.New("insufficient local storage space remaining")
1718
)
1819

1920
var (
@@ -44,7 +45,7 @@ type (
4445
Status string
4546
}
4647
Store interface {
47-
Put(context.Context, io.ReadCloser) (*Descriptor, error)
48+
Put(context.Context, io.Reader) (*Descriptor, error)
4849
Describe(context.Context, ID) (*Descriptor, error)
4950
Get(context.Context, ID) (io.ReadSeekCloser, error)
5051
}

blob/local_store.go

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package blob
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"io"
78
"os"
89
"path/filepath"
10+
11+
"github.com/gammazero/fsutil/disk"
912
)
1013

1114
var _ Store = (*LocalStore)(nil)
@@ -14,15 +17,18 @@ var _ Store = (*LocalStore)(nil)
1417
// Blobs are stored as flat files, named by their ID with .bin extension.
1518
// This store is used primarily for testing purposes.
1619
type LocalStore struct {
17-
dir string
20+
dir string
21+
minFreeSpace uint64
1822
}
1923

2024
// NewLocalStore instantiates a new LocalStore and uses the given dir as the place to store blobs.
2125
// Blobs are stored as flat files, named by their ID with .bin extension.
22-
func NewLocalStore(dir string) *LocalStore {
26+
func NewLocalStore(dir string, options ...Option) *LocalStore {
27+
opts := getOpts(options)
2328
logger.Debugw("Instantiated local store", "dir", dir)
2429
return &LocalStore{
25-
dir: dir,
30+
dir: dir,
31+
minFreeSpace: opts.minFreeSpace,
2632
}
2733
}
2834

@@ -32,25 +38,53 @@ func (l *LocalStore) Dir() string {
3238
}
3339

3440
// Put reads the given reader fully and stores its content in the store directory as flat files.
35-
// The reader content is first stored in a temporary directory and upon successful storage is moved to the store directory.
36-
// The Descriptor.ModificationTime is set to the modification date of the file that corresponds to the content.
37-
// The Descriptor.ID is randomly generated; see NewID.
38-
func (l *LocalStore) Put(_ context.Context, reader io.ReadCloser) (*Descriptor, error) {
39-
// TODO: add size limiter here and return ErrBlobTooLarge.
40-
id, err := NewID()
41-
if err != nil {
42-
return nil, err
41+
//
42+
// The reader content is first stored in a temporary directory and upon
43+
// successful storage is moved to the store directory. The
44+
// Descriptor.ModificationTime is set to the modification date of the file that
45+
// corresponds to the content. The Descriptor.ID is randomly generated; see
46+
// NewID.
47+
//
48+
// Before a blob is written, the minimum amount of free space must be available
49+
// on the local disk. If writing the blob consumes more then the available
50+
// space (free space - minimum free), then this results in an error.
51+
func (l *LocalStore) Put(_ context.Context, reader io.Reader) (*Descriptor, error) {
52+
var limit int64
53+
if l.minFreeSpace != 0 {
54+
usage, err := disk.Usage(l.dir)
55+
if err != nil {
56+
return nil, fmt.Errorf("cannot get disk usage: %w", err)
57+
}
58+
if usage.Free <= l.minFreeSpace {
59+
return nil, ErrNotEnoughSpace
60+
}
61+
62+
// Do not write more than the remaining storage - minimum free space.
63+
limit = int64(usage.Free - l.minFreeSpace)
64+
reader = io.LimitReader(reader, limit)
4365
}
66+
4467
dest, err := os.CreateTemp(l.dir, "motion_local_store_*.bin.temp")
4568
if err != nil {
4669
return nil, err
4770
}
4871
defer dest.Close()
72+
4973
written, err := io.Copy(dest, reader)
5074
if err != nil {
5175
os.Remove(dest.Name())
5276
return nil, err
5377
}
78+
79+
if limit != 0 && written == limit {
80+
os.Remove(dest.Name())
81+
return nil, ErrBlobTooLarge
82+
}
83+
84+
id, err := NewID()
85+
if err != nil {
86+
return nil, err
87+
}
5488
if err = os.Rename(dest.Name(), filepath.Join(l.dir, id.String()+".bin")); err != nil {
5589
return nil, err
5690
}

blob/local_store_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package blob_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"testing"
8+
9+
"github.com/filecoin-project/motion/blob"
10+
"github.com/gammazero/fsutil/disk"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestWriteOK(t *testing.T) {
15+
tmpDir := t.TempDir()
16+
17+
store := blob.NewLocalStore(tmpDir)
18+
buf := []byte("This is a test")
19+
readCloser := io.NopCloser(bytes.NewReader(buf))
20+
21+
desc, err := store.Put(context.Background(), readCloser)
22+
require.NoError(t, err)
23+
require.NotNil(t, desc)
24+
require.Equal(t, uint64(len(buf)), desc.Size)
25+
}
26+
27+
func TestInsufficientSpace(t *testing.T) {
28+
tmpDir := t.TempDir()
29+
usage, err := disk.Usage(tmpDir)
30+
require.NoError(t, err)
31+
32+
store := blob.NewLocalStore(tmpDir, blob.WithMinFreeSpace(int64(usage.Free+blob.Gib)))
33+
readCloser := io.NopCloser(bytes.NewReader([]byte("This is a test")))
34+
35+
_, err = store.Put(context.Background(), readCloser)
36+
require.ErrorIs(t, err, blob.ErrNotEnoughSpace)
37+
}
38+
39+
func TestWriteTooLarge(t *testing.T) {
40+
tmpDir := t.TempDir()
41+
usage, err := disk.Usage(tmpDir)
42+
require.NoError(t, err)
43+
44+
store := blob.NewLocalStore(tmpDir, blob.WithMinFreeSpace(int64(usage.Free-5*blob.Kib)))
45+
46+
buf := make([]byte, 32*blob.Kib)
47+
readCloser := io.NopCloser(bytes.NewReader(buf))
48+
49+
_, err = store.Put(context.Background(), readCloser)
50+
require.ErrorIs(t, err, blob.ErrBlobTooLarge)
51+
}

blob/option.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package blob
2+
3+
const (
4+
Kib = 1 << (10 * (iota + 1))
5+
Mib
6+
Gib
7+
)
8+
9+
const defaultMinFreeSpace = 64 * Mib
10+
11+
// config contains all options for LocalStore.
12+
type config struct {
13+
minFreeSpace uint64
14+
}
15+
16+
// Option is a function that sets a value in a config.
17+
type Option func(*config)
18+
19+
// getOpts creates a config and applies Options to it.
20+
func getOpts(options []Option) config {
21+
cfg := config{
22+
minFreeSpace: defaultMinFreeSpace,
23+
}
24+
for _, opt := range options {
25+
opt(&cfg)
26+
}
27+
return cfg
28+
}
29+
30+
// WithMinFreeSpace seta the minimum amount of free dist space that must remain
31+
// after writing a blob. If unset or 0 then defaultMinFreeSpace is used. If -1, then
32+
// no free space checks are performed.
33+
func WithMinFreeSpace(space int64) Option {
34+
return func(c *config) {
35+
if space == 0 {
36+
space = defaultMinFreeSpace
37+
} else if space < 0 {
38+
space = 0
39+
}
40+
c.minFreeSpace = uint64(space)
41+
}
42+
}

cmd/motion/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ func main() {
3737
Value: os.TempDir(),
3838
EnvVars: []string{"MOTION_STORE_DIR"},
3939
},
40+
&cli.Int64Flag{
41+
Name: "minFreeDiskSpace",
42+
Usage: "Minumum amount of free space that must remaio on disk after writing blob. Set -1 to disable checks.",
43+
DefaultText: "64 Mib",
44+
EnvVars: []string{"MIN_FREE_DISK_SPACE"},
45+
},
4046
&cli.StringFlag{
4147
Name: "walletKey",
4248
Usage: "Hex encoded private key for the wallet to use with motion",
@@ -205,6 +211,7 @@ func main() {
205211
singularity.WithScheduleDealNumber(cctx.Int("experimentalSingularityScheduleDealNumber")),
206212
singularity.WithVerifiedDeal(cctx.Bool("verifiedDeal")),
207213
singularity.WithCleanupInterval(cctx.Duration("experimentalSingularityCleanupInterval")),
214+
singularity.WithMinFreeSpace(cctx.Int64("minFreeDiskSpace")),
208215
)
209216
if err != nil {
210217
logger.Errorw("Failed to instantiate singularity store", "err", err)
@@ -222,7 +229,7 @@ func main() {
222229
}()
223230
store = singularityStore
224231
} else {
225-
store = blob.NewLocalStore(storeDir)
232+
store = blob.NewLocalStore(storeDir, blob.WithMinFreeSpace(cctx.Int64("minFreeDiskSpace")))
226233
logger.Infow("Using local blob store", "storeDir", storeDir)
227234
}
228235

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/data-preservation-programs/singularity v0.5.8
77
github.com/filecoin-project/go-address v1.1.0
88
github.com/filecoin-project/go-state-types v0.12.0
9+
github.com/gammazero/fsutil v0.0.1
910
github.com/google/uuid v1.3.1
1011
github.com/gotidy/ptr v1.4.0
1112
github.com/ipfs/go-log/v2 v2.5.1
@@ -160,7 +161,7 @@ require (
160161
golang.org/x/mod v0.12.0 // indirect
161162
golang.org/x/net v0.17.0 // indirect
162163
golang.org/x/sync v0.3.0 // indirect
163-
golang.org/x/sys v0.13.0 // indirect
164+
golang.org/x/sys v0.14.0 // indirect
164165
golang.org/x/text v0.13.0 // indirect
165166
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
166167
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD
103103
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
104104
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
105105
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
106+
github.com/gammazero/fsutil v0.0.1 h1:ELBzTchuwz+f6Xh5a3PS0J1ssuvMRP+bU4/h+eTBGaQ=
107+
github.com/gammazero/fsutil v0.0.1/go.mod h1:RUdM7Ubfblvprq9CJmUtDFa1HA2Qp1kxsWIKwTVvFvw=
106108
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
107109
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
108110
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -791,8 +793,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
791793
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
792794
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
793795
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
794-
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
795-
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
796+
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
797+
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
796798
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
797799
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
798800
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

integration/ribs/store.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func (s *Store) Start(_ context.Context) error {
8686
// TODO: change RIBS to take context.
8787
return s.ribs.Start()
8888
}
89-
func (s *Store) Put(ctx context.Context, in io.ReadCloser) (*blob.Descriptor, error) {
89+
func (s *Store) Put(ctx context.Context, in io.Reader) (*blob.Descriptor, error) {
9090

9191
// Generate ID early to fail early if generation fails.
9292
id, err := uuid.NewRandom()

integration/singularity/options.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type (
4343
maxPendingDealSize string
4444
maxPendingDealNumber int
4545
cleanupInterval time.Duration
46+
minFreeSpace int64
4647
}
4748
)
4849

@@ -331,3 +332,13 @@ func WithCleanupInterval(v time.Duration) Option {
331332
return nil
332333
}
333334
}
335+
336+
// WithMinFreeSpce configures the minimul free disk space that must remain
337+
// after storing a blob. A value of zero uses the default value and -1 disabled
338+
// checks.
339+
func WithMinFreeSpace(space int64) Option {
340+
return func(o *options) error {
341+
o.minFreeSpace = space
342+
return nil
343+
}
344+
}

integration/singularity/store.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func NewStore(o ...Option) (*Store, error) {
5252

5353
return &Store{
5454
options: opts,
55-
local: blob.NewLocalStore(opts.storeDir),
55+
local: blob.NewLocalStore(opts.storeDir, blob.WithMinFreeSpace(opts.minFreeSpace)),
5656
sourceName: "source",
5757
toPack: make(chan uint64, 1),
5858
closing: make(chan struct{}),
@@ -374,7 +374,7 @@ func (s *Store) Shutdown(ctx context.Context) error {
374374
return nil
375375
}
376376

377-
func (s *Store) Put(ctx context.Context, reader io.ReadCloser) (*blob.Descriptor, error) {
377+
func (s *Store) Put(ctx context.Context, reader io.Reader) (*blob.Descriptor, error) {
378378
desc, err := s.local.Put(ctx, reader)
379379
if err != nil {
380380
return nil, fmt.Errorf("failed to put file locally: %w", err)

0 commit comments

Comments
 (0)