mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	As in, more dots than just to separate the extension and any language indicator. Fixes #4559
		
			
				
	
	
		
			328 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			328 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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 (
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/spf13/afero"
 | 
						|
)
 | 
						|
 | 
						|
const hugoFsMarker = "__hugofs"
 | 
						|
 | 
						|
var (
 | 
						|
	_ LanguageAnnouncer = (*LanguageFileInfo)(nil)
 | 
						|
	_ FilePather        = (*LanguageFileInfo)(nil)
 | 
						|
	_ afero.Lstater     = (*LanguageFs)(nil)
 | 
						|
)
 | 
						|
 | 
						|
// LanguageAnnouncer is aware of its language.
 | 
						|
type LanguageAnnouncer interface {
 | 
						|
	Lang() string
 | 
						|
	TranslationBaseName() string
 | 
						|
}
 | 
						|
 | 
						|
// FilePather is aware of its file's location.
 | 
						|
type FilePather interface {
 | 
						|
	// Filename gets the full path and filename to the file.
 | 
						|
	Filename() string
 | 
						|
 | 
						|
	// Path gets the content relative path including file name and extension.
 | 
						|
	// The directory is relative to the content root where "content" is a broad term.
 | 
						|
	Path() string
 | 
						|
 | 
						|
	// RealName is FileInfo.Name in its original form.
 | 
						|
	RealName() string
 | 
						|
 | 
						|
	BaseDir() string
 | 
						|
}
 | 
						|
 | 
						|
var LanguageDirsMerger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) {
 | 
						|
	m := make(map[string]*LanguageFileInfo)
 | 
						|
 | 
						|
	for _, fi := range lofi {
 | 
						|
		fil, ok := fi.(*LanguageFileInfo)
 | 
						|
		if !ok {
 | 
						|
			return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi)
 | 
						|
		}
 | 
						|
		m[fil.virtualName] = fil
 | 
						|
	}
 | 
						|
 | 
						|
	for _, fi := range bofi {
 | 
						|
		fil, ok := fi.(*LanguageFileInfo)
 | 
						|
		if !ok {
 | 
						|
			return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi)
 | 
						|
		}
 | 
						|
		existing, found := m[fil.virtualName]
 | 
						|
 | 
						|
		if !found || existing.weight < fil.weight {
 | 
						|
			m[fil.virtualName] = fil
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	merged := make([]os.FileInfo, len(m))
 | 
						|
	i := 0
 | 
						|
	for _, v := range m {
 | 
						|
		merged[i] = v
 | 
						|
		i++
 | 
						|
	}
 | 
						|
 | 
						|
	return merged, nil
 | 
						|
}
 | 
						|
 | 
						|
type LanguageFileInfo struct {
 | 
						|
	os.FileInfo
 | 
						|
	lang                string
 | 
						|
	baseDir             string
 | 
						|
	realFilename        string
 | 
						|
	relFilename         string
 | 
						|
	name                string
 | 
						|
	realName            string
 | 
						|
	virtualName         string
 | 
						|
	translationBaseName string
 | 
						|
 | 
						|
	// We add some weight to the files in their own language's content directory.
 | 
						|
	weight int
 | 
						|
}
 | 
						|
 | 
						|
func (fi *LanguageFileInfo) Filename() string {
 | 
						|
	return fi.realFilename
 | 
						|
}
 | 
						|
 | 
						|
func (fi *LanguageFileInfo) Path() string {
 | 
						|
	return fi.relFilename
 | 
						|
}
 | 
						|
 | 
						|
func (fi *LanguageFileInfo) RealName() string {
 | 
						|
	return fi.realName
 | 
						|
}
 | 
						|
 | 
						|
func (fi *LanguageFileInfo) BaseDir() string {
 | 
						|
	return fi.baseDir
 | 
						|
}
 | 
						|
 | 
						|
func (fi *LanguageFileInfo) Lang() string {
 | 
						|
	return fi.lang
 | 
						|
}
 | 
						|
 | 
						|
