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 }