// 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 } // Name returns the file's name. Note that we follow symlinks, // if supported by the file system, and the Name given here will be the // name of the symlink, which is what Hugo needs in all situations. func (fi *fileInfoMeta) Name() string { if name := fi.m.Name(); name != "" { return name } return fi.FileInfo.Name() } 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 } func fileInfosToNames(fis []os.FileInfo) []string { names := make([]string, len(fis)) for i, d := range fis { names[i] = d.Name() } return names }