fileinfo.go (6381B)
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 provides the file systems used by Hugo.
15 package hugofs
16
17 import (
18 "os"
19 "path/filepath"
20 "reflect"
21 "runtime"
22 "sort"
23 "strings"
24 "time"
25
26 "github.com/gohugoio/hugo/hugofs/glob"
27
28 "github.com/gohugoio/hugo/hugofs/files"
29 "golang.org/x/text/unicode/norm"
30
31 "errors"
32
33 "github.com/gohugoio/hugo/common/hreflect"
34 "github.com/gohugoio/hugo/common/htime"
35
36 "github.com/spf13/afero"
37 )
38
39 func NewFileMeta() *FileMeta {
40 return &FileMeta{}
41 }
42
43 // PathFile returns the relative file path for the file source.
44 func (f *FileMeta) PathFile() string {
45 if f.BaseDir == "" {
46 return ""
47 }
48 return strings.TrimPrefix(strings.TrimPrefix(f.Filename, f.BaseDir), filepathSeparator)
49 }
50
51 type FileMeta struct {
52 Name string
53 Filename string
54 Path string
55 PathWalk string
56 OriginalFilename string
57 BaseDir string
58
59 SourceRoot string
60 MountRoot string
61 Module string
62
63 Weight int
64 IsOrdered bool
65 IsSymlink bool
66 IsRootFile bool
67 IsProject bool
68 Watch bool
69
70 Classifier files.ContentClass
71
72 SkipDir bool
73
74 Lang string
75 TranslationBaseName string
76 TranslationBaseNameWithExt string
77 Translations []string
78
79 Fs afero.Fs
80 OpenFunc func() (afero.File, error)
81 JoinStatFunc func(name string) (FileMetaInfo, error)
82
83 // Include only files or directories that match.
84 InclusionFilter *glob.FilenameFilter
85 }
86
87 func (m *FileMeta) Copy() *FileMeta {
88 if m == nil {
89 return NewFileMeta()
90 }
91 c := *m
92 return &c
93 }
94
95 func (m *FileMeta) Merge(from *FileMeta) {
96 if m == nil || from == nil {
97 return
98 }
99 dstv := reflect.Indirect(reflect.ValueOf(m))
100 srcv := reflect.Indirect(reflect.ValueOf(from))
101
102 for i := 0; i < dstv.NumField(); i++ {
103 v := dstv.Field(i)
104 if !v.CanSet() {
105 continue
106 }
107 if !hreflect.IsTruthfulValue(v) {
108 v.Set(srcv.Field(i))
109 }
110 }
111
112 if m.InclusionFilter == nil {
113 m.InclusionFilter = from.InclusionFilter
114 }
115 }
116
117 func (f *FileMeta) Open() (afero.File, error) {
118 if f.OpenFunc == nil {
119 return nil, errors.New("OpenFunc not set")
120 }
121 return f.OpenFunc()
122 }
123
124 func (f *FileMeta) JoinStat(name string) (FileMetaInfo, error) {
125 if f.JoinStatFunc == nil {
126 return nil, os.ErrNotExist
127 }
128 return f.JoinStatFunc(name)
129 }
130
131 type FileMetaInfo interface {
132 os.FileInfo
133 Meta() *FileMeta
134 }
135
136 type fileInfoMeta struct {
137 os.FileInfo
138
139 m *FileMeta
140 }
141
142 type filenameProvider interface {
143 Filename() string
144 }
145
146 var _ filenameProvider = (*fileInfoMeta)(nil)
147
148 // Filename returns the full filename.
149 func (fi *fileInfoMeta) Filename() string {
150 return fi.m.Filename
151 }
152
153 // Name returns the file's name. Note that we follow symlinks,
154 // if supported by the file system, and the Name given here will be the
155 // name of the symlink, which is what Hugo needs in all situations.
156 func (fi *fileInfoMeta) Name() string {
157 if name := fi.m.Name; name != "" {
158 return name
159 }
160 return fi.FileInfo.Name()
161 }
162
163 func (fi *fileInfoMeta) Meta() *FileMeta {
164 return fi.m
165 }
166
167 func NewFileMetaInfo(fi os.FileInfo, m *FileMeta) FileMetaInfo {
168 if m == nil {
169 panic("FileMeta must be set")
170 }
171 if fim, ok := fi.(FileMetaInfo); ok {
172 m.Merge(fim.Meta())
173 }
174 return &fileInfoMeta{FileInfo: fi, m: m}
175 }
176
177 type dirNameOnlyFileInfo struct {
178 name string
179 modTime time.Time
180 }
181
182 func (fi *dirNameOnlyFileInfo) Name() string {
183 return fi.name
184 }
185
186 func (fi *dirNameOnlyFileInfo) Size() int64 {
187 panic("not implemented")
188 }
189
190 func (fi *dirNameOnlyFileInfo) Mode() os.FileMode {
191 return os.ModeDir
192 }
193
194 func (fi *dirNameOnlyFileInfo) ModTime() time.Time {
195 return fi.modTime
196 }
197
198 func (fi *dirNameOnlyFileInfo) IsDir() bool {
199 return true
200 }
201
202 func (fi *dirNameOnlyFileInfo) Sys() any {
203 return nil
204 }
205
206 func newDirNameOnlyFileInfo(name string, meta *FileMeta, fileOpener func() (afero.File, error)) FileMetaInfo {
207 name = normalizeFilename(name)
208 _, base := filepath.Split(name)
209
210 m := meta.Copy()
211 if m.Filename == "" {
212 m.Filename = name
213 }
214 m.OpenFunc = fileOpener
215 m.IsOrdered = false
216
217 return NewFileMetaInfo(
218 &dirNameOnlyFileInfo{name: base, modTime: htime.Now()},
219 m,
220 )
221 }
222
223 func decorateFileInfo(
224 fi os.FileInfo,
225 fs afero.Fs, opener func() (afero.File, error),
226 filename, filepath string, inMeta *FileMeta) FileMetaInfo {
227 var meta *FileMeta
228 var fim FileMetaInfo
229
230 filepath = strings.TrimPrefix(filepath, filepathSeparator)
231
232 var ok bool
233 if fim, ok = fi.(FileMetaInfo); ok {
234 meta = fim.Meta()
235 } else {
236 meta = NewFileMeta()
237 fim = NewFileMetaInfo(fi, meta)
238 }
239
240 if opener != nil {
241 meta.OpenFunc = opener
242 }
243 if fs != nil {
244 meta.Fs = fs
245 }
246 nfilepath := normalizeFilename(filepath)
247 nfilename := normalizeFilename(filename)
248 if nfilepath != "" {
249 meta.Path = nfilepath
250 }
251 if nfilename != "" {
252 meta.Filename = nfilename
253 }
254
255 meta.Merge(inMeta)
256
257 return fim
258 }
259
260 func isSymlink(fi os.FileInfo) bool {
261 return fi != nil && fi.Mode()&os.ModeSymlink == os.ModeSymlink
262 }
263
264 func fileInfosToFileMetaInfos(fis []os.FileInfo) []FileMetaInfo {
265 fims := make([]FileMetaInfo, len(fis))
266 for i, v := range fis {
267 fims[i] = v.(FileMetaInfo)
268 }
269 return fims
270 }
271
272 func normalizeFilename(filename string) string {
273 if filename == "" {
274 return ""
275 }
276 if runtime.GOOS == "darwin" {
277 // When a file system is HFS+, its filepath is in NFD form.
278 return norm.NFC.String(filename)
279 }
280 return filename
281 }
282
283 func fileInfosToNames(fis []os.FileInfo) []string {
284 names := make([]string, len(fis))
285 for i, d := range fis {
286 names[i] = d.Name()
287 }
288 return names
289 }
290
291 func fromSlash(filenames []string) []string {
292 for i, name := range filenames {
293 filenames[i] = filepath.FromSlash(name)
294 }
295 return filenames
296 }
297
298 func sortFileInfos(fis []os.FileInfo) {
299 sort.Slice(fis, func(i, j int) bool {
300 fimi, fimj := fis[i].(FileMetaInfo), fis[j].(FileMetaInfo)
301 return fimi.Meta().Filename < fimj.Meta().Filename
302 })
303 }