diff options
Diffstat (limited to 'hugofs/language_fs.go')
-rw-r--r-- | hugofs/language_fs.go | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/hugofs/language_fs.go b/hugofs/language_fs.go new file mode 100644 index 000000000..95ec0831e --- /dev/null +++ b/hugofs/language_fs.go @@ -0,0 +1,328 @@ +// 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 +} |