// TranslationBaseName returns the base filename without any extension or language
 | 
						|
// identificator.
 | 
						|
func (fi *LanguageFileInfo) TranslationBaseName() string {
 | 
						|
	return fi.translationBaseName
 | 
						|
}
 | 
						|
 | 
						|
// Name is the name of the file within this filesystem without any path info.
 | 
						|
// It will be marked with language information so we can identify it as ours.
 | 
						|
func (fi *LanguageFileInfo) Name() string {
 | 
						|
	return fi.name
 | 
						|
}
 | 
						|
 | 
						|
type languageFile struct {
 | 
						|
	afero.File
 | 
						|
	fs *LanguageFs
 | 
						|
}
 | 
						|
 | 
						|
// Readdir creates FileInfo entries by calling Lstat if possible.
 | 
						|
func (l *languageFile) Readdir(c int) (ofi []os.FileInfo, err error) {
 | 
						|
	names, err := l.File.Readdirnames(c)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	fis := make([]os.FileInfo, len(names))
 | 
						|
 | 
						|
	for i, name := range names {
 | 
						|
		fi, _, err := l.fs.LstatIfPossible(filepath.Join(l.Name(), name))
 | 
						|
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		fis[i] = fi
 | 
						|
	}
 | 
						|
 | 
						|
	return fis, err
 | 
						|
}
 | 
						|
 | 
						|
type LanguageFs struct {
 | 
						|
	// This Fs is usually created with a BasePathFs
 | 
						|
	basePath   string
 | 
						|
	lang       string
 | 
						|
	nameMarker string
 | 
						|
	languages  map[string]bool
 | 
						|
	afero.Fs
 | 
						|
}
 | 
						|
 | 
						|
func NewLanguageFs(lang string, languages map[string]bool, fs afero.Fs) *LanguageFs {
 | 
						|
	if lang == "" {
 | 
						|
		panic("no lang set for the language fs")
 | 
						|
	}
 | 
						|
	var basePath string
 | 
						|
 | 
						|
	if bfs, ok := fs.(*afero.BasePathFs); ok {
 | 
						|
		basePath, _ = bfs.RealPath("")
 | 
						|
	}
 | 
						|
 | 
						|
	marker := hugoFsMarker + "_" + lang + "_"
 | 
						|
 | 
						|
	return &LanguageFs{lang: lang, languages: languages, basePath: basePath, Fs: fs, nameMarker: marker}
 | 
						|
}
 | 
						|
 | 
						|
func (fs *LanguageFs) Lang() string {
 | 
						|
	return fs.lang
 | 
						|
}
 | 
						|
 | 
						|
func (fs *LanguageFs) Stat(name string) (os.FileInfo, error) {
 | 
						|
	name, err := fs.realName(name)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	fi, err := fs.Fs.Stat(name)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return fs.newLanguageFileInfo(name, fi)
 | 
						|
}
 | 
						|
 | 
						|
func (fs *LanguageFs) Open(name string) (afero.File, error) {
 | 
						|
	name, err := fs.realName(name)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	f, err := fs.Fs.Open(name)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return &languageFile{File: f, fs: fs}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (fs *LanguageFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
 | 
						|
	name, err := fs.realName(name)
 | 
						|
	if err != nil {
 | 
						|
		return nil, false, err
 | 
						|
	}
 | 
						|
 | 
						|
	var fi os.FileInfo
 | 
						|
	var b bool
 | 
						|
 | 
						|
	if lif, ok := fs.Fs.(afero.Lstater); ok {
 | 
						|
		fi, b, err = lif.LstatIfPossible(name)
 | 
						|
	} else {
 | 
						|
		fi, err = fs.Fs.Stat(name)
 | 
						|
	}
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return nil, b, err
 | 
						|
	}
 | 
						|
 | 
						|
	lfi, err := fs.newLanguageFileInfo(name, fi)
 | 
						|
 | 
						|
	return lfi, b, err
 | 
						|
}
 | 
						|
 | 
						|
