hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }