// Copyright 2024 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"
"fmt"
iofs "io/fs"
"os"
"path"
"path/filepath"
"strings"
"sync/atomic"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/paths"
"github.com/bep/overlayfs"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugofs/glob"
radix "github.com/armon/go-radix"
"github.com/spf13/afero"
)
var filepathSeparator = string(filepath.Separator)
var _ ReverseLookupProvder = (*RootMappingFs)(nil)
// NewRootMappingFs creates a new RootMappingFs on top of the provided with
// root mappings with some optional metadata about the root.
// Note that From represents a virtual root that maps to the actual filename in To.
func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
rootMapToReal := radix.New()
realMapToRoot := radix.New()
id := fmt.Sprintf("rfs-%d", rootMappingFsCounter.Add(1))
addMapping := func(key string, rm RootMapping, to *radix.Tree) {
var mappings []RootMapping
v, found := to.Get(key)
if found {
// There may be more than one language pointing to the same root.
mappings = v.([]RootMapping)
}
mappings = append(mappings, rm)
to.Insert(key, mappings)
}
for _, rm := range rms {
(&rm).clean()
rm.FromBase = files.ResolveComponentFolder(rm.From)
if len(rm.To) < 2 {
panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To))
}
fi, err := fs.Stat(rm.To)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
if rm.Meta == nil {
rm.Meta = NewFileMeta()
}
if rm.FromBase == "" {
panic(" rm.FromBase is empty")
}
rm.Meta.Component = rm.FromBase
rm.Meta.Module = rm.Module
rm.Meta.ModuleOrdinal = rm.ModuleOrdinal
rm.Meta.IsProject = rm.IsProject
rm.Meta.BaseDir = rm.ToBase
if !fi.IsDir() {
// We do allow single file mounts.
// However, the file system logic will be much simpler with just directories.
// So, convert this mount into a directory mount with a renamer,
// which will tell the caller if name should be included.
dirFrom, nameFrom := filepath.Split(rm.From)
dirTo, nameTo := filepath.Split(rm.To)
dirFrom, dirTo = strings.TrimSuffix(dirFrom, filepathSeparator), strings.TrimSuffix(dirTo, filepathSeparator)
rm.From = dirFrom
singleFileMeta := rm.Meta.Copy()
singleFileMeta.Name = nameFrom
rm.fiSingleFile = NewFileMetaInfo(fi, singleFileMeta)
rm.To = dirTo
rm.Meta.Rename = func(name string, toFrom bool) (string, bool) {
if toFrom {
if name == nameTo {
return nameFrom, true
}
return "", false
}
if name == nameFrom {
return nameTo, true
}
return "", false
}
nameToFilename := filepathSeparator + nameTo
rm.Meta.InclusionFilter = rm.Meta.InclusionFilter.Append(glob.NewFilenameFilterForInclusionFunc(
func(filename string) bool {
return nameToFilename == filename
},