github.com/gohugoio/hugo@v0.88.1/hugofs/rootmapping_fs.go (about)

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