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