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 hugofs
15
16import (
17	"fmt"
18	"io"
19	"os"
20	"path/filepath"
21	"sort"
22	"strings"
23	"syscall"
24	"time"
25
26	"github.com/gohugoio/hugo/hugofs/files"
27
28	"github.com/spf13/afero"
29)
30
31var (
32	_ afero.Fs      = (*FilterFs)(nil)
33	_ afero.Lstater = (*FilterFs)(nil)
34	_ afero.File    = (*filterDir)(nil)
35)
36
37func NewLanguageFs(langs map[string]int, fs afero.Fs) (afero.Fs, error) {
38	applyMeta := func(fs *FilterFs, name string, fis []os.FileInfo) {
39		for i, fi := range fis {
40			if fi.IsDir() {
41				filename := filepath.Join(name, fi.Name())
42				fis[i] = decorateFileInfo(fi, fs, fs.getOpener(filename), "", "", nil)
43				continue
44			}
45
46			meta := fi.(FileMetaInfo).Meta()
47			lang := meta.Lang
48
49			fileLang, translationBaseName, translationBaseNameWithExt := langInfoFrom(langs, fi.Name())
50			weight := 0
51
52			if fileLang != "" {
53				weight = 1
54				if fileLang == lang {
55					// Give priority to myfile.sv.txt inside the sv filesystem.
56					weight++
57				}
58				lang = fileLang
59			}
60
61			fim := NewFileMetaInfo(
62				fi,
63				&FileMeta{
64					Lang:                       lang,
65					Weight:                     weight,
66					Ordinal:                    langs[lang],
67					TranslationBaseName:        translationBaseName,
68					TranslationBaseNameWithExt: translationBaseNameWithExt,
69					Classifier:                 files.ClassifyContentFile(fi.Name(), meta.OpenFunc),
70				})
71
72			fis[i] = fim
73		}
74	}
75
76	all := func(fis []os.FileInfo) {
77		// Maps translation base name to a list of language codes.
78		translations := make(map[string][]string)
79		trackTranslation := func(meta *FileMeta) {
80			name := meta.TranslationBaseNameWithExt
81			translations[name] = append(translations[name], meta.Lang)
82		}
83		for _, fi := range fis {
84			if fi.IsDir() {
85				continue
86			}
87			meta := fi.(FileMetaInfo).Meta()
88
89			trackTranslation(meta)
90
91		}
92
93		for _, fi := range fis {
94			fim := fi.(FileMetaInfo)
95			langs := translations[fim.Meta().TranslationBaseNameWithExt]
96			if len(langs) > 0 {
97				fim.Meta().Translations = sortAndremoveStringDuplicates(langs)
98			}
99		}
100	}
101
102	return &FilterFs{
103		fs:             fs,
104		applyPerSource: applyMeta,
105		applyAll:       all,
106	}, nil
107}
108
109func NewFilterFs(fs afero.Fs) (afero.Fs, error) {
110	applyMeta := func(fs *FilterFs, name string, fis []os.FileInfo) {
111		for i, fi := range fis {
112			if fi.IsDir() {
113				fis[i] = decorateFileInfo(fi, fs, fs.getOpener(fi.(FileMetaInfo).Meta().Filename), "", "", nil)
114			}
115		}
116	}
117
118	ffs := &FilterFs{
119		fs:             fs,
120		applyPerSource: applyMeta,
121	}
122
123	return ffs, nil
124}
125
126// FilterFs is an ordered composite filesystem.
127type FilterFs struct {
128	fs afero.Fs
129
130	applyPerSource func(fs *FilterFs, name string, fis []os.FileInfo)
131	applyAll       func(fis []os.FileInfo)
132}
133
134func (fs *FilterFs) Chmod(n string, m os.FileMode) error {
135	return syscall.EPERM
136}
137
138func (fs *FilterFs) Chtimes(n string, a, m time.Time) error {
139	return syscall.EPERM
140}
141
142func (fs *FilterFs) Chown(n string, uid, gid int) error {
143	return syscall.EPERM
144}
145
146func (fs *FilterFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
147	fi, b, err := lstatIfPossible(fs.fs, name)
148	if err != nil {
149		return nil, false, err
150	}
151
152	if fi.IsDir() {
153		return decorateFileInfo(fi, fs, fs.getOpener(name), "", "", nil), false, nil
154	}
155
156	parent := filepath.Dir(name)
157	fs.applyFilters(parent, -1, fi)
158
159	return fi, b, nil
160}
161
162func (fs *FilterFs) Mkdir(n string, p os.FileMode) error {
163	return syscall.EPERM
164}
165
166func (fs *FilterFs) MkdirAll(n string, p os.FileMode) error {
167	return syscall.EPERM
168}
169
170func (fs *FilterFs) Name() string {
171	return "WeightedFileSystem"
172}
173
174func (fs *FilterFs) Open(name string) (afero.File, error) {
175	f, err := fs.fs.Open(name)
176	if err != nil {
177		return nil, err
178	}
179
180	return &filterDir{
181		File: f,
182		ffs:  fs,
183	}, nil
184}
185
186func (fs *FilterFs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
187	return fs.fs.Open(name)
188}
189
190func (fs *FilterFs) ReadDir(name string) ([]os.FileInfo, error) {
191	panic("not implemented")
192}
193
194func (fs *FilterFs) Remove(n string) error {
195	return syscall.EPERM
196}
197
198func (fs *FilterFs) RemoveAll(p string) error {
199	return syscall.EPERM
200}
201
202func (fs *FilterFs) Rename(o, n string) error {
203	return syscall.EPERM
204}
205
206func (fs *FilterFs) Stat(name string) (os.FileInfo, error) {
207	fi, _, err := fs.LstatIfPossible(name)
208	return fi, err
209}
210
211func (fs *FilterFs) Create(n string) (afero.File, error) {
212	return nil, syscall.EPERM
213}
214
215func (fs *FilterFs) getOpener(name string) func() (afero.File, error) {
216	return func() (afero.File, error) {
217		return fs.Open(name)
218	}
219}
220
221func (fs *FilterFs) applyFilters(name string, count int, fis ...os.FileInfo) ([]os.FileInfo, error) {
222	if fs.applyPerSource != nil {
223		fs.applyPerSource(fs, name, fis)
224	}
225
226	seen := make(map[string]bool)
227	var duplicates []int
228	for i, dir := range fis {
229		if !dir.IsDir() {
230			continue
231		}
232		if seen[dir.Name()] {
233			duplicates = append(duplicates, i)
234		} else {
235			seen[dir.Name()] = true
236		}
237	}
238
239	// Remove duplicate directories, keep first.
240	if len(duplicates) > 0 {
241		for i := len(duplicates) - 1; i >= 0; i-- {
242			idx := duplicates[i]
243			fis = append(fis[:idx], fis[idx+1:]...)
244		}
245	}
246
247	if fs.applyAll != nil {
248		fs.applyAll(fis)
249	}
250
251	if count > 0 && len(fis) >= count {
252		return fis[:count], nil
253	}
254
255	return fis, nil
256}
257
258type filterDir struct {
259	afero.File
260	ffs *FilterFs
261}
262
263func (f *filterDir) Readdir(count int) ([]os.FileInfo, error) {
264	fis, err := f.File.Readdir(-1)
265	if err != nil {
266		return nil, err
267	}
268	return f.ffs.applyFilters(f.Name(), count, fis...)
269}
270
271func (f *filterDir) Readdirnames(count int) ([]string, error) {
272	dirsi, err := f.Readdir(count)
273	if err != nil {
274		return nil, err
275	}
276
277	dirs := make([]string, len(dirsi))
278	for i, d := range dirsi {
279		dirs[i] = d.Name()
280	}
281	return dirs, nil
282}
283
284// Try to extract the language from the given filename.
285// Any valid language identifier in the name will win over the
286// language set on the file system, e.g. "mypost.en.md".
287func langInfoFrom(languages map[string]int, name string) (string, string, string) {
288	var lang string
289
290	baseName := filepath.Base(name)
291	ext := filepath.Ext(baseName)
292	translationBaseName := baseName
293
294	if ext != "" {
295		translationBaseName = strings.TrimSuffix(translationBaseName, ext)
296	}
297
298	fileLangExt := filepath.Ext(translationBaseName)
299	fileLang := strings.TrimPrefix(fileLangExt, ".")
300
301	if _, found := languages[fileLang]; found {
302		lang = fileLang
303		translationBaseName = strings.TrimSuffix(translationBaseName, fileLangExt)
304	}
305
306	translationBaseNameWithExt := translationBaseName
307
308	if ext != "" {
309		translationBaseNameWithExt += ext
310	}
311
312	return lang, translationBaseName, translationBaseNameWithExt
313}
314
315func printFs(fs afero.Fs, path string, w io.Writer) {
316	if fs == nil {
317		return
318	}
319	afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
320		fmt.Println("p:::", path)
321		return nil
322	})
323}
324
325func sortAndremoveStringDuplicates(s []string) []string {
326	ss := sort.StringSlice(s)
327	ss.Sort()
328	i := 0
329	for j := 1; j < len(s); j++ {
330		if !ss.Less(i, j) {
331			continue
332		}
333		i++
334		s[i] = s[j]
335	}
336
337	return s[:i+1]
338}
339