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 }