summaryrefslogtreecommitdiffstats
path: root/hugofs
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-03-21 09:35:15 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-04-08 13:26:17 +0200
commitd070bdf10f14d233288f7318a4e9f7555f070a65 (patch)
treefff8d59f98bdab3027bb45c4e10ca88594332872 /hugofs
parentb08193971a821fc27e549a73120c15e5e5186775 (diff)
Rework the Destination filesystem to make --renderStaticToDisk work
See #9626
Diffstat (limited to 'hugofs')
-rw-r--r--hugofs/createcounting_fs.go8
-rw-r--r--hugofs/decorators.go8
-rw-r--r--hugofs/filename_filter_fs.go8
-rw-r--r--hugofs/filter_fs.go8
-rw-r--r--hugofs/fs.go142
-rw-r--r--hugofs/fs_test.go48
-rw-r--r--hugofs/hashing_fs.go9
-rw-r--r--hugofs/language_composite_fs.go13
-rw-r--r--hugofs/nosymlink_fs.go8
-rw-r--r--hugofs/rootmapping_fs.go8
-rw-r--r--hugofs/rootmapping_fs_test.go5
-rw-r--r--hugofs/slice_fs.go15
-rw-r--r--hugofs/stacktracer_fs.go11
13 files changed, 238 insertions, 53 deletions
diff --git a/hugofs/createcounting_fs.go b/hugofs/createcounting_fs.go
index 802806b7a..1737ad5ce 100644
--- a/hugofs/createcounting_fs.go
+++ b/hugofs/createcounting_fs.go
@@ -33,10 +33,18 @@ type DuplicatesReporter interface {
ReportDuplicates() string
}
+var (
+ _ FilesystemUnwrapper = (*createCountingFs)(nil)
+)
+
func NewCreateCountingFs(fs afero.Fs) afero.Fs {
return &createCountingFs{Fs: fs, fileCount: make(map[string]int)}
}
+func (fs *createCountingFs) UnwrapFilesystem() afero.Fs {
+ return fs.Fs
+}
+
// ReportDuplicates reports filenames written more than once.
func (c *createCountingFs) ReportDuplicates() string {
c.mu.Lock()
diff --git a/hugofs/decorators.go b/hugofs/decorators.go
index 364a3e23e..be0ae495d 100644
--- a/hugofs/decorators.go
+++ b/hugofs/decorators.go
@@ -23,6 +23,10 @@ import (
"github.com/spf13/afero"
)
+var (
+ _ FilesystemUnwrapper = (*baseFileDecoratorFs)(nil)
+)
+
func decorateDirs(fs afero.Fs, meta *FileMeta) afero.Fs {
ffs := &baseFileDecoratorFs{Fs: fs}
@@ -151,6 +155,10 @@ type baseFileDecoratorFs struct {
decorate func(fi os.FileInfo, filename string) (os.FileInfo, error)
}
+func (fs *baseFileDecoratorFs) UnwrapFilesystem() afero.Fs {
+ return fs.Fs
+}
+
func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) {
fi, err := fs.Fs.Stat(name)
if err != nil {
diff --git a/hugofs/filename_filter_fs.go b/hugofs/filename_filter_fs.go
index 2a11335a3..4ecd1f55a 100644
--- a/hugofs/filename_filter_fs.go
+++ b/hugofs/filename_filter_fs.go
@@ -23,6 +23,10 @@ import (
"github.com/spf13/afero"
)
+var (
+ _ FilesystemUnwrapper = (*filenameFilterFs)(nil)
+)
+
func newFilenameFilterFs(fs afero.Fs, base string, filter *glob.FilenameFilter) afero.Fs {
return &filenameFilterFs{
fs: fs,
@@ -39,6 +43,10 @@ type filenameFilterFs struct {
filter *glob.FilenameFilter
}
+func (fs *filenameFilterFs) UnwrapFilesystem() afero.Fs {
+ return fs.fs
+}
+
func (fs *filenameFilterFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
fi, b, err := fs.fs.(afero.Lstater).LstatIfPossible(name)
if err != nil {
diff --git a/hugofs/filter_fs.go b/hugofs/filter_fs.go
index ec3897d9e..351b4d0f7 100644
--- a/hugofs/filter_fs.go
+++ b/hugofs/filter_fs.go
@@ -121,6 +121,10 @@ func NewFilterFs(fs afero.Fs) (afero.Fs, error) {
return ffs, nil
}
+var (
+ _ FilesystemUnwrapper = (*FilterFs)(nil)
+)
+
// FilterFs is an ordered composite filesystem.
type FilterFs struct {
fs afero.Fs
@@ -141,6 +145,10 @@ func (fs *FilterFs) Chown(n string, uid, gid int) error {
return syscall.EPERM
}
+func (fs *FilterFs) UnwrapFilesystem() afero.Fs {
+ return fs.fs
+}
+
func (fs *FilterFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
fi, b, err := lstatIfPossible(fs.fs, name)
if err != nil {
diff --git a/hugofs/fs.go b/hugofs/fs.go
index 95645204e..436387f13 100644
--- a/hugofs/fs.go
+++ b/hugofs/fs.go
@@ -19,6 +19,8 @@ import (
"os"
"strings"
+ "github.com/bep/overlayfs"
+ "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
)
@@ -26,32 +28,43 @@ import (
// Os points to the (real) Os filesystem.
var Os = &afero.OsFs{}
-// Fs abstracts the file system to separate source and destination file systems
-// and allows both to be mocked for testing.
+// Fs holds the core filesystems used by Hugo.
type Fs struct {
// Source is Hugo's source file system.
+ // Note that this will always be a "plain" Afero filesystem:
+ // * afero.OsFs when running in production
+ // * afero.MemMapFs for many of the tests.
Source afero.Fs
- // Destination is Hugo's destination file system.
- Destination afero.Fs
+ // PublishDir is where Hugo publishes its rendered content.
+ // It's mounted inside publishDir (default /public).
+ PublishDir afero.Fs
- // Destination used for `renderStaticToDisk`
- DestinationStatic afero.Fs
+ // PublishDirStatic is the file system used for static files when --renderStaticToDisk is set.
+ // When this is set, PublishDir is set to write to memory.
+ PublishDirStatic afero.Fs
+
+ // PublishDirServer is the file system used for serving the public directory with Hugo's development server.
+ // This will typically be the same as PublishDir, but not if --renderStaticToDisk is set.
+ PublishDirServer afero.Fs
// Os is an OS file system.
// NOTE: Field is currently unused.
Os afero.Fs
- // WorkingDir is a read-only file system
+ // WorkingDirReadOnly is a read-only file system
+ // restricted to the project working dir.
+ WorkingDirReadOnly afero.Fs
+
+ // WorkingDirWritable is a writable file system
// restricted to the project working dir.
- // TODO(bep) get rid of this (se BaseFs)
- WorkingDir *afero.BasePathFs
+ WorkingDirWritable afero.Fs
}
// NewDefault creates a new Fs with the OS file system
// as source and destination file systems.
func NewDefault(cfg config.Provider) *Fs {
- fs := &afero.OsFs{}
+ fs := Os
return newFs(fs, cfg)
}
@@ -71,23 +84,49 @@ func NewFrom(fs afero.Fs, cfg config.Provider) *Fs {
}
func newFs(base afero.Fs, cfg config.Provider) *Fs {
+ workingDir := cfg.GetString("workingDir")
+ publishDir := cfg.GetString("publishDir")
+ if publishDir == "" {
+ panic("publishDir is empty")
+ }
+
+ // Sanity check
+ if IsOsFs(base) && len(workingDir) < 2 {
+ panic("workingDir is too short")
+ }
+
+ absPublishDir := paths.AbsPathify(workingDir, publishDir)
+
+ // Make sure we always have the /public folder ready to use.
+ if err := base.MkdirAll(absPublishDir, 0777); err != nil && !os.IsExist(err) {
+ panic(err)
+ }
+
+ pubFs := afero.NewBasePathFs(base, absPublishDir)
+
return &Fs{
- Source: base,
- Destination: base,
- DestinationStatic: base,
- Os: &afero.OsFs{},
- WorkingDir: getWorkingDirFs(base, cfg),
+ Source: base,
+ PublishDir: pubFs,
+ PublishDirServer: pubFs,
+ PublishDirStatic: pubFs,
+ Os: &afero.OsFs{},
+ WorkingDirReadOnly: getWorkingDirFsReadOnly(base, workingDir),
+ WorkingDirWritable: getWorkingDirFsWritable(base, workingDir),
}
}
-func getWorkingDirFs(base afero.Fs, cfg config.Provider) *afero.BasePathFs {
- workingDir := cfg.GetString("workingDir")
-
- if workingDir != "" {
- return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir).(*afero.BasePathFs)
+func getWorkingDirFsReadOnly(base afero.Fs, workingDir string) afero.Fs {
+ if workingDir == "" {
+ return afero.NewReadOnlyFs(base)
}
+ return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir)
+}
- return nil
+func getWorkingDirFsWritable(base afero.Fs, workingDir string) afero.Fs {
+ if workingDir == "" {
+ return base
+ }
+ return afero.NewBasePathFs(base, workingDir)
}
func isWrite(flag int) bool {
@@ -117,3 +156,64 @@ func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error)
})
return counter, fs.RemoveAll(dir)
}
+
+// HasOsFs returns whether fs is an OsFs or if it fs wraps an OsFs.
+// TODO(bep) make this nore robust.
+func IsOsFs(fs afero.Fs) bool {
+ var isOsFs bool
+ WalkFilesystems(fs, func(fs afero.Fs) bool {
+ switch base := fs.(type) {
+ case *afero.MemMapFs:
+ isOsFs = false
+ case *afero.OsFs:
+ isOsFs = true
+ case *afero.BasePathFs:
+ _, supportsLstat, _ := base.LstatIfPossible("asdfasdfasdf")
+ isOsFs = supportsLstat
+ }
+ return isOsFs
+ })
+ return isOsFs
+}
+
+// FilesystemsUnwrapper returns the underlying filesystems.
+type FilesystemsUnwrapper interface {
+ UnwrapFilesystems() []afero.Fs
+}
+
+// FilesystemsProvider returns the underlying filesystem.
+type FilesystemUnwrapper interface {
+ UnwrapFilesystem() afero.Fs
+}
+
+// WalkFn is the walk func for WalkFilesystems.
+type WalkFn func(fs afero.Fs) bool
+
+// WalkFilesystems walks fs recursively and calls fn.
+// If fn returns true, walking is stopped.
+func WalkFilesystems(fs afero.Fs, fn WalkFn) bool {
+ if fn(fs) {
+ return true
+ }
+
+ if afs, ok := fs.(FilesystemUnwrapper); ok {
+ if WalkFilesystems(afs.UnwrapFilesystem(), fn) {
+ return true
+ }
+
+ } else if bfs, ok := fs.(FilesystemsUnwrapper); ok {
+ for _, sf := range bfs.UnwrapFilesystems() {
+ if WalkFilesystems(sf, fn) {
+ return true
+ }
+ }
+ } else if cfs, ok := fs.(overlayfs.FilesystemIterator); ok {
+ for i := 0; i < cfs.NumFilesystems(); i++ {
+ if WalkFilesystems(cfs.Filesystem(i), fn) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
diff --git a/hugofs/fs_test.go b/hugofs/fs_test.go
index 8d52267af..f7203fac9 100644
--- a/hugofs/fs_test.go
+++ b/hugofs/fs_test.go
@@ -23,38 +23,46 @@ import (
"github.com/spf13/afero"
)
+func TestIsOsFs(t *testing.T) {
+ c := qt.New(t)
+
+ c.Assert(IsOsFs(Os), qt.Equals, true)
+ c.Assert(IsOsFs(&afero.MemMapFs{}), qt.Equals, false)
+ c.Assert(IsOsFs(afero.NewBasePathFs(&afero.MemMapFs{}, "/public")), qt.Equals, false)
+ c.Assert(IsOsFs(afero.NewBasePathFs(Os, t.TempDir())), qt.Equals, true)
+
+}
+
func TestNewDefault(t *testing.T) {
c := qt.New(t)
- v := config.New()
+ v := config.NewWithTestDefaults()
+ v.Set("workingDir", t.TempDir())
f := NewDefault(v)
- c.Assert(f.Source, qt.Not(qt.IsNil))
+ c.Assert(f.Source, qt.IsNotNil)
c.Assert(f.Source, hqt.IsSameType, new(afero.OsFs))
- c.Assert(f.Os, qt.Not(qt.IsNil))
- c.Assert(f.WorkingDir, qt.IsNil)
+ c.Assert(f.Os, qt.IsNotNil)
+ c.Assert(f.WorkingDirReadOnly, qt.IsNotNil)
+ c.Assert(f.WorkingDirReadOnly, hqt.IsSameType, new(afero.BasePathFs))
+ c.Assert(IsOsFs(f.Source), qt.IsTrue)
+ c.Assert(IsOsFs(f.WorkingDirReadOnly), qt.IsTrue)
+ c.Assert(IsOsFs(f.PublishDir), qt.IsTrue)
+ c.Assert(IsOsFs(f.Os), qt.IsTrue)
}
func TestNewMem(t *testing.T) {
c := qt.New(t)
- v := config.New()
+ v := config.NewWithTestDefaults()
f := NewMem(v)
c.Assert(f.Source, qt.Not(qt.IsNil))
c.Assert(f.Source, hqt.IsSameType, new(afero.MemMapFs))
- c.Assert(f.Destination, qt.Not(qt.IsNil))
- c.Assert(f.Destination, hqt.IsSameType, new(afero.MemMapFs))
+ c.Assert(f.PublishDir, qt.Not(qt.IsNil))
+ c.Assert(f.PublishDir, hqt.IsSameType, new(afero.BasePathFs))
c.Assert(f.Os, hqt.IsSameType, new(afero.OsFs))
- c.Assert(f.WorkingDir, qt.IsNil)
-}
-
-func TestWorkingDir(t *testing.T) {
- c := qt.New(t)
- v := config.New()
-
- v.Set("workingDir", "/a/b/")
-
- f := NewMem(v)
-
- c.Assert(f.WorkingDir, qt.Not(qt.IsNil))
- c.Assert(f.WorkingDir, hqt.IsSameType, new(afero.BasePathFs))
+ c.Assert(f.WorkingDirReadOnly, qt.IsNotNil)
+ c.Assert(IsOsFs(f.Source), qt.IsFalse)
+ c.Assert(IsOsFs(f.WorkingDirReadOnly), qt.IsFalse)
+ c.Assert(IsOsFs(f.PublishDir), qt.IsFalse)
+ c.Assert(IsOsFs(f.Os), qt.IsTrue)
}
diff --git a/hugofs/hashing_fs.go b/hugofs/hashing_fs.go
index d7b6329c9..d15ba5863 100644
--- a/hugofs/hashing_fs.go
+++ b/hugofs/hashing_fs.go
@@ -22,7 +22,10 @@ import (
"github.com/spf13/afero"
)
-var _ afero.Fs = (*md5HashingFs)(nil)
+var (
+ _ afero.Fs = (*md5HashingFs)(nil)
+ _ FilesystemUnwrapper = (*md5HashingFs)(nil)
+)
// FileHashReceiver will receive the filename an the content's MD5 sum on file close.
type FileHashReceiver interface {
@@ -45,6 +48,10 @@ func NewHashingFs(delegate afero.Fs, hashReceiver FileHashReceiver) afero.Fs {
return &md5HashingFs{Fs: delegate, hashReceiver: hashReceiver}
}
+func (fs *md5HashingFs) UnwrapFilesystem() afero.Fs {
+ return fs.Fs
+}
+
func (fs *md5HashingFs) Create(name string) (afero.File, error) {
f, err := fs.Fs.Create(name)
if err == nil {
diff --git a/hugofs/language_composite_fs.go b/hugofs/language_composite_fs.go
index 65bc89e71..9b4bc4cfd 100644
--- a/hugofs/language_composite_fs.go
+++ b/hugofs/language_composite_fs.go
@@ -20,11 +20,14 @@ import (
)
var (
- _ afero.Fs = (*languageCompositeFs)(nil)
- _ afero.Lstater = (*languageCompositeFs)(nil)
+ _ afero.Fs = (*languageCompositeFs)(nil)
+ _ afero.Lstater = (*languageCompositeFs)(nil)
+ _ FilesystemsUnwrapper = (*languageCompositeFs)(nil)
)
type languageCompositeFs struct {
+ base afero.Fs
+ overlay afero.Fs
*afero.CopyOnWriteFs
}
@@ -33,7 +36,11 @@ type languageCompositeFs struct {
// to the target filesystem. This information is available in Readdir, Stat etc. via the
// special LanguageFileInfo FileInfo implementation.
func NewLanguageCompositeFs(base, overlay afero.Fs) afero.Fs {
- return &languageCompositeFs{afero.NewCopyOnWriteFs(base, overlay).(*afero.CopyOnWriteFs)}
+ return &languageCompositeFs{base, overlay, afero.NewCopyOnWriteFs(base, overlay).(*afero.CopyOnWriteFs)}
+}
+
+func (fs *languageCompositeFs) UnwrapFilesystems() []afero.Fs {
+ return []afero.Fs{fs.base, fs.overlay}
}
// Open takes the full path to the file in the target filesystem. If it is a directory, it gets merged
diff --git a/hugofs/nosymlink_fs.go b/hugofs/nosymlink_fs.go
index ff9503257..d3cad5e74 100644
--- a/hugofs/nosymlink_fs.go
+++ b/hugofs/nosymlink_fs.go
@@ -30,6 +30,10 @@ func NewNoSymlinkFs(fs afero.Fs, logger loggers.Logger, allowFiles bool) afero.F
return &noSymlinkFs{Fs: fs, logger: logger, allowFiles: allowFiles}
}
+var (
+ _ FilesystemUnwrapper = (*noSymlinkFs)(nil)
+)
+
// noSymlinkFs is a filesystem that prevents symlinking.
type noSymlinkFs struct {
allowFiles bool // block dirs only
@@ -67,6 +71,10 @@ func (f *noSymlinkFile) Readdirnames(count int) ([]string, error) {
return fileInfosToNames(dirs), nil
}
+func (fs *noSymlinkFs) UnwrapFilesystem() afero.Fs {
+ return fs.Fs
+}
+
func (fs *noSymlinkFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
return fs.stat(name)
}
diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go
index 28458155c..a891ba8de 100644
--- a/hugofs/rootmapping_fs.go
+++ b/hugofs/rootmapping_fs.go
@@ -151,6 +151,10 @@ func (r RootMapping) trimFrom(name string) string {
return strings.TrimPrefix(name, r.From)
}
+var (
+ _ FilesystemUnwrapper = (*RootMappingFs)(nil)
+)
+
// A RootMappingFs maps several roots into one. Note that the root of this filesystem
// is directories only, and they will be returned in Readdir and Readdirnames
// in the order given.
@@ -200,6 +204,10 @@ func (fs *RootMappingFs) Dirs(base string) ([]FileMetaInfo, error) {
return fss, nil
}
+func (fs *RootMappingFs) UnwrapFilesystem() afero.Fs {
+ return fs.Fs
+}
+
// Filter creates a copy of this filesystem with only mappings matching a filter.
func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs {
rootMapToReal := radix.New()
diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go
index c650e8f11..c843866fc 100644
--- a/hugofs/rootmapping_fs_test.go
+++ b/hugofs/rootmapping_fs_test.go
@@ -20,9 +20,8 @@ import (
"sort"
"testing"
- "github.com/gohugoio/hugo/hugofs/glob"
-
"github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugofs/glob"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting"
@@ -31,7 +30,7 @@ import (
func TestLanguageRootMapping(t *testing.T) {
c := qt.New(t)
- v := config.New()
+ v := config.NewWithTestDefaults()
v.Set("contentDir", "content")
fs := NewBaseFileDecorator(afero.NewMemMapFs())
diff --git a/hugofs/slice_fs.go b/hugofs/slice_fs.go
index 0f0d3850a..a9a3f1bbc 100644
--- a/hugofs/slice_fs.go
+++ b/hugofs/slice_fs.go
@@ -24,9 +24,10 @@ import (
)
var (
- _ afero.Fs = (*SliceFs)(nil)
- _ afero.Lstater = (*SliceFs)(nil)
- _ afero.File = (*sliceDir)(nil)
+ _ afero.Fs = (*SliceFs)(nil)
+ _ afero.Lstater = (*SliceFs)(nil)
+ _ FilesystemsUnwrapper = (*SliceFs)(nil)
+ _ afero.File = (*sliceDir)(nil)
)
func NewSliceFs(dirs ...FileMetaInfo) (afero.Fs, error) {
@@ -52,6 +53,14 @@ type SliceFs struct {
dirs []FileMetaInfo
}
+func (fs *SliceFs) UnwrapFilesystems() []afero.Fs {
+ var fss []afero.Fs
+ for _, dir := range fs.dirs {
+ fss = append(fss, dir.Meta().Fs)
+ }
+ return fss
+}
+
func (fs *SliceFs) Chmod(n string, m os.FileMode) error {
return syscall.EPERM
}
diff --git a/hugofs/stacktracer_fs.go b/hugofs/stacktracer_fs.go
index d3769f903..4411dbfde 100644
--- a/hugofs/stacktracer_fs.go
+++ b/hugofs/stacktracer_fs.go
@@ -24,8 +24,11 @@ import (
"github.com/spf13/afero"
)
-// Make sure we don't accidentally use this in the real Hugo.
-var _ types.DevMarker = (*stacktracerFs)(nil)
+var (
+ // Make sure we don't accidentally use this in the real Hugo.
+ _ types.DevMarker = (*stacktracerFs)(nil)
+ _ FilesystemUnwrapper = (*stacktracerFs)(nil)
+)
// NewStacktracerFs wraps the given fs printing stack traces for file creates
// matching the given regexp pattern.
@@ -45,6 +48,10 @@ type stacktracerFs struct {
func (fs *stacktracerFs) DevOnly() {
}
+func (fs *stacktracerFs) UnwrapFilesystem() afero.Fs {
+ return fs.Fs
+}
+
func (fs *stacktracerFs) onCreate(filename string) {
if fs.re.MatchString(filename) {
trace := make([]byte, 1500)