diff options
Diffstat (limited to 'hugofs')
-rw-r--r-- | hugofs/base_fs.go | 35 | ||||
-rw-r--r-- | hugofs/noop_fs.go | 79 | ||||
-rw-r--r-- | hugofs/rootmapping_fs.go | 180 | ||||
-rw-r--r-- | hugofs/rootmapping_fs_test.go | 93 |
4 files changed, 352 insertions, 35 deletions
diff --git a/hugofs/base_fs.go b/hugofs/base_fs.go deleted file mode 100644 index 77af66dfe..000000000 --- a/hugofs/base_fs.go +++ /dev/null @@ -1,35 +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 ( - "github.com/spf13/afero" -) - -// BaseFs contains the core base filesystems used by Hugo. The name "base" is used -// to underline that even if they can be composites, they all have a base path set to a specific -// resource folder, e.g "/my-project/content". So, no absolute filenames needed. -type BaseFs struct { - // The filesystem used to capture content. This can be a composite and - // language aware file system. - ContentFs afero.Fs - - // The filesystem used to store resources (processed images etc.). - // This usually maps to /my-project/resources. - ResourcesFs afero.Fs - - // The filesystem used to publish the rendered site. - // This usually maps to /my-project/public. - PublishFs afero.Fs -} diff --git a/hugofs/noop_fs.go b/hugofs/noop_fs.go new file mode 100644 index 000000000..2d06622e4 --- /dev/null +++ b/hugofs/noop_fs.go @@ -0,0 +1,79 @@ +// 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 ( + "errors" + "os" + "time" + + "github.com/spf13/afero" +) + +var ( + noOpErr = errors.New("this is a filesystem that does nothing and this operation is not supported") + _ afero.Fs = (*noOpFs)(nil) + NoOpFs = &noOpFs{} +) + +type noOpFs struct { +} + +func (fs noOpFs) Create(name string) (afero.File, error) { + return nil, noOpErr +} + +func (fs noOpFs) Mkdir(name string, perm os.FileMode) error { + return noOpErr +} + +func (fs noOpFs) MkdirAll(path string, perm os.FileMode) error { + return noOpErr +} + +func (fs noOpFs) Open(name string) (afero.File, error) { + return nil, os.ErrNotExist +} + +func (fs noOpFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { + return nil, os.ErrNotExist +} + +func (fs noOpFs) Remove(name string) error { + return noOpErr +} + +func (fs noOpFs) RemoveAll(path string) error { + return noOpErr +} + +func (fs noOpFs) Rename(oldname string, newname string) error { + return noOpErr +} + +func (fs noOpFs) Stat(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist +} + +func (fs noOpFs) Name() string { + return "noOpFs" +} + +func (fs noOpFs) Chmod(name string, mode os.FileMode) error { + return noOpErr +} + +func (fs noOpFs) Chtimes(name string, atime time.Time, mtime time.Time) error { + return noOpErr +} diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go new file mode 100644 index 000000000..59f49f3a9 --- /dev/null +++ b/hugofs/rootmapping_fs.go @@ -0,0 +1,180 @@ +// 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" + "path/filepath" + "strings" + "time" + + radix "github.com/hashicorp/go-immutable-radix" + "github.com/spf13/afero" +) + +var filepathSeparator = string(filepath.Separator) + +// 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. +type RootMappingFs struct { + afero.Fs + rootMapToReal *radix.Node + virtualRoots []string +} + +type rootMappingFile struct { + afero.File + fs *RootMappingFs + name string +} + +type rootMappingFileInfo struct { + name string +} + +func (fi *rootMappingFileInfo) Name() string { + return fi.name +} + +func (fi *rootMappingFileInfo) Size() int64 { + panic("not implemented") +} + +func (fi *rootMappingFileInfo) Mode() os.FileMode { + return os.ModeDir +} + +func (fi *rootMappingFileInfo) ModTime() time.Time { + panic("not implemented") +} + +func (fi *rootMappingFileInfo) IsDir() bool { + return true +} + +func (fi *rootMappingFileInfo) Sys() interface{} { + return nil +} + +func newRootMappingDirFileInfo(name string) *rootMappingFileInfo { + return &rootMappingFileInfo{name: name} +} + +// NewRootMappingFs creates a new RootMappingFs on top of the provided with +// a list of from, to string pairs of root mappings. +// Note that 'from' represents a virtual root that maps to the actual filename in 'to'. +func NewRootMappingFs(fs afero.Fs, fromTo ...string) (*RootMappingFs, error) { + rootMapToReal := radix.New().Txn() + var virtualRoots []string + + for i := 0; i < len(fromTo); i += 2 { + vr := filepath.Clean(fromTo[i]) + rr := filepath.Clean(fromTo[i+1]) + + // We need to preserve the original order for Readdir + virtualRoots = append(virtualRoots, vr) + + rootMapToReal.Insert([]byte(vr), rr) + } + + return &RootMappingFs{Fs: fs, + virtualRoots: virtualRoots, + rootMapToReal: rootMapToReal.Commit().Root()}, nil +} + +func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) { + if fs.isRoot(name) { + return newRootMappingDirFileInfo(name), nil + } + realName := fs.realName(name) + return fs.Fs.Stat(realName) +} + +func (fs *RootMappingFs) isRoot(name string) bool { + return name == "" || name == filepathSeparator + +} + +func (fs *RootMappingFs) Open(name string) (afero.File, error) { + if fs.isRoot(name) { + return &rootMappingFile{name: name, fs: fs}, nil + } + realName := fs.realName(name) + f, err := fs.Fs.Open(realName) + if err != nil { + return nil, err + } + return &rootMappingFile{File: f, name: name, fs: fs}, nil +} + +func (fs *RootMappingFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + if fs.isRoot(name) { + return newRootMappingDirFileInfo(name), false, nil + } + name = fs.realName(name) + if ls, ok := fs.Fs.(afero.Lstater); ok { + return ls.LstatIfPossible(name) + } + fi, err := fs.Stat(name) + return fi, false, err +} + +func (fs *RootMappingFs) realName(name string) string { + key, val, found := fs.rootMapToReal.LongestPrefix([]byte(filepath.Clean(name))) + if !found { + return name + } + keystr := string(key) + + return filepath.Join(val.(string), strings.TrimPrefix(name, keystr)) +} + +func (f *rootMappingFile) Readdir(count int) ([]os.FileInfo, error) { + if f.File == nil { + dirsn := make([]os.FileInfo, 0) + for i := 0; i < len(f.fs.virtualRoots); i++ { + if count != -1 && i >= count { + break + } + dirsn = append(dirsn, newRootMappingDirFileInfo(f.fs.virtualRoots[i])) + } + return dirsn, nil + } + return f.File.Readdir(count) + +} + +func (f *rootMappingFile) Readdirnames(count int) ([]string, error) { + dirs, err := f.Readdir(count) + if err != nil { + return nil, err + } + dirss := make([]string, len(dirs)) + for i, d := range dirs { + dirss[i] = d.Name() + } + return dirss, nil +} + +func (f *rootMappingFile) Name() string { + return f.name +} + +func (f *rootMappingFile) Close() error { + if f.File == nil { + return nil + } + return f.File.Close() +} diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go new file mode 100644 index 000000000..a84f41151 --- /dev/null +++ b/hugofs/rootmapping_fs_test.go @@ -0,0 +1,93 @@ +// 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 ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func TestRootMappingFsRealName(t *testing.T) { + assert := require.New(t) + fs := afero.NewMemMapFs() + + rfs, err := NewRootMappingFs(fs, "f1", "f1t", "f2", "f2t") + assert.NoError(err) + + assert.Equal(filepath.FromSlash("f1t/foo/file.txt"), rfs.realName(filepath.Join("f1", "foo", "file.txt"))) + +} + +func TestRootMappingFsDirnames(t *testing.T) { + assert := require.New(t) + fs := afero.NewMemMapFs() + + testfile := "myfile.txt" + assert.NoError(fs.Mkdir("f1t", 0755)) + assert.NoError(fs.Mkdir("f2t", 0755)) + assert.NoError(fs.Mkdir("f3t", 0755)) + assert.NoError(afero.WriteFile(fs, filepath.Join("f2t", testfile), []byte("some content"), 0755)) + + rfs, err := NewRootMappingFs(fs, "bf1", "f1t", "cf2", "f2t", "af3", "f3t") + assert.NoError(err) + + fif, err := rfs.Stat(filepath.Join("cf2", testfile)) + assert.NoError(err) + assert.Equal("myfile.txt", fif.Name()) + + root, err := rfs.Open(filepathSeparator) + assert.NoError(err) + + dirnames, err := root.Readdirnames(-1) + assert.NoError(err) + assert.Equal([]string{"bf1", "cf2", "af3"}, dirnames) + +} + +func TestRootMappingFsOs(t *testing.T) { + assert := require.New(t) + fs := afero.NewOsFs() + + d, err := ioutil.TempDir("", "hugo-root-mapping") + assert.NoError(err) + defer func() { + os.RemoveAll(d) + }() + + testfile := "myfile.txt" + assert.NoError(fs.Mkdir(filepath.Join(d, "f1t"), 0755)) + assert.NoError(fs.Mkdir(filepath.Join(d, "f2t"), 0755)) + assert.NoError(fs.Mkdir(filepath.Join(d, "f3t"), 0755)) + assert.NoError(afero.WriteFile(fs, filepath.Join(d, "f2t", testfile), []byte("some content"), 0755)) + + rfs, err := NewRootMappingFs(fs, "bf1", filepath.Join(d, "f1t"), "cf2", filepath.Join(d, "f2t"), "af3", filepath.Join(d, "f3t")) + assert.NoError(err) + + fif, err := rfs.Stat(filepath.Join("cf2", testfile)) + assert.NoError(err) + assert.Equal("myfile.txt", fif.Name()) + + root, err := rfs.Open(filepathSeparator) + assert.NoError(err) + + dirnames, err := root.Readdirnames(-1) + assert.NoError(err) + assert.Equal([]string{"bf1", "cf2", "af3"}, dirnames) + +} |