-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathroot.go
452 lines (417 loc) · 13.8 KB
/
root.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
package ocfl
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"iter"
"path"
"github.com/srerickson/ocfl-go/extension"
)
const (
layoutConfigFile = "ocfl_layout.json"
descriptionKey = `description`
extensionKey = `extension`
extensionConfigFile = "config.json"
)
var ErrLayoutUndefined = errors.New("storage root's layout is undefined")
// Root represents an OCFL Storage Root.
type Root struct {
fs FS // root's fs
dir string // root's director relative to FS
spec Spec // OCFL spec version in storage root declaration
layout extension.Layout // layout used to resolve object ids
layoutConfig map[string]string // contents of `ocfl_layout.json`
// initArgs is used to initialize new root. Values
// are set by InitRoot option.
initArgs *initRootArgs
}
// NewRoot returns a new *[Root] for working with the OCFL storage root at
// directory dir in fsys. It can be used to initialize new storage roots if the
// [InitRoot] option is used, fsys is an ocfl.WriteFS, and dir is a non-existing
// or empty directory.
func NewRoot(ctx context.Context, fsys FS, dir string, opts ...RootOption) (*Root, error) {
r := &Root{fs: fsys, dir: dir}
for _, opt := range opts {
opt(r)
}
entries, err := fsys.ReadDir(ctx, dir)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
// try to inititializing a new storage root
if len(entries) < 1 && r.initArgs != nil {
if err := r.init(ctx); err != nil {
return nil, fmt.Errorf("initializing new storage root: %w", err)
}
return r, nil
}
// find storage root declaration
decl, err := FindNamaste(entries)
if err == nil && decl.Type != NamasteTypeStore {
err = fmt.Errorf("NAMASTE declaration has wrong type: %q", decl.Type)
}
if err != nil {
return nil, fmt.Errorf("not an OCFL storage root: %w", err)
}
if _, err := getOCFL(decl.Version); err != nil {
return nil, fmt.Errorf(" OCFL v%s: %w", decl.Version, err)
}
// initialize existing Root
r.spec = decl.Version
if err := r.getLayout(ctx); err != nil {
return nil, err
}
return r, nil
}
// Description returns the description string from the storage roots
// `ocfl_layout.json` file, which may be empty.
func (r *Root) Description() string {
return r.layoutConfig[descriptionKey]
}
// FS returns the Root's FS
func (r *Root) FS() FS {
return r.fs
}
// Layout returns the storage root's layout, which may be nil.ß
func (r *Root) Layout() extension.Layout {
return r.layout
}
// LayoutName returns the name of the root's layout extension or an empty string
// if the root has no layout.
func (r *Root) LayoutName() string {
if r.layout == nil {
return ""
}
return r.layout.Name()
}
// NewObject returns an *Object for managing the OCFL object with the given ID
// in the root. If the object does not exist, the returned *Object can be used
// to create it. If the Root has not storage layout for resovling object IDs,
// the returned error is ErrLayoutUndefined.
func (r *Root) NewObject(ctx context.Context, id string, opts ...ObjectOption) (*Object, error) {
objPath, err := r.ResolveID(id)
if err != nil {
return nil, err
}
opts = append(opts, objectExpectedID(id))
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) {
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
// the root has no layout, the returned error is ErrLayoutUndefined.
func (r *Root) ResolveID(id string) (string, error) {
if r.layout == nil {
return "", ErrLayoutUndefined
}
objPath, err := r.layout.Resolve(id)
if err != nil {
return "", fmt.Errorf("object id: %q: %w", id, err)
}
if !fs.ValidPath(objPath) {
return "", fmt.Errorf("layout resolved id to an invalid path: %s", objPath)
}
return objPath, nil
}
// ObjectDeclarations returns an iterator that yields all OCFL object
// declaration files in r. If an error occurs during iteration, it is returned
// by the error function.
func (r *Root) ObjectDeclarations(ctx context.Context) (FileSeq, func() error) {
allFiles, errFn := WalkFiles(ctx, r.fs, r.dir)
decls := allFiles.Filter(func(f *FileRef) bool { return f.Namaste().IsObject() })
return decls, errFn
}
// 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, 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, opts...) {
if !yield(obj, err) {
return
}
}
if err := listErr(); err != nil {
yield(nil, err)
}
}
}
// 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, 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, opts...)
for obj, err := range objs {
if !yield(obj, err) {
return
}
}
if err := listErr(); err != nil {
yield(nil, err)
}
}
}
// Path returns the root's dir relative to its FS
func (r *Root) Path() string {
return r.dir
}
// Spec returns the root's OCFL specification number
func (r *Root) Spec() Spec {
return r.spec
}
// ValidateObject validates the object with the given id. If the id cannot be
// resolved, the error is reported as a fatal error in the returned
// *ObjectValidation.
func (r *Root) ValidateObject(ctx context.Context, id string, opts ...ObjectValidationOption) *ObjectValidation {
objPath, err := r.ResolveID(id)
if err != nil {
v := newObjectValidation(r.fs, path.Join(r.dir, objPath), opts...)
v.AddFatal(err)
return v
}
return r.ValidateObjectDir(ctx, objPath, opts...)
}
// ValidateObjectDir validates the object at a path relative to the root.
func (r *Root) ValidateObjectDir(ctx context.Context, dir string, opts ...ObjectValidationOption) *ObjectValidation {
return ValidateObject(ctx, r.fs, path.Join(r.dir, dir), opts...)
}
func (r *Root) init(ctx context.Context) error {
if r.initArgs == nil {
return nil
}
if r.initArgs.spec.Empty() {
return errors.New("can't initialize storage root: missing OCFL spec version")
}
if _, err := getOCFL(r.initArgs.spec); err != nil {
return fmt.Errorf(" OCFL v%s: %w", r.initArgs.spec, err)
}
writeFS, isWriteFS := r.fs.(WriteFS)
if !isWriteFS {
return fmt.Errorf("storage root backend is not writable")
}
decl := Namaste{Version: r.initArgs.spec, Type: NamasteTypeStore}
if err := WriteDeclaration(ctx, writeFS, r.dir, decl); err != nil {
return err
}
r.spec = r.initArgs.spec
var haveLayout bool
for _, e := range r.initArgs.extensions {
layout, isLayout := e.(extension.Layout)
if isLayout && !haveLayout {
if err := r.setLayout(ctx, layout, r.initArgs.layoutDesc); err != nil {
return err
}
haveLayout = true
continue
}
if err := writeExtensionConfig(ctx, writeFS, r.dir, e); err != nil {
return err
}
}
return nil
}
func (r *Root) getLayout(ctx context.Context) error {
r.layoutConfig = nil
r.layout = nil
if err := r.readLayoutConfig(ctx); err != nil {
return err
}
if r.layoutConfig == nil || r.layoutConfig[extensionKey] == "" {
return nil
}
name := r.layoutConfig[extensionKey]
ext, err := readExtensionConfig(ctx, r.fs, r.dir, name)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
// Allow for missing extension config: If the config doesn't exist, use
// the extensions default values.
ext, err = extension.Get(name)
if err != nil {
return err
}
}
layout, ok := ext.(extension.Layout)
if !ok {
return fmt.Errorf("extension: %q: %w", name, extension.ErrNotLayout)
}
r.layout = layout
return nil
}
// readLayoutConfig reads the `ocfl_layout.json` files in the storage root
// and unmarshals into the value pointed to by layout
func (r *Root) readLayoutConfig(ctx context.Context) error {
f, err := r.fs.OpenFile(ctx, path.Join(r.dir, layoutConfigFile))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
r.layoutConfig = nil
return nil
}
return err
}
defer f.Close()
layout := map[string]string{}
if err := json.NewDecoder(f).Decode(&layout); err != nil {
return fmt.Errorf("decoding %s: %w", layoutConfigFile, err)
}
r.layoutConfig = layout
return nil
}
// setLayout marshals the value pointe to by layout and writes the result to
// the `ocfl_layout.json` files in the storage root.
func (r *Root) setLayout(ctx context.Context, layout extension.Layout, desc string) error {
writeFS, isWriteFS := r.fs.(WriteFS)
if !isWriteFS {
return fmt.Errorf("storage root backend is not writable")
}
layoutPath := path.Join(r.dir, layoutConfigFile)
if layout == nil && r.layout != nil {
r.layout = nil
r.layoutConfig = nil
// remove existing file
if err := writeFS.Remove(ctx, layoutPath); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}
return nil
}
newConfig := map[string]string{
extensionKey: layout.Name(),
descriptionKey: desc,
}
b, err := json.Marshal(newConfig)
if err != nil {
return fmt.Errorf("encoding %s: %w", layoutConfigFile, err)
}
_, err = writeFS.Write(ctx, layoutPath, bytes.NewBuffer(b))
if err != nil {
return fmt.Errorf("writing %s: %w", layoutConfigFile, err)
}
if err := writeExtensionConfig(ctx, writeFS, r.dir, layout); err != nil {
return fmt.Errorf("setting root layout extension: %w", err)
}
r.layoutConfig = newConfig
r.layout = layout
return nil
}
// readExtensionConfig reads the extension config file for ext in the storage root's
// extensions directory. The value is unmarshalled into the value pointed to by
// ext. If the extension config does not exist, nil is returned.
func readExtensionConfig(ctx context.Context, fsys FS, root string, name string) (extension.Extension, error) {
confPath := path.Join(root, extensionsDir, name, extensionConfigFile)
f, err := fsys.OpenFile(ctx, confPath)
if err != nil {
return nil, fmt.Errorf("can't open config for extension %s: %w", name, err)
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("reading config for extension %s: %w", name, err)
}
return extension.DefaultRegistry().Unmarshal(b)
}
// writeExtensionConfig writes the configuration files for the ext to the
// extensions directory in the storage root with at root.
func writeExtensionConfig(ctx context.Context, fsys WriteFS, root string, config extension.Extension) error {
confPath := path.Join(root, extensionsDir, config.Name(), extensionConfigFile)
b, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("encoding config for extension %s: %w", config.Name(), err)
}
_, err = fsys.Write(ctx, confPath, bytes.NewBuffer(b))
if err != nil {
return fmt.Errorf("writing config for extension %s: %w", config.Name(), err)
}
return nil
}
// RootOption is used to configure the behavior of [NewRoot]()
type RootOption func(*Root)
type initRootArgs struct {
spec Spec
layoutDesc string
extensions []extension.Extension
}
// InitRoot returns a RootOption for initializing a new storage root as part of
// the call to NewRoot().
func InitRoot(spec Spec, layoutDesc string, extensions ...extension.Extension) RootOption {
return func(root *Root) {
root.initArgs = &initRootArgs{
spec: spec,
layoutDesc: layoutDesc,
extensions: extensions,
}
}
}
// 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)
// }
// }
// }