hugo

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

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

walk.go (6899B)

    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 	"sort"
   21 	"strings"
   22 
   23 	"github.com/gohugoio/hugo/common/loggers"
   24 
   25 	"errors"
   26 
   27 	"github.com/spf13/afero"
   28 )
   29 
   30 type (
   31 	WalkFunc func(path string, info FileMetaInfo, err error) error
   32 	WalkHook func(dir FileMetaInfo, path string, readdir []FileMetaInfo) ([]FileMetaInfo, error)
   33 )
   34 
   35 type Walkway struct {
   36 	fs       afero.Fs
   37 	root     string
   38 	basePath string
   39 
   40 	logger loggers.Logger
   41 
   42 	// May be pre-set
   43 	fi         FileMetaInfo
   44 	dirEntries []FileMetaInfo
   45 
   46 	walkFn WalkFunc
   47 	walked bool
   48 
   49 	// We may traverse symbolic links and bite ourself.
   50 	seen map[string]bool
   51 
   52 	// Optional hooks
   53 	hookPre  WalkHook
   54 	hookPost WalkHook
   55 }
   56 
   57 type WalkwayConfig struct {
   58 	Fs       afero.Fs
   59 	Root     string
   60 	BasePath string
   61 
   62 	Logger loggers.Logger
   63 
   64 	// One or both of these may be pre-set.
   65 	Info       FileMetaInfo
   66 	DirEntries []FileMetaInfo
   67 
   68 	WalkFn   WalkFunc
   69 	HookPre  WalkHook
   70 	HookPost WalkHook
   71 }
   72 
   73 func NewWalkway(cfg WalkwayConfig) *Walkway {
   74 	var fs afero.Fs
   75 	if cfg.Info != nil {
   76 		fs = cfg.Info.Meta().Fs
   77 	} else {
   78 		fs = cfg.Fs
   79 	}
   80 
   81 	basePath := cfg.BasePath
   82 	if basePath != "" && !strings.HasSuffix(basePath, filepathSeparator) {
   83 		basePath += filepathSeparator
   84 	}
   85 
   86 	logger := cfg.Logger
   87 	if logger == nil {
   88 		logger = loggers.NewWarningLogger()
   89 	}
   90 
   91 	return &Walkway{
   92 		fs:         fs,
   93 		root:       cfg.Root,
   94 		basePath:   basePath,
   95 		fi:         cfg.Info,
   96 		dirEntries: cfg.DirEntries,
   97 		walkFn:     cfg.WalkFn,
   98 		hookPre:    cfg.HookPre,
   99 		hookPost:   cfg.HookPost,
  100 		logger:     logger,
  101 		seen:       make(map[string]bool),
  102 	}
  103 }
  104 
  105 func (w *Walkway) Walk() error {
  106 	if w.walked {
  107 		panic("this walkway is already walked")
  108 	}
  109 	w.walked = true
  110 
  111 	if w.fs == NoOpFs {
  112 		return nil
  113 	}
  114 
  115 	var fi FileMetaInfo
  116 	if w.fi != nil {
  117 		fi = w.fi
  118 	} else {
  119 		info, _, err := lstatIfPossible(w.fs, w.root)
  120 		if err != nil {
  121 			if os.IsNotExist(err) {
  122 				return nil
  123 			}
  124 
  125 			if w.checkErr(w.root, err) {
  126 				return nil
  127 			}
  128 			return w.walkFn(w.root, nil, fmt.Errorf("walk: %q: %w", w.root, err))
  129 		}
  130 		fi = info.(FileMetaInfo)
  131 	}
  132 
  133 	if !fi.IsDir() {
  134 		return w.walkFn(w.root, nil, errors.New("file to walk must be a directory"))
  135 	}
  136 
  137 	return w.walk(w.root, fi, w.dirEntries, w.walkFn)
  138 }
  139 
  140 // if the filesystem supports it, use Lstat, else use fs.Stat
  141 func lstatIfPossible(fs afero.Fs, path string) (os.FileInfo, bool, error) {
  142 	if lfs, ok := fs.(afero.Lstater); ok {
  143 		fi, b, err := lfs.LstatIfPossible(path)
  144 		return fi, b, err
  145 	}
  146 	fi, err := fs.Stat(path)
  147 	return fi, false, err
  148 }
  149 
  150 // checkErr returns true if the error is handled.
  151 func (w *Walkway) checkErr(filename string, err error) bool {
  152 	if err == ErrPermissionSymlink {
  153 		logUnsupportedSymlink(filename, w.logger)
  154 		return true
  155 	}
  156 
  157 	if os.IsNotExist(err) {
  158 		// The file may be removed in process.
  159 		// This may be a ERROR situation, but it is not possible
  160 		// to determine as a general case.
  161 		w.logger.Warnf("File %q not found, skipping.", filename)
  162 		return true
  163 	}
  164 
  165 	return false
  166 }
  167 
  168 func logUnsupportedSymlink(filename string, logger loggers.Logger) {
  169 	logger.Warnf("Unsupported symlink found in %q, skipping.", filename)
  170 }
  171 
  172 // walk recursively descends path, calling walkFn.
  173 // It follow symlinks if supported by the filesystem, but only the same path once.
  174 func (w *Walkway) walk(path string, info FileMetaInfo, dirEntries []FileMetaInfo, walkFn WalkFunc) error {
  175 	err := walkFn(path, info, nil)
  176 	if err != nil {
  177 		if info.IsDir() && err == filepath.SkipDir {
  178 			return nil
  179 		}
  180 		return err
  181 	}
  182 	if !info.IsDir() {
  183 		return nil
  184 	}
  185 
  186 	meta := info.Meta()
  187 	filename := meta.Filename
  188 
  189 	if dirEntries == nil {
  190 		f, err := w.fs.Open(path)
  191 		if err != nil {
  192 			if w.checkErr(path, err) {
  193 				return nil
  194 			}
  195 			return walkFn(path, info, fmt.Errorf("walk: open %q (%q): %w", path, w.root, err))
  196 		}
  197 
  198 		fis, err := f.Readdir(-1)
  199 		f.Close()
  200 		if err != nil {
  201 			if w.checkErr(filename, err) {
  202 				return nil
  203 			}
  204 			return walkFn(path, info, fmt.Errorf("walk: Readdir: %w", err))
  205 		}
  206 
  207 		dirEntries = fileInfosToFileMetaInfos(fis)
  208 
  209 		if !meta.IsOrdered {
  210 			sort.Slice(dirEntries, func(i, j int) bool {
  211 				fii := dirEntries[i]
  212 				fij := dirEntries[j]
  213 
  214 				fim, fjm := fii.Meta(), fij.Meta()
  215 
  216 				// Pull bundle headers to the top.
  217 				ficlass, fjclass := fim.Classifier, fjm.Classifier
  218 				if ficlass != fjclass {
  219 					return ficlass < fjclass
  220 				}
  221 
  222 				// With multiple content dirs with different languages,
  223 				// there can be duplicate files, and a weight will be added
  224 				// to the closest one.
  225 				fiw, fjw := fim.Weight, fjm.Weight
  226 				if fiw != fjw {
  227 
  228 					return fiw > fjw
  229 				}
  230 
  231 				// When we walk into a symlink, we keep the reference to
  232 				// the original name.
  233 				fin, fjn := fim.Name, fjm.Name
  234 				if fin != "" && fjn != "" {
  235 					return fin < fjn
  236 				}
  237 
  238 				return fii.Name() < fij.Name()
  239 			})
  240 		}
  241 	}
  242 
  243 	// First add some metadata to the dir entries
  244 	for _, fi := range dirEntries {
  245 		fim := fi.(FileMetaInfo)
  246 
  247 		meta := fim.Meta()
  248 
  249 		// Note that we use the original Name even if it's a symlink.
  250 		name := meta.Name
  251 		if name == "" {
  252 			name = fim.Name()
  253 		}
  254 
  255 		if name == "" {
  256 			panic(fmt.Sprintf("[%s] no name set in %v", path, meta))
  257 		}
  258 		pathn := filepath.Join(path, name)
  259 
  260 		pathMeta := pathn
  261 		if w.basePath != "" {
  262 			pathMeta = strings.TrimPrefix(pathn, w.basePath)
  263 		}
  264 
  265 		meta.Path = normalizeFilename(pathMeta)
  266 		meta.PathWalk = pathn
  267 
  268 		if fim.IsDir() && meta.IsSymlink && w.isSeen(meta.Filename) {
  269 			// Prevent infinite recursion
  270 			// Possible cyclic reference
  271 			meta.SkipDir = true
  272 		}
  273 	}
  274 
  275 	if w.hookPre != nil {
  276 		dirEntries, err = w.hookPre(info, path, dirEntries)
  277 		if err != nil {
  278 			if err == filepath.SkipDir {
  279 				return nil
  280 			}
  281 			return err
  282 		}
  283 	}
  284 
  285 	for _, fi := range dirEntries {
  286 		fim := fi.(FileMetaInfo)
  287 		meta := fim.Meta()
  288 
  289 		if meta.SkipDir {
  290 			continue
  291 		}
  292 
  293 		err := w.walk(meta.PathWalk, fim, nil, walkFn)
  294 		if err != nil {
  295 			if !fi.IsDir() || err != filepath.SkipDir {
  296 				return err
  297 			}
  298 		}
  299 	}
  300 
  301 	if w.hookPost != nil {
  302 		dirEntries, err = w.hookPost(info, path, dirEntries)
  303 		if err != nil {
  304 			if err == filepath.SkipDir {
  305 				return nil
  306 			}
  307 			return err
  308 		}
  309 	}
  310 	return nil
  311 }
  312 
  313 func (w *Walkway) isSeen(filename string) bool {
  314 	if filename == "" {
  315 		return false
  316 	}
  317 
  318 	if w.seen[filename] {
  319 		return true
  320 	}
  321 
  322 	w.seen[filename] = true
  323 	return false
  324 }