Skip to content

Commit b56cb39

Browse files
committed
Combine WriteFileArtifactProcessor and MkDirArtifactProcessor
1 parent 9b37b1e commit b56cb39

File tree

5 files changed

+221
-371
lines changed

5 files changed

+221
-371
lines changed

fileartifactprocessor.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package specter
2+
3+
import (
4+
"fmt"
5+
"github.com/morebec/go-errors/errors"
6+
"os"
7+
"sync"
8+
)
9+
10+
const WriteFileArtifactsProcessorErrorCode = "write_file_artifacts_processor_error"
11+
12+
type WriteMode string
13+
14+
const (
15+
// RecreateMode mode indicating that the artifact should be recreated on every run.
16+
RecreateMode WriteMode = "RECREATE"
17+
18+
// WriteOnceMode is used to indicate that a file should be created only once, and not be recreated for subsequent
19+
// executions of the processing. This can be useful in situations where scaffolding is required.
20+
WriteOnceMode WriteMode = "WRITE_ONCE"
21+
)
22+
23+
const DefaultWriteMode WriteMode = WriteOnceMode
24+
25+
// FileArtifact is a data structure that can be used by a SpecificationProcessor to generate file artifacts
26+
// that can be written by the FileArtifactProcessor.
27+
type FileArtifact struct {
28+
Path string
29+
Data []byte
30+
FileMode os.FileMode
31+
WriteMode WriteMode
32+
}
33+
34+
func NewDirectoryArtifact(path string, fileMode os.FileMode, writeMode WriteMode) *FileArtifact {
35+
return &FileArtifact{
36+
Path: path,
37+
FileMode: fileMode | os.ModeDir,
38+
WriteMode: writeMode,
39+
Data: nil,
40+
}
41+
}
42+
43+
func (a FileArtifact) ID() ArtifactID {
44+
return ArtifactID(a.Path)
45+
}
46+
47+
func (a FileArtifact) IsDir() bool {
48+
return a.FileMode&os.ModeDir != 0
49+
}
50+
51+
// FileArtifactProcessor is a processor responsible for writing Artifact referring to files.
52+
// To perform its work this processor looks at the processing context for any FileArtifact.
53+
type FileArtifactProcessor struct {
54+
FileSystem FileSystem
55+
}
56+
57+
func (p FileArtifactProcessor) Name() string {
58+
return "file_artifacts_processor"
59+
}
60+
61+
func (p FileArtifactProcessor) Process(ctx ArtifactProcessingContext) error {
62+
ctx.Logger.Info("Writing file artifacts ...")
63+
64+
files, err := p.findFileArtifactsFromContext(ctx)
65+
if err != nil {
66+
return err
67+
}
68+
69+
if err := p.cleanRegistry(ctx); err != nil {
70+
ctx.Logger.Error("failed cleaning artifact registry")
71+
return err
72+
}
73+
74+
errs := errors.NewGroup(WriteFileArtifactsProcessorErrorCode)
75+
76+
// Write files concurrently to speed up process.
77+
var mu sync.Mutex
78+
var wg sync.WaitGroup
79+
80+
for _, file := range files {
81+
if err := CheckContextDone(ctx); err != nil {
82+
return err
83+
}
84+
wg.Add(1)
85+
go func(ctx ArtifactProcessingContext, file FileArtifact) {
86+
defer wg.Done()
87+
if err := p.processFileArtifact(ctx, file); err != nil {
88+
ctx.Logger.Error(fmt.Sprintf("failed writing artifact file at %q", file.ID()))
89+
mu.Lock()
90+
defer mu.Unlock()
91+
errs = errs.Append(err)
92+
}
93+
}(ctx, file)
94+
}
95+
wg.Wait()
96+
97+
if errs.HasErrors() {
98+
return errs
99+
}
100+
101+
ctx.Logger.Success("Artifact files written successfully.")
102+
103+
return nil
104+
}
105+
106+
func (p FileArtifactProcessor) findFileArtifactsFromContext(ctx ArtifactProcessingContext) ([]FileArtifact, error) {
107+
var files []FileArtifact
108+
var errs errors.Group
109+
110+
for _, a := range ctx.Artifacts {
111+
fa, ok := a.(FileArtifact)
112+
if !ok {
113+
continue
114+
}
115+
116+
if fa.WriteMode == "" {
117+
ctx.Logger.Trace(fmt.Sprintf("File artifact %q does not have a write mode, defaulting to %q", fa.ID(), DefaultWriteMode))
118+
fa.WriteMode = DefaultWriteMode
119+
}
120+
121+
if fa.Path == "" {
122+
errs = errs.Append(errors.NewWithMessage(
123+
WriteFileArtifactsProcessorErrorCode,
124+
fmt.Sprintf("file artifact %q does not have a path", fa.ID()),
125+
))
126+
}
127+
128+
files = append(files, fa)
129+
}
130+
return files, nil
131+
}
132+
133+
func (p FileArtifactProcessor) processFileArtifact(ctx ArtifactProcessingContext, fa FileArtifact) error {
134+
filePath, err := p.FileSystem.Abs(fa.Path)
135+
if err != nil {
136+
return err
137+
}
138+
139+
fileExists := true
140+
if _, err := p.FileSystem.StatPath(filePath); err != nil {
141+
if !os.IsNotExist(err) {
142+
return err
143+
}
144+
fileExists = false
145+
}
146+
147+
if fa.WriteMode == WriteOnceMode && fileExists {
148+
return nil
149+
}
150+
151+
// At this point if the file still already exists, this means that the clean step has not
152+
// been executed properly.
153+
154+
if fa.IsDir() {
155+
ctx.Logger.Info(fmt.Sprintf("Creating directory %q ...", filePath))
156+
ctx.Logger.Trace(fmt.Sprintf("making directory %q for %q ...", filePath, fa.ID()))
157+
if err := p.FileSystem.WriteFile(filePath, fa.Data, os.ModePerm); err != nil {
158+
return err
159+
}
160+
} else {
161+
ctx.Logger.Info(fmt.Sprintf("Writing file %q ...", filePath))
162+
ctx.Logger.Trace(fmt.Sprintf("creating directory %q for %q ...", filePath, fa.ID()))
163+
if err := p.FileSystem.WriteFile(filePath, fa.Data, os.ModePerm); err != nil {
164+
return err
165+
}
166+
}
167+
168+
if fa.WriteMode != WriteOnceMode {
169+
ctx.AddToRegistry(fa.ID())
170+
}
171+
172+
return nil
173+
}
174+
175+
func (p FileArtifactProcessor) cleanRegistry(ctx ArtifactProcessingContext) error {
176+
var wg sync.WaitGroup
177+
cleanFile := func(ctx ArtifactProcessingContext, o ArtifactID) {
178+
defer wg.Done()
179+
if err := p.FileSystem.Remove(string(o)); err != nil {
180+
if errors.Is(err, os.ErrNotExist) {
181+
return
182+
}
183+
panic(errors.Wrap(err, "failed cleaning artifact registry files"))
184+
}
185+
ctx.RemoveFromRegistry(o)
186+
}
187+
188+
for _, o := range ctx.RegistryArtifacts() {
189+
wg.Add(1)
190+
go cleanFile(ctx, o)
191+
}
192+
wg.Wait()
193+
194+
return nil
195+
}

