fileInfo.go (8252B)
1 // Copyright 2021 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 source 15 16 import ( 17 "fmt" 18 "path/filepath" 19 "strings" 20 "sync" 21 22 "github.com/gohugoio/hugo/common/paths" 23 24 "github.com/gohugoio/hugo/hugofs/files" 25 26 "github.com/gohugoio/hugo/common/hugio" 27 28 "github.com/gohugoio/hugo/hugofs" 29 30 "github.com/gohugoio/hugo/helpers" 31 ) 32 33 // fileInfo implements the File interface. 34 var ( 35 _ File = (*FileInfo)(nil) 36 ) 37 38 // File represents a source file. 39 // This is a temporary construct until we resolve page.Page conflicts. 40 // TODO(bep) remove this construct once we have resolved page deprecations 41 type File interface { 42 fileOverlap 43 FileWithoutOverlap 44 } 45 46 // Temporary to solve duplicate/deprecated names in page.Page 47 type fileOverlap interface { 48 // Path gets the relative path including file name and extension. 49 // The directory is relative to the content root. 50 Path() string 51 52 // Section is first directory below the content root. 53 // For page bundles in root, the Section will be empty. 54 Section() string 55 56 // Lang is the language code for this page. It will be the 57 // same as the site's language code. 58 Lang() string 59 60 IsZero() bool 61 } 62 63 type FileWithoutOverlap interface { 64 65 // Filename gets the full path and filename to the file. 66 Filename() string 67 68 // Dir gets the name of the directory that contains this file. 69 // The directory is relative to the content root. 70 Dir() string 71 72 // Extension is an alias to Ext(). 73 // Deprecated: Use Ext instead. 74 Extension() string 75 76 // Ext gets the file extension, i.e "myblogpost.md" will return "md". 77 Ext() string 78 79 // LogicalName is filename and extension of the file. 80 LogicalName() string 81 82 // BaseFileName is a filename without extension. 83 BaseFileName() string 84 85 // TranslationBaseName is a filename with no extension, 86 // not even the optional language extension part. 87 TranslationBaseName() string 88 89 // ContentBaseName is a either TranslationBaseName or name of containing folder 90 // if file is a leaf bundle. 91 ContentBaseName() string 92 93 // UniqueID is the MD5 hash of the file's path and is for most practical applications, 94 // Hugo content files being one of them, considered to be unique. 95 UniqueID() string 96 97 FileInfo() hugofs.FileMetaInfo 98 } 99 100 // FileInfo describes a source file. 101 type FileInfo struct { 102 103 // Absolute filename to the file on disk. 104 filename string 105 106 sp *SourceSpec 107 108 fi hugofs.FileMetaInfo 109 110 // Derived from filename 111 ext string // Extension without any "." 112 lang string 113 114 name string 115 116 dir string 117 relDir string 118 relPath string 119 baseName string 120 translationBaseName string 121 contentBaseName string 122 section string 123 classifier files.ContentClass 124 125 uniqueID string 126 127 lazyInit sync.Once 128 } 129 130 // Filename returns a file's absolute path and filename on disk. 131 func (fi *FileInfo) Filename() string { return fi.filename } 132 133 // Path gets the relative path including file name and extension. The directory 134 // is relative to the content root. 135 func (fi *FileInfo) Path() string { return fi.relPath } 136 137 // Dir gets the name of the directory that contains this file. The directory is 138 // relative to the content root. 139 func (fi *FileInfo) Dir() string { return fi.relDir } 140 141 // Extension is an alias to Ext(). 142 func (fi *FileInfo) Extension() string { 143 helpers.Deprecated(".File.Extension", "Use .File.Ext instead. ", false) 144 return fi.Ext() 145 } 146 147 // Ext returns a file's extension without the leading period (ie. "md"). 148 func (fi *FileInfo) Ext() string { return fi.ext } 149 150 // Lang returns a file's language (ie. "sv"). 151 func (fi *FileInfo) Lang() string { return fi.lang } 152 153 // LogicalName returns a file's name and extension (ie. "page.sv.md"). 154 func (fi *FileInfo) LogicalName() string { return fi.name } 155 156 // BaseFileName returns a file's name without extension (ie. "page.sv"). 157 func (fi *FileInfo) BaseFileName() string { return fi.baseName } 158 159 // TranslationBaseName returns a file's translation base name without the 160 // language segment (ie. "page"). 161 func (fi *FileInfo) TranslationBaseName() string { return fi.translationBaseName } 162 163 // ContentBaseName is a either TranslationBaseName or name of containing folder 164 // if file is a leaf bundle. 165 func (fi *FileInfo) ContentBaseName() string { 166 fi.init() 167 return fi.contentBaseName 168 } 169 170 // Section returns a file's section. 171 func (fi *FileInfo) Section() string { 172 fi.init() 173 return fi.section 174 } 175 176 // UniqueID returns a file's unique, MD5 hash identifier. 177 func (fi *FileInfo) UniqueID() string { 178 fi.init() 179 return fi.uniqueID 180 } 181 182 // FileInfo returns a file's underlying os.FileInfo. 183 func (fi *FileInfo) FileInfo() hugofs.FileMetaInfo { return fi.fi } 184 185 func (fi *FileInfo) String() string { return fi.BaseFileName() } 186 187 // Open implements ReadableFile. 188 func (fi *FileInfo) Open() (hugio.ReadSeekCloser, error) { 189 f, err := fi.fi.Meta().Open() 190 191 return f, err 192 } 193 194 func (fi *FileInfo) IsZero() bool { 195 return fi == nil 196 } 197 198 // We create a lot of these FileInfo objects, but there are parts of it used only 199 // in some cases that is slightly expensive to construct. 200 func (fi *FileInfo) init() { 201 fi.lazyInit.Do(func() { 202 relDir := strings.Trim(fi.relDir, helpers.FilePathSeparator) 203 parts := strings.Split(relDir, helpers.FilePathSeparator) 204 var section string 205 if (fi.classifier != files.ContentClassLeaf && len(parts) == 1) || len(parts) > 1 { 206 section = parts[0] 207 } 208 fi.section = section 209 210 if fi.classifier.IsBundle() && len(parts) > 0 { 211 fi.contentBaseName = parts[len(parts)-1] 212 } else { 213 fi.contentBaseName = fi.translationBaseName 214 } 215 216 fi.uniqueID = helpers.MD5String(filepath.ToSlash(fi.relPath)) 217 }) 218 } 219 220 // NewTestFile creates a partially filled File used in unit tests. 221 // TODO(bep) improve this package 222 func NewTestFile(filename string) *FileInfo { 223 base := filepath.Base(filepath.Dir(filename)) 224 return &FileInfo{ 225 filename: filename, 226 translationBaseName: base, 227 } 228 } 229 230 func (sp *SourceSpec) NewFileInfoFrom(path, filename string) (*FileInfo, error) { 231 meta := &hugofs.FileMeta{ 232 Filename: filename, 233 Path: path, 234 } 235 236 return sp.NewFileInfo(hugofs.NewFileMetaInfo(nil, meta)) 237 } 238 239 func (sp *SourceSpec) NewFileInfo(fi hugofs.FileMetaInfo) (*FileInfo, error) { 240 m := fi.Meta() 241 242 filename := m.Filename 243 relPath := m.Path 244 245 if relPath == "" { 246 return nil, fmt.Errorf("no Path provided by %v (%T)", m, m.Fs) 247 } 248 249 if filename == "" { 250 return nil, fmt.Errorf("no Filename provided by %v (%T)", m, m.Fs) 251 } 252 253 relDir := filepath.Dir(relPath) 254 if relDir == "." { 255 relDir = "" 256 } 257 if !strings.HasSuffix(relDir, helpers.FilePathSeparator) { 258 relDir = relDir + helpers.FilePathSeparator 259 } 260 261 lang := m.Lang 262 translationBaseName := m.TranslationBaseName 263 264 dir, name := filepath.Split(relPath) 265 if !strings.HasSuffix(dir, helpers.FilePathSeparator) { 266 dir = dir + helpers.FilePathSeparator 267 } 268 269 ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), ".")) 270 baseName := paths.Filename(name) 271 272 if translationBaseName == "" { 273 // This is usually provided by the filesystem. But this FileInfo is also 274 // created in a standalone context when doing "hugo new". This is 275 // an approximate implementation, which is "good enough" in that case. 276 fileLangExt := filepath.Ext(baseName) 277 translationBaseName = strings.TrimSuffix(baseName, fileLangExt) 278 } 279 280 f := &FileInfo{ 281 sp: sp, 282 filename: filename, 283 fi: fi, 284 lang: lang, 285 ext: ext, 286 dir: dir, 287 relDir: relDir, // Dir() 288 relPath: relPath, // Path() 289 name: name, 290 baseName: baseName, // BaseFileName() 291 translationBaseName: translationBaseName, 292 classifier: m.Classifier, 293 } 294 295 return f, nil 296 }