diff options
Diffstat (limited to 'hugolib/pagebundler_capture.go')
-rw-r--r-- | hugolib/pagebundler_capture.go | 773 |
1 files changed, 0 insertions, 773 deletions
diff --git a/hugolib/pagebundler_capture.go b/hugolib/pagebundler_capture.go deleted file mode 100644 index 7c01a751d..000000000 --- a/hugolib/pagebundler_capture.go +++ /dev/null @@ -1,773 +0,0 @@ -// Copyright 2017-present 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 hugolib - -import ( - "errors" - "fmt" - "os" - "path" - "path/filepath" - - "github.com/gohugoio/hugo/config" - - "github.com/gohugoio/hugo/common/loggers" - _errors "github.com/pkg/errors" - - "sort" - "strings" - "sync" - - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/helpers" - - "golang.org/x/sync/errgroup" - - "github.com/gohugoio/hugo/source" -) - -var errSkipCyclicDir = errors.New("skip potential cyclic dir") - -type capturer struct { - // To prevent symbolic link cycles: Visit same folder only once. - seen map[string]bool - seenMu sync.Mutex - - handler captureResultHandler - - sourceSpec *source.SourceSpec - fs afero.Fs - logger *loggers.Logger - - // Filenames limits the content to process to a list of filenames/directories. - // This is used for partial building in server mode. - filenames []string - - // Used to determine how to handle content changes in server mode. - contentChanges *contentChangeMap - - // Semaphore used to throttle the concurrent sub directory handling. - sem chan bool -} - -func newCapturer( - logger *loggers.Logger, - sourceSpec *source.SourceSpec, - handler captureResultHandler, - contentChanges *contentChangeMap, - filenames ...string) *capturer { - - numWorkers := config.GetNumWorkerMultiplier() - - // TODO(bep) the "index" vs "_index" check/strings should be moved in one place. - isBundleHeader := func(filename string) bool { - base := filepath.Base(filename) - name := helpers.Filename(base) - return IsContentFile(base) && (name == "index" || name == "_index") - } - - // Make sure that any bundle header files are processed before the others. This makes - // sure that any bundle head is processed before its resources. - sort.Slice(filenames, func(i, j int) bool { - a, b := filenames[i], filenames[j] - ac, bc := isBundleHeader(a), isBundleHeader(b) - - if ac { - return true - } - - if bc { - return false - } - - return a < b - }) - - c := &capturer{ - sem: make(chan bool, numWorkers), - handler: handler, - sourceSpec: sourceSpec, - fs: sourceSpec.SourceFs, - logger: logger, - contentChanges: contentChanges, - seen: make(map[string]bool), - filenames: filenames} - - return c -} - -// Captured files and bundles ready to be processed will be passed on to -// these channels. -type captureResultHandler interface { - handleSingles(fis ...*fileInfo) - handleCopyFile(fi pathLangFile) - captureBundlesHandler -} - -type captureBundlesHandler interface { - handleBundles(b *bundleDirs) -} - -type captureResultHandlerChain struct { - handlers []captureBundlesHandler -} - -func (c *captureResultHandlerChain) handleSingles(fis ...*fileInfo) { - for _, h := range c.handlers { - if hh, ok := h.(captureResultHandler); ok { - hh.handleSingles(fis...) - } - } -} -func (c *captureResultHandlerChain) handleBundles(b *bundleDirs) { - for _, h := range c.handlers { - h.handleBundles(b) - } -} - -func (c *captureResultHandlerChain) handleCopyFile(file pathLangFile) { - for _, h := range c.handlers { - if hh, ok := h.(captureResultHandler); ok { - hh.handleCopyFile(file) - } - } -} - -func (c *capturer) capturePartial(filenames ...string) error { - handled := make(map[string]bool) - - for _, filename := range filenames { - dir, resolvedFilename, tp := c.contentChanges.resolveAndRemove(filename) - if handled[resolvedFilename] { - continue - } - - handled[resolvedFilename] = true - - switch tp { - case bundleLeaf: - if err := c.handleDir(resolvedFilename); err != nil { - // Directory may have been deleted. - if !os.IsNotExist(err) { - return err - } - } - case bundleBranch: - if err := c.handleBranchDir(resolvedFilename); err != nil { - // Directory may have been deleted. - if !os.IsNotExist(err) { - return err - } - } - default: - fi, err := c.resolveRealPath(resolvedFilename) - if os.IsNotExist(err) { - // File has been deleted. - continue - } - - // Just in case the owning dir is a new symlink -- this will - // create the proper mapping for it. - c.resolveRealPath(dir) - - f, active := c.newFileInfo(fi, tp) - if active { - c.copyOrHandleSingle(f) - } - } - } - - return nil -} - -func (c *capturer) capture() error { - if len(c.filenames) > 0 { - return c.capturePartial(c.filenames...) - } - - err := c.handleDir(helpers.FilePathSeparator) - if err != nil { - return err - } - - return nil -} - -func (c *capturer) handleNestedDir(dirname string) error { - select { - case c.sem <- true: - var g errgroup.Group - - g.Go(func() error { - defer func() { - <-c.sem - }() - return c.handleDir(dirname) - }) - return g.Wait() - default: - // For deeply nested file trees, waiting for a semaphore wil deadlock. - return c.handleDir(dirname) - } -} - -// This handles a bundle branch and its resources only. This is used -// in server mode on changes. If this dir does not (anymore) represent a bundle -// branch, the handling is upgraded to the full handleDir method. -func (c *capturer) handleBranchDir(dirname string) error { - files, err := c.readDir(dirname) - if err != nil { - - return err - } - - var ( - dirType bundleDirType - ) - - for _, fi := range files { - if !fi.IsDir() { - tp, _ := classifyBundledFile(fi.RealName()) - if dirType == bundleNot { - dirType = tp - } - - if dirType == bundleLeaf { - return c.handleDir(dirname) - } - } - } - - if dirType != bundleBranch { - return c.handleDir(dirname) - } - - dirs := newBundleDirs(bundleBranch, c) - - var secondPass []*fileInfo - - // Handle potential bundle headers first. - for _, fi := range files { - if fi.IsDir() { - continue - } - - tp, isContent := classifyBundledFile(fi.RealName()) - - f, active := c.newFileInfo(fi, tp) - - if !active { - continue - } - - if !f.isOwner() { - if !isContent { - // This is a partial update -- we only care about the files that - // is in this bundle. - secondPass = append(secondPass, f) - } - continue - } - dirs.addBundleHeader(f) - } - - for _, f := range secondPass { - dirs.addBundleFiles(f) - } - - c.handler.handleBundles(dirs) - - return nil - -} - -func (c *capturer) handleDir(dirname string) error { - - files, err := c.readDir(dirname) - if err != nil { - return err - } - - type dirState int - - const ( - dirStateDefault dirState = iota - - dirStateAssetsOnly - dirStateSinglesOnly - ) - - var ( - fileBundleTypes = make([]bundleDirType, len(files)) - - // Start with the assumption that this dir contains only non-content assets (images etc.) - // If that is still true after we had a first look at the list of files, we - // can just copy the files to destination. We will still have to look at the - // sub-folders for potential bundles. - state = dirStateAssetsOnly - - // Start with the assumption that this dir is not a bundle. - // A directory is a bundle if it contains a index content file, - // e.g. index.md (a leaf bundle) or a _index.md (a branch bundle). - bundleType = bundleNot - ) - - /* First check for any content files. - - If there are none, then this is a assets folder only (images etc.) - and we can just plainly copy them to - destination. - - If this is a section with no image etc. or similar, we can just handle it - as it was a single content file. - */ - var hasNonContent, isBranch bool - - for i, fi := range files { - if !fi.IsDir() { - tp, isContent := classifyBundledFile(fi.RealName()) - - fileBundleTypes[i] = tp - if !isBranch { - isBranch = tp == bundleBranch - } - - if isContent { - // This is not a assets-only folder. - state = dirStateDefault - } else { - hasNonContent = true - } - } - } - - if isBranch && !hasNonContent { - // This is a section or similar with no need for any bundle handling. - state = dirStateSinglesOnly - } - - if state > dirStateDefault { - return c.handleNonBundle(dirname, files, state == dirStateSinglesOnly) - } - - var fileInfos = make([]*fileInfo, 0, len(files)) - - for i, fi := range files { - - currentType := bundleNot - - if !fi.IsDir() { - currentType = fileBundleTypes[i] - if bundleType == bundleNot && currentType != bundleNot { - bundleType = currentType - } - } - - if bundleType == bundleNot && currentType != bundleNot { - bundleType = currentType - } - - f, active := c.newFileInfo(fi, currentType) - - if !active { - continue - } - - fileInfos = append(fileInfos, f) - } - - var todo []*fileInfo - - if bundleType != bundleLeaf { - for _, fi := range fileInfos { - if fi.FileInfo().IsDir() { - // Handle potential nested bundles. - if err := c.handleNestedDir(fi.Path()); err != nil { - return err - } - } else if bundleType == bundleNot || (!fi.isOwner() && fi.isContentFile()) { - // Not in a bundle. - c.copyOrHandleSingle(fi) - } else { - // This is a section folder or similar with non-content files in it. - todo = append(todo, fi) - } - } - } else { - todo = fileInfos - } - - if len(todo) == 0 { - return nil - } - - dirs, err := c.createBundleDirs(todo, bundleType) - if err != nil { - return err - } - - // Send the bundle to the next step in the processor chain. - c.handler.handleBundles(dirs) - - return nil -} - -func (c *capturer) handleNonBundle( - dirname string, - fileInfos pathLangFileFis, - singlesOnly bool) error { - - for _, fi := range fileInfos { - if fi.IsDir() { - if err := c.handleNestedDir(fi.Filename()); err != nil { - return err - } - } else { - if singlesOnly { - f, active := c.newFileInfo(fi, bundleNot) - if !active { - continue - } - c.handler.handleSingles(f) - } else { - c.handler.handleCopyFile(fi) - } - } - } - - return nil -} - -func (c *capturer) copyOrHandleSingle(fi *fileInfo) { - if fi.isContentFile() { - c.handler.handleSingles(fi) - } else { - // These do not currently need any further processing. - c.handler.handleCopyFile(fi) - } -} - -func (c *capturer) createBundleDirs(fileInfos []*fileInfo, bundleType bundleDirType) (*bundleDirs, error) { - dirs := newBundleDirs(bundleType, c) - - for _, fi := range fileInfos { - if fi.FileInfo().IsDir() { - var collector func(fis ...*fileInfo) - - if bundleType == bundleBranch { - // All files in the current directory are part of this bundle. - // Trying to include sub folders in these bundles are filled with ambiguity. - collector = func(fis ...*fileInfo) { - for _, fi := range fis { - c.copyOrHandleSingle(fi) - } - } - } else { - // All nested files and directories are part of this bundle. - collector = func(fis ...*fileInfo) { - fileInfos = append(fileInfos, fis...) - } - } - err := c.collectFiles(fi.Path(), collector) - if err != nil { - return nil, err - } - - } else if fi.isOwner() { - // There can be more than one language, so: - // 1. Content files must be attached to its language's bundle. - // 2. Other files must be attached to all languages. - // 3. Every content file needs a bundle header. - dirs.addBundleHeader(fi) - } - } - - for _, fi := range fileInfos { - if fi.FileInfo().IsDir() || fi.isOwner() { - continue - } - - if fi.isContentFile() { - if bundleType != bundleBranch { - dirs.addBundleContentFile(fi) - } - } else { - dirs.addBundleFiles(fi) - } - } - - return dirs, nil -} - -func (c *capturer) collectFiles(dirname string, handleFiles func(fis ...*fileInfo)) error { - - filesInDir, err := c.readDir(dirname) - if err != nil { - return err - } - - for _, fi := range filesInDir { - if fi.IsDir() { - err := c.collectFiles(fi.Filename(), handleFiles) - if err != nil { - return err - } - } else { - f, active := c.newFileInfo(fi, bundleNot) - if active { - handleFiles(f) - } - } - } - - return nil -} - -func (c *capturer) readDir(dirname string) (pathLangFileFis, error) { - if c.sourceSpec.IgnoreFile(dirname) { - return nil, nil - } - - dir, err := c.fs.Open(dirname) - if err != nil { - return nil, err - } - defer dir.Close() - fis, err := dir.Readdir(-1) - if err != nil { - return nil, err - } - - pfis := make(pathLangFileFis, 0, len(fis)) - - for _, fi := range fis { - fip := fi.(pathLangFileFi) - - if !c.sourceSpec.IgnoreFile(fip.Filename()) { - - err := c.resolveRealPathIn(fip) - - if err != nil { - // It may have been deleted in the meantime. - if err == errSkipCyclicDir || os.IsNotExist(err) { - continue - } - return nil, err - } - - pfis = append(pfis, fip) - } - } - - return pfis, nil -} - -func (c *capturer) newFileInfo(fi pathLangFileFi, tp bundleDirType) (*fileInfo, bool) { - f := newFileInfo(c.sourceSpec, "", "", fi, tp) - return f, !f.disabled -} - -type pathLangFile interface { - hugofs.LanguageAnnouncer - hugofs.FilePather -} - -type pathLangFileFi interface { - os.FileInfo - pathLangFile -} - -type pathLangFileFis []pathLangFileFi - -type bundleDirs struct { - tp bundleDirType - // Maps languages to bundles. - bundles map[string]*bundleDir - - // Keeps track of language overrides for non-content files, e.g. logo.en.png. - langOverrides map[string]bool - - c *capturer -} - -func newBundleDirs(tp bundleDirType, c *capturer) *bundleDirs { - return &bundleDirs{tp: tp, bundles: make(map[string]*bundleDir), langOverrides: make(map[string]bool), c: c} -} - -type bundleDir struct { - tp bundleDirType - fi *fileInfo - - resources map[string]*fileInfo -} - -func (b bundleDir) clone() *bundleDir { - b.resources = make(map[string]*fileInfo) - fic := *b.fi - b.fi = &fic - return &b -} - -func newBundleDir(fi *fileInfo, bundleType bundleDirType) *bundleDir { - return &bundleDir{fi: fi, tp: bundleType, resources: make(map[string]*fileInfo)} -} - -func (b *bundleDirs) addBundleContentFile(fi *fileInfo) { - dir, found := b.bundles[fi.Lang()] - if !found { - // Every bundled content file needs a bundle header. - // If one does not exist in its language, we pick the default - // language version, or a random one if that doesn't exist, either. - tl := b.c.sourceSpec.DefaultContentLanguage - ldir, found := b.bundles[tl] - if !found { - // Just pick one. - for _, v := range b.bundles { - ldir = v - break - } - } - - if ldir == nil { - panic(fmt.Sprintf("bundle not found for file %q", fi.Filename())) - } - - dir = ldir.clone() - dir.fi.overriddenLang = fi.Lang() - b.bundles[fi.Lang()] = dir - } - - dir.resources[fi.Path()] = fi -} - -func (b *bundleDirs) addBundleFiles(fi *fileInfo) { - dir := filepath.ToSlash(fi.Dir()) - p := dir + fi.TranslationBaseName() + "." + fi.Ext() - for lang, bdir := range b.bundles { - key := path.Join(lang, p) - - // Given mypage.de.md (German translation) and mypage.md we pick the most - // specific for that language. - if fi.Lang() == lang || !b.langOverrides[key] { - bdir.resources[key] = fi - } - b.langOverrides[key] = true - } -} - -func (b *bundleDirs) addBundleHeader(fi *fileInfo) { - b.bundles[fi.Lang()] = newBundleDir(fi, b.tp) -} - -func (c *capturer) isSeen(dirname string) bool { - c.seenMu.Lock() - defer c.seenMu.Unlock() - seen := c.seen[dirname] - c.seen[dirname] = true - if seen { - c.logger.INFO.Printf("Content dir %q already processed; skipped to avoid infinite recursion.", dirname) - return true - - } - return false -} - -func (c *capturer) resolveRealPath(path string) (pathLangFileFi, error) { - fileInfo, err := c.lstatIfPossible(path) - if err != nil { - return nil, err - } - return fileInfo, c.resolveRealPathIn(fileInfo) -} - -func (c *capturer) resolveRealPathIn(fileInfo pathLangFileFi) error { - - basePath := fileInfo.BaseDir() - path := fileInfo.Filename() - - realPath := path - - if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { - link, err := filepath.EvalSymlinks(path) - if err != nil { - return _errors.Wrapf(err, "Cannot read symbolic link %q, error was:", path) - } - - // This is a file on the outside of any base fs, so we have to use the os package. - sfi, err := os.Stat(link) - if err != nil { - return _errors.Wrapf(err, "Cannot stat %q, error was:", link) - } - - // TODO(bep) improve all of this. - if a, ok := fileInfo.(*hugofs.LanguageFileInfo); ok { - a.FileInfo = sfi - } - - realPath = link - - if realPath != path && sfi.IsDir() && c.isSeen(realPath) { - // Avoid cyclic symlinks. - // Note that this may prevent some uses that isn't cyclic and also - // potential useful, but this implementation is both robust and simple: - // We stop at the first directory that we have seen before, e.g. - // /content/blog will only be processed once. - return errSkipCyclicDir - } - - if c.contentChanges != nil { - // Keep track of symbolic links in watch mode. - var from, to string - if sfi.IsDir() { - from = realPath - to = path - - if !strings.HasSuffix(to, helpers.FilePathSeparator) { - to = to + helpers.FilePathSeparator - } - if !strings.HasSuffix(from, helpers.FilePathSeparator) { - from = from + helpers.FilePathSeparator - } - - if !strings.HasSuffix(basePath, helpers.FilePathSeparator) { - basePath = basePath + helpers.FilePathSeparator - } - - if strings.HasPrefix(from, basePath) { - // With symbolic links inside /content we need to keep - // a reference to both. This may be confusing with --navigateToChanged - // but the user has chosen this him or herself. - c.contentChanges.addSymbolicLinkMapping(from, from) - } - - } else { - from = realPath - to = path - } - - c.contentChanges.addSymbolicLinkMapping(from, to) - } - } - - return nil -} - -func (c *capturer) lstatIfPossible(path string) (pathLangFileFi, error) { - fi, err := helpers.LstatIfPossible(c.fs, path) - if err != nil { - return nil, err - } - return fi.(pathLangFileFi), nil -} |