writefileartfproc_test.go renamed to fileartifactprocessor_test.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package specter
33
import (
44
"context"
55
"github.com/stretchr/testify/assert"
6+
"github.com/stretchr/testify/require"
7+
"os"
68
"testing"
79
)
810

@@ -18,8 +20,8 @@ func TestWriteFileArtifactProcessor_Process(t *testing.T) {
1820
name: "GIVEN file artifacts THEN successful file creation",
1921
mockFS: &mockFileSystem{},
2022
artifacts: []Artifact{
21-
FileArtifact{Path: "/path/to/file1", Mode: 0755},
22-
FileArtifact{Path: "/path/to/file2", Mode: 0755},
23+
FileArtifact{Path: "/path/to/file1", FileMode: 0755},
24+
FileArtifact{Path: "/path/to/file2", FileMode: 0755},
2325
},
2426
expectedFiles: []string{"/path/to/file1", "/path/to/file2"},
2527
expectError: nil,
@@ -28,7 +30,7 @@ func TestWriteFileArtifactProcessor_Process(t *testing.T) {
2830
name: "GIVEN non-file artifacts THEN skip and return no error",
2931
mockFS: &mockFileSystem{},
3032
artifacts: []Artifact{
31-
FileArtifact{Path: "/path/to/file1", Mode: 0755},
33+
FileArtifact{Path: "/path/to/file1", FileMode: 0755},
3234
mockArtifact{},
3335
},
3436
expectedFiles: []string{"/path/to/file1"},
@@ -40,7 +42,7 @@ func TestWriteFileArtifactProcessor_Process(t *testing.T) {
4042
writeFileErr: assert.AnError,
4143
},
4244
artifacts: []Artifact{
43-
FileArtifact{Path: "/path/to/file1", Mode: 0755},
45+
FileArtifact{Path: "/path/to/file1", FileMode: 0755},
4446
},
4547
expectedFiles: []string{},
4648
expectError: assert.AnError,
@@ -53,7 +55,7 @@ func TestWriteFileArtifactProcessor_Process(t *testing.T) {
5355
},
5456
},
5557
artifacts: []Artifact{
56-
FileArtifact{Path: "/path/to/file1", Mode: 0755, WriteMode: WriteOnceMode},
58+
FileArtifact{Path: "/path/to/file1", FileMode: 0755, WriteMode: WriteOnceMode},
5759
},
5860
expectedFiles: []string{},
5961
expectError: nil,
@@ -62,7 +64,7 @@ func TestWriteFileArtifactProcessor_Process(t *testing.T) {
6264

6365
for _, tt := range tests {
6466
t.Run(tt.name, func(t *testing.T) {
65-
processor := WriteFileArtifactProcessor{FileSystem: tt.mockFS}
67+
processor := FileArtifactProcessor{FileSystem: tt.mockFS}
6668
ctx := ArtifactProcessingContext{
6769
Context: context.Background(),
6870
Artifacts: tt.artifacts,
@@ -88,5 +90,22 @@ func TestWriteFileArtifactProcessor_Process(t *testing.T) {
8890
}
8991

9092
func TestWriteFileArtifactProcessor_Name(t *testing.T) {
91-
assert.NotEqual(t, "", WriteFileArtifactProcessor{}.Name())
93+
assert.NotEqual(t, "", FileArtifactProcessor{}.Name())
94+
}
95+
96+
func TestNewDirectoryArtifact(t *testing.T) {
97+
dir := NewDirectoryArtifact("/dir", os.ModePerm, RecreateMode)
98+
require.NotNil(t, dir)
99+
assert.Equal(t, dir.Path, "/dir")
100+
assert.Equal(t, dir.FileMode, os.ModePerm|os.ModeDir)
101+
assert.Equal(t, dir.WriteMode, RecreateMode)
102+
assert.Nil(t, dir.Data)
103+
}
104+
105+
func TestFileArtifact_IsDir(t *testing.T) {
106+
f := FileArtifact{FileMode: os.ModePerm}
107+
assert.False(t, f.IsDir())
108+
109+
f = FileArtifact{FileMode: os.ModePerm | os.ModeDir}
110+
assert.True(t, f.IsDir())
92111
}

0 commit comments

Comments
 (0)