1
0
Fork 0
hugo/hugofs/fs.go

253 lines
6.9 KiB
Go

// 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 (
"fmt"
"os"
"strings"
"github.com/bep/overlayfs"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
)
// Os points to the (real) Os filesystem.
var Os = &afero.OsFs{}
// 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
// PublishDir is where Hugo publishes its rendered content.
// It's mounted inside publishDir (default /public).
PublishDir afero.Fs
// PublishDirStatic is the file system used for static files.
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
// 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.
WorkingDirWritable afero.Fs
}
func NewDefault(cfg config.Provider) *Fs {
workingDir, publishDir := getWorkingPublishDir(cfg)
fs := Os
return newFs(fs, fs, workingDir, publishDir)
}
// NewFrom creates a new Fs based on the provided Afero Fs
// as source and destination file systems.
// Useful for testing.
func NewFrom(fs afero.Fs, conf config.BaseConfig) *Fs {
return newFs(fs, fs, conf.WorkingDir, conf.PublishDir)
}
func NewFromOld(fs afero.Fs, cfg config.Provider) *Fs {
workingDir, publishDir := getWorkingPublishDir(cfg)
return newFs(fs, fs, workingDir, publishDir)
}
// NewFromSourceAndDestination creates a new Fs based on the provided Afero Fss
// as the source and destination file systems.
func NewFromSourceAndDestination(source, destination afero.Fs, cfg config.Provider) *Fs {
workingDir, publishDir := getWorkingPublishDir(cfg)
return newFs(source, destination, workingDir, publishDir)
}
func getWorkingPublishDir(cfg config.Provider) (string, string) {
workingDir := cfg.GetString("workingDir")
publishDir := cfg.GetString("publishDirDynamic")
if publishDir == "" {
publishDir = cfg.GetString("publishDir")
}
return workingDir, publishDir
}
func newFs(source, destination afero.Fs, workingDir, publishDir string) *Fs {
if publishDir == "" {
panic("publishDir is empty")
}
if workingDir == "." {
workingDir = ""
}
// Sanity check
if IsOsFs(source) && len(workingDir) < 2 {
panic("workingDir is too short")
}
// If this does not exist, it will be created later.
absPublishDir := paths.AbsPathify(workingDir, publishDir)
pubFs := NewBasePathFs(destination, absPublishDir)
return &Fs{
Source: source,
PublishDir: pubFs,
PublishDirServer: pubFs,
PublishDirStatic: pubFs,
Os: &afero.OsFs{},
WorkingDirReadOnly: getWorkingDirFsReadOnly(source, workingDir),
WorkingDirWritable: getWorkingDirFsWritable(source, workingDir),
}
}
func getWorkingDirFsReadOnly(base afero.Fs, workingDir string) afero.Fs {
if workingDir == "" {
return NewReadOnlyFs(base)
}
return NewBasePathFs(NewReadOnlyFs(base), workingDir)
}
func getWorkingDirFsWritable(base afero.Fs, workingDir string) afero.Fs {
if workingDir == "" {
return base
}
return NewBasePathFs(base, workingDir)
}
func isWrite(flag int) bool {
return flag&os.O_RDWR != 0 || flag&os.O_WRONLY != 0
}
// MakeReadableAndRemoveAllModulePkgDir makes any subdir in dir readable and then
// removes the root.
// TODO(bep) move this to a more suitable place.
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
// Safe guard
// Note that the base directory changed from pkg to gomod_cache in Go 1.23.
if !strings.Contains(dir, "pkg") && !strings.Contains(dir, "gomod") {
panic(fmt.Sprint("invalid dir:", dir))
}
counter := 0
afero.Walk(fs, dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
counter++
fs.Chmod(path, 0o777)
}
return nil
})
return counter, fs.RemoveAll(dir)
}
// IsOsFs returns whether fs is an OsFs or if it fs wraps an OsFs.
// TODO(bep) make this more robust.
func IsOsFs(fs afero.Fs) bool {
var isOsFs bool
WalkFilesystems(fs, func(fs afero.Fs) bool {
switch fs.(type) {
case *afero.MemMapFs:
isOsFs = false
case *afero.OsFs:
isOsFs = true
}
return isOsFs
})
return isOsFs
}
// FilesystemsUnwrapper returns the underlying filesystems.
type FilesystemsUnwrapper interface {
UnwrapFilesystems() []afero.Fs
}
// FilesystemUnwrapper 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
}
var _ FilesystemUnwrapper = (*filesystemsWrapper)(nil)
// NewBasePathFs creates a new BasePathFs.
func NewBasePathFs(source afero.Fs, path string) afero.Fs {
return WrapFilesystem(afero.NewBasePathFs(source, path), source)
}
// NewReadOnlyFs creates a new ReadOnlyFs.
func NewReadOnlyFs(source afero.Fs) afero.Fs {
return WrapFilesystem(afero.NewReadOnlyFs(source), source)
}
// WrapFilesystem is typically used to wrap a afero.BasePathFs to allow
// access to the underlying filesystem if needed.
func WrapFilesystem(container, content afero.Fs) afero.Fs {
return filesystemsWrapper{Fs: container, content: content}
}
type filesystemsWrapper struct {
afero.Fs
content afero.Fs
}
func (w filesystemsWrapper) UnwrapFilesystem() afero.Fs {
return w.content
}