github.com/schumacherfm/hugo@v0.47.1/hugofs/language_fs.go (about) 1 // Copyright 2018 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugofs 15 16 import ( 17 "fmt" 18 "os" 19 "path/filepath" 20 "strings" 21 22 "github.com/spf13/afero" 23 ) 24 25 const hugoFsMarker = "__hugofs" 26 27 var ( 28 _ LanguageAnnouncer = (*LanguageFileInfo)(nil) 29 _ FilePather = (*LanguageFileInfo)(nil) 30 _ afero.Lstater = (*LanguageFs)(nil) 31 ) 32 33 // LanguageAnnouncer is aware of its language. 34 type LanguageAnnouncer interface { 35 Lang() string 36 TranslationBaseName() string 37 } 38 39 // FilePather is aware of its file's location. 40 type FilePather interface { 41 // Filename gets the full path and filename to the file. 42 Filename() string 43 44 // Path gets the content relative path including file name and extension. 45 // The directory is relative to the content root where "content" is a broad term. 46 Path() string 47 48 // RealName is FileInfo.Name in its original form. 49 RealName() string 50 51 BaseDir() string 52 } 53 54 var LanguageDirsMerger = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) { 55 m := make(map[string]*LanguageFileInfo) 56 57 for _, fi := range lofi { 58 fil, ok := fi.(*LanguageFileInfo) 59 if !ok { 60 return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi) 61 } 62 m[fil.virtualName] = fil 63 } 64 65 for _, fi := range bofi { 66 fil, ok := fi.(*LanguageFileInfo) 67 if !ok { 68 return nil, fmt.Errorf("received %T, expected *LanguageFileInfo", fi) 69 } 70 existing, found := m[fil.virtualName] 71 72 if !found || existing.weight < fil.weight { 73 m[fil.virtualName] = fil 74 } 75 } 76 77 merged := make([]os.FileInfo, len(m)) 78 i := 0 79 for _, v := range m { 80 merged[i] = v 81 i++ 82 } 83 84 return merged, nil 85 } 86 87 type LanguageFileInfo struct { 88 os.FileInfo 89 lang string 90 baseDir string 91 realFilename string 92 relFilename string 93 name string 94 realName string 95 virtualName string 96 translationBaseName string 97 98 // We add some weight to the files in their own language's content directory. 99 weight int 100 } 101 102 func (fi *LanguageFileInfo) Filename() string { 103 return fi.realFilename 104 } 105 106 func (fi *LanguageFileInfo) Path() string { 107 return fi.relFilename 108 } 109 110 func (fi *LanguageFileInfo) RealName() string { 111 return fi.realName 112 } 113 114 func (fi *LanguageFileInfo) BaseDir() string { 115 return fi.baseDir 116 } 117 118 func (fi *LanguageFileInfo) Lang() string { 119 return fi.lang 120 } 121 122 // TranslationBaseName returns the base filename without any extension or language 123 // identificator. 124 func (fi *LanguageFileInfo) TranslationBaseName() string { 125 return fi.translationBaseName 126 } 127 128 // Name is the name of the file within this filesystem without any path info. 129 // It will be marked with language information so we can identify it as ours. 130 func (fi *LanguageFileInfo) Name() string { 131 return fi.name 132 } 133 134 type languageFile struct { 135 afero.File 136 fs *LanguageFs 137 } 138 139 // Readdir creates FileInfo entries by calling Lstat if possible. 140 func (l *languageFile) Readdir(c int) (ofi []os.FileInfo, err error) { 141 names, err := l.File.Readdirnames(c) 142 if err != nil { 143 return nil, err 144 } 145 146 fis := make([]os.FileInfo, len(names)) 147 148 for i, name := range names { 149 fi, _, err := l.fs.LstatIfPossible(filepath.Join(l.Name(), name)) 150 151 if err != nil { 152 return nil, err 153 } 154 fis[i] = fi 155 } 156 157 return fis, err 158 } 159 160 type LanguageFs struct { 161 // This Fs is usually created with a BasePathFs 162 basePath string 163 lang string 164 nameMarker string 165 languages map[string]bool 166 afero.Fs 167 } 168 169 func NewLanguageFs(lang string, languages map[string]bool, fs afero.Fs) *LanguageFs { 170 if lang == "" { 171 panic("no lang set for the language fs") 172 } 173 var basePath string 174 175 if bfs, ok := fs.(*afero.BasePathFs); ok { 176 basePath, _ = bfs.RealPath("") 177 } 178 179 marker := hugoFsMarker + "_" + lang + "_" 180 181 return &LanguageFs{lang: lang, languages: languages, basePath: basePath, Fs: fs, nameMarker: marker} 182 } 183 184 func (fs *LanguageFs) Lang() string { 185 return fs.lang 186 } 187 188 func (fs *LanguageFs) Stat(name string) (os.FileInfo, error) { 189 name, err := fs.realName(name) 190 if err != nil { 191 return nil, err 192 } 193 194 fi, err := fs.Fs.Stat(name) 195 if err != nil { 196 return nil, err 197 } 198 199 return fs.newLanguageFileInfo(name, fi) 200 } 201 202 func (fs *LanguageFs) Open(name string) (afero.File, error) { 203 name, err := fs.realName(name) 204 if err != nil { 205 return nil, err 206 } 207 f, err := fs.Fs.Open(name) 208 209 if err != nil { 210 return nil, err 211 } 212 return &languageFile{File: f, fs: fs}, nil 213 } 214 215 func (fs *LanguageFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { 216 name, err := fs.realName(name) 217 if err != nil { 218 return nil, false, err 219 } 220 221 var fi os.FileInfo 222 var b bool 223 224 if lif, ok := fs.Fs.(afero.Lstater); ok { 225 fi, b, err = lif.LstatIfPossible(name) 226 } else { 227 fi, err = fs.Fs.Stat(name) 228 } 229 230 if err != nil { 231 return nil, b, err 232 } 233 234 lfi, err := fs.newLanguageFileInfo(name, fi) 235 236 return lfi, b, err 237 } 238 239 func (fs *LanguageFs) realPath(name string) (string, error) { 240 if baseFs, ok := fs.Fs.(*afero.BasePathFs); ok { 241 return baseFs.RealPath(name) 242 } 243 return name, nil 244 } 245 246 func (fs *LanguageFs) realName(name string) (string, error) { 247 if strings.Contains(name, hugoFsMarker) { 248 if !strings.Contains(name, fs.nameMarker) { 249 return "", os.ErrNotExist 250 } 251 return strings.Replace(name, fs.nameMarker, "", 1), nil 252 } 253 254 if fs.basePath == "" { 255 return name, nil 256 } 257 258 return strings.TrimPrefix(name, fs.basePath), nil 259 } 260 261 func (fs *LanguageFs) newLanguageFileInfo(filename string, fi os.FileInfo) (*LanguageFileInfo, error) { 262 filename = filepath.Clean(filename) 263 _, name := filepath.Split(filename) 264 265 realName := name 266 virtualName := name 267 268 realPath, err := fs.realPath(filename) 269 if err != nil { 270 return nil, err 271 } 272 273 lang := fs.Lang() 274 275 baseNameNoExt := "" 276 277 if !fi.IsDir() { 278 279 // Try to extract the language from the file name. 280 // Any valid language identificator in the name will win over the 281 // language set on the file system, e.g. "mypost.en.md". 282 baseName := filepath.Base(name) 283 ext := filepath.Ext(baseName) 284 baseNameNoExt = baseName 285 286 if ext != "" { 287 baseNameNoExt = strings.TrimSuffix(baseNameNoExt, ext) 288 } 289 290 fileLangExt := filepath.Ext(baseNameNoExt) 291 fileLang := strings.TrimPrefix(fileLangExt, ".") 292 293 if fs.languages[fileLang] { 294 lang = fileLang 295 baseNameNoExt = strings.TrimSuffix(baseNameNoExt, fileLangExt) 296 } 297 298 // This connects the filename to the filesystem, not the language. 299 virtualName = baseNameNoExt + "." + lang + ext 300 301 name = fs.nameMarker + name 302 } 303 304 weight := 1 305 // If this file's language belongs in this directory, add some weight to it 306 // to make it more important. 307 if lang == fs.Lang() { 308 weight = 2 309 } 310 311 if fi.IsDir() { 312 // For directories we always want to start from the union view. 313 realPath = strings.TrimPrefix(realPath, fs.basePath) 314 } 315 316 return &LanguageFileInfo{ 317 lang: lang, 318 weight: weight, 319 realFilename: realPath, 320 realName: realName, 321 relFilename: strings.TrimPrefix(strings.TrimPrefix(realPath, fs.basePath), string(os.PathSeparator)), 322 name: name, 323 virtualName: virtualName, 324 translationBaseName: baseNameNoExt, 325 baseDir: fs.basePath, 326 FileInfo: fi}, nil 327 }