Skip to content

Commit a8544ce

Browse files
committed
Reintroduce fixity
1 parent 77382ad commit a8544ce

File tree

10 files changed

+203
-2
lines changed

10 files changed

+203
-2
lines changed

config.json.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,16 @@
8484
}
8585
]
8686
}
87+
},
88+
89+
// ===========================================================================
90+
// WORKFLOW BEHAVIOUR
91+
// ---------------------------------------------------------------------------
92+
// Toggle additional workflow steps. When unset, all options default to false.
93+
"workflows": {
94+
"move": {
95+
// Run a fixity check via the Storage Service before moving an AIP.
96+
"check_fixity": false
97+
}
8798
}
8899
}

internal/application/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ type Config struct {
174174

175175
// Storage Service configuration.
176176
StorageService StorageServiceConfig `json:"storage_service"`
177+
178+
// Workflow-level configuration toggles.
179+
Workflows WorkflowConfig `json:"workflows"`
177180
}
178181

179182
type TemporalConfig struct {
@@ -245,6 +248,16 @@ type ReplicationTarget struct {
245248
Name string `json:"name"`
246249
}
247250

251+
// WorkflowConfig holds configuration for individual workflows.
252+
type WorkflowConfig struct {
253+
Move WorkflowMoveConfig `json:"move"`
254+
}
255+
256+
// WorkflowMoveConfig controls behaviour specific to the move workflow.
257+
type WorkflowMoveConfig struct {
258+
CheckFixity bool `json:"check_fixity"`
259+
}
260+
248261
type DatabaseConfig struct {
249262
Engine string `json:"engine"`
250263
SQLite SQLiteConfig `json:"sqlite"`

internal/application/config_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,6 @@ func TestLoadConfig(t *testing.T) {
5555

5656
assert.Equal(t, cfg.Database.Engine, "sqlite")
5757
assert.Equal(t, cfg.Database.SQLite.Path, DefaultSQLitePath())
58+
59+
assert.Assert(t, !cfg.Workflows.Move.CheckFixity)
5860
}

internal/application/events.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func (a Action) String() string {
2121

2222
var (
2323
ActionFind = Action{"find"}
24+
ActionFixity = Action{"fixity"}
2425
ActionMove = Action{"move"}
2526
ActionReplicate = Action{"Replicate"}
2627
ActionIndex = Action{"index"}
@@ -57,6 +58,7 @@ func StartEvent(a Action) Event {
5758
}
5859
}
5960

61+
// EndEvent marks the AIP with the provided status and stores the event.
6062
func EndEvent(ctx context.Context, s AIPStatus, a *App, e Event, aip *models.Aip) error {
6163
e.End = time.Now()
6264
if err := a.UpdateAIPStatus(ctx, aip.ID, s); err != nil {
@@ -69,13 +71,15 @@ func EndEvent(ctx context.Context, s AIPStatus, a *App, e Event, aip *models.Aip
6971
return aip.InsertEvents(ctx, a.DB, setter)
7072
}
7173

74+
// EndEventNoChange stores the event without changing the AIP status.
7275
func EndEventNoChange(ctx context.Context, a *App, e Event, aip *models.Aip) error {
7376
if err := aip.Reload(ctx, a.DB); err != nil {
7477
return err
7578
}
7679
return EndEvent(ctx, AIPStatus(aip.Status), a, e, aip)
7780
}
7881

82+
// EndEventErr records the error, marks the AIP as failed, and stores the event.
7983
func EndEventErr(ctx context.Context, a *App, e Event, aip *models.Aip, eventErr string) error {
8084
e.End = time.Now()
8185
a.AddAIPError(ctx, aip, eventErr)
@@ -89,6 +93,17 @@ func EndEventErr(ctx context.Context, a *App, e Event, aip *models.Aip, eventErr
8993
return aip.InsertEvents(ctx, a.DB, setter)
9094
}
9195

96+
// EndEventErrNoFailure records the error and stores the event without changing the AIP status.
97+
func EndEventErrNoFailure(ctx context.Context, a *App, e Event, aip *models.Aip, eventErr string) error {
98+
e.End = time.Now()
99+
a.AddAIPError(ctx, aip, eventErr)
100+
setter, err := EventToSetter(e)
101+
if err != nil {
102+
return err
103+
}
104+
return aip.InsertEvents(ctx, a.DB, setter)
105+
}
106+
92107
func EventToSetter(e Event) (*models.EventSetter, error) {
93108
formatDetails, err := e.FormatDetails()
94109
if err != nil {

internal/application/fixity.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package application
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/artefactual-labs/migrate/internal/database/gen/models"
9+
"github.com/artefactual-labs/migrate/internal/storage_service"
10+
)
11+
12+
const FixityActivityName = "fixity-activity"
13+
14+
type FixityActivityParams struct {
15+
UUID string `json:"uuid"`
16+
}
17+
18+
type FixityActivityResult struct {
19+
Status string
20+
}
21+
22+
func (a *App) FixityA(ctx context.Context, params FixityActivityParams) (*FixityActivityResult, error) {
23+
aip, err := a.GetAIPByID(ctx, params.UUID)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
if aip.FixityRun {
29+
return &FixityActivityResult{Status: aip.Status}, nil
30+
}
31+
32+
if err := checkFixity(ctx, a, a.StorageClient, aip); err != nil {
33+
return nil, err
34+
}
35+
36+
if err := aip.Reload(ctx, a.DB); err != nil {
37+
return nil, err
38+
}
39+
40+
if aip.Status != string(AIPStatusFixityChecked) {
41+
return &FixityActivityResult{Status: aip.Status}, fmt.Errorf("fixity did not complete successfully, status: %s", aip.Status)
42+
}
43+
44+
return &FixityActivityResult{Status: aip.Status}, nil
45+
}
46+
47+
func checkFixity(ctx context.Context, a *App, storageClient *storage_service.API, aip *models.Aip) error {
48+
if aip.FixityRun {
49+
return nil
50+
}
51+
52+
e := StartEvent(ActionFixity)
53+
e.AddDetail(fmt.Sprintf("Running fixity for: %s", aip.UUID))
54+
55+
res, err := storageClient.Packages.CheckFixity(ctx, aip.UUID)
56+
if err != nil {
57+
var ssErr storage_service.SSError
58+
if errors.As(err, &ssErr) && ssErr.StatusCode >= 500 {
59+
if eventErr := EndEventErrNoFailure(ctx, a, e, aip, err.Error()); eventErr != nil {
60+
return eventErr
61+
}
62+
return fmt.Errorf("storage service fixity call failed: %w", err)
63+
}
64+
if eventErr := EndEventErrNoFailure(ctx, a, e, aip, err.Error()); eventErr != nil {
65+
return eventErr
66+
}
67+
return nil
68+
}
69+
70+
if res.Success {
71+
if err := EndEvent(ctx, AIPStatusFixityChecked, a, e, aip); err != nil {
72+
return err
73+
}
74+
return nil
75+
}
76+
77+
a.AddAIPError(ctx, aip, res.Message)
78+
for _, f := range res.Failures.Files.Changed {
79+
e.AddDetail("file changed: " + f)
80+
}
81+
for _, f := range res.Failures.Files.Untracked {
82+
e.AddDetail("file untracked: " + f)
83+
}
84+
for _, f := range res.Failures.Files.Missing {
85+
e.AddDetail("file missing: " + f)
86+
}
87+
if err := EndEventErr(ctx, a, e, aip, res.Message); err != nil {
88+
return err
89+
}
90+
91+
return nil
92+
}

internal/application/move_workflow.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ func (w *MoveWorkflow) Run(ctx workflow.Context, params MoveWorkflowParams) (*Mo
6565
return result, nil
6666
}
6767

68+
if w.App.Config.Workflows.Move.CheckFixity {
69+
fixityParams := FixityActivityParams{UUID: params.UUID.String()}
70+
fixityResult := FixityActivityResult{}
71+
err = workflow.ExecuteActivity(ctx, FixityActivityName, fixityParams).Get(ctx, &fixityResult)
72+
if err != nil {
73+
return nil, err
74+
}
75+
result.MoveDetails = append(result.MoveDetails, "Fixity status: "+fixityResult.Status)
76+
}
77+
6878
moveParams := MoveActivityParams{UUID: params.UUID.String()}
6979
moveResult := MoveActivityResult{}
7080
err = workflow.ExecuteActivity(ctx, MoveActivityName, moveParams).Get(ctx, &moveResult)

internal/cmd/workercmd/workercmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func registerWorker(app *application.App) worker.Worker {
7878
w.RegisterActivityWithOptions(app.ReplicateA, activity.RegisterOptions{Name: application.ReplicateAName})
7979
w.RegisterActivityWithOptions(app.FindA, activity.RegisterOptions{Name: application.FindAName})
8080
w.RegisterActivityWithOptions(app.CheckReplicationStatus, activity.RegisterOptions{Name: application.CheckReplicationStatusName})
81+
w.RegisterActivityWithOptions(app.FixityA, activity.RegisterOptions{Name: application.FixityActivityName})
8182
w.RegisterActivityWithOptions(app.MoveA, activity.RegisterOptions{Name: application.MoveActivityName})
8283

8384
return w

internal/ssmock/server.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,19 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) {
215215
http.NotFound(w, r)
216216
return
217217
}
218+
if strings.HasSuffix(remainder, "/check_fixity/") {
219+
if r.Method != http.MethodGet {
220+
methodNotAllowed(w, http.MethodGet)
221+
return
222+
}
223+
id := strings.TrimSuffix(remainder, "/check_fixity/")
224+
if id == "" {
225+
http.NotFound(w, r)
226+
return
227+
}
228+
s.handleCheckFixity(w, r, id)
229+
return
230+
}
218231
if strings.HasSuffix(remainder, "/move/") {
219232
if r.Method != http.MethodPost {
220233
methodNotAllowed(w, http.MethodPost)
@@ -248,6 +261,23 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) {
248261
writeJSON(w, pkg)
249262
}
250263

264+
func (s *Server) handleCheckFixity(w http.ResponseWriter, r *http.Request, id string) {
265+
s.mu.RLock()
266+
pkg, ok := s.state.clonePackage(id)
267+
s.mu.RUnlock()
268+
if !ok {
269+
http.NotFound(w, r)
270+
return
271+
}
272+
273+
resp := storage_service.FixityResponse{
274+
Success: true,
275+
Message: fmt.Sprintf("Fixity check completed for %s", pkg.UUID),
276+
Timestamp: time.Now().UTC().Format(time.RFC3339),
277+
}
278+
writeJSON(w, &resp)
279+
}
280+
251281
func (s *Server) handleMove(w http.ResponseWriter, r *http.Request, id string) {
252282
if err := r.ParseForm(); err != nil {
253283
http.Error(w, fmt.Sprintf("invalid form: %v", err), http.StatusBadRequest)

internal/storage_service/package.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,25 @@ func (s *PackageService) Move(ctx context.Context, packageID, locationID string)
4141
p.Set("location_uuid", locationID)
4242
return s.client.Call(ctx, http.MethodPost, path, p.Encode(), nil)
4343
}
44+
45+
type FixityResponse struct {
46+
Success bool `json:"success"`
47+
Message string `json:"message"`
48+
Timestamp string `json:"timestamp"`
49+
Failures FixityFailures `json:"failures"`
50+
}
51+
52+
type FixityFailures struct {
53+
Files struct {
54+
Missing []string `json:"missing"`
55+
Changed []string `json:"changed"`
56+
Untracked []string `json:"untracked"`
57+
} `json:"files"`
58+
}
59+
60+
func (s *PackageService) CheckFixity(ctx context.Context, id string) (*FixityResponse, error) {
61+
res := &FixityResponse{}
62+
path := fmt.Sprintf("/api/v2/file/%s/check_fixity/", id)
63+
err := s.client.Call(ctx, http.MethodGet, path, nil, res)
64+
return res, err
65+
}

testdata/move.txtar

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ cmp stdout ssmock-snapshot.toml
2626
"move_target_location_id": "71cb2196-5629-4225-aaf7-d8431b0895c4",
2727
"replication_targets": []
2828
}
29+
},
30+
"workflows": {
31+
"move": {
32+
"check_fixity": true
33+
}
2934
}
3035
}
3136
-- ssmock.toml --
@@ -47,7 +52,7 @@ id = '2faa61dc-ed33-49f4-8b36-954f203bab4a'
4752
status = 'UPLOADED'
4853
-- move-report.expected.csv --
4954
UUID,AIPStatus,Duration,fixity-run,moved,cleaned,replicated,re-indexed,size,Duration Nanoseconds,New Path,Old Path,Replica UUID,Local copy Path,Staged Copy Path,Errors
50-
2faa61dc-ed33-49f4-8b36-954f203bab4a,moved,Not Done,Done,Not Done,Not Done,Not Done,0 B,
55+
2faa61dc-ed33-49f4-8b36-954f203bab4a,moved,Done,Done,Not Done,Not Done,Not Done,0 B,
5156
-- db.aips.expected.csv --
5257
id,uuid,status,found,fixity_run,moved,cleaned,replicated,re_indexed,current_location,size,location_uuid
53-
1,2faa61dc-ed33-49f4-8b36-954f203bab4a,moved,1,0,1,0,0,0,/api/v2/location/71cb2196-5629-4225-aaf7-d8431b0895c4/|/api/v2/location/72a9c518-2747-4cb5-aeba-e6309d946e79/,0,
58+
1,2faa61dc-ed33-49f4-8b36-954f203bab4a,moved,1,1,1,0,0,0,/api/v2/location/71cb2196-5629-4225-aaf7-d8431b0895c4/|/api/v2/location/72a9c518-2747-4cb5-aeba-e6309d946e79/,0,

0 commit comments

Comments
 (0)