summaryrefslogtreecommitdiffstats
path: root/hugofs/walk.go
diff options
context:
space:
mode:
Diffstat (limited to 'hugofs/walk.go')
-rw-r--r--hugofs/walk.go308
1 files changed, 308 insertions, 0 deletions
diff --git a/hugofs/walk.go b/hugofs/walk.go
new file mode 100644
index 000000000..eca746737
--- /dev/null
+++ b/hugofs/walk.go
@@ -0,0 +1,308 @@
+// 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.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugofs
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "github.com/pkg/errors"
+
+ "github.com/spf13/afero"
+)
+
+type (
+ WalkFunc func(path string, info FileMetaInfo, err error) error
+ WalkHook func(dir FileMetaInfo, path string, readdir []FileMetaInfo) ([]FileMetaInfo, error)
+)
+
+type Walkway struct {
+ fs afero.Fs
+ root string
+ basePath string
+
+ logger *loggers.Logger
+
+ // May be pre-set
+ fi FileMetaInfo
+ dirEntries []FileMetaInfo
+
+ walkFn WalkFunc
+ walked bool
+
+ // We may traverse symbolic links and bite ourself.
+ seen map[string]bool
+
+ // Optional hooks
+ hookPre WalkHook
+ hookPost WalkHook
+}
+
+type WalkwayConfig struct {
+ Fs afero.Fs
+ Root string
+ BasePath string
+
+ Logger *loggers.Logger
+
+ // One or both of these may be pre-set.
+ Info FileMetaInfo
+ DirEntries []FileMetaInfo
+
+ WalkFn WalkFunc
+ HookPre WalkHook
+ HookPost WalkHook
+}
+
+func NewWalkway(cfg WalkwayConfig) *Walkway {
+ var fs afero.Fs
+ if cfg.Info != nil {
+ fs = cfg.Info.Meta().Fs()
+ } else {
+ fs = cfg.Fs
+ }
+
+ basePath := cfg.BasePath
+ if basePath != "" && !strings.HasSuffix(basePath, filepathSeparator) {
+ basePath += filepathSeparator
+ }
+
+ logger := cfg.Logger
+ if logger == nil {
+ logger = loggers.NewWarningLogger()
+ }
+
+ return &Walkway{
+ fs: fs,
+ root: cfg.Root,
+ basePath: basePath,
+ fi: cfg.Info,
+ dirEntries: cfg.DirEntries,
+ walkFn: cfg.WalkFn,
+ hookPre: cfg.HookPre,
+ hookPost: cfg.HookPost,
+ logger: logger,
+ seen: make(map[string]bool)}
+}
+
+func (w *Walkway) Walk() error {
+ if w.walked {
+ panic("this walkway is already walked")
+ }
+ w.walked = true
+
+ if w.fs == NoOpFs {
+ return nil
+ }
+
+ var fi FileMetaInfo
+ if w.fi != nil {
+ fi = w.fi
+ } else {
+ info, _, err := lstatIfPossible(w.fs, w.root)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+
+ if err == ErrPermissionSymlink {
+ w.logger.WARN.Printf("Unsupported symlink found in %q, skipping.", w.root)
+ return nil
+ }
+
+ return w.walkFn(w.root, nil, errors.Wrapf(err, "walk: %q", w.root))
+ }
+ fi = info.(FileMetaInfo)
+ }
+
+ if !fi.IsDir() {
+ return w.walkFn(w.root, nil, errors.New("file to walk must be a directory"))
+ }
+
+ return w.walk(w.root, fi, w.dirEntries, w.walkFn)
+
+}
+
+// if the filesystem supports it, use Lstat, else use fs.Stat
+func lstatIfPossible(fs afero.Fs, path string) (os.FileInfo, bool, error) {
+ if lfs, ok := fs.(afero.Lstater); ok {
+ fi, b, err := lfs.LstatIfPossible(path)
+ return fi, b, err
+ }
+ fi, err := fs.Stat(path)
+ return fi, false, err
+}
+
+// walk recursively descends path, calling walkFn.
+// It follow symlinks if supported by the filesystem, but only the same path once.
+func (w *Walkway) walk(path string, info FileMetaInfo, dirEntries []FileMetaInfo, walkFn WalkFunc) error {
+ err := walkFn(path, info, nil)
+ if err != nil {
+ if info.IsDir() && err == filepath.SkipDir {
+ return nil
+ }
+ return err
+ }
+ if !info.IsDir() {
+ return nil
+ }
+
+ meta := info.Meta()
+ filename := meta.Filename()
+
+ if dirEntries == nil {
+ f, err := w.fs.Open(path)
+
+ if err != nil {
+ return walkFn(path, info, errors.Wrapf(err, "walk: open %q (%q)", path, w.root))
+ }
+
+ fis, err := f.Readdir(-1)
+ f.Close()
+ if err != nil {
+ if err == ErrPermissionSymlink {
+ w.logger.WARN.Printf("Unsupported symlink found in %q, skipping.", filename)
+ return nil
+ }
+ return walkFn(path, info, errors.Wrap(err, "walk: Readdir"))
+ }
+
+ dirEntries = fileInfosToFileMetaInfos(fis)
+
+ if !meta.IsOrdered() {
+ sort.Slice(dirEntries, func(i, j int) bool {
+ fii := dirEntries[i]
+ fij := dirEntries[j]
+
+ fim, fjm := fii.Meta(), fij.Meta()
+
+ // Pull bundle headers to the top.
+ ficlass, fjclass := fim.Classifier(), fjm.Classifier()
+ if ficlass != fjclass {
+ return ficlass < fjclass
+ }
+
+ // With multiple content dirs with different languages,
+ // there can be duplicate files, and a weight will be added
+ // to the closest one.
+ fiw, fjw := fim.Weight(), fjm.Weight()
+ if fiw != fjw {
+ return fiw > fjw
+ }
+
+ // Explicit order set.
+ fio, fjo := fim.Ordinal(), fjm.Ordinal()
+ if fio != fjo {
+ return fio < fjo
+ }
+
+ // When we walk into a symlink, we keep the reference to
+ // the original name.
+ fin, fjn := fim.Name(), fjm.Name()
+ if fin != "" && fjn != "" {
+ return fin < fjn
+ }
+
+ return fii.Name() < fij.Name()
+ })
+ }
+ }
+
+ // First add some metadata to the dir entries
+ for _, fi := range dirEntries {
+ fim := fi.(FileMetaInfo)
+
+ meta := fim.Meta()
+
+ // Note that we use the original Name even if it's a symlink.
+ name := meta.Name()
+ if name == "" {
+ name = fim.Name()
+ }
+
+ if name == "" {
+ panic(fmt.Sprintf("[%s] no name set in %v", path, meta))
+ }
+ pathn := filepath.Join(path, name)
+
+ pathMeta := pathn
+ if w.basePath != "" {
+ pathMeta = strings.TrimPrefix(pathn, w.basePath)
+ }
+
+ meta[metaKeyPath] = normalizeFilename(pathMeta)
+ meta[metaKeyPathWalk] = pathn
+
+ if fim.IsDir() && w.isSeen(meta.Filename()) {
+ // Prevent infinite recursion
+ // Possible cyclic reference
+ meta[metaKeySkipDir] = true
+ }
+ }
+
+ if w.hookPre != nil {
+ dirEntries, err = w.hookPre(info, path, dirEntries)
+ if err != nil {
+ if err == filepath.SkipDir {
+ return nil
+ }
+ return err
+ }
+ }
+
+ for _, fi := range dirEntries {
+ fim := fi.(FileMetaInfo)
+ meta := fim.Meta()
+
+ if meta.SkipDir() {
+ continue
+ }
+
+ err := w.walk(meta.GetString(metaKeyPathWalk), fim, nil, walkFn)
+ if err != nil {
+ if !fi.IsDir() || err != filepath.SkipDir {
+ return err
+ }
+ }
+ }
+
+ if w.hookPost != nil {
+ dirEntries, err = w.hookPost(info, path, dirEntries)
+ if err != nil {
+ if err == filepath.SkipDir {
+ return nil
+ }
+ return err
+ }
+ }
+ return nil
+}
+
+func (w *Walkway) isSeen(filename string) bool {
+ if filename == "" {
+ return false
+ }
+
+ if w.seen[filename] {
+ return true
+ }
+
+ w.seen[filename] = true
+ return false
+}