Skip to content

Commit c90f933

Browse files
authored
Merge pull request #111 from discentem/pr111
consolidate metadata writing
2 parents d697a81 + ba75bda commit c90f933

File tree

9 files changed

+111
-56
lines changed

9 files changed

+111
-56
lines changed

WORKSPACE

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ go_repository(
9696
version = "v0.3.0",
9797
)
9898

99+
go_repository(
100+
name = "com_github_hashicorp_go_multierror",
101+
importpath = "github.com/hashicorp/go-multierror",
102+
sum = "h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=",
103+
version = "v1.1.1",
104+
)
105+
106+
go_repository(
107+
name = "com_github_hashicorp_errwrap",
108+
importpath = "github.com/hashicorp/errwrap",
109+
sum = "h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=",
110+
version = "v1.1.0",
111+
)
112+
99113
bazel_skylib_workspace()
100114

101115
load("//:repositories.bzl", "go_repositories")

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ require (
5454
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
5555
github.com/gorilla/handlers v1.5.1 // indirect
5656
github.com/gorilla/mux v1.8.0 // indirect
57+
github.com/hashicorp/errwrap v1.0.0 // indirect
58+
github.com/hashicorp/go-multierror v1.1.1 // indirect
5759
github.com/hashicorp/hcl v1.0.0 // indirect
5860
github.com/inconshreveable/mousetrap v1.1.0 // indirect
5961
github.com/jmespath/go-jmespath v0.4.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
230230
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
231231
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
232232
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
233+
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
234+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
235+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
236+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
233237
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
234238
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
235239
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=

internal/metadata/metadata.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313

1414
const MetadataFileExtension string = "cfile"
1515

16+
var ErrFileExtensionEmpty = fmt.Errorf("options.MetadatafileExtension cannot be %q", "")
17+
1618
type ObjectMetaData struct {
1719
Name string `json:"name"`
1820
Checksum string `json:"checksum"`

internal/stores/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ go_library(
1717
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
1818
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
1919
"@com_github_google_logger//:logger",
20+
"@com_github_hashicorp_go_multierror//:go-multierror",
2021
"@com_github_spf13_afero//:afero",
2122
"@com_google_cloud_go_storage//:storage",
2223
"@org_golang_google_api//option",

internal/stores/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Stores are the interface by which metadata is managed and files are uploaded/ret
44
## Upload
55
When uploading files, the following items should be handled in the implementation:
66

7-
* Metadata should be generated from the file and written alongside the binary on disk
7+
* Metadata should be generated from the file and written alongside the binary on the filesystem.
88
* Upload the file to the bucket
99

1010
## Retrieve

internal/stores/gcs.go

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package stores
22

33
import (
44
"context"
5-
"encoding/json"
65
"errors"
76
"fmt"
87
"io"
@@ -14,6 +13,7 @@ import (
1413
gcsStorage "cloud.google.com/go/storage"
1514
"github.com/discentem/cavorite/internal/metadata"
1615
"github.com/google/logger"
16+
"github.com/hashicorp/go-multierror"
1717
"github.com/spf13/afero"
1818
"google.golang.org/api/option"
1919
)
@@ -63,12 +63,13 @@ func (s *GCSStore) GetOptions() Options {
6363
return s.Options
6464
}
6565

66+
func (s *GCSStore) GetFsys() afero.Fs {
67+
return s.fsys
68+
}
69+
6670
// Upload generates the metadata, writes it s.fsys and uploads the file to the GCS bucket
6771
func (s *GCSStore) Upload(ctx context.Context, objects ...string) error {
68-
if s.Options.MetadataFileExtension == "" {
69-
return ErrMetadataFileExtensionEmpty
70-
}
71-
72+
var multErr error
7273
for _, o := range objects {
7374
logger.V(2).Infof("Object: %s\n", o)
7475
f, err := s.fsys.Open(o)
@@ -77,22 +78,13 @@ func (s *GCSStore) Upload(ctx context.Context, objects ...string) error {
7778
}
7879
defer f.Close()
7980

80-
// Generate cavorite metadata
81-
m, err := metadata.GenerateFromFile(f)
82-
if err != nil {
83-
return err
84-
}
85-
logger.V(2).Infof("%s has a checksum of %q", o, m.Checksum)
86-
// convert metadata to json
87-
blob, err := json.MarshalIndent(m, "", " ")
81+
// cleanupFn is function that can be called if
82+
// uploading to s3 fails. cleanupFn deletes the cfile
83+
// so that we don't retain a cfile without a corresponding binary
84+
cleanupFn, err := WriteMetadataToFsys(s, o, f)
8885
if err != nil {
8986
return err
9087
}
91-
// Write metadata to fsys
92-
metadataPath := fmt.Sprintf("%s.%s", o, s.Options.MetadataFileExtension)
93-
if err := afero.WriteFile(s.fsys, metadataPath, blob, 0644); err != nil {
94-
return err
95-
}
9688

9789
// ToDo(natewalck) Expose this timeout as a setting
9890
ctx, cancel := context.WithTimeout(ctx, time.Second*1800)
@@ -104,22 +96,36 @@ func (s *GCSStore) Upload(ctx context.Context, objects ...string) error {
10496
wc := gcsObject.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)
10597

10698
// Reset to the start of the file because metadata generation has already read it once
107-
_, err = f.Seek(0, io.SeekStart)
99+
_, seekErr := f.Seek(0, io.SeekStart)
108100
if err != nil {
109-
return err
101+
// seek failed, add this failure to multErr
102+
multErr = multierror.Append(multErr, fmt.Errorf("f.Seek() error: %w", seekErr))
103+
if cleanupErr := cleanupFn(); err != nil {
104+
// cleanup also failed, add to multErr
105+
multErr = multierror.Append(multErr, fmt.Errorf("cleanupFn() error: %w", cleanupErr))
106+
}
107+
// return multiple errors
108+
return multErr
110109
}
111110
_, err = io.Copy(wc, f)
112111
if err != nil {
113-
logger.V(2).Infof("Failed to upload %s", o)
114-
return err
112+
multErr = multierror.Append(multErr, fmt.Errorf("io.Copy() error: %w", err))
113+
if cleanupErr := cleanupFn(); err != nil {
114+
multErr = multierror.Append(multErr, fmt.Errorf("cleanupFn() error: %w", cleanupErr))
115+
}
116+
return multErr
115117
}
116118

117119
if err := wc.Close(); err != nil {
118120
// Error will contain this string if the DoesNotExist condition isn't met
119121
if strings.Contains(err.Error(), "conditionNotMet") {
120122
logger.Infof("%s already exists, skipping...", o)
121123
} else {
122-
return err
124+
multErr = multierror.Append(multErr, fmt.Errorf("wc.Close() err: %w", err))
125+
if cleanupErr := cleanupFn(); err != nil {
126+
multErr = multierror.Append(multErr, fmt.Errorf("cleanupFn() error: %w", cleanupErr))
127+
}
128+
return multErr
123129
}
124130
}
125131
}

internal/stores/s3.go

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package stores
22

33
import (
4-
"bytes"
54
"context"
6-
"encoding/json"
75
"errors"
86
"fmt"
97
"io"
10-
"os"
118
"path"
129
"path/filepath"
1310
"strings"
@@ -130,45 +127,29 @@ func (s *S3Store) GetOptions() Options {
130127
return s.Options
131128
}
132129

133-
// TODO(discentem): #34 largely copy-pasted from stores/local/local.go. Can be consolidated
130+
func (s *S3Store) GetFsys() afero.Fs {
131+
return s.fsys
132+
}
133+
134134
// Upload generates the metadata, writes it to disk and uploads the file to the S3 bucket
135135
func (s *S3Store) Upload(ctx context.Context, objects ...string) error {
136-
if s.Options.MetadataFileExtension == "" {
137-
return ErrMetadataFileExtensionEmpty
138-
}
139-
140136
for _, o := range objects {
141-
logger.V(2).Infof("object: %s", o)
142137
f, err := s.fsys.Open(o)
143138
if err != nil {
144139
return err
145140
}
146-
defer f.Close()
147-
// TODO(discentem): probably inefficient, reading entire file into memory
148-
b, err := afero.ReadFile(s.fsys, o)
141+
// cleanupFn is function that can be called if
142+
// uploading to s3 fails. cleanupFn deletes the cfile
143+
// so that we don't retain a cfile without a corresponding binary
144+
cleanupFn, err := WriteMetadataToFsys(s, o, f)
149145
if err != nil {
150146
return err
151147
}
152-
153-
// generate metadata
154-
m, err := metadata.GenerateFromFile(f)
148+
_, err = f.Seek(0, io.SeekStart)
155149
if err != nil {
156-
return err
157-
}
158-
logger.V(2).Infof("%s has a checksum of %q", o, m.Checksum)
159-
// convert metadata to json
160-
blob, err := json.MarshalIndent(m, "", " ")
161-
if err != nil {
162-
return err
163-
}
164-
// Create path for metadata if it doesn't already exist
165-
if err := s.fsys.MkdirAll(filepath.Dir(filepath.Dir(o)), os.ModePerm); err != nil {
166-
return err
167-
}
168-
// Write metadata to disk
169-
metadataPath := fmt.Sprintf("%s.%s", o, s.Options.MetadataFileExtension)
170-
logger.V(2).Infof("writing metadata to %s", metadataPath)
171-
if err := afero.WriteFile(s.fsys, metadataPath, blob, 0644); err != nil {
150+
if err := cleanupFn(); err != nil {
151+
return err
152+
}
172153
return err
173154
}
174155

@@ -177,13 +158,19 @@ func (s *S3Store) Upload(ctx context.Context, objects ...string) error {
177158
obj := s3.PutObjectInput{
178159
Bucket: aws.String(buck),
179160
Key: &o,
180-
Body: bytes.NewReader(b),
161+
Body: f,
181162
}
182163
out, err := s.s3Uploader.Upload(ctx, &obj)
183164
if err != nil {
165+
if err := cleanupFn(); err != nil {
166+
return fmt.Errorf("cleanup() failed after Upload failure: %w", err)
167+
}
184168
logger.Error(out)
185169
return err
186170
}
171+
if err := f.Close(); err != nil {
172+
return err
173+
}
187174
}
188175
return nil
189176
}

internal/stores/stores.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package stores
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
7+
"fmt"
68
"os"
79
"path/filepath"
810
"strings"
911

12+
"github.com/discentem/cavorite/internal/metadata"
13+
"github.com/google/logger"
1014
"github.com/spf13/afero"
1115
)
1216

@@ -26,6 +30,7 @@ type Store interface {
2630
Upload(ctx context.Context, objects ...string) error
2731
Retrieve(ctx context.Context, objects ...string) error
2832
GetOptions() Options
33+
GetFsys() afero.Fs
2934
}
3035

3136
func openOrCreateFile(fsys afero.Fs, filename string) (afero.File, error) {
@@ -39,3 +44,37 @@ func openOrCreateFile(fsys afero.Fs, filename string) (afero.File, error) {
3944
func inferObjPath(cfilePath string) string {
4045
return strings.TrimSuffix(cfilePath, filepath.Ext(cfilePath))
4146
}
47+
48+
// WriteMetadata generates Cavorite metadata for obj and writes it to s.Fsys
49+
func WriteMetadataToFsys(s Store, obj string, f afero.File) (cleanup func() error, err error) {
50+
opts := s.GetOptions()
51+
if opts.MetadataFileExtension == "" {
52+
return nil, metadata.ErrFileExtensionEmpty
53+
}
54+
fsys := s.GetFsys()
55+
logger.V(2).Infof("object: %s", obj)
56+
57+
// generate metadata
58+
m, err := metadata.GenerateFromFile(f)
59+
if err != nil {
60+
return nil, err
61+
}
62+
logger.V(2).Infof("%s has a checksum of %q", obj, m.Checksum)
63+
// convert metadata to json
64+
blob, err := json.MarshalIndent(m, "", " ")
65+
if err != nil {
66+
return nil, err
67+
}
68+
// Write metadata to disk
69+
metadataPath := fmt.Sprintf("%s.%s", obj, opts.MetadataFileExtension)
70+
logger.V(2).Infof("writing metadata to %s", metadataPath)
71+
if err := afero.WriteFile(fsys, metadataPath, blob, 0644); err != nil {
72+
return nil, err
73+
}
74+
75+
cleanup = func() error {
76+
return fsys.Remove(metadataPath)
77+
}
78+
79+
return cleanup, nil
80+
}

0 commit comments

Comments
 (0)