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