func (fs *LanguageFs) realPath(name string) (string, error) {
 | 
						|
	if baseFs, ok := fs.Fs.(*afero.BasePathFs); ok {
 | 
						|
		return baseFs.RealPath(name)
 | 
						|
	}
 | 
						|
	return name, nil
 | 
						|
}
 | 
						|
 | 
						|
func (fs *LanguageFs) realName(name string) (string, error) {
 | 
						|
	if strings.Contains(name, hugoFsMarker) {
 | 
						|
		if !strings.Contains(name, fs.nameMarker) {
 | 
						|
			return "", os.ErrNotExist
 | 
						|
		}
 | 
						|
		return strings.Replace(name, fs.nameMarker, "", 1), nil
 | 
						|
	}
 | 
						|
 | 
						|
	if fs.basePath == "" {
 | 
						|
		return name, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return strings.TrimPrefix(name, fs.basePath), nil
 | 
						|
}
 | 
						|
 | 
						|
func (fs *LanguageFs) newLanguageFileInfo(filename string, fi os.FileInfo) (*LanguageFileInfo, error) {
 | 
						|
	filename = filepath.Clean(filename)
 | 
						|
	_, name := filepath.Split(filename)
 | 
						|
 | 
						|
	realName := name
 | 
						|
	virtualName := name
 | 
						|
 | 
						|
	realPath, err := fs.realPath(filename)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	lang := fs.Lang()
 | 
						|
 | 
						|
	baseNameNoExt := ""
 | 
						|
 | 
						|
	if !fi.IsDir() {
 | 
						|
 | 
						|
		// Try to extract the language from the file name.
 | 
						|
		// Any valid language identificator in the name will win over the
 | 
						|
		// language set on the file system, e.g. "mypost.en.md".
 | 
						|
		baseName := filepath.Base(name)
 | 
						|
		ext := filepath.Ext(baseName)
 | 
						|
		baseNameNoExt = baseName
 | 
						|
 | 
						|
		if ext != "" {
 | 
						|
			baseNameNoExt = strings.TrimSuffix(baseNameNoExt, ext)
 | 
						|
		}
 | 
						|
 | 
						|
		fileLangExt := filepath.Ext(baseNameNoExt)
 | 
						|
		fileLang := strings.TrimPrefix(fileLangExt, ".")
 | 
						|
 | 
						|
		if fs.languages[fileLang] {
 | 
						|
			lang = fileLang
 | 
						|
			baseNameNoExt = strings.TrimSuffix(baseNameNoExt, fileLangExt)
 | 
						|
		}
 | 
						|
 | 
						|
		// This connects the filename to the filesystem, not the language.
 | 
						|
		virtualName = baseNameNoExt + "." + lang + ext
 | 
						|
 | 
						|
		name = fs.nameMarker + name
 | 
						|
	}
 | 
						|
 | 
						|
	weight := 1
 | 
						|
	// If this file's language belongs in this directory, add some weight to it
 | 
						|
	// to make it more important.
 | 
						|
	if lang == fs.Lang() {
 | 
						|
		weight = 2
 | 
						|
	}
 | 
						|
 | 
						|
	if fi.IsDir() {
 | 
						|
		// For directories we always want to start from the union view.
 | 
						|
		realPath = strings.TrimPrefix(realPath, fs.basePath)
 | 
						|
	}
 | 
						|
 | 
						|
	return &LanguageFileInfo{
 | 
						|
		lang:                lang,
 | 
						|
		weight:              weight,
 | 
						|
		realFilename:        realPath,
 | 
						|
		realName:            realName,
 | 
						|
		relFilename:         strings.TrimPrefix(strings.TrimPrefix(realPath, fs.basePath), string(os.PathSeparator)),
 | 
						|
		name:                name,
 | 
						|
		virtualName:         virtualName,
 | 
						|
		translationBaseName: baseNameNoExt,
 | 
						|
		baseDir:             fs.basePath,
 | 
						|
		FileInfo:            fi}, nil
 | 
						|
}
 |