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