rootmapping_fs.go (15269B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package hugofs
15
16 import (
17 "fmt"
18 "os"
19 "path/filepath"
20 "strings"
21
22 "github.com/gohugoio/hugo/hugofs/files"
23
24 radix "github.com/armon/go-radix"
25 "github.com/spf13/afero"
26 )
27
28 var filepathSeparator = string(filepath.Separator)
29
30 // NewRootMappingFs creates a new RootMappingFs on top of the provided with
31 // root mappings with some optional metadata about the root.
32 // Note that From represents a virtual root that maps to the actual filename in To.
33 func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
34 rootMapToReal := radix.New()
35 var virtualRoots []RootMapping
36
37 for _, rm := range rms {
38 (&rm).clean()
39
40 fromBase := files.ResolveComponentFolder(rm.From)
41
42 if len(rm.To) < 2 {
43 panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To))
44 }
45
46 fi, err := fs.Stat(rm.To)
47 if err != nil {
48 if os.IsNotExist(err) {
49 continue
50 }
51 return nil, err
52 }
53 // Extract "blog" from "content/blog"
54 rm.path = strings.TrimPrefix(strings.TrimPrefix(rm.From, fromBase), filepathSeparator)
55 if rm.Meta == nil {
56 rm.Meta = NewFileMeta()
57 }
58
59 rm.Meta.SourceRoot = rm.To
60 rm.Meta.BaseDir = rm.ToBasedir
61 rm.Meta.MountRoot = rm.path
62 rm.Meta.Module = rm.Module
63 rm.Meta.IsProject = rm.IsProject
64
65 meta := rm.Meta.Copy()
66
67 if !fi.IsDir() {
68 _, name := filepath.Split(rm.From)
69 meta.Name = name
70 }
71
72 rm.fi = NewFileMetaInfo(fi, meta)
73
74 key := filepathSeparator + rm.From
75 var mappings []RootMapping
76 v, found := rootMapToReal.Get(key)
77 if found {
78 // There may be more than one language pointing to the same root.
79 mappings = v.([]RootMapping)
80 }
81 mappings = append(mappings, rm)
82 rootMapToReal.Insert(key, mappings)
83
84 virtualRoots = append(virtualRoots, rm)
85 }
86
87 rootMapToReal.Insert(filepathSeparator, virtualRoots)
88
89 rfs := &RootMappingFs{
90 Fs: fs,
91 rootMapToReal: rootMapToReal,
92 }
93
94 return rfs, nil
95 }
96
97 func newRootMappingFsFromFromTo(
98 baseDir string,
99 fs afero.Fs,
100 fromTo ...string,
101 ) (*RootMappingFs, error) {
102 rms := make([]RootMapping, len(fromTo)/2)
103 for i, j := 0, 0; j < len(fromTo); i, j = i+1, j+2 {
104 rms[i] = RootMapping{
105 From: fromTo[j],
106 To: fromTo[j+1],
107 ToBasedir: baseDir,
108 }
109 }
110
111 return NewRootMappingFs(fs, rms...)
112 }
113
114 // RootMapping describes a virtual file or directory mount.
115 type RootMapping struct {
116 From string // The virtual mount.
117 To string // The source directory or file.
118 ToBasedir string // The base of To. May be empty if an absolute path was provided.
119 Module string // The module path/ID.
120 IsProject bool // Whether this is a mount in the main project.
121 Meta *FileMeta // File metadata (lang etc.)
122
123 fi FileMetaInfo
124 path string // The virtual mount point, e.g. "blog".
125
126 }
127
128 type keyRootMappings struct {
129 key string
130 roots []RootMapping
131 }
132
133 func (rm *RootMapping) clean() {
134 rm.From = strings.Trim(filepath.Clean(rm.From), filepathSeparator)
135 rm.To = filepath.Clean(rm.To)
136 }
137
138 func (r RootMapping) filename(name string) string {
139 if name == "" {
140 return r.To
141 }
142 return filepath.Join(r.To, strings.TrimPrefix(name, r.From))
143 }
144
145 func (r RootMapping) trimFrom(name string) string {
146 if name == "" {
147 return ""
148 }
149 return strings.TrimPrefix(name, r.From)
150 }
151
152 var (
153 _ FilesystemUnwrapper = (*RootMappingFs)(nil)
154 )
155
156 // A RootMappingFs maps several roots into one. Note that the root of this filesystem
157 // is directories only, and they will be returned in Readdir and Readdirnames
158 // in the order given.
159 type RootMappingFs struct {
160 afero.Fs
161 rootMapToReal *radix.Tree
162 }
163
164 func (fs *RootMappingFs) Dirs(base string) ([]FileMetaInfo, error) {
165 base = filepathSeparator + fs.cleanName(base)
166 roots := fs.getRootsWithPrefix(base)
167
168 if roots == nil {
169 return nil, nil
170 }
171
172 fss := make([]FileMetaInfo, len(roots))
173 for i, r := range roots {
174 bfs := afero.NewBasePathFs(fs.Fs, r.To)
175 bfs = decoratePath(bfs, func(name string) string {
176 p := strings.TrimPrefix(name, r.To)
177 if r.path != "" {
178 // Make sure it's mounted to a any sub path, e.g. blog
179 p = filepath.Join(r.path, p)
180 }
181 p = strings.TrimLeft(p, filepathSeparator)
182 return p
183 })
184
185 fs := bfs
186 if r.Meta.InclusionFilter != nil {
187 fs = newFilenameFilterFs(fs, r.To, r.Meta.InclusionFilter)
188 }
189 fs = decorateDirs(fs, r.Meta)
190 fi, err := fs.Stat("")
191 if err != nil {
192 return nil, fmt.Errorf("RootMappingFs.Dirs: %w", err)
193 }
194
195 if !fi.IsDir() {
196 fi.(FileMetaInfo).Meta().Merge(r.Meta)
197 }
198
199 fss[i] = fi.(FileMetaInfo)
200 }
201
202 return fss, nil
203 }
204
205 func (fs *RootMappingFs) UnwrapFilesystem() afero.Fs {
206 return fs.Fs
207 }
208
209 // Filter creates a copy of this filesystem with only mappings matching a filter.
210 func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs {
211 rootMapToReal := radix.New()
212 fs.rootMapToReal.Walk(func(b string, v any) bool {
213 rms := v.([]RootMapping)
214 var nrms []RootMapping
215 for _, rm := range rms {
216 if f(rm) {
217 nrms = append(nrms, rm)
218 }
219 }
220 if len(nrms) != 0 {
221 rootMapToReal.Insert(b, nrms)
222 }
223 return false
224 })
225
226 fs.rootMapToReal = rootMapToReal
227
228 return &fs
229 }
230
231 // LstatIfPossible returns the os.FileInfo structure describing a given file.
232 func (fs *RootMappingFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
233 fis, err := fs.doLstat(name)
234 if err != nil {
235 return nil, false, err
236 }
237 return fis[0], false, nil
238 }
239
240 // Open opens the named file for reading.
241 func (fs *RootMappingFs) Open(name string) (afero.File, error) {
242 fis, err := fs.doLstat(name)
243 if err != nil {
244 return nil, err
245 }
246
247 return fs.newUnionFile(fis...)
248 }
249
250 // Stat returns the os.FileInfo structure describing a given file. If there is
251 // an error, it will be of type *os.PathError.
252 func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) {
253 fi, _, err := fs.LstatIfPossible(name)
254 return fi, err
255 }
256
257 func (fs *RootMappingFs) hasPrefix(prefix string) bool {
258 hasPrefix := false
259 fs.rootMapToReal.WalkPrefix(prefix, func(b string, v any) bool {
260 hasPrefix = true
261 return true
262 })
263
264 return hasPrefix
265 }
266
267 func (fs *RootMappingFs) getRoot(key string) []RootMapping {
268 v, found := fs.rootMapToReal.Get(key)
269 if !found {
270 return nil
271 }
272
273 return v.([]RootMapping)
274 }
275
276 func (fs *RootMappingFs) getRoots(key string) (string, []RootMapping) {
277 s, v, found := fs.rootMapToReal.LongestPrefix(key)
278 if !found || (s == filepathSeparator && key != filepathSeparator) {
279 return "", nil
280 }
281 return s, v.([]RootMapping)
282 }
283
284 func (fs *RootMappingFs) debug() {
285 fmt.Println("debug():")
286 fs.rootMapToReal.Walk(func(s string, v any) bool {
287 fmt.Println("Key", s)
288 return false
289 })
290 }
291
292 func (fs *RootMappingFs) getRootsWithPrefix(prefix string) []RootMapping {
293 var roots []RootMapping
294 fs.rootMapToReal.WalkPrefix(prefix, func(b string, v any) bool {
295 roots = append(roots, v.([]RootMapping)...)
296 return false
297 })
298
299 return roots
300 }
301
302 func (fs *RootMappingFs) getAncestors(prefix string) []keyRootMappings {
303 var roots []keyRootMappings
304 fs.rootMapToReal.WalkPath(prefix, func(s string, v any) bool {
305 if strings.HasPrefix(prefix, s+filepathSeparator) {
306 roots = append(roots, keyRootMappings{
307 key: s,
308 roots: v.([]RootMapping),
309 })
310 }
311 return false
312 })
313
314 return roots
315 }
316
317 func (fs *RootMappingFs) newUnionFile(fis ...FileMetaInfo) (afero.File, error) {
318 meta := fis[0].Meta()
319 f, err := meta.Open()
320 if err != nil {
321 return nil, err
322 }
323 if len(fis) == 1 {
324 return f, nil
325 }
326
327 rf := &rootMappingFile{File: f, fs: fs, name: meta.Name, meta: meta}
328 if len(fis) == 1 {
329 return rf, err
330 }
331
332 next, err := fs.newUnionFile(fis[1:]...)
333 if err != nil {
334 return nil, err
335 }
336
337 uf := &afero.UnionFile{Base: rf, Layer: next}
338
339 uf.Merger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) {
340 // Ignore duplicate directory entries
341 seen := make(map[string]bool)
342 var result []os.FileInfo
343
344 for _, fis := range [][]os.FileInfo{bofi, lofi} {
345 for _, fi := range fis {
346
347 if fi.IsDir() && seen[fi.Name()] {
348 continue
349 }
350
351 if fi.IsDir() {
352 seen[fi.Name()] = true
353 }
354
355 result = append(result, fi)
356 }
357 }
358
359 return result, nil
360 }
361
362 return uf, nil
363 }
364
365 func (fs *RootMappingFs) cleanName(name string) string {
366 return strings.Trim(filepath.Clean(name), filepathSeparator)
367 }
368
369 func (fs *RootMappingFs) collectDirEntries(prefix string) ([]os.FileInfo, error) {
370 prefix = filepathSeparator + fs.cleanName(prefix)
371
372 var fis []os.FileInfo
373
374 seen := make(map[string]bool) // Prevent duplicate directories
375 level := strings.Count(prefix, filepathSeparator)
376
377 collectDir := func(rm RootMapping, fi FileMetaInfo) error {
378 f, err := fi.Meta().Open()
379 if err != nil {
380 return err
381 }
382 direntries, err := f.Readdir(-1)
383 if err != nil {
384 f.Close()
385 return err
386 }
387
388 for _, fi := range direntries {
389 meta := fi.(FileMetaInfo).Meta()
390 meta.Merge(rm.Meta)
391 if !rm.Meta.InclusionFilter.Match(strings.TrimPrefix(meta.Filename, meta.SourceRoot), fi.IsDir()) {
392 continue
393 }
394
395 if fi.IsDir() {
396 name := fi.Name()
397 if seen[name] {
398 continue
399 }
400 seen[name] = true
401 opener := func() (afero.File, error) {
402 return fs.Open(filepath.Join(rm.From, name))
403 }
404 fi = newDirNameOnlyFileInfo(name, meta, opener)
405 }
406
407 fis = append(fis, fi)
408 }
409
410 f.Close()
411
412 return nil
413 }
414
415 // First add any real files/directories.
416 rms := fs.getRoot(prefix)
417 for _, rm := range rms {
418 if err := collectDir(rm, rm.fi); err != nil {
419 return nil, err
420 }
421 }
422
423 // Next add any file mounts inside the given directory.
424 prefixInside := prefix + filepathSeparator
425 fs.rootMapToReal.WalkPrefix(prefixInside, func(s string, v any) bool {
426 if (strings.Count(s, filepathSeparator) - level) != 1 {
427 // This directory is not part of the current, but we
428 // need to include the first name part to make it
429 // navigable.
430 path := strings.TrimPrefix(s, prefixInside)
431 parts := strings.Split(path, filepathSeparator)
432 name := parts[0]
433
434 if seen[name] {
435 return false
436 }
437 seen[name] = true
438 opener := func() (afero.File, error) {
439 return fs.Open(path)
440 }
441
442 fi := newDirNameOnlyFileInfo(name, nil, opener)
443 fis = append(fis, fi)
444
445 return false
446 }
447
448 rms := v.([]RootMapping)
449 for _, rm := range rms {
450 if !rm.fi.IsDir() {
451 // A single file mount
452 fis = append(fis, rm.fi)
453 continue
454 }
455 name := filepath.Base(rm.From)
456 if seen[name] {
457 continue
458 }
459 seen[name] = true
460
461 opener := func() (afero.File, error) {
462 return fs.Open(rm.From)
463 }
464
465 fi := newDirNameOnlyFileInfo(name, rm.Meta, opener)
466
467 fis = append(fis, fi)
468
469 }
470
471 return false
472 })
473
474 // Finally add any ancestor dirs with files in this directory.
475 ancestors := fs.getAncestors(prefix)
476 for _, root := range ancestors {
477 subdir := strings.TrimPrefix(prefix, root.key)
478 for _, rm := range root.roots {
479 if rm.fi.IsDir() {
480 fi, err := rm.fi.Meta().JoinStat(subdir)
481 if err == nil {
482 if err := collectDir(rm, fi); err != nil {
483 return nil, err
484 }
485 }
486 }
487 }
488 }
489
490 return fis, nil
491 }
492
493 func (fs *RootMappingFs) doLstat(name string) ([]FileMetaInfo, error) {
494 name = fs.cleanName(name)
495 key := filepathSeparator + name
496
497 roots := fs.getRoot(key)
498
499 if roots == nil {
500 if fs.hasPrefix(key) {
501 // We have directories mounted below this.
502 // Make it look like a directory.
503 return []FileMetaInfo{newDirNameOnlyFileInfo(name, nil, fs.virtualDirOpener(name))}, nil
504 }
505
506 // Find any real files or directories with this key.
507 _, roots := fs.getRoots(key)
508 if roots == nil {
509 return nil, &os.PathError{Op: "LStat", Path: name, Err: os.ErrNotExist}
510 }
511
512 var err error
513 var fis []FileMetaInfo
514
515 for _, rm := range roots {
516 var fi FileMetaInfo
517 fi, _, err = fs.statRoot(rm, name)
518 if err == nil {
519 fis = append(fis, fi)
520 }
521 }
522
523 if fis != nil {
524 return fis, nil
525 }
526
527 if err == nil {
528 err = &os.PathError{Op: "LStat", Path: name, Err: err}
529 }
530
531 return nil, err
532 }
533
534 fileCount := 0
535 var wasFiltered bool
536 for _, root := range roots {
537 meta := root.fi.Meta()
538 if !meta.InclusionFilter.Match(strings.TrimPrefix(meta.Filename, meta.SourceRoot), root.fi.IsDir()) {
539 wasFiltered = true
540 continue
541 }
542
543 if !root.fi.IsDir() {
544 fileCount++
545 }
546 if fileCount > 1 {
547 break
548 }
549 }
550
551 if fileCount == 0 {
552 if wasFiltered {
553 return nil, os.ErrNotExist
554 }
555 // Dir only.
556 return []FileMetaInfo{newDirNameOnlyFileInfo(name, roots[0].Meta, fs.virtualDirOpener(name))}, nil
557 }
558
559 if fileCount > 1 {
560 // Not supported by this filesystem.
561 return nil, fmt.Errorf("found multiple files with name %q, use .Readdir or the source filesystem directly", name)
562 }
563
564 return []FileMetaInfo{roots[0].fi}, nil
565 }
566
567 func (fs *RootMappingFs) statRoot(root RootMapping, name string) (FileMetaInfo, bool, error) {
568 if !root.Meta.InclusionFilter.Match(root.trimFrom(name), root.fi.IsDir()) {
569 return nil, false, os.ErrNotExist
570 }
571 filename := root.filename(name)
572
573 fi, b, err := lstatIfPossible(fs.Fs, filename)
574 if err != nil {
575 return nil, b, err
576 }
577
578 var opener func() (afero.File, error)
579 if fi.IsDir() {
580 // Make sure metadata gets applied in Readdir.
581 opener = fs.realDirOpener(filename, root.Meta)
582 } else {
583 // Opens the real file directly.
584 opener = func() (afero.File, error) {
585 return fs.Fs.Open(filename)
586 }
587 }
588
589 return decorateFileInfo(fi, fs.Fs, opener, "", "", root.Meta), b, nil
590 }
591
592 func (fs *RootMappingFs) virtualDirOpener(name string) func() (afero.File, error) {
593 return func() (afero.File, error) { return &rootMappingFile{name: name, fs: fs}, nil }
594 }
595
596 func (fs *RootMappingFs) realDirOpener(name string, meta *FileMeta) func() (afero.File, error) {
597 return func() (afero.File, error) {
598 f, err := fs.Fs.Open(name)
599 if err != nil {
600 return nil, err
601 }
602 return &rootMappingFile{name: name, meta: meta, fs: fs, File: f}, nil
603 }
604 }
605
606 type rootMappingFile struct {
607 afero.File
608 fs *RootMappingFs
609 name string
610 meta *FileMeta
611 }
612
613 func (f *rootMappingFile) Close() error {
614 if f.File == nil {
615 return nil
616 }
617 return f.File.Close()
618 }
619
620 func (f *rootMappingFile) Name() string {
621 return f.name
622 }
623
624 func (f *rootMappingFile) Readdir(count int) ([]os.FileInfo, error) {
625 if f.File != nil {
626
627 fis, err := f.File.Readdir(count)
628 if err != nil {
629 return nil, err
630 }
631
632 var result []os.FileInfo
633 for _, fi := range fis {
634 fim := decorateFileInfo(fi, f.fs, nil, "", "", f.meta)
635 meta := fim.Meta()
636 if f.meta.InclusionFilter.Match(strings.TrimPrefix(meta.Filename, meta.SourceRoot), fim.IsDir()) {
637 result = append(result, fim)
638 }
639 }
640 return result, nil
641 }
642
643 return f.fs.collectDirEntries(f.name)
644 }
645
646 func (f *rootMappingFile) Readdirnames(count int) ([]string, error) {
647 dirs, err := f.Readdir(count)
648 if err != nil {
649 return nil, err
650 }
651 return fileInfosToNames(dirs), nil
652 }