summaryrefslogtreecommitdiffstats
path: root/hugofs
diff options
context:
space:
mode:
Diffstat (limited to 'hugofs')
-rw-r--r--hugofs/basepath_real_filename_fs.go91
-rw-r--r--hugofs/decorators.go205
-rw-r--r--hugofs/fileinfo.go297
-rw-r--r--hugofs/files/classifier.go121
-rw-r--r--hugofs/files/classifier_test.go49
-rw-r--r--hugofs/filter_fs.go341
-rw-r--r--hugofs/filter_fs_test.go48
-rw-r--r--hugofs/fs.go6
-rw-r--r--hugofs/language_composite_fs.go40
-rw-r--r--hugofs/language_composite_fs_test.go107
-rw-r--r--hugofs/language_fs.go346
-rw-r--r--hugofs/language_fs_test.go100
-rw-r--r--hugofs/nolstat_fs.go39
-rw-r--r--hugofs/nosymlink_fs.go85
-rw-r--r--hugofs/nosymlink_test.go97
-rw-r--r--hugofs/rootmapping_fs.go457
-rw-r--r--hugofs/rootmapping_fs_test.go199
-rw-r--r--hugofs/slice_fs.go293
-rw-r--r--hugofs/walk.go308
-rw-r--r--hugofs/walk_test.go225
20 files changed, 2663 insertions, 791 deletions
diff --git a/hugofs/basepath_real_filename_fs.go b/hugofs/basepath_real_filename_fs.go
deleted file mode 100644
index 1024c4d30..000000000
--- a/hugofs/basepath_real_filename_fs.go
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright 2018 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 (
- "os"
-
- "github.com/spf13/afero"
-)
-
-// RealFilenameInfo is a thin wrapper around os.FileInfo adding the real filename.
-type RealFilenameInfo interface {
- os.FileInfo
-
- // This is the real filename to the file in the underlying filesystem.
- RealFilename() string
-}
-
-type realFilenameInfo struct {
- os.FileInfo
- realFilename string
-}
-
-func (f *realFilenameInfo) RealFilename() string {
- return f.realFilename
-}
-
-// NewBasePathRealFilenameFs returns a new BasePathRealFilenameFs instance
-// using base.
-func NewBasePathRealFilenameFs(base *afero.BasePathFs) *BasePathRealFilenameFs {
- return &BasePathRealFilenameFs{BasePathFs: base}
-}
-
-// BasePathRealFilenameFs is a thin wrapper around afero.BasePathFs that
-// provides the real filename in Stat and LstatIfPossible.
-type BasePathRealFilenameFs struct {
- *afero.BasePathFs
-}
-
-// Stat returns the os.FileInfo structure describing a given file. If there is
-// an error, it will be of type *os.PathError.
-func (b *BasePathRealFilenameFs) Stat(name string) (os.FileInfo, error) {
- fi, err := b.BasePathFs.Stat(name)
- if err != nil {
- return nil, err
- }
-
- if _, ok := fi.(RealFilenameInfo); ok {
- return fi, nil
- }
-
- filename, err := b.RealPath(name)
- if err != nil {
- return nil, &os.PathError{Op: "stat", Path: name, Err: err}
- }
-
- return &realFilenameInfo{FileInfo: fi, realFilename: filename}, nil
-}
-
-// LstatIfPossible returns the os.FileInfo structure describing a given file.
-// It attempts to use Lstat if supported or defers to the os. In addition to
-// the FileInfo, a boolean is returned telling whether Lstat was called.
-func (b *BasePathRealFilenameFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
-
- fi, ok, err := b.BasePathFs.LstatIfPossible(name)
- if err != nil {
- return nil, false, err
- }
-
- if _, ok := fi.(RealFilenameInfo); ok {
- return fi, ok, nil
- }
-
- filename, err := b.RealPath(name)
- if err != nil {
- return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err}
- }
-
- return &realFilenameInfo{FileInfo: fi, realFilename: filename}, ok, nil
-}
diff --git a/hugofs/decorators.go b/hugofs/decorators.go
new file mode 100644
index 000000000..0a2b39712
--- /dev/null
+++ b/hugofs/decorators.go
@@ -0,0 +1,205 @@
+// 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 (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/pkg/errors"
+
+ "github.com/spf13/afero"
+)
+
+func decorateDirs(fs afero.Fs, meta FileMeta) afero.Fs {
+ ffs := &baseFileDecoratorFs{Fs: fs}
+
+ decorator := func(fi os.FileInfo, name string) (os.FileInfo, error) {
+ if !fi.IsDir() {
+ // Leave regular files as they are.
+ return fi, nil
+ }
+
+ return decorateFileInfo(fi, fs, nil, "", "", meta), nil
+ }
+
+ ffs.decorate = decorator
+
+ return ffs
+
+}
+
+func decoratePath(fs afero.Fs, createPath func(name string) string) afero.Fs {
+
+ ffs := &baseFileDecoratorFs{Fs: fs}
+
+ decorator := func(fi os.FileInfo, name string) (os.FileInfo, error) {
+ path := createPath(name)
+
+ return decorateFileInfo(fi, fs, nil, "", path, nil), nil
+ }
+
+ ffs.decorate = decorator
+
+ return ffs
+
+}
+
+// DecorateBasePathFs adds Path info to files and directories in the
+// provided BasePathFs, using the base as base.
+func DecorateBasePathFs(base *afero.BasePathFs) afero.Fs {
+ basePath, _ := base.RealPath("")
+ if !strings.HasSuffix(basePath, filepathSeparator) {
+ basePath += filepathSeparator
+ }
+
+ ffs := &baseFileDecoratorFs{Fs: base}
+
+ decorator := func(fi os.FileInfo, name string) (os.FileInfo, error) {
+ path := strings.TrimPrefix(name, basePath)
+
+ return decorateFileInfo(fi, base, nil, "", path, nil), nil
+ }
+
+ ffs.decorate = decorator
+
+ return ffs
+}
+
+// NewBaseFileDecorator decorates the given Fs to provide the real filename
+// and an Opener func. If
+func NewBaseFileDecorator(fs afero.Fs) afero.Fs {
+
+ ffs := &baseFileDecoratorFs{Fs: fs}
+
+ decorator := func(fi os.FileInfo, filename string) (os.FileInfo, error) {
+ // Store away the original in case it's a symlink.
+ meta := FileMeta{metaKeyName: fi.Name()}
+ isSymlink := isSymlink(fi)
+ if isSymlink {
+ meta[metaKeyOriginalFilename] = filename
+ link, err := filepath.EvalSymlinks(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ fi, err = fs.Stat(link)
+ if err != nil {
+ return nil, err
+ }
+
+ filename = link
+ meta[metaKeyIsSymlink] = true
+
+ }
+
+ opener := func() (afero.File, error) {
+ return ffs.open(filename)
+
+ }
+
+ return decorateFileInfo(fi, ffs, opener, filename, "", meta), nil
+ }
+
+ ffs.decorate = decorator
+ return ffs
+}
+
+type baseFileDecoratorFs struct {
+ afero.Fs
+ decorate func(fi os.FileInfo, filename string) (os.FileInfo, error)
+}
+
+func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) {
+ fi, err := fs.Fs.Stat(name)
+ if err != nil {
+ return nil, err
+ }
+
+ return fs.decorate(fi, name)
+
+}
+
+func (fs *baseFileDecoratorFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
+ var (
+ fi os.FileInfo
+ err error
+ ok bool
+ )
+
+ if lstater, isLstater := fs.Fs.(afero.Lstater); isLstater {
+ fi, ok, err = lstater.LstatIfPossible(name)
+ } else {
+ fi, err = fs.Fs.Stat(name)
+ }
+
+ if err != nil {
+ return nil, false, err
+ }
+
+ fi, err = fs.decorate(fi, name)
+
+ return fi, ok, err
+}
+
+func (fs *baseFileDecoratorFs) Open(name string) (afero.File, error) {
+ return fs.open(name)
+}
+
+func (fs *baseFileDecoratorFs) open(name string) (afero.File, error) {
+ f, err := fs.Fs.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ return &baseFileDecoratorFile{File: f, fs: fs}, nil
+}
+
+type baseFileDecoratorFile struct {
+ afero.File
+ fs *baseFileDecoratorFs
+}
+
+func (l *baseFileDecoratorFile) Readdir(c int) (ofi []os.FileInfo, err error) {
+ dirnames, err := l.File.Readdirnames(c)
+ if err != nil {
+ return nil, err
+ }
+
+ fisp := make([]os.FileInfo, 0, len(dirnames))
+
+ for _, dirname := range dirnames {
+ filename := dirname
+
+ if l.Name() != "" && l.Name() != filepathSeparator {
+ filename = filepath.Join(l.Name(), dirname)
+ }
+
+ // We need to resolve any symlink info.
+ fi, _, err := lstatIfPossible(l.fs.Fs, filename)
+ if err != nil {
+ if os.IsNotExist(err) {
+ continue
+ }
+ return nil, err
+ }
+ fi, err = l.fs.decorate(fi, filename)
+ if err != nil {
+ return nil, errors.Wrap(err, "decorate")
+ }
+ fisp = append(fisp, fi)
+ }
+
+ return fisp, err
+}
diff --git a/hugofs/fileinfo.go b/hugofs/fileinfo.go
new file mode 100644
index 000000000..a2f12c429
--- /dev/null
+++ b/hugofs/fileinfo.go
@@ -0,0 +1,297 @@
+// 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 provides the file systems used by Hugo.
+package hugofs
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/hugofs/files"
+ "golang.org/x/text/unicode/norm"
+
+ "github.com/pkg/errors"
+
+ "github.com/spf13/cast"
+
+ "github.com/gohugoio/hugo/common/hreflect"
+
+ "github.com/spf13/afero"
+)
+
+const (
+ metaKeyFilename = "filename"
+ metaKeyOriginalFilename = "originalFilename"
+ metaKeyName = "name"
+ metaKeyPath = "path"
+ metaKeyPathWalk = "pathWalk"
+ metaKeyLang = "lang"
+ metaKeyWeight = "weight"
+ metaKeyOrdinal = "ordinal"
+ metaKeyFs = "fs"
+ metaKeyOpener = "opener"
+ metaKeyIsOrdered = "isOrdered"
+ metaKeyIsSymlink = "isSymlink"
+ metaKeySkipDir = "skipDir"
+ metaKeyClassifier = "classifier"
+ metaKeyTranslationBaseName = "translationBaseName"
+ metaKeyTranslationBaseNameWithExt = "translationBaseNameWithExt"
+ metaKeyTranslations = "translations"
+ metaKeyDecoraterPath = "decoratorPath"
+)
+
+type FileMeta map[string]interface{}
+
+func (f FileMeta) GetInt(key string) int {
+ return cast.ToInt(f[key])
+}
+
+func (f FileMeta) GetString(key string) string {
+ return cast.ToString(f[key])
+}
+
+func (f FileMeta) GetBool(key string) bool {
+ return cast.ToBool(f[key])
+}
+
+func (f FileMeta) Filename() string {
+ return f.stringV(metaKeyFilename)
+}
+
+func (f FileMeta) OriginalFilename() string {
+ return f.stringV(metaKeyOriginalFilename)
+}
+
+func (f FileMeta) SkipDir() bool {
+ return f.GetBool(metaKeySkipDir)
+}
+func (f FileMeta) TranslationBaseName() string {
+ return f.stringV(metaKeyTranslationBaseName)
+}
+
+func (f FileMeta) TranslationBaseNameWithExt() string {
+ return f.stringV(metaKeyTranslationBaseNameWithExt)
+}
+
+func (f FileMeta) Translations() []string {
+ return cast.ToStringSlice(f[metaKeyTranslations])
+}
+
+func (f FileMeta) Name() string {
+ return f.stringV(metaKeyName)
+}
+
+func (f FileMeta) Classifier() string {
+ c := f.stringV(metaKeyClassifier)
+ if c != "" {
+ return c
+ }
+
+ return files.ContentClassFile // For sorting
+}
+
+func (f FileMeta) Lang() string {
+ return f.stringV(metaKeyLang)
+}
+
+func (f FileMeta) Path() string {
+ return f.stringV(metaKeyPath)
+}
+
+func (f FileMeta) Weight() int {
+ return f.GetInt(metaKeyWeight)
+}
+
+func (f FileMeta) Ordinal() int {
+ return f.GetInt(metaKeyOrdinal)
+}
+
+func (f FileMeta) IsOrdered() bool {
+ return f.GetBool(metaKeyIsOrdered)
+}
+
+// IsSymlink returns whether this comes from a symlinked file or directory.
+func (f FileMeta) IsSymlink() bool {
+ return f.GetBool(metaKeyIsSymlink)
+}
+
+func (f FileMeta) Watch() bool {
+ if v, found := f["watch"]; found {
+ return v.(bool)
+ }
+ return false
+}
+
+func (f FileMeta) Fs() afero.Fs {
+ if v, found := f[metaKeyFs]; found {
+ return v.(afero.Fs)
+ }
+ return nil
+}
+
+func (f FileMeta) GetOpener() func() (afero.File, error) {
+ o, found := f[metaKeyOpener]
+ if !found {
+ return nil
+ }
+ return o.(func() (afero.File, error))
+}
+
+func (f FileMeta) Open() (afero.File, error) {
+ v, found := f[metaKeyOpener]
+ if !found {
+ return nil, errors.New("file opener not found")
+ }
+ return v.(func() (afero.File, error))()
+}
+
+func (f FileMeta) stringV(key string) string {
+ if v, found := f[key]; found {
+ return v.(string)
+ }
+ return ""
+}
+
+func (f FileMeta) setIfNotZero(key string, val interface{}) {
+ if !hreflect.IsTruthful(val) {
+ return
+ }
+ f[key] = val
+}
+
+type FileMetaInfo interface {
+ os.FileInfo
+ Meta() FileMeta
+}
+
+type fileInfoMeta struct {
+ os.FileInfo
+ m FileMeta
+}
+
+func (fi *fileInfoMeta) Meta() FileMeta {
+ return fi.m
+}
+
+func NewFileMetaInfo(fi os.FileInfo, m FileMeta) FileMetaInfo {
+
+ if fim, ok := fi.(FileMetaInfo); ok {
+ mergeFileMeta(fim.Meta(), m)
+ }
+ return &fileInfoMeta{FileInfo: fi, m: m}
+}
+
+// Merge metadata, last entry wins.
+func mergeFileMeta(from, to FileMeta) {
+ if from == nil {
+ return
+ }
+ for k, v := range from {
+ if _, found := to[k]; !found {
+ to[k] = v
+ }
+ }
+}
+
+type dirNameOnlyFileInfo struct {
+ name string
+}
+
+func (fi *dirNameOnlyFileInfo) Name() string {
+ return fi.name
+}
+
+func (fi *dirNameOnlyFileInfo) Size() int64 {
+ panic("not implemented")
+}
+
+func (fi *dirNameOnlyFileInfo) Mode() os.FileMode {
+ return os.ModeDir
+}
+
+func (fi *dirNameOnlyFileInfo) ModTime() time.Time {
+ return time.Time{}
+}
+
+func (fi *dirNameOnlyFileInfo) IsDir() bool {
+ return true
+}
+
+func (fi *dirNameOnlyFileInfo) Sys() interface{} {
+ return nil
+}
+
+func newDirNameOnlyFileInfo(name string, isOrdered bool, fileOpener func() (afero.File, error)) FileMetaInfo {
+ name = normalizeFilename(name)
+ _, base := filepath.Split(name)
+ return NewFileMetaInfo(&dirNameOnlyFileInfo{name: base}, FileMeta{
+ metaKeyFilename: name,
+ metaKeyIsOrdered: isOrdered,
+ metaKeyOpener: fileOpener})
+}
+
+func decorateFileInfo(
+ fi os.FileInfo,
+ fs afero.Fs, opener func() (afero.File, error),
+ filename, filepath string, inMeta FileMeta) FileMetaInfo {
+
+ var meta FileMeta
+ var fim FileMetaInfo
+
+ filepath = strings.TrimPrefix(filepath, filepathSeparator)
+
+ var ok bool
+ if fim, ok = fi.(FileMetaInfo); ok {
+ meta = fim.Meta()
+ } else {
+ meta = make(FileMeta)
+ fim = NewFileMetaInfo(fi, meta)
+ }
+
+ meta.setIfNotZero(metaKeyOpener, opener)
+ meta.setIfNotZero(metaKeyFs, fs)
+ meta.setIfNotZero(metaKeyPath, normalizeFilename(filepath))
+ meta.setIfNotZero(metaKeyFilename, normalizeFilename(filename))
+
+ mergeFileMeta(inMeta, meta)
+
+ return fim
+
+}
+
+func isSymlink(fi os.FileInfo) bool {
+ return fi != nil && fi.Mode()&os.ModeSymlink == os.ModeSymlink
+}
+
+func fileInfosToFileMetaInfos(fis []os.FileInfo) []FileMetaInfo {
+ fims := make([]FileMetaInfo, len(fis))
+ for i, v := range fis {
+ fims[i] = v.(FileMetaInfo)
+ }
+ return fims
+}
+
+func normalizeFilename(filename string) string {
+ if filename == "" {
+ return ""
+ }
+ if runtime.GOOS == "darwin" {
+ // When a file system is HFS+, its filepath is in NFD form.
+ return norm.NFC.String(filename)
+ }
+ return filename
+}
diff --git a/hugofs/files/classifier.go b/hugofs/files/classifier.go
new file mode 100644
index 000000000..9aa2476b7
--- /dev/null
+++ b/hugofs/files/classifier.go
@@ -0,0 +1,121 @@
+// 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 files
+
+import (
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+var (
+ // This should be the only list of valid extensions for content files.
+ contentFileExtensions = []string{
+ "html", "htm",
+ "mdown", "markdown", "md",
+ "asciidoc", "adoc", "ad",
+ "rest", "rst",
+ "mmark",
+ "org",
+ "pandoc", "pdc"}
+
+ contentFileExtensionsSet map[string]bool
+)
+
+func init() {
+ contentFileExtensionsSet = make(map[string]bool)
+ for _, ext := range contentFileExtensions {
+ contentFileExtensionsSet[ext] = true
+ }
+}
+
+func IsContentFile(filename string) bool {
+ return contentFileExtensionsSet[strings.TrimPrefix(filepath.Ext(filename), ".")]
+}
+
+func IsContentExt(ext string) bool {
+ return contentFileExtensionsSet[ext]
+}
+
+const (
+ ContentClassLeaf = "leaf"
+ ContentClassBranch = "branch"
+ ContentClassFile = "zfile" // Sort below
+ ContentClassContent = "zcontent"
+)
+
+func ClassifyContentFile(filename string) string {
+ if !IsContentFile(filename) {
+ return ContentClassFile
+ }
+ if strings.HasPrefix(filename, "_index.") {
+ return ContentClassBranch
+ }
+
+ if strings.HasPrefix(filename, "index.") {
+ return ContentClassLeaf
+ }
+
+ return ContentClassContent
+}
+
+const (
+ ComponentFolderArchetypes = "archetypes"
+ ComponentFolderStatic = "static"
+ ComponentFolderLayouts = "layouts"
+ ComponentFolderContent = "content"
+ ComponentFolderData = "data"
+ ComponentFolderAssets = "assets"
+ ComponentFolderI18n = "i18n"
+
+ FolderResources = "resources"
+)
+
+var (
+ ComponentFolders = []string{
+ ComponentFolderArchetypes,
+ ComponentFolderStatic,
+ ComponentFolderLayouts,
+ ComponentFolderContent,
+ ComponentFolderData,
+ ComponentFolderAssets,
+ ComponentFolderI18n,
+ }
+
+ componentFoldersSet = make(map[string]bool)
+)
+
+func init() {
+ sort.Strings(ComponentFolders)
+ for _, f := range ComponentFolders {
+ componentFoldersSet[f] = true
+ }
+}
+
+// ResolveComponentFolder returns "content" from "content/blog/foo.md" etc.
+func ResolveComponentFolder(filename string) string {
+ filename = strings.TrimPrefix(filename, string(os.PathSeparator))
+ for _, cf := range ComponentFolders {
+ if strings.HasPrefix(filename, cf) {
+ return cf
+ }
+ }
+
+ return ""
+}
+
+func IsComponentFolder(name string) bool {
+ return componentFoldersSet[name]
+}
diff --git a/hugofs/files/classifier_test.go b/hugofs/files/classifier_test.go
new file mode 100644
index 000000000..d576b4e58
--- /dev/null
+++ b/hugofs/files/classifier_test.go
@@ -0,0 +1,49 @@
+// 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 files
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestIsContentFile(t *testing.T) {
+ assert := require.New(t)
+
+ assert.True(IsContentFile(filepath.FromSlash("my/file.md")))
+ assert.True(IsContentFile(filepath.FromSlash("my/file.ad")))
+ assert.False(IsContentFile(filepath.FromSlash("textfile.txt")))
+ assert.True(IsContentExt("md"))
+ assert.False(IsContentExt("json"))
+}
+
+func TestComponentFolders(t *testing.T) {
+ assert := require.New(t)
+
+ // It's important that these are absolutely right and not changed.
+ assert.Equal(len(ComponentFolders), len(componentFoldersSet))
+ assert.True(IsComponentFolder("archetypes"))
+ assert.True(IsComponentFolder("layouts"))
+ assert.True(IsComponentFolder("data"))
+ assert.True(IsComponentFolder("i18n"))
+ assert.True(IsComponentFolder("assets"))
+ assert.False(IsComponentFolder("resources"))
+ assert.True(IsComponentFolder("static"))
+ assert.True(IsComponentFolder("content"))
+ assert.False(IsComponentFolder("foo"))
+ assert.False(IsComponentFolder(""))
+
+}
diff --git a/hugofs/filter_fs.go b/hugofs/filter_fs.go
new file mode 100644
index 000000000..952b276cf
--- /dev/null
+++ b/hugofs/filter_fs.go
@@ -0,0 +1,341 @@
+// 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"
+ "io"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/gohugoio/hugo/hugofs/files"
+
+ "github.com/spf13/afero"
+)
+
+var (
+ _ afero.Fs = (*FilterFs)(nil)
+ _ afero.Lstater = (*FilterFs)(nil)
+ _ afero.File = (*filterDir)(nil)
+)
+
+func NewLanguageFs(langs map[string]int, fs afero.Fs) (afero.Fs, error) {
+
+ applyMeta := func(fs *FilterFs, name string, fis []os.FileInfo) {
+
+ for i, fi := range fis {
+ if fi.IsDir() {
+ filename := filepath.Join(name, fi.Name())
+ fis[i] = decorateFileInfo(fi, fs, fs.getOpener(filename), "", "", nil)
+ continue
+ }
+
+ meta := fi.(FileMetaInfo).Meta()
+ lang := meta.Lang()
+
+ fileLang, translationBaseName, translationBaseNameWithExt := langInfoFrom(langs, fi.Name())
+ weight := 0
+
+ if fileLang != "" {
+ weight = 1
+ if fileLang == lang {
+ // Give priority to myfile.sv.txt inside the sv filesystem.
+ weight++
+ }
+ lang = fileLang
+ }
+
+ fim := NewFileMetaInfo(fi, FileMeta{
+ metaKeyLang: lang,
+ metaKeyWeight: weight,
+ metaKeyOrdinal: langs[lang],
+ metaKeyTranslationBaseName: translationBaseName,
+ metaKeyTranslationBaseNameWithExt: translationBaseNameWithExt,
+ metaKeyClassifier: files.ClassifyContentFile(fi.Name()),
+ })
+
+ fis[i] = fim
+ }
+ }
+
+ all := func(fis []os.FileInfo) {
+ // Maps translation base name to a list of language codes.
+ translations := make(map[string][]string)
+ trackTranslation := func(meta FileMeta) {
+ name := meta.TranslationBaseNameWithExt()
+ translations[name] = append(translations[name], meta.Lang())
+ }
+ for _, fi := range fis {
+ if fi.IsDir() {
+ continue
+ }
+ meta := fi.(FileMetaInfo).Meta()
+
+ trackTranslation(meta)
+
+ }
+
+ for _, fi := range fis {
+ fim := fi.(FileMetaInfo)
+ langs := translations[fim.Meta().TranslationBaseNameWithExt()]
+ if len(langs) > 0 {
+ fim.Meta()["translations"] = sortAndremoveString