1// Copyright 2019 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 14package filesystems 15 16import ( 17 "errors" 18 "fmt" 19 "os" 20 "path/filepath" 21 "strings" 22 "testing" 23 24 "github.com/gobwas/glob" 25 26 "github.com/gohugoio/hugo/config" 27 28 "github.com/gohugoio/hugo/langs" 29 30 "github.com/spf13/afero" 31 32 qt "github.com/frankban/quicktest" 33 "github.com/gohugoio/hugo/hugofs" 34 "github.com/gohugoio/hugo/hugolib/paths" 35 "github.com/gohugoio/hugo/modules" 36 37) 38 39func initConfig(fs afero.Fs, cfg config.Provider) error { 40 if _, err := langs.LoadLanguageSettings(cfg, nil); err != nil { 41 return err 42 } 43 44 modConfig, err := modules.DecodeConfig(cfg) 45 if err != nil { 46 return err 47 } 48 49 workingDir := cfg.GetString("workingDir") 50 themesDir := cfg.GetString("themesDir") 51 if !filepath.IsAbs(themesDir) { 52 themesDir = filepath.Join(workingDir, themesDir) 53 } 54 globAll := glob.MustCompile("**", '/') 55 modulesClient := modules.NewClient(modules.ClientConfig{ 56 Fs: fs, 57 WorkingDir: workingDir, 58 ThemesDir: themesDir, 59 ModuleConfig: modConfig, 60 IgnoreVendor: globAll, 61 }) 62 63 moduleConfig, err := modulesClient.Collect() 64 if err != nil { 65 return err 66 } 67 68 if err := modules.ApplyProjectConfigDefaults(cfg, moduleConfig.ActiveModules[0]); err != nil { 69 return err 70 } 71 72 cfg.Set("allModules", moduleConfig.ActiveModules) 73 74 return nil 75} 76 77func TestNewBaseFs(t *testing.T) { 78 c := qt.New(t) 79 v := config.New() 80 81 fs := hugofs.NewMem(v) 82 83 themes := []string{"btheme", "atheme"} 84 85 workingDir := filepath.FromSlash("/my/work") 86 v.Set("workingDir", workingDir) 87 v.Set("contentDir", "content") 88 v.Set("themesDir", "themes") 89 v.Set("defaultContentLanguage", "en") 90 v.Set("theme", themes[:1]) 91 92 // Write some data to the themes 93 for _, theme := range themes { 94 for _, dir := range []string{"i18n", "data", "archetypes", "layouts"} { 95 base := filepath.Join(workingDir, "themes", theme, dir) 96 filenameTheme := filepath.Join(base, fmt.Sprintf("theme-file-%s.txt", theme)) 97 filenameOverlap := filepath.Join(base, "f3.txt") 98 fs.Source.Mkdir(base, 0755) 99 content := []byte(fmt.Sprintf("content:%s:%s", theme, dir)) 100 afero.WriteFile(fs.Source, filenameTheme, content, 0755) 101 afero.WriteFile(fs.Source, filenameOverlap, content, 0755) 102 } 103 // Write some files to the root of the theme 104 base := filepath.Join(workingDir, "themes", theme) 105 afero.WriteFile(fs.Source, filepath.Join(base, fmt.Sprintf("theme-root-%s.txt", theme)), []byte(fmt.Sprintf("content:%s", theme)), 0755) 106 afero.WriteFile(fs.Source, filepath.Join(base, "file-theme-root.txt"), []byte(fmt.Sprintf("content:%s", theme)), 0755) 107 } 108 109 afero.WriteFile(fs.Source, filepath.Join(workingDir, "file-root.txt"), []byte("content-project"), 0755) 110 111 afero.WriteFile(fs.Source, filepath.Join(workingDir, "themes", "btheme", "config.toml"), []byte(` 112theme = ["atheme"] 113`), 0755) 114 115 setConfigAndWriteSomeFilesTo(fs.Source, v, "contentDir", "mycontent", 3) 116 setConfigAndWriteSomeFilesTo(fs.Source, v, "i18nDir", "myi18n", 4) 117 setConfigAndWriteSomeFilesTo(fs.Source, v, "layoutDir", "mylayouts", 5) 118 setConfigAndWriteSomeFilesTo(fs.Source, v, "staticDir", "mystatic", 6) 119 setConfigAndWriteSomeFilesTo(fs.Source, v, "dataDir", "mydata", 7) 120 setConfigAndWriteSomeFilesTo(fs.Source, v, "archetypeDir", "myarchetypes", 8) 121 setConfigAndWriteSomeFilesTo(fs.Source, v, "assetDir", "myassets", 9) 122 setConfigAndWriteSomeFilesTo(fs.Source, v, "resourceDir", "myrsesource", 10) 123 124 v.Set("publishDir", "public") 125 c.Assert(initConfig(fs.Source, v), qt.IsNil) 126 127 p, err := paths.New(fs, v) 128 c.Assert(err, qt.IsNil) 129 130 bfs, err := NewBase(p, nil) 131 c.Assert(err, qt.IsNil) 132 c.Assert(bfs, qt.Not(qt.IsNil)) 133 134 root, err := bfs.I18n.Fs.Open("") 135 c.Assert(err, qt.IsNil) 136 dirnames, err := root.Readdirnames(-1) 137 c.Assert(err, qt.IsNil) 138 c.Assert(dirnames, qt.DeepEquals, []string{"f1.txt", "f2.txt", "f3.txt", "f4.txt", "f3.txt", "theme-file-btheme.txt", "f3.txt", "theme-file-atheme.txt"}) 139 140 root, err = bfs.Data.Fs.Open("") 141 c.Assert(err, qt.IsNil) 142 dirnames, err = root.Readdirnames(-1) 143 c.Assert(err, qt.IsNil) 144 c.Assert(dirnames, qt.DeepEquals, []string{"f1.txt", "f2.txt", "f3.txt", "f4.txt", "f5.txt", "f6.txt", "f7.txt", "f3.txt", "theme-file-btheme.txt", "f3.txt", "theme-file-atheme.txt"}) 145 146 checkFileCount(bfs.Layouts.Fs, "", c, 7) 147 148 checkFileCount(bfs.Content.Fs, "", c, 3) 149 checkFileCount(bfs.I18n.Fs, "", c, 8) // 4 + 4 themes 150 151 checkFileCount(bfs.Static[""].Fs, "", c, 6) 152 checkFileCount(bfs.Data.Fs, "", c, 11) // 7 + 4 themes 153 checkFileCount(bfs.Archetypes.Fs, "", c, 10) // 8 + 2 themes 154 checkFileCount(bfs.Assets.Fs, "", c, 9) 155 checkFileCount(bfs.Work, "", c, 82) 156 157 c.Assert(bfs.IsData(filepath.Join(workingDir, "mydata", "file1.txt")), qt.Equals, true) 158 c.Assert(bfs.IsI18n(filepath.Join(workingDir, "myi18n", "file1.txt")), qt.Equals, true) 159 c.Assert(bfs.IsLayout(filepath.Join(workingDir, "mylayouts", "file1.txt")), qt.Equals, true) 160 c.Assert(bfs.IsStatic(filepath.Join(workingDir, "mystatic", "file1.txt")), qt.Equals, true) 161 c.Assert(bfs.IsAsset(filepath.Join(workingDir, "myassets", "file1.txt")), qt.Equals, true) 162 163 contentFilename := filepath.Join(workingDir, "mycontent", "file1.txt") 164 c.Assert(bfs.IsContent(contentFilename), qt.Equals, true) 165 rel := bfs.RelContentDir(contentFilename) 166 c.Assert(rel, qt.Equals, "file1.txt") 167 168 // Check Work fs vs theme 169 checkFileContent(bfs.Work, "file-root.txt", c, "content-project") 170 checkFileContent(bfs.Work, "theme-root-atheme.txt", c, "content:atheme") 171 172 // https://github.com/gohugoio/hugo/issues/5318 173 // Check both project and theme. 174 for _, fs := range []afero.Fs{bfs.Archetypes.Fs, bfs.Layouts.Fs} { 175 for _, filename := range []string{"/f1.txt", "/theme-file-atheme.txt"} { 176 filename = filepath.FromSlash(filename) 177 f, err := fs.Open(filename) 178 c.Assert(err, qt.IsNil) 179 f.Close() 180 } 181 } 182} 183 184func createConfig() config.Provider { 185 v := config.New() 186 v.Set("contentDir", "mycontent") 187 v.Set("i18nDir", "myi18n") 188 v.Set("staticDir", "mystatic") 189 v.Set("dataDir", "mydata") 190 v.Set("layoutDir", "mylayouts") 191 v.Set("archetypeDir", "myarchetypes") 192 v.Set("assetDir", "myassets") 193 v.Set("resourceDir", "resources") 194 v.Set("publishDir", "public") 195 v.Set("defaultContentLanguage", "en") 196 197 return v 198} 199 200func TestNewBaseFsEmpty(t *testing.T) { 201 c := qt.New(t) 202 v := createConfig() 203 fs := hugofs.NewMem(v) 204 c.Assert(initConfig(fs.Source, v), qt.IsNil) 205 206 p, err := paths.New(fs, v) 207 c.Assert(err, qt.IsNil) 208 bfs, err := NewBase(p, nil) 209 c.Assert(err, qt.IsNil) 210 c.Assert(bfs, qt.Not(qt.IsNil)) 211 c.Assert(bfs.Archetypes.Fs, qt.Not(qt.IsNil)) 212 c.Assert(bfs.Layouts.Fs, qt.Not(qt.IsNil)) 213 c.Assert(bfs.Data.Fs, qt.Not(qt.IsNil)) 214 c.Assert(bfs.I18n.Fs, qt.Not(qt.IsNil)) 215 c.Assert(bfs.Work, qt.Not(qt.IsNil)) 216 c.Assert(bfs.Content.Fs, qt.Not(qt.IsNil)) 217 c.Assert(bfs.Static, qt.Not(qt.IsNil)) 218} 219 220func TestRealDirs(t *testing.T) { 221 c := qt.New(t) 222 v := createConfig() 223 fs := hugofs.NewDefault(v) 224 sfs := fs.Source 225 226 root, err := afero.TempDir(sfs, "", "realdir") 227 c.Assert(err, qt.IsNil) 228 themesDir, err := afero.TempDir(sfs, "", "themesDir") 229 c.Assert(err, qt.IsNil) 230 defer func() { 231 os.RemoveAll(root) 232 os.RemoveAll(themesDir) 233 }() 234 235 v.Set("workingDir", root) 236 v.Set("themesDir", themesDir) 237 v.Set("theme", "mytheme") 238 239 c.Assert(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf1"), 0755), qt.IsNil) 240 c.Assert(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf2"), 0755), qt.IsNil) 241 c.Assert(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2"), 0755), qt.IsNil) 242 c.Assert(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf3"), 0755), qt.IsNil) 243 c.Assert(sfs.MkdirAll(filepath.Join(root, "resources"), 0755), qt.IsNil) 244 c.Assert(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "resources"), 0755), qt.IsNil) 245 246 c.Assert(sfs.MkdirAll(filepath.Join(root, "myassets", "js", "f2"), 0755), qt.IsNil) 247 248 afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "scss", "sf1", "a1.scss")), []byte("content"), 0755) 249 afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "scss", "sf2", "a3.scss")), []byte("content"), 0755) 250 afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "scss", "a2.scss")), []byte("content"), 0755) 251 afero.WriteFile(sfs, filepath.Join(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2", "a3.scss")), []byte("content"), 0755) 252 afero.WriteFile(sfs, filepath.Join(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf3", "a4.scss")), []byte("content"), 0755) 253 254 afero.WriteFile(sfs, filepath.Join(filepath.Join(themesDir, "mytheme", "resources", "t1.txt")), []byte("content"), 0755) 255 afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "resources", "p1.txt")), []byte("content"), 0755) 256 afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "resources", "p2.txt")), []byte("content"), 0755) 257 258 afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "js", "f2", "a1.js")), []byte("content"), 0755) 259 afero.WriteFile(sfs, filepath.Join(filepath.Join(root, "myassets", "js", "a2.js")), []byte("content"), 0755) 260 261 c.Assert(initConfig(fs.Source, v), qt.IsNil) 262 263 p, err := paths.New(fs, v) 264 c.Assert(err, qt.IsNil) 265 bfs, err := NewBase(p, nil) 266 c.Assert(err, qt.IsNil) 267 c.Assert(bfs, qt.Not(qt.IsNil)) 268 269 checkFileCount(bfs.Assets.Fs, "", c, 6) 270 271 realDirs := bfs.Assets.RealDirs("scss") 272 c.Assert(len(realDirs), qt.Equals, 2) 273 c.Assert(realDirs[0], qt.Equals, filepath.Join(root, "myassets/scss")) 274 c.Assert(realDirs[len(realDirs)-1], qt.Equals, filepath.Join(themesDir, "mytheme/assets/scss")) 275 276 c.Assert(bfs.theBigFs, qt.Not(qt.IsNil)) 277} 278 279func TestStaticFs(t *testing.T) { 280 c := qt.New(t) 281 v := createConfig() 282 workDir := "mywork" 283 v.Set("workingDir", workDir) 284 v.Set("themesDir", "themes") 285 v.Set("theme", []string{"t1", "t2"}) 286 287 fs := hugofs.NewMem(v) 288 289 themeStaticDir := filepath.Join(workDir, "themes", "t1", "static") 290 themeStaticDir2 := filepath.Join(workDir, "themes", "t2", "static") 291 292 afero.WriteFile(fs.Source, filepath.Join(workDir, "mystatic", "f1.txt"), []byte("Hugo Rocks!"), 0755) 293 afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f1.txt"), []byte("Hugo Themes Rocks!"), 0755) 294 afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f2.txt"), []byte("Hugo Themes Still Rocks!"), 0755) 295 afero.WriteFile(fs.Source, filepath.Join(themeStaticDir2, "f2.txt"), []byte("Hugo Themes Rocks in t2!"), 0755) 296 297 c.Assert(initConfig(fs.Source, v), qt.IsNil) 298 299 p, err := paths.New(fs, v) 300 c.Assert(err, qt.IsNil) 301 bfs, err := NewBase(p, nil) 302 c.Assert(err, qt.IsNil) 303 304 sfs := bfs.StaticFs("en") 305 checkFileContent(sfs, "f1.txt", c, "Hugo Rocks!") 306 checkFileContent(sfs, "f2.txt", c, "Hugo Themes Still Rocks!") 307} 308 309func TestStaticFsMultiHost(t *testing.T) { 310 c := qt.New(t) 311 v := createConfig() 312 workDir := "mywork" 313 v.Set("workingDir", workDir) 314 v.Set("themesDir", "themes") 315 v.Set("theme", "t1") 316 v.Set("defaultContentLanguage", "en") 317 318 langConfig := map[string]interface{}{ 319 "no": map[string]interface{}{ 320 "staticDir": "static_no", 321 "baseURL": "https://example.org/no/", 322 }, 323 "en": map[string]interface{}{ 324 "baseURL": "https://example.org/en/", 325 }, 326 } 327 328 v.Set("languages", langConfig) 329 330 fs := hugofs.NewMem(v) 331 332 themeStaticDir := filepath.Join(workDir, "themes", "t1", "static") 333 334 afero.WriteFile(fs.Source, filepath.Join(workDir, "mystatic", "f1.txt"), []byte("Hugo Rocks!"), 0755) 335 afero.WriteFile(fs.Source, filepath.Join(workDir, "static_no", "f1.txt"), []byte("Hugo Rocks in Norway!"), 0755) 336 337 afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f1.txt"), []byte("Hugo Themes Rocks!"), 0755) 338 afero.WriteFile(fs.Source, filepath.Join(themeStaticDir, "f2.txt"), []byte("Hugo Themes Still Rocks!"), 0755) 339 340 c.Assert(initConfig(fs.Source, v), qt.IsNil) 341 342 p, err := paths.New(fs, v) 343 c.Assert(err, qt.IsNil) 344 bfs, err := NewBase(p, nil) 345 c.Assert(err, qt.IsNil) 346 enFs := bfs.StaticFs("en") 347 checkFileContent(enFs, "f1.txt", c, "Hugo Rocks!") 348 checkFileContent(enFs, "f2.txt", c, "Hugo Themes Still Rocks!") 349 350 noFs := bfs.StaticFs("no") 351 checkFileContent(noFs, "f1.txt", c, "Hugo Rocks in Norway!") 352 checkFileContent(noFs, "f2.txt", c, "Hugo Themes Still Rocks!") 353} 354 355func TestMakePathRelative(t *testing.T) { 356 c := qt.New(t) 357 v := createConfig() 358 fs := hugofs.NewMem(v) 359 workDir := "mywork" 360 v.Set("workingDir", workDir) 361 362 c.Assert(fs.Source.MkdirAll(filepath.Join(workDir, "dist", "d1"), 0777), qt.IsNil) 363 c.Assert(fs.Source.MkdirAll(filepath.Join(workDir, "static", "d2"), 0777), qt.IsNil) 364 c.Assert(fs.Source.MkdirAll(filepath.Join(workDir, "dust", "d2"), 0777), qt.IsNil) 365 366 moduleCfg := map[string]interface{}{ 367 "mounts": []interface{}{ 368 map[string]interface{}{ 369 "source": "dist", 370 "target": "static/mydist", 371 }, 372 map[string]interface{}{ 373 "source": "dust", 374 "target": "static/foo/bar", 375 }, 376 map[string]interface{}{ 377 "source": "static", 378 "target": "static", 379 }, 380 }, 381 } 382 383 v.Set("module", moduleCfg) 384 385 c.Assert(initConfig(fs.Source, v), qt.IsNil) 386 387 p, err := paths.New(fs, v) 388 c.Assert(err, qt.IsNil) 389 bfs, err := NewBase(p, nil) 390 c.Assert(err, qt.IsNil) 391 392 sfs := bfs.Static[""] 393 c.Assert(sfs, qt.Not(qt.IsNil)) 394 395 makeRel := func(s string) string { 396 r, _ := sfs.MakePathRelative(s) 397 return r 398 } 399 400 c.Assert(makeRel(filepath.Join(workDir, "dist", "d1", "foo.txt")), qt.Equals, filepath.FromSlash("mydist/d1/foo.txt")) 401 c.Assert(makeRel(filepath.Join(workDir, "static", "d2", "foo.txt")), qt.Equals, filepath.FromSlash("d2/foo.txt")) 402 c.Assert(makeRel(filepath.Join(workDir, "dust", "d3", "foo.txt")), qt.Equals, filepath.FromSlash("foo/bar/d3/foo.txt")) 403} 404 405func checkFileCount(fs afero.Fs, dirname string, c *qt.C, expected int) { 406 count, _, err := countFilesAndGetFilenames(fs, dirname) 407 c.Assert(err, qt.IsNil) 408 c.Assert(count, qt.Equals, expected) 409} 410 411func checkFileContent(fs afero.Fs, filename string, c *qt.C, expected ...string) { 412 b, err := afero.ReadFile(fs, filename) 413 c.Assert(err, qt.IsNil) 414 415 content := string(b) 416 417 for _, e := range expected { 418 c.Assert(content, qt.Contains, e) 419 } 420} 421 422func countFilesAndGetFilenames(fs afero.Fs, dirname string) (int, []string, error) { 423 if fs == nil { 424 return 0, nil, errors.New("no fs") 425 } 426 427 counter := 0 428 var filenames []string 429 430 wf := func(path string, info hugofs.FileMetaInfo, err error) error { 431 if err != nil { 432 return err 433 } 434 if !info.IsDir() { 435 counter++ 436 } 437 438 if info.Name() != "." { 439 name := info.Name() 440 name = strings.Replace(name, filepath.FromSlash("/my/work"), "WORK_DIR", 1) 441 filenames = append(filenames, name) 442 } 443 444 return nil 445 } 446 447 w := hugofs.NewWalkway(hugofs.WalkwayConfig{Fs: fs, Root: dirname, WalkFn: wf}) 448 449 if err := w.Walk(); err != nil { 450 return -1, nil, err 451 } 452 453 return counter, filenames, nil 454} 455 456func setConfigAndWriteSomeFilesTo(fs afero.Fs, v config.Provider, key, val string, num int) { 457 workingDir := v.GetString("workingDir") 458 v.Set(key, val) 459 fs.Mkdir(val, 0755) 460 for i := 0; i < num; i++ { 461 filename := filepath.Join(workingDir, val, fmt.Sprintf("f%d.txt", i+1)) 462 afero.WriteFile(fs, filename, []byte(fmt.Sprintf("content:%s:%d", key, i+1)), 0755) 463 } 464} 465