diff options
Diffstat (limited to 'hugofs/rootmapping_fs.go')
-rw-r--r-- | hugofs/rootmapping_fs.go | 457 |
1 files changed, 364 insertions, 93 deletions
diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go index 2b8b8d2c0..a1214a02c 100644 --- a/hugofs/rootmapping_fs.go +++ b/hugofs/rootmapping_fs.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2019 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,10 +14,14 @@ package hugofs import ( + "fmt" "os" "path/filepath" "strings" - "time" + + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/pkg/errors" radix "github.com/hashicorp/go-immutable-radix" "github.com/spf13/afero" @@ -25,151 +29,429 @@ import ( var filepathSeparator = string(filepath.Separator) -// A RootMappingFs maps several roots into one. Note that the root of this filesystem -// is directories only, and they will be returned in Readdir and Readdirnames -// in the order given. -type RootMappingFs struct { - afero.Fs - rootMapToReal *radix.Node - virtualRoots []string +// NewRootMappingFs creates a new RootMappingFs on top of the provided with +// of root mappings with some optional metadata about the root. +// Note that From represents a virtual root that maps to the actual filename in To. +func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) { + rootMapToReal := radix.New().Txn() + + for _, rm := range rms { + (&rm).clean() + + fromBase := files.ResolveComponentFolder(rm.From) + if fromBase == "" { + panic("unrecognised component folder in" + rm.From) + } + + if len(rm.To) < 2 { + panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To)) + } + + _, err := fs.Stat(rm.To) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + + // Extract "blog" from "content/blog" + rm.path = strings.TrimPrefix(strings.TrimPrefix(rm.From, fromBase), filepathSeparator) + + key := []byte(rm.rootKey()) + var mappings []RootMapping + v, found := rootMapToReal.Get(key) + if found { + // There may be more than one language pointing to the same root. + mappings = v.([]RootMapping) + } + mappings = append(mappings, rm) + rootMapToReal.Insert(key, mappings) + } + + rfs := &RootMappingFs{Fs: fs, + virtualRoots: rms, + rootMapToReal: rootMapToReal.Commit().Root()} + + return rfs, nil } -type rootMappingFile struct { - afero.File - fs *RootMappingFs - name string +// NewRootMappingFsFromFromTo is a convenicence variant of NewRootMappingFs taking +// From and To as string pairs. +func NewRootMappingFsFromFromTo(fs afero.Fs, fromTo ...string) (*RootMappingFs, error) { + rms := make([]RootMapping, len(fromTo)/2) + for i, j := 0, 0; j < len(fromTo); i, j = i+1, j+2 { + rms[i] = RootMapping{ + From: fromTo[j], + To: fromTo[j+1], + } + } + + return NewRootMappingFs(fs, rms...) } -type rootMappingFileInfo struct { - name string +type RootMapping struct { + From string + To string + + path string // The virtual mount point, e.g. "blog". + Meta FileMeta // File metadata (lang etc.) } -func (fi *rootMappingFileInfo) Name() string { - return fi.name +func (rm *RootMapping) clean() { + rm.From = filepath.Clean(rm.From) + rm.To = filepath.Clean(rm.To) } -func (fi *rootMappingFileInfo) Size() int64 { - panic("not implemented") +func (r RootMapping) filename(name string) string { + return filepath.Join(r.To, strings.TrimPrefix(name, r.From)) } -func (fi *rootMappingFileInfo) Mode() os.FileMode { - return os.ModeDir +func (r RootMapping) rootKey() string { + return r.From } -func (fi *rootMappingFileInfo) ModTime() time.Time { - panic("not implemented") +// A RootMappingFs maps several roots into one. Note that the root of this filesystem +// is directories only, and they will be returned in Readdir and Readdirnames +// in the order given. +type RootMappingFs struct { + afero.Fs + rootMapToReal *radix.Node + virtualRoots []RootMapping + filter func(r RootMapping) bool } -func (fi *rootMappingFileInfo) IsDir() bool { - return true +func (fs *RootMappingFs) Dirs(base string) ([]FileMetaInfo, error) { + roots := fs.getRootsWithPrefix(base) + + if roots == nil { + return nil, nil + } + + fss := make([]FileMetaInfo, len(roots)) + for i, r := range roots { + bfs := afero.NewBasePathFs(fs.Fs, r.To) + bfs = decoratePath(bfs, func(name string) string { + p := strings.TrimPrefix(name, r.To) + if r.path != "" { + // Make sure it's mounted to a any sub path, e.g. blog + p = filepath.Join(r.path, p) + } + p = strings.TrimLeft(p, filepathSeparator) + return p + }) + fs := decorateDirs(bfs, r.Meta) + fi, err := fs.Stat("") + if err != nil { + return nil, errors.Wrap(err, "RootMappingFs.Dirs") + } + fss[i] = fi.(FileMetaInfo) + } + + return fss, nil } -func (fi *rootMappingFileInfo) Sys() interface{} { - return nil +// LstatIfPossible returns the os.FileInfo structure describing a given file. +func (fs *RootMappingFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + fis, b, err := fs.doLstat(name, false) + if err != nil { + return nil, b, err + } + return fis[0], b, nil + } -func newRootMappingDirFileInfo(name string) *rootMappingFileInfo { - return &rootMappingFileInfo{name: name} +func (fs *RootMappingFs) virtualDirOpener(name string, isRoot bool) func() (afero.File, error) { + return func() (afero.File, error) { return &rootMappingFile{name: name, isRoot: isRoot, fs: fs}, nil } } -// NewRootMappingFs creates a new RootMappingFs on top of the provided with -// a list of from, to string pairs of root mappings. -// Note that 'from' represents a virtual root that maps to the actual filename in 'to'. -func NewRootMappingFs(fs afero.Fs, fromTo ...string) (*RootMappingFs, error) { - rootMapToReal := radix.New().Txn() - var virtualRoots []string +func (fs *RootMappingFs) doLstat(name string, allowMultiple bool) ([]FileMetaInfo, bool, error) { + + if fs.isRoot(name) { + return []FileMetaInfo{newDirNameOnlyFileInfo(name, true, fs.virtualDirOpener(name, true))}, false, nil + } + + roots := fs.getRoots(name) - for i := 0; i < len(fromTo); i += 2 { - vr := filepath.Clean(fromTo[i]) - rr := filepath.Clean(fromTo[i+1]) + if len(roots) == 0 { + roots := fs.getRootsWithPrefix(name) + if len(roots) != 0 { + // We have root mappings below name, let's make it look like + // a directory. + return []FileMetaInfo{newDirNameOnlyFileInfo(name, true, fs.virtualDirOpener(name, false))}, false, nil + } + + return nil, false, os.ErrNotExist + } - // We need to preserve the original order for Readdir - virtualRoots = append(virtualRoots, vr) + var ( + fis []FileMetaInfo + b bool + fi os.FileInfo + root RootMapping + err error + ) + + for _, root = range roots { + fi, b, err = fs.statRoot(root, name) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, false, err + } + fim := fi.(FileMetaInfo) + fis = append(fis, fim) + } - rootMapToReal.Insert([]byte(vr), rr) + if len(fis) == 0 { + return nil, false, os.ErrNotExist } - return &RootMappingFs{Fs: fs, - virtualRoots: virtualRoots, - rootMapToReal: rootMapToReal.Commit().Root()}, nil + if allowMultiple || len(fis) == 1 { + return fis, b, nil + } + + // Open it in this composite filesystem. + opener := func() (afero.File, error) { + return fs.Open(name) + } + + return []FileMetaInfo{decorateFileInfo(fi, fs, opener, "", "", root.Meta)}, b, nil + } -// Stat returns the os.FileInfo structure describing a given file. If there is -// an error, it will be of type *os.PathError. -func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) { +// Open opens the namedrootMappingFile file for reading. +func (fs *RootMappingFs) Open(name string) (afero.File, error) { if fs.isRoot(name) { - return newRootMappingDirFileInfo(name), nil + return &rootMappingFile{name: name, fs: fs, isRoot: true}, nil } - realName := fs.realName(name) - fi, err := fs.Fs.Stat(realName) - if rfi, ok := fi.(RealFilenameInfo); ok { - return rfi, err + fis, _, err := fs.doLstat(name, true) + if err != nil { + return nil, err } - return &realFilenameInfo{FileInfo: fi, realFilename: realName}, err + if len(fis) == 1 { + fi := fis[0] + meta := fi.(FileMetaInfo).Meta() + f, err := meta.Open() + if err != nil { + return nil, err + } + return &rootMappingFile{File: f, fs: fs, name: name, meta: meta}, nil + } + + return fs.newUnionFile(fis...) } +// Stat returns the os.FileInfo structure describing a given file. If there is +// an error, it will be of type *os.PathError. +func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) { + fi, _, err := fs.LstatIfPossible(name) + return fi, err + +} + +// Filter creates a copy of this filesystem with the applied filter. +func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs { + fs.filter = f + return &fs +} + func (fs *RootMappingFs) isRoot(name string) bool { return name == "" || name == filepathSeparator } -// Open opens the named file for reading. -func (fs *RootMappingFs) Open(name string) (afero.File, error) { - if fs.isRoot(name) { - return &rootMappingFile{name: name, fs: fs}, nil +func (fs *RootMappingFs) getRoots(name string) []RootMapping { + nameb := []byte(filepath.Clean(name)) + _, v, found := fs.rootMapToReal.LongestPrefix(nameb) + if !found { + return nil } - realName := fs.realName(name) - f, err := fs.Fs.Open(realName) + + rm := v.([]RootMapping) + + if fs.filter != nil { + var filtered []RootMapping + for _, m := range rm { + if fs.filter(m) { + filtered = append(filtered, m) + } + } + return filtered + } + + return rm +} + +func (fs *RootMappingFs) getRootsWithPrefix(prefix string) []RootMapping { + if fs.isRoot(prefix) { + return fs.virtualRoots + } + prefixb := []byte(filepath.Clean(prefix)) + var roots []RootMapping + + fs.rootMapToReal.WalkPrefix(prefixb, func(b []byte, v interface{}) bool { + roots = append(roots, v.([]RootMapping)...) + return false + }) + + return roots +} + +func (fs *RootMappingFs) newUnionFile(fis ...FileMetaInfo) (afero.File, error) { + meta := fis[0].Meta() + f, err := meta.Open() if err != nil { return nil, err } - return &rootMappingFile{File: f, name: name, fs: fs}, nil -} + rf := &rootMappingFile{File: f, fs: fs, name: meta.Name(), meta: meta} + if len(fis) == 1 { + return rf, err + } -// LstatIfPossible returns the os.FileInfo structure describing a given file. -// It attempts to use Lstat if supported or defers to the os. In addition to -// the FileInfo, a boolean is returned telling whether Lstat was called. -func (fs *RootMappingFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + next, err := fs.newUnionFile(fis[1:]...) + if err != nil { + return nil, err + } - if fs.isRoot(name) { - return newRootMappingDirFileInfo(name), false, nil + uf := &afero.UnionFile{Base: rf, Layer: next} + + uf.Merger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) { + // Ignore duplicate directory entries + seen := make(map[string]bool) + var result []os.FileInfo + + for _, fis := range [][]os.FileInfo{bofi, lofi} { + for _, fi := range fis { + + if fi.IsDir() && seen[fi.Name()] { + continue + } + + if fi.IsDir() { + seen[fi.Name()] = true + } + + result = append(result, fi) + } + } + + return result, nil } - name = fs.realName(name) + + return uf, nil + +} + +func (fs *RootMappingFs) statRoot(root RootMapping, name string) (os.FileInfo, bool, error) { + filename := root.filename(name) + + var b bool + var fi os.FileInfo + var err error if ls, ok := fs.Fs.(afero.Lstater); ok { - fi, b, err := ls.LstatIfPossible(name) - return &realFilenameInfo{FileInfo: fi, realFilename: name}, b, err + fi, b, err = ls.LstatIfPossible(filename) + if err != nil { + return nil, b, err + } + + } else { + fi, err = fs.Fs.Stat(filename) + if err != nil { + return nil, b, err + } + } + + // Opens the real directory/file. + opener := func() (afero.File, error) { + return fs.Fs.Open(filename) + } + + if fi.IsDir() { + _, name = filepath.Split(name) + fi = newDirNameOnlyFileInfo(name, false, opener) } - fi, err := fs.Stat(name) - return fi, false, err + + return decorateFileInfo(fi, fs.Fs, opener, "", "", root.Meta), b, nil + } -func (fs *RootMappingFs) realName(name string) string { - key, val, found := fs.rootMapToReal.LongestPrefix([]byte(filepath.Clean(name))) - if !found { - return name +type rootMappingFile struct { + afero.File + fs *RootMappingFs + name string + meta FileMeta + isRoot bool +} + +func (f *rootMappingFile) Close() error { + if f.File == nil { + return nil } - keystr := string(key) + return f.File.Close() +} - return filepath.Join(val.(string), strings.TrimPrefix(name, keystr)) +func (f *rootMappingFile) Name() string { + return f.name } func (f *rootMappingFile) Readdir(count int) ([]os.FileInfo, error) { if f.File == nil { dirsn := make([]os.FileInfo, 0) - for i := 0; i < len(f.fs.virtualRoots); i++ { - if count != -1 && i >= count { + roots := f.fs.getRootsWithPrefix(f.name) + seen := make(map[string]bool) + + j := 0 + for _, rm := range roots { + if count != -1 && j >= count { break } - dirsn = append(dirsn, newRootMappingDirFileInfo(f.fs.virtualRoots[i])) + + opener := func() (afero.File, error) { + return f.fs.Open(rm.From) + } + + name := rm.From + if !f.isRoot { + _, name = filepath.Split(rm.From) + } + + if seen[name] { + continue + } + seen[name] = true + + j++ + + fi := newDirNameOnlyFileInfo(name, false, opener) + if rm.Meta != nil { + mergeFileMeta(rm.Meta, fi.Meta()) + } + + dirsn = append(dirsn, fi) } return dirsn, nil } - return f.File.Readdir(count) + if f.File == nil { + panic(fmt.Sprintf("no File for %q", f.name)) + } + + fis, err := f.File.Readdir(count) + if err != nil { + return nil, err + } + + for i, fi := range fis { + fis[i] = decorateFileInfo(fi, f.fs, nil, "", "", f.meta) + } + + return fis, nil } func (f *rootMappingFile) Readdirnames(count int) ([]string, error) { @@ -183,14 +465,3 @@ func (f *rootMappingFile) Readdirnames(count int) ([]string, error) { } return dirss, nil } - -func (f *rootMappingFile) Name() string { - return f.name -} - -func (f *rootMappingFile) Close() error { - if f.File == nil { - return nil - } - return f.File.Close() -} |