From ccb6c2259728c6cad827ccb3163241e2f0a24be4 Mon Sep 17 00:00:00 2001 From: Seth Erickson Date: Fri, 20 Dec 2024 21:07:16 +0000 Subject: [PATCH] object knows its Root --- examples/listobjects/main.go | 3 +- files.go | 13 +-- files_test.go | 2 +- go.mod | 32 +++---- go.sum | 64 ++++++------- inventory.go | 4 +- object.go | 172 +++++++++++++++++------------------ ocfl.go | 2 +- ocflv1.go | 4 +- root.go | 124 +++++++++++++------------ root_test.go | 2 + validation.go | 2 +- 12 files changed, 212 insertions(+), 212 deletions(-) diff --git a/examples/listobjects/main.go b/examples/listobjects/main.go index 6d7f2479..66ba28c2 100644 --- a/examples/listobjects/main.go +++ b/examples/listobjects/main.go @@ -51,8 +51,7 @@ func listObjects(ctx context.Context, storeConn string, numgos int, log *slog.Lo log.Error(err.Error()) continue } - id := obj.Inventory().ID() - fmt.Println(id) + fmt.Println(obj.Inventory().ID()) } return nil } diff --git a/files.go b/files.go index 62bb4f8a..27f690a5 100644 --- a/files.go +++ b/files.go @@ -93,8 +93,9 @@ func (f *FileRef) Open(ctx context.Context) (fs.File, error) { // OpenObject opens the f's directory as an existing object. If the the // directory is not an object, an error is returned. -func (f *FileRef) OpenObject(ctx context.Context) (*Object, error) { - return NewObject(ctx, f.FS, f.FullPathDir(), ObjectMustExist()) +func (f *FileRef) OpenObject(ctx context.Context, opts ...ObjectOption) (*Object, error) { + opts = append(opts, ObjectMustExist()) + return NewObject(ctx, f.FS, f.FullPathDir(), opts...) } // Stat() calls StatFile on the file at f. @@ -181,10 +182,10 @@ func (files FileSeq) DigestBatch(ctx context.Context, numgos int, alg digest.Alg // OpenObjects returns an iterator with results from calling OpenObject() on // each *FileRef in files. Unlike [FileSeq.OpenObjectsBatch], objects are opened // sequentially, in the same goroutine as the caller. -func (files FileSeq) OpenObjects(ctx context.Context) iter.Seq2[*Object, error] { +func (files FileSeq) OpenObjects(ctx context.Context, opts ...ObjectOption) iter.Seq2[*Object, error] { return func(yield func(*Object, error) bool) { for file := range files { - if !yield(file.OpenObject(ctx)) { + if !yield(file.OpenObject(ctx, opts...)) { break } } @@ -194,8 +195,8 @@ func (files FileSeq) OpenObjects(ctx context.Context) iter.Seq2[*Object, error] // OpenObjectsBatch returns an iterator with results from calling OpenObjects() // on each *FileRef in files. Unlike [FileSeq.OpenObjects], objects are opened // in separate goroutines and may not be yielded in the same order as the input. -func (files FileSeq) OpenObjectsBatch(ctx context.Context, numgos int) iter.Seq2[*Object, error] { - openObj := func(ref *FileRef) (*Object, error) { return ref.OpenObject(ctx) } +func (files FileSeq) OpenObjectsBatch(ctx context.Context, numgos int, opts ...ObjectOption) iter.Seq2[*Object, error] { + openObj := func(ref *FileRef) (*Object, error) { return ref.OpenObject(ctx, opts...) } filesSeq := iter.Seq[*FileRef](files) return func(yield func(*Object, error) bool) { for result := range pipeline.Results(filesSeq, openObj, numgos) { diff --git a/files_test.go b/files_test.go index a853b9cb..f0becf0b 100644 --- a/files_test.go +++ b/files_test.go @@ -256,7 +256,7 @@ func TestValidateFileDigests(t *testing.T) { fixtureFiles, walkFn := ocfl.WalkFiles(ctx, testdataFS, "content-fixture") fixtureResults := fixtureFiles.Digest(ctx, digest.SHA256, digest.SHA1, digest.MD5) var makeInvalid ocfl.FileDigestsSeq = func(yield func(*ocfl.FileDigests) bool) { - for digests, _ := range fixtureResults { + for digests := range fixtureResults { for id := range digests.Digests { digests.Digests[id] = "bad value" break // just one bad value diff --git a/go.mod b/go.mod index 7a662a05..b48042d7 100644 --- a/go.mod +++ b/go.mod @@ -3,33 +3,33 @@ module github.com/srerickson/ocfl-go go 1.23 require ( - github.com/aws/aws-sdk-go-v2 v1.32.6 - github.com/aws/aws-sdk-go-v2/config v1.28.6 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43 - github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0 + github.com/aws/aws-sdk-go-v2 v1.32.7 + github.com/aws/aws-sdk-go-v2/config v1.28.7 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.44 + github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1 github.com/carlmjohnson/be v0.23.2 github.com/charmbracelet/log v0.4.0 github.com/hashicorp/go-multierror v1.1.1 golang.org/x/crypto v0.31.0 - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 golang.org/x/sync v0.10.0 ) require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.47 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.48 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect github.com/aws/smithy-go v1.22.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/lipgloss v1.0.0 // indirect diff --git a/go.sum b/go.sum index 212db9c7..c64d73f1 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,39 @@ -github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= -github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw= +github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc= -github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo= -github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko= -github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw= -github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43 h1:iLdpkYZ4cXIQMO7ud+cqMWR1xK5ESbt1rvN77tRi1BY= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.43/go.mod h1:OgbsKPAswXDd5kxnR4vZov69p3oYjbvUyIRBAAV0y9o= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= +github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE= +github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.44 h1:2zxMLXLedpB4K1ilbJFxtMKsVKaexOqDttOhc0QGm3Q= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.44/go.mod h1:VuLHdqwjSvgftNC7yqPWyGVhEwPmJpeRi07gOgOfHF8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25 h1:r67ps7oHCYnflpgDy2LZU0MAQtQbYIOqNNnqGO6xQkE= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.25/go.mod h1:GrGY+Q4fIokYLtjCVB/aFfCVL6hhGUFl8inD18fDalE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26 h1:GeNJsIFHB+WW5ap2Tec4K6dzcVTsRbsT1Lra46Hv9ME= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.26/go.mod h1:zfgMpwHDXX2WGoG84xG2H+ZlPTkJUU4YUvx2svLQYWo= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6 h1:HCpPsWqmYQieU7SS6E9HXfdAMSud0pteVXieJmcpIRI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.6/go.mod h1:ngUiVRCco++u+soRRVBIvBZxSMMvOVMXA4PJ36JLfSw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6 h1:BbGDtTi0T1DYlmjBiCr/le3wzhA37O8QTC5/Ab8+EXk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.6/go.mod h1:hLMJt7Q8ePgViKupeymbqI0la+t9/iYFBjxQCFwuAwI= -github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0 h1:nyuzXooUNJexRT0Oy0UQY6AhOzxPxhtt4DcBIHyCnmw= -github.com/aws/aws-sdk-go-v2/service/s3 v1.71.0/go.mod h1:sT/iQz8JK3u/5gZkT+Hmr7GzVZehUMkRZpOaAwYXeGY= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7 h1:tB4tNw83KcajNAzaIMhkhVI2Nt8fAZd5A5ro113FEMY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.7/go.mod h1:lvpyBGkZ3tZ9iSsUIcC2EWp+0ywa7aK3BLT+FwZi+mQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7 h1:Hi0KGbrnr57bEHWM0bJ1QcBzxLrL/k2DHvGYhb8+W1w= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.7/go.mod h1:wKNgWgExdjjrm4qvfbTorkvocEstaoDl4WCvGfeCy9c= +github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1 h1:aOVVZJgWbaH+EJYPvEgkNhCEbXXvH7+oML36oaPK3zE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.71.1/go.mod h1:r+xl5yzMk9083rMR+sJ5TYj9Tihvf/l1oxzZXDgGj2Q= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc= github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -72,8 +72,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/inventory.go b/inventory.go index 0a25624a..e8b0a5d8 100644 --- a/inventory.go +++ b/inventory.go @@ -79,7 +79,7 @@ func ReadSidecarDigest(ctx context.Context, fsys FS, name string) (string, error } // ValidateInventoryBytes parses and fully validates the byts as contents of an -// inventory.json file. +// inventory.json file. This is mostly used for testing. func ValidateInventoryBytes(byts []byte) (Inventory, *Validation) { imp, _ := getInventoryOCFL(byts) if imp == nil { @@ -114,7 +114,7 @@ func validateInventory(inv Inventory) *Validation { imp, err := getOCFL(inv.Spec()) if err != nil { v := &Validation{} - err := fmt.Errorf("inventory uses unknown or unspecified OCFL version") + err := fmt.Errorf("inventory has invalid 'type':%w", err) v.AddFatal(err) return v } diff --git a/object.go b/object.go index feeff813..d0816029 100644 --- a/object.go +++ b/object.go @@ -23,13 +23,14 @@ type Object struct { fs FS // path in FS for object root directory path string - // object's root inventory + // object's root inventory. May be nil if the object doesn't (yet) exist. inventory Inventory // object id used to open the object from the root expectID string // the object must exist: don't create a new object. mustExist bool - //TODO pointer to object's storage root. + // object's storage root + root *Root } // NewObject returns an *Object for managing the OCFL object at path in fsys. @@ -42,15 +43,11 @@ func NewObject(ctx context.Context, fsys FS, dir string, opts ...ObjectOption) ( // read root inventory: we don't know what OCFL spec it uses. inv, err := ReadInventory(ctx, fsys, dir) if err != nil { - var pthError *fs.PathError - if !errors.As(err, &pthError) { - return nil, err - } - if path.Base(pthError.Path) != inventoryBase { - // error is not from opening `inventory.json` - return nil, err + // continue of err is ErrNotExist and !mustExist + if !obj.mustExist && errors.Is(err, fs.ErrNotExist) { + err = nil } - if !errors.Is(err, fs.ErrNotExist) || obj.mustExist { + if err != nil { return nil, err } } @@ -61,12 +58,12 @@ func NewObject(ctx context.Context, fsys FS, dir string, opts ...ObjectOption) ( err := fmt.Errorf("object has unexpected ID: %q; expected: %q", inv.ID(), obj.expectID) return nil, err } - obj.setInventory(inv) + obj.inventory = inv return obj, nil } - // inventory.json doesn't exist: open as uninitialized object. The object - // root directory must not exist or be an empty directory. Note, the object's - // ocfl implementation is not set! + // inventory doesn't exist: open as uninitialized object. The object + // root directory must not exist or be an empty directory. the object's + // inventory is nil. entries, err := fsys.ReadDir(ctx, dir) if err != nil { if !errors.Is(err, fs.ErrNotExist) { @@ -84,15 +81,6 @@ func NewObject(ctx context.Context, fsys FS, dir string, opts ...ObjectOption) ( } } -// create a new *Object with required feilds and apply options -func newObject(fsys FS, dir string, opts ...ObjectOption) *Object { - obj := &Object{fs: fsys, path: dir} - for _, optFn := range opts { - optFn(obj) - } - return obj -} - // Commit creates a new object version based on values in commit. func (obj *Object) Commit(ctx context.Context, commit *Commit) error { if _, isWriteFS := obj.FS().(WriteFS); !isWriteFS { @@ -137,7 +125,7 @@ func (obj *Object) Commit(ctx context.Context, commit *Commit) error { return nil } -// Exists returns true if the object has an existing version. +// Exists returns true if the object's inventory exists. func (obj *Object) Exists() bool { return obj.inventory != nil } @@ -184,28 +172,19 @@ func (obj *Object) Path() string { // index (1...HEAD). If i is 0, the most recent version is used. func (obj *Object) OpenVersion(ctx context.Context, i int) (*ObjectVersionFS, error) { if !obj.Exists() { - return nil, ErrNamasteNotExist + // FIXME: unified error to use here? + return nil, errors.New("object has no versions to open") } inv := obj.Inventory() - if inv == nil { - // FIXME; better error - return nil, errors.New("object is missing an inventory") - } if i == 0 { i = inv.Head().num } ver := inv.Version(i) if ver == nil { - // FIXME; better error - return nil, errors.New("version not found") - } - ioFS := obj.VersionFS(ctx, i) - if ioFS == nil { - // FIXME; better error - return nil, errors.New("version not found") + return nil, fmt.Errorf("object has no version with index %d", i) } vfs := &ObjectVersionFS{ - fsys: ioFS, + fsys: obj.versionFS(ctx, ver), ver: ver, num: i, inv: inv, @@ -213,8 +192,40 @@ func (obj *Object) OpenVersion(ctx context.Context, i int) (*ObjectVersionFS, er return vfs, nil } -func (obj *Object) setInventory(inv Inventory) { - obj.inventory = inv +// Root returns the object's Root, if known. It is nil unless the *Object was +// created using [Root.NewObject] +func (o *Object) Root() *Root { + return o.root +} + +func (o *Object) versionFS(ctx context.Context, ver ObjectVersion) fs.FS { + // FIXME: This is a hack to make versionFS replicates the filemode of + // the undering FS. Open a random content file to get the file mode used by + // the underlying FS. + regfileType := fs.FileMode(0) + for _, paths := range o.inventory.Manifest() { + if len(paths) < 1 { + continue + } + f, err := o.fs.OpenFile(ctx, path.Join(o.path, paths[0])) + if err != nil { + continue + } + defer f.Close() + info, err := f.Stat() + if err != nil { + continue + } + regfileType = info.Mode().Type() + break + } + return &versionFS{ + ctx: ctx, + obj: o, + paths: ver.State().PathMap(), + created: ver.Created(), + regMode: regfileType, + } } // ValidateObject fully validates the OCFL Object at dir in fsys @@ -336,56 +347,6 @@ func (vfs *ObjectVersionFS) Stage() *Stage { } } -// ObjectOptions are used to configure the behavior of NewObject() -type ObjectOption func(*Object) - -// ObjectMustExists requires the object to exist -func ObjectMustExist() ObjectOption { - return func(o *Object) { - o.mustExist = true - } -} - -func objectExpectedID(id string) ObjectOption { - return func(o *Object) { - o.expectID = id - } -} - -func (o *Object) VersionFS(ctx context.Context, i int) fs.FS { - ver := o.inventory.Version(i) - if ver == nil { - return nil - } - // FIXME: This is a hack to make versionFS replicates the filemode of - // the undering FS. Open a random content file to get the file mode used by - // the underlying FS. - regfileType := fs.FileMode(0) - for _, paths := range o.inventory.Manifest() { - if len(paths) < 1 { - break - } - f, err := o.fs.OpenFile(ctx, path.Join(o.path, paths[0])) - if err != nil { - return nil - } - defer f.Close() - info, err := f.Stat() - if err != nil { - return nil - } - regfileType = info.Mode().Type() - break - } - return &versionFS{ - ctx: ctx, - obj: o, - paths: ver.State().PathMap(), - created: ver.Created(), - regMode: regfileType, - } -} - type versionFS struct { ctx context.Context obj *Object @@ -544,3 +505,36 @@ func (dir *vfsDirFile) Read(_ []byte) (int, error) { return 0, nil } func (dir *vfsDirFile) Size() int64 { return 0 } func (dir *vfsDirFile) Stat() (fs.FileInfo, error) { return dir, nil } func (dir *vfsDirFile) Sys() any { return nil } + +// create a new *Object with required feilds and apply options +func newObject(fsys FS, dir string, opts ...ObjectOption) *Object { + obj := &Object{fs: fsys, path: dir} + for _, optFn := range opts { + optFn(obj) + } + return obj +} + +// ObjectOptions are used to configure the behavior of NewObject() +type ObjectOption func(*Object) + +// ObjectMustExists requires the object to exist +func ObjectMustExist() ObjectOption { + return func(o *Object) { + o.mustExist = true + } +} + +// objectExpectedID is an ObjectOption to set the expected ID (i.e., from ) +func objectExpectedID(id string) ObjectOption { + return func(o *Object) { + o.expectID = id + } +} + +// objectWithRoot is an ObjectOption that sets the object's storage root +func objectWithRoot(root *Root) ObjectOption { + return func(o *Object) { + o.root = root + } +} diff --git a/ocfl.go b/ocfl.go index 5297adc9..6196bc07 100644 --- a/ocfl.go +++ b/ocfl.go @@ -52,7 +52,7 @@ type ocfl interface { ValidateObjectRoot(ctx context.Context, v *ObjectValidation, state *ObjectState) error // Validate all contents of an object version directory and add contents to the object validation ValidateObjectVersion(ctx context.Context, v *ObjectValidation, vnum VNum, versionInv, prevInv Inventory) error - // Validate contents added to the object validation + // Validate contents added to the object validation. ValidateObjectContent(ctx context.Context, v *ObjectValidation) error } diff --git a/ocflv1.go b/ocflv1.go index 66987a9d..e42c9ec8 100644 --- a/ocflv1.go +++ b/ocflv1.go @@ -23,7 +23,7 @@ import ( "golang.org/x/sync/errgroup" ) -// ocflv1 is animplementation of ocfl v1.x +// ocflv1 is an implementation of ocfl v1.x type ocflV1 struct { v1Spec Spec // "1.0" or "1.1" } @@ -509,7 +509,7 @@ func (imp ocflV1) Commit(ctx context.Context, obj *Object, commit *Commit) error err = fmt.Errorf("writing new inventories or inventory sidecars: %w", err) return &CommitError{Err: err, Dirty: true} } - obj.setInventory(newInv) + obj.inventory = newInv return nil } diff --git a/root.go b/root.go index 1a25b725..4c8b7177 100644 --- a/root.go +++ b/root.go @@ -110,14 +110,16 @@ func (r *Root) NewObject(ctx context.Context, id string, opts ...ObjectOption) ( return nil, err } opts = append(opts, objectExpectedID(id)) - return NewObject(ctx, r.fs, path.Join(r.dir, objPath), opts...) + return r.NewObjectDir(ctx, objPath, opts...) } // NewObjectDir returns an *Object for managing the OCFL object at path dir in // root. If the object does not exist, the returned *Object can be used to // create it. func (r *Root) NewObjectDir(ctx context.Context, dir string, opts ...ObjectOption) (*Object, error) { - return NewObject(ctx, r.fs, path.Join(r.dir, dir), opts...) + opts = append(opts, objectWithRoot(r)) + objPath := path.Join(r.dir, dir) + return NewObject(ctx, r.fs, objPath, opts...) } // ResolveID resolves the object id to a path relative to the root. If @@ -147,10 +149,11 @@ func (r *Root) ObjectDeclarations(ctx context.Context) (FileSeq, func() error) { // Objects returns an iterator that yields objects or an error for every object // declaration file in the root. -func (r *Root) Objects(ctx context.Context) iter.Seq2[*Object, error] { +func (r *Root) Objects(ctx context.Context, opts ...ObjectOption) iter.Seq2[*Object, error] { return func(yield func(*Object, error) bool) { + opts = append(opts, objectWithRoot(r)) decls, listErr := r.ObjectDeclarations(ctx) - for obj, err := range decls.OpenObjects(ctx) { + for obj, err := range decls.OpenObjects(ctx, opts...) { if !yield(obj, err) { return } @@ -163,12 +166,13 @@ func (r *Root) Objects(ctx context.Context) iter.Seq2[*Object, error] { // ObjectsBatch returns an iterator that uses [FileSeq.OpenObjectsBatch] to open // objects in the root in numgos separate goroutines, yielding the results -func (r *Root) ObjectsBatch(ctx context.Context, numgos int) iter.Seq2[*Object, error] { +func (r *Root) ObjectsBatch(ctx context.Context, numgos int, opts ...ObjectOption) iter.Seq2[*Object, error] { return func(yield func(*Object, error) bool) { allFiles, listErr := WalkFiles(ctx, r.fs, r.dir) + opts = append(opts, objectWithRoot(r)) objs := allFiles. Filter(func(f *FileRef) bool { return f.Namaste().IsObject() }). - OpenObjectsBatch(ctx, numgos) + OpenObjectsBatch(ctx, numgos, opts...) for obj, err := range objs { if !yield(obj, err) { return @@ -388,61 +392,61 @@ func InitRoot(spec Spec, layoutDesc string, extensions ...extension.Extension) R } // TODO: export a function for iterating of object root's directory entries. -// + // WalkObjectDirs returns an iterator that walks r's directory structure, // yielding paths and directory entries for all objects. If an error is // encountered, iteration terminates. The terminating error is accessed with the // returned error function. -func (r *Root) walkObjectDirs(ctx context.Context) (iter.Seq2[string, []fs.DirEntry], func() error) { - var walkErr error - dirs := func(yield func(string, []fs.DirEntry) bool) { - for dir, err := range r.walkDirs(ctx) { - if err != nil { - walkErr = err - break - } - if ParseObjectDir(dir.entries).HasNamaste() { - if !yield(dir.path, dir.entries) { - break - } - } - } - } - return dirs, func() error { return walkErr } -} - -func (root *Root) walkDirs(ctx context.Context) iter.Seq2[*rootDirRef, error] { - return func(yield func(*rootDirRef, error) bool) { - root.walkDir(ctx, &rootDirRef{path: "."}, yield) - } -} - -// rootDirRef is a directory in a storage root -type rootDirRef struct { - path string // path relative to the storage root - entries []fs.DirEntry -} - -// walkDir reads dir, yields the result, and calls walkDir on each subdirectory -// unless dir is an object root -func (root *Root) walkDir(ctx context.Context, ref *rootDirRef, yield func(*rootDirRef, error) bool) { - var err error - ref.entries, err = root.fs.ReadDir(ctx, path.Join(root.dir, ref.path)) - if !yield(ref, err) { - return - } - if len(ref.entries) < 1 { - return - } - if ParseObjectDir(ref.entries).HasNamaste() { - // don't descend below object root directory - return - } - // TODO: don't descent below extension directories - for _, e := range ref.entries { - if e.IsDir() { - next := &rootDirRef{path: path.Join(ref.path, e.Name())} - root.walkDir(ctx, next, yield) - } - } -} +// func (r *Root) walkObjectDirs(ctx context.Context) (iter.Seq2[string, []fs.DirEntry], func() error) { +// var walkErr error +// dirs := func(yield func(string, []fs.DirEntry) bool) { +// for dir, err := range r.walkDirs(ctx) { +// if err != nil { +// walkErr = err +// break +// } +// if ParseObjectDir(dir.entries).HasNamaste() { +// if !yield(dir.path, dir.entries) { +// break +// } +// } +// } +// } +// return dirs, func() error { return walkErr } +// } + +// func (root *Root) walkDirs(ctx context.Context) iter.Seq2[*rootDirRef, error] { +// return func(yield func(*rootDirRef, error) bool) { +// root.walkDir(ctx, &rootDirRef{path: "."}, yield) +// } +// } + +// // rootDirRef is a directory in a storage root +// type rootDirRef struct { +// path string // path relative to the storage root +// entries []fs.DirEntry +// } + +// // walkDir reads dir, yields the result, and calls walkDir on each subdirectory +// // unless dir is an object root +// func (root *Root) walkDir(ctx context.Context, ref *rootDirRef, yield func(*rootDirRef, error) bool) { +// var err error +// ref.entries, err = root.fs.ReadDir(ctx, path.Join(root.dir, ref.path)) +// if !yield(ref, err) { +// return +// } +// if len(ref.entries) < 1 { +// return +// } +// if ParseObjectDir(ref.entries).HasNamaste() { +// // don't descend below object root directory +// return +// } +// // TODO: don't descent below extension directories +// for _, e := range ref.entries { +// if e.IsDir() { +// next := &rootDirRef{path: path.Join(ref.path, e.Name())} +// root.walkDir(ctx, next, yield) +// } +// } +// } diff --git a/root_test.go b/root_test.go index 0fe2ec7f..bc501c21 100644 --- a/root_test.go +++ b/root_test.go @@ -85,6 +85,7 @@ func TestRoot(t *testing.T) { for obj, err := range root.Objects(ctx) { be.NilErr(t, err) count++ + be.Equal(t, root, obj.Root()) be.True(t, obj.Exists()) } be.Equal(t, 3, count) @@ -102,6 +103,7 @@ func TestRoot(t *testing.T) { be.NilErr(t, err) count++ be.True(t, obj.Exists()) + be.Equal(t, root, obj.Root()) } be.Equal(t, 3, count) }) diff --git a/validation.go b/validation.go index 05670273..dbdc2281 100644 --- a/validation.go +++ b/validation.go @@ -235,7 +235,7 @@ func (v *ObjectValidation) addInventory(inv Inventory, isRoot bool) error { return err } if isRoot { - v.obj.setInventory(inv) + v.obj.inventory = inv } return nil }