test(snapshot): verify cursor close failures
- commit
- 725406b0ff34217059b91b6d6b64b6faa4e1456b
- author
- brice · · 2026-07-21 21:34 UTC
- parent
- 91d6720
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +135 -23
M internal/adapters/snapshot/maintenance.go +48 -11
| @@ -246,7 +246,7 @@ func (r *Repository) readMaintenanceDir(dir string, n int, purpose string) ([]ma | |||
| 246 | 246 | r.maintenanceCursors[id] = cursor | |
| 247 | 247 | } | |
| 248 | 248 | ||
| 249 | - | entries, done, err := readMaintenanceDirent(dir, n, cursor) | |
| 249 | + | entries, done, err := r.readMaintenanceDirent(dir, n, cursor) | |
| 250 | 250 | if err != nil { | |
| 251 | 251 | delete(r.maintenanceCursors, id) | |
| 252 | 252 | return nil, false, err | |
| @@ -257,17 +257,55 @@ func (r *Repository) readMaintenanceDir(dir string, n int, purpose string) ([]ma | |||
| 257 | 257 | return entries, done, nil | |
| 258 | 258 | } | |
| 259 | 259 | ||
| 260 | + | // maintenanceDirectory is the small syscall surface needed to resume a | |
| 261 | + | // bounded getdents cursor. It permits deterministic fault injection without | |
| 262 | + | // retaining directory descriptors between maintenance passes. | |
| 263 | + | type maintenanceDirectory interface { | |
| 264 | + | Seek(int64, int) (int64, error) | |
| 265 | + | ReadDirent([]byte) (int, error) | |
| 266 | + | Close() error | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | type osMaintenanceDirectory struct{ file *os.File } | |
| 270 | + | ||
| 271 | + | func (d osMaintenanceDirectory) Seek(offset int64, whence int) (int64, error) { | |
| 272 | + | return syscall.Seek(int(d.file.Fd()), offset, whence) | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | func (d osMaintenanceDirectory) ReadDirent(buffer []byte) (int, error) { | |
| 276 | + | return syscall.ReadDirent(int(d.file.Fd()), buffer) | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | func (d osMaintenanceDirectory) Close() error { return d.file.Close() } | |
| 280 | + | ||
| 281 | + | func (r *Repository) openMaintenanceDirectory(dir string) (maintenanceDirectory, error) { | |
| 282 | + | if open := r.hooks.openMaintenanceDirectory; open != nil { | |
| 283 | + | return open(dir) | |
| 284 | + | } | |
| 285 | + | file, err := os.Open(dir) | |
| 286 | + | if err != nil { | |
| 287 | + | return nil, err | |
| 288 | + | } | |
| 289 | + | return osMaintenanceDirectory{file: file}, nil | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | // maintenanceDirectoryError adds stable operation context and removes any | |
| 293 | + | // filesystem path while retaining the underlying cause for errors.Is. | |
| 294 | + | func maintenanceDirectoryError(operation string, err error) error { | |
| 295 | + | return fmt.Errorf("%s: %w", operation, safeFilesystemError(err)) | |
| 296 | + | } | |
| 297 | + | ||
| 260 | 298 | // readMaintenanceDirent uses the Linux getdents seek cookie from each record. | |
| 261 | 299 | // syscall.Dirent's Off field is the d_off member of linux_dirent64 and | |
| 262 | 300 | // syscall.Seek is lseek(2), as defined by Go's syscall Linux sources. | |
| 263 | - | func readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (entries []maintenanceDirEntry, done bool, err error) { | |
| 264 | - | file, err := os.Open(dir) | |
| 301 | + | func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (entries []maintenanceDirEntry, done bool, err error) { | |
| 302 | + | file, err := r.openMaintenanceDirectory(dir) | |
| 265 | 303 | if err != nil { | |
| 266 | - | return nil, false, err | |
| 304 | + | return nil, false, maintenanceDirectoryError("open maintenance directory", err) | |
| 267 | 305 | } | |
| 268 | 306 | defer func() { | |
| 269 | 307 | if closeErr := file.Close(); closeErr != nil { | |
| 270 | - | closeErr = fmt.Errorf("close maintenance directory %q: %w", dir, closeErr) | |
| 308 | + | closeErr = maintenanceDirectoryError("close maintenance directory", closeErr) | |
| 271 | 309 | if err != nil { | |
| 272 | 310 | err = errors.Join(err, closeErr) | |
| 273 | 311 | } else { | |
| @@ -277,19 +315,18 @@ func readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (en | |||
| 277 | 315 | } | |
| 278 | 316 | } | |
| 279 | 317 | }() | |
| 280 | - | fd := int(file.Fd()) | |
| 281 | 318 | if cursor.offset != 0 { | |
| 282 | - | if _, err := syscall.Seek(fd, cursor.offset, io.SeekStart); err != nil { | |
| 283 | - | return nil, false, err | |
| 319 | + | if _, err := file.Seek(cursor.offset, io.SeekStart); err != nil { | |
| 320 | + | return nil, false, maintenanceDirectoryError("seek maintenance directory", err) | |
| 284 | 321 | } | |
| 285 | 322 | } | |
| 286 | 323 | ||
| 287 | 324 | entries = make([]maintenanceDirEntry, 0, limit) | |
| 288 | 325 | buffer := make([]byte, 8192) | |
| 289 | 326 | for len(entries) < limit { | |
| 290 | - | count, err := syscall.ReadDirent(fd, buffer) | |
| 327 | + | count, err := file.ReadDirent(buffer) | |
| 291 | 328 | if err != nil { | |
| 292 | - | return nil, false, err | |
| 329 | + | return nil, false, maintenanceDirectoryError("read maintenance directory", err) | |
| 293 | 330 | } | |
| 294 | 331 | if count == 0 { | |
| 295 | 332 | return entries, true, nil | |
| @@ -319,7 +356,7 @@ func readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (en | |||
| 319 | 356 | continue | |
| 320 | 357 | } | |
| 321 | 358 | if err != nil { | |
| 322 | - | return nil, false, err | |
| 359 | + | return nil, false, maintenanceDirectoryError("stat maintenance directory entry", err) | |
| 323 | 360 | } | |
| 324 | 361 | entries = append(entries, maintenanceDirEntry{name: name, isDir: info.IsDir()}) | |
| 325 | 362 | } | |
| @@ -246,7 +246,7 @@ func (r *Repository) readMaintenanceDir(dir string, n int, purpose string) ([]ma | |||||
| 246 | r.maintenanceCursors[id] = cursor | 246 | r.maintenanceCursors[id] = cursor | ||
| 247 | } | 247 | } | ||
| 248 | 248 | ||||
| 249 | - | entries, done, err := readMaintenanceDirent(dir, n, cursor) | 249 | + | entries, done, err := r.readMaintenanceDirent(dir, n, cursor) |
| 250 | if err != nil { | 250 | if err != nil { | ||
| 251 | delete(r.maintenanceCursors, id) | 251 | delete(r.maintenanceCursors, id) | ||
| 252 | return nil, false, err | 252 | return nil, false, err | ||
| @@ -257,17 +257,55 @@ func (r *Repository) readMaintenanceDir(dir string, n int, purpose string) ([]ma | |||||
| 257 | return entries, done, nil | 257 | return entries, done, nil | ||
| 258 | } | 258 | } | ||
| 259 | 259 | ||||
| 260 | + | // maintenanceDirectory is the small syscall surface needed to resume a | |||
| 261 | + | // bounded getdents cursor. It permits deterministic fault injection without | |||
| 262 | + | // retaining directory descriptors between maintenance passes. | |||
| 263 | + | type maintenanceDirectory interface { | |||
| 264 | + | Seek(int64, int) (int64, error) | |||
| 265 | + | ReadDirent([]byte) (int, error) | |||
| 266 | + | Close() error | |||
| 267 | + | } | |||
| 268 | + | ||||
| 269 | + | type osMaintenanceDirectory struct{ file *os.File } | |||
| 270 | + | ||||
| 271 | + | func (d osMaintenanceDirectory) Seek(offset int64, whence int) (int64, error) { | |||
| 272 | + | return syscall.Seek(int(d.file.Fd()), offset, whence) | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | func (d osMaintenanceDirectory) ReadDirent(buffer []byte) (int, error) { | |||
| 276 | + | return syscall.ReadDirent(int(d.file.Fd()), buffer) | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | func (d osMaintenanceDirectory) Close() error { return d.file.Close() } | |||
| 280 | + | ||||
| 281 | + | func (r *Repository) openMaintenanceDirectory(dir string) (maintenanceDirectory, error) { | |||
| 282 | + | if open := r.hooks.openMaintenanceDirectory; open != nil { | |||
| 283 | + | return open(dir) | |||
| 284 | + | } | |||
| 285 | + | file, err := os.Open(dir) | |||
| 286 | + | if err != nil { | |||
| 287 | + | return nil, err | |||
| 288 | + | } | |||
| 289 | + | return osMaintenanceDirectory{file: file}, nil | |||
| 290 | + | } | |||
| 291 | + | ||||
| 292 | + | // maintenanceDirectoryError adds stable operation context and removes any | |||
| 293 | + | // filesystem path while retaining the underlying cause for errors.Is. | |||
| 294 | + | func maintenanceDirectoryError(operation string, err error) error { | |||
| 295 | + | return fmt.Errorf("%s: %w", operation, safeFilesystemError(err)) | |||
| 296 | + | } | |||
| 297 | + | ||||
| 260 | // readMaintenanceDirent uses the Linux getdents seek cookie from each record. | 298 | // readMaintenanceDirent uses the Linux getdents seek cookie from each record. | ||
| 261 | // syscall.Dirent's Off field is the d_off member of linux_dirent64 and | 299 | // syscall.Dirent's Off field is the d_off member of linux_dirent64 and | ||
| 262 | // syscall.Seek is lseek(2), as defined by Go's syscall Linux sources. | 300 | // syscall.Seek is lseek(2), as defined by Go's syscall Linux sources. | ||
| 263 | - | func readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (entries []maintenanceDirEntry, done bool, err error) { | 301 | + | func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (entries []maintenanceDirEntry, done bool, err error) { |
| 264 | - | file, err := os.Open(dir) | 302 | + | file, err := r.openMaintenanceDirectory(dir) |
| 265 | if err != nil { | 303 | if err != nil { | ||
| 266 | - | return nil, false, err | 304 | + | return nil, false, maintenanceDirectoryError("open maintenance directory", err) |
| 267 | } | 305 | } | ||
| 268 | defer func() { | 306 | defer func() { | ||
| 269 | if closeErr := file.Close(); closeErr != nil { | 307 | if closeErr := file.Close(); closeErr != nil { | ||
| 270 | - | closeErr = fmt.Errorf("close maintenance directory %q: %w", dir, closeErr) | 308 | + | closeErr = maintenanceDirectoryError("close maintenance directory", closeErr) |
| 271 | if err != nil { | 309 | if err != nil { | ||
| 272 | err = errors.Join(err, closeErr) | 310 | err = errors.Join(err, closeErr) | ||
| 273 | } else { | 311 | } else { | ||
| @@ -277,19 +315,18 @@ func readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (en | |||||
| 277 | } | 315 | } | ||
| 278 | } | 316 | } | ||
| 279 | }() | 317 | }() | ||
| 280 | - | fd := int(file.Fd()) | |||
| 281 | if cursor.offset != 0 { | 318 | if cursor.offset != 0 { | ||
| 282 | - | if _, err := syscall.Seek(fd, cursor.offset, io.SeekStart); err != nil { | 319 | + | if _, err := file.Seek(cursor.offset, io.SeekStart); err != nil { |
| 283 | - | return nil, false, err | 320 | + | return nil, false, maintenanceDirectoryError("seek maintenance directory", err) |
| 284 | } | 321 | } | ||
| 285 | } | 322 | } | ||
| 286 | 323 | ||||
| 287 | entries = make([]maintenanceDirEntry, 0, limit) | 324 | entries = make([]maintenanceDirEntry, 0, limit) | ||
| 288 | buffer := make([]byte, 8192) | 325 | buffer := make([]byte, 8192) | ||
| 289 | for len(entries) < limit { | 326 | for len(entries) < limit { | ||
| 290 | - | count, err := syscall.ReadDirent(fd, buffer) | 327 | + | count, err := file.ReadDirent(buffer) |
| 291 | if err != nil { | 328 | if err != nil { | ||
| 292 | - | return nil, false, err | 329 | + | return nil, false, maintenanceDirectoryError("read maintenance directory", err) |
| 293 | } | 330 | } | ||
| 294 | if count == 0 { | 331 | if count == 0 { | ||
| 295 | return entries, true, nil | 332 | return entries, true, nil | ||
| @@ -319,7 +356,7 @@ func readMaintenanceDirent(dir string, limit int, cursor *maintenanceCursor) (en | |||||
| 319 | continue | 356 | continue | ||
| 320 | } | 357 | } | ||
| 321 | if err != nil { | 358 | if err != nil { | ||
| 322 | - | return nil, false, err | 359 | + | return nil, false, maintenanceDirectoryError("stat maintenance directory entry", err) |
| 323 | } | 360 | } | ||
| 324 | entries = append(entries, maintenanceDirEntry{name: name, isDir: info.IsDir()}) | 361 | entries = append(entries, maintenanceDirEntry{name: name, isDir: info.IsDir()}) | ||
| 325 | } | 362 | } | ||
M internal/adapters/snapshot/maintenance_test.go +74 -0
| @@ -13,6 +13,80 @@ import ( | |||
| 13 | 13 | "github.com/bnema/vev/internal/ports" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| 16 | + | func TestReadMaintenanceDirentPreservesCursorAndCloseFailuresWithoutPaths(t *testing.T) { | |
| 17 | + | privatePath := filepath.Join(t.TempDir(), "private-maintenance-directory") | |
| 18 | + | closeCause := errors.New("injected close failure") | |
| 19 | + | seekCause := errors.New("injected seek failure") | |
| 20 | + | readCause := errors.New("injected read failure") | |
| 21 | + | ||
| 22 | + | for _, tc := range []struct { | |
| 23 | + | name string | |
| 24 | + | offset int64 | |
| 25 | + | seekErr error | |
| 26 | + | readErr error | |
| 27 | + | wantCause []error | |
| 28 | + | wantOps []string | |
| 29 | + | }{ | |
| 30 | + | { | |
| 31 | + | name: "close only", | |
| 32 | + | wantCause: []error{closeCause}, | |
| 33 | + | wantOps: []string{"close maintenance directory"}, | |
| 34 | + | }, | |
| 35 | + | { | |
| 36 | + | name: "seek and close", | |
| 37 | + | offset: 1, | |
| 38 | + | seekErr: &os.PathError{Op: "seek", Path: privatePath, Err: seekCause}, | |
| 39 | + | wantCause: []error{seekCause, closeCause}, | |
| 40 | + | wantOps: []string{"seek maintenance directory", "close maintenance directory"}, | |
| 41 | + | }, | |
| 42 | + | { | |
| 43 | + | name: "read and close", | |
| 44 | + | readErr: &os.PathError{Op: "read", Path: privatePath, Err: readCause}, | |
| 45 | + | wantCause: []error{readCause, closeCause}, | |
| 46 | + | wantOps: []string{"read maintenance directory", "close maintenance directory"}, | |
| 47 | + | }, | |
| 48 | + | } { | |
| 49 | + | t.Run(tc.name, func(t *testing.T) { | |
| 50 | + | repo := NewRepository(privateDir(t)) | |
| 51 | + | repo.hooks.openMaintenanceDirectory = func(string) (maintenanceDirectory, error) { | |
| 52 | + | return fakeMaintenanceDirectory{ | |
| 53 | + | seekErr: tc.seekErr, | |
| 54 | + | readErr: tc.readErr, | |
| 55 | + | closeErr: &os.PathError{Op: "close", Path: privatePath, Err: closeCause}, | |
| 56 | + | }, nil | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | _, _, err := repo.readMaintenanceDirent(privatePath, 1, &maintenanceCursor{offset: tc.offset}) | |
| 60 | + | if err == nil { | |
| 61 | + | t.Fatal("readMaintenanceDirent error = nil, want failure") | |
| 62 | + | } | |
| 63 | + | for _, cause := range tc.wantCause { | |
| 64 | + | if !errors.Is(err, cause) { | |
| 65 | + | t.Errorf("errors.Is(%v, %v) = false, want true", err, cause) | |
| 66 | + | } | |
| 67 | + | } | |
| 68 | + | for _, operation := range tc.wantOps { | |
| 69 | + | if !strings.Contains(err.Error(), operation) { | |
| 70 | + | t.Errorf("error = %q, want stable operation context %q", err, operation) | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | if strings.Contains(err.Error(), privatePath) { | |
| 74 | + | t.Errorf("error leaks private path: %q", err) | |
| 75 | + | } | |
| 76 | + | }) | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | type fakeMaintenanceDirectory struct { | |
| 81 | + | seekErr error | |
| 82 | + | readErr error | |
| 83 | + | closeErr error | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | func (d fakeMaintenanceDirectory) Seek(int64, int) (int64, error) { return 0, d.seekErr } | |
| 87 | + | func (d fakeMaintenanceDirectory) ReadDirent([]byte) (int, error) { return 0, d.readErr } | |
| 88 | + | func (d fakeMaintenanceDirectory) Close() error { return d.closeErr } | |
| 89 | + | ||
| 16 | 90 | func TestRepositoryDeleteQuarantinesCanonicalSessionBeforeCleanup(t *testing.T) { | |
| 17 | 91 | repo := NewRepository(privateDir(t)) | |
| 18 | 92 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | |
| @@ -13,6 +13,80 @@ import ( | |||||
| 13 | "github.com/bnema/vev/internal/ports" | 13 | "github.com/bnema/vev/internal/ports" | ||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| 16 | + | func TestReadMaintenanceDirentPreservesCursorAndCloseFailuresWithoutPaths(t *testing.T) { | |||
| 17 | + | privatePath := filepath.Join(t.TempDir(), "private-maintenance-directory") | |||
| 18 | + | closeCause := errors.New("injected close failure") | |||
| 19 | + | seekCause := errors.New("injected seek failure") | |||
| 20 | + | readCause := errors.New("injected read failure") | |||
| 21 | + | ||||
| 22 | + | for _, tc := range []struct { | |||
| 23 | + | name string | |||
| 24 | + | offset int64 | |||
| 25 | + | seekErr error | |||
| 26 | + | readErr error | |||
| 27 | + | wantCause []error | |||
| 28 | + | wantOps []string | |||
| 29 | + | }{ | |||
| 30 | + | { | |||
| 31 | + | name: "close only", | |||
| 32 | + | wantCause: []error{closeCause}, | |||
| 33 | + | wantOps: []string{"close maintenance directory"}, | |||
| 34 | + | }, | |||
| 35 | + | { | |||
| 36 | + | name: "seek and close", | |||
| 37 | + | offset: 1, | |||
| 38 | + | seekErr: &os.PathError{Op: "seek", Path: privatePath, Err: seekCause}, | |||
| 39 | + | wantCause: []error{seekCause, closeCause}, | |||
| 40 | + | wantOps: []string{"seek maintenance directory", "close maintenance directory"}, | |||
| 41 | + | }, | |||
| 42 | + | { | |||
| 43 | + | name: "read and close", | |||
| 44 | + | readErr: &os.PathError{Op: "read", Path: privatePath, Err: readCause}, | |||
| 45 | + | wantCause: []error{readCause, closeCause}, | |||
| 46 | + | wantOps: []string{"read maintenance directory", "close maintenance directory"}, | |||
| 47 | + | }, | |||
| 48 | + | } { | |||
| 49 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 50 | + | repo := NewRepository(privateDir(t)) | |||
| 51 | + | repo.hooks.openMaintenanceDirectory = func(string) (maintenanceDirectory, error) { | |||
| 52 | + | return fakeMaintenanceDirectory{ | |||
| 53 | + | seekErr: tc.seekErr, | |||
| 54 | + | readErr: tc.readErr, | |||
| 55 | + | closeErr: &os.PathError{Op: "close", Path: privatePath, Err: closeCause}, | |||
| 56 | + | }, nil | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | _, _, err := repo.readMaintenanceDirent(privatePath, 1, &maintenanceCursor{offset: tc.offset}) | |||
| 60 | + | if err == nil { | |||
| 61 | + | t.Fatal("readMaintenanceDirent error = nil, want failure") | |||
| 62 | + | } | |||
| 63 | + | for _, cause := range tc.wantCause { | |||
| 64 | + | if !errors.Is(err, cause) { | |||
| 65 | + | t.Errorf("errors.Is(%v, %v) = false, want true", err, cause) | |||
| 66 | + | } | |||
| 67 | + | } | |||
| 68 | + | for _, operation := range tc.wantOps { | |||
| 69 | + | if !strings.Contains(err.Error(), operation) { | |||
| 70 | + | t.Errorf("error = %q, want stable operation context %q", err, operation) | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | if strings.Contains(err.Error(), privatePath) { | |||
| 74 | + | t.Errorf("error leaks private path: %q", err) | |||
| 75 | + | } | |||
| 76 | + | }) | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | type fakeMaintenanceDirectory struct { | |||
| 81 | + | seekErr error | |||
| 82 | + | readErr error | |||
| 83 | + | closeErr error | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | func (d fakeMaintenanceDirectory) Seek(int64, int) (int64, error) { return 0, d.seekErr } | |||
| 87 | + | func (d fakeMaintenanceDirectory) ReadDirent([]byte) (int, error) { return 0, d.readErr } | |||
| 88 | + | func (d fakeMaintenanceDirectory) Close() error { return d.closeErr } | |||
| 89 | + | ||||
| 16 | func TestRepositoryDeleteQuarantinesCanonicalSessionBeforeCleanup(t *testing.T) { | 90 | func TestRepositoryDeleteQuarantinesCanonicalSessionBeforeCleanup(t *testing.T) { | ||
| 17 | repo := NewRepository(privateDir(t)) | 91 | repo := NewRepository(privateDir(t)) | ||
| 18 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | 92 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | ||
M internal/adapters/snapshot/repository.go +13 -12
| @@ -48,18 +48,19 @@ type Repository struct { | |||
| 48 | 48 | // immediately before their respective syscall and are intentionally package | |
| 49 | 49 | // private so production callers cannot weaken repository guarantees. | |
| 50 | 50 | type repositoryHooks struct { | |
| 51 | - | beforeBlobWrite func(string) error | |
| 52 | - | beforeManifestWrite func(string) error | |
| 53 | - | beforeHeadWrite func(string) error | |
| 54 | - | createTemp func(string) error | |
| 55 | - | writeTemp func(string) error | |
| 56 | - | syncFile func(string) error | |
| 57 | - | closeFile func(string) error | |
| 58 | - | installImmutable func(string) error | |
| 59 | - | rename func(string) error | |
| 60 | - | syncDirectory func(string) error | |
| 61 | - | remove func(string) error | |
| 62 | - | beforeSessionLock func(string) | |
| 51 | + | beforeBlobWrite func(string) error | |
| 52 | + | beforeManifestWrite func(string) error | |
| 53 | + | beforeHeadWrite func(string) error | |
| 54 | + | createTemp func(string) error | |
| 55 | + | writeTemp func(string) error | |
| 56 | + | syncFile func(string) error | |
| 57 | + | closeFile func(string) error | |
| 58 | + | installImmutable func(string) error | |
| 59 | + | rename func(string) error | |
| 60 | + | syncDirectory func(string) error | |
| 61 | + | remove func(string) error | |
| 62 | + | openMaintenanceDirectory func(string) (maintenanceDirectory, error) | |
| 63 | + | beforeSessionLock func(string) | |
| 63 | 64 | } | |
| 64 | 65 | ||
| 65 | 66 | var _ ports.SnapshotRepository = (*Repository)(nil) | |
| @@ -48,18 +48,19 @@ type Repository struct { | |||||
| 48 | // immediately before their respective syscall and are intentionally package | 48 | // immediately before their respective syscall and are intentionally package | ||
| 49 | // private so production callers cannot weaken repository guarantees. | 49 | // private so production callers cannot weaken repository guarantees. | ||
| 50 | type repositoryHooks struct { | 50 | type repositoryHooks struct { | ||
| 51 | - | beforeBlobWrite func(string) error | 51 | + | beforeBlobWrite func(string) error |
| 52 | - | beforeManifestWrite func(string) error | 52 | + | beforeManifestWrite func(string) error |
| 53 | - | beforeHeadWrite func(string) error | 53 | + | beforeHeadWrite func(string) error |
| 54 | - | createTemp func(string) error | 54 | + | createTemp func(string) error |
| 55 | - | writeTemp func(string) error | 55 | + | writeTemp func(string) error |
| 56 | - | syncFile func(string) error | 56 | + | syncFile func(string) error |
| 57 | - | closeFile func(string) error | 57 | + | closeFile func(string) error |
| 58 | - | installImmutable func(string) error | 58 | + | installImmutable func(string) error |
| 59 | - | rename func(string) error | 59 | + | rename func(string) error |
| 60 | - | syncDirectory func(string) error | 60 | + | syncDirectory func(string) error |
| 61 | - | remove func(string) error | 61 | + | remove func(string) error |
| 62 | - | beforeSessionLock func(string) | 62 | + | openMaintenanceDirectory func(string) (maintenanceDirectory, error) |
| 63 | + | beforeSessionLock func(string) | |||
| 63 | } | 64 | } | ||
| 64 | 65 | ||||
| 65 | var _ ports.SnapshotRepository = (*Repository)(nil) | 66 | var _ ports.SnapshotRepository = (*Repository)(nil) | ||