content.go (9243B)
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 create provides functions to create new content.
15 package create
16
17 import (
18 "bytes"
19 "fmt"
20 "io"
21 "os"
22 "path/filepath"
23 "strings"
24
25 "github.com/gohugoio/hugo/hugofs/glob"
26
27 "github.com/gohugoio/hugo/common/hexec"
28 "github.com/gohugoio/hugo/common/paths"
29
30 "errors"
31
32 "github.com/gohugoio/hugo/hugofs/files"
33
34 "github.com/gohugoio/hugo/hugofs"
35
36 "github.com/gohugoio/hugo/helpers"
37 "github.com/gohugoio/hugo/hugolib"
38 "github.com/spf13/afero"
39 )
40
41 const (
42 // DefaultArchetypeTemplateTemplate is the template used in 'hugo new site'
43 // and the template we use as a fall back.
44 DefaultArchetypeTemplateTemplate = `---
45 title: "{{ replace .Name "-" " " | title }}"
46 date: {{ .Date }}
47 draft: true
48 ---
49
50 `
51 )
52
53 // NewContent creates a new content file in h (or a full bundle if the archetype is a directory)
54 // in targetPath.
55 func NewContent(h *hugolib.HugoSites, kind, targetPath string) error {
56 if h.BaseFs.Content.Dirs == nil {
57 return errors.New("no existing content directory configured for this project")
58 }
59
60 cf := hugolib.NewContentFactory(h)
61
62 if kind == "" {
63 var err error
64 kind, err = cf.SectionFromFilename(targetPath)
65 if err != nil {
66 return err
67 }
68 }
69
70 b := &contentBuilder{
71 archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs,
72 sourceFs: h.PathSpec.Fs.Source,
73 ps: h.PathSpec,
74 h: h,
75 cf: cf,
76
77 kind: kind,
78 targetPath: targetPath,
79 }
80
81 ext := paths.Ext(targetPath)
82
83 b.setArcheTypeFilenameToUse(ext)
84
85 withBuildLock := func() (string, error) {
86 unlock, err := h.BaseFs.LockBuild()
87 if err != nil {
88 return "", fmt.Errorf("failed to acquire a build lock: %s", err)
89 }
90 defer unlock()
91
92 if b.isDir {
93 return "", b.buildDir()
94 }
95
96 if ext == "" {
97 return "", fmt.Errorf("failed to resolve %q to a archetype template", targetPath)
98 }
99
100 if !files.IsContentFile(b.targetPath) {
101 return "", fmt.Errorf("target path %q is not a known content format", b.targetPath)
102 }
103
104 return b.buildFile()
105
106 }
107
108 filename, err := withBuildLock()
109 if err != nil {
110 return err
111 }
112
113 if filename != "" {
114 return b.openInEditorIfConfigured(filename)
115 }
116
117 return nil
118
119 }
120
121 type contentBuilder struct {
122 archeTypeFs afero.Fs
123 sourceFs afero.Fs
124
125 ps *helpers.PathSpec
126 h *hugolib.HugoSites
127 cf hugolib.ContentFactory
128
129 // Builder state
130 archetypeFilename string
131 targetPath string
132 kind string
133 isDir bool
134 dirMap archetypeMap
135 }
136
137 func (b *contentBuilder) buildDir() error {
138 // Split the dir into content files and the rest.
139 if err := b.mapArcheTypeDir(); err != nil {
140 return err
141 }
142
143 var contentTargetFilenames []string
144 var baseDir string
145
146 for _, fi := range b.dirMap.contentFiles {
147 targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename))
148 abs, err := b.cf.CreateContentPlaceHolder(targetFilename)
149 if err != nil {
150 return err
151 }
152 if baseDir == "" {
153 baseDir = strings.TrimSuffix(abs, targetFilename)
154 }
155
156 contentTargetFilenames = append(contentTargetFilenames, abs)
157 }
158
159 var contentInclusionFilter *glob.FilenameFilter
160 if !b.dirMap.siteUsed {
161 // We don't need to build everything.
162 contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
163 filename = strings.TrimPrefix(filename, string(os.PathSeparator))
164 for _, cn := range contentTargetFilenames {
165 if strings.Contains(cn, filename) {
166 return true
167 }
168 }
169 return false
170 })
171
172 }
173
174 if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
175 return err
176 }
177
178 for i, filename := range contentTargetFilenames {
179 if err := b.applyArcheType(filename, b.dirMap.contentFiles[i].Meta().Path); err != nil {
180 return err
181 }
182 }
183
184 // Copy the rest as is.
185 for _, f := range b.dirMap.otherFiles {
186 meta := f.Meta()
187 filename := meta.Path
188
189 in, err := meta.Open()
190 if err != nil {
191 return fmt.Errorf("failed to open non-content file: %w", err)
192 }
193
194 targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(filename, b.archetypeFilename))
195 targetDir := filepath.Dir(targetFilename)
196
197 if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) {
198 return fmt.Errorf("failed to create target directory for %q: %w", targetDir, err)
199 }
200
201 out, err := b.sourceFs.Create(targetFilename)
202 if err != nil {
203 return err
204 }
205
206 _, err = io.Copy(out, in)
207 if err != nil {
208 return err
209 }
210
211 in.Close()
212 out.Close()
213 }
214
215 b.h.Log.Printf("Content dir %q created", filepath.Join(baseDir, b.targetPath))
216
217 return nil
218 }
219
220 func (b *contentBuilder) buildFile() (string, error) {
221 contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath)
222 if err != nil {
223 return "", err
224 }
225
226 usesSite, err := b.usesSiteVar(b.archetypeFilename)
227 if err != nil {
228 return "", err
229 }
230
231 var contentInclusionFilter *glob.FilenameFilter
232 if !usesSite {
233 // We don't need to build everything.
234 contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
235 filename = strings.TrimPrefix(filename, string(os.PathSeparator))
236 return strings.Contains(contentPlaceholderAbsFilename, filename)
237 })
238 }
239
240 if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
241 return "", err
242 }
243
244 if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFilename); err != nil {
245 return "", err
246 }
247
248 b.h.Log.Printf("Content %q created", contentPlaceholderAbsFilename)
249
250 return contentPlaceholderAbsFilename, nil
251 }
252
253 func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
254 var pathsToCheck []string
255
256 if b.kind != "" {
257 pathsToCheck = append(pathsToCheck, b.kind+ext)
258 }
259
260 pathsToCheck = append(pathsToCheck, "default"+ext)
261
262 for _, p := range pathsToCheck {
263 fi, err := b.archeTypeFs.Stat(p)
264 if err == nil {
265 b.archetypeFilename = p
266 b.isDir = fi.IsDir()
267 return
268 }
269 }
270
271 }
272
273 func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename string) error {
274 p := b.h.GetContentPage(contentFilename)
275 if p == nil {
276 panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename))
277 }
278
279 f, err := b.sourceFs.Create(contentFilename)
280 if err != nil {
281 return err
282 }
283 defer f.Close()
284
285 if archetypeFilename == "" {
286 return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate)
287 }
288
289 return b.cf.ApplyArchetypeFilename(f, p, b.kind, archetypeFilename)
290
291 }
292
293 func (b *contentBuilder) mapArcheTypeDir() error {
294 var m archetypeMap
295
296 walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
297 if err != nil {
298 return err
299 }
300
301 if fi.IsDir() {
302 return nil
303 }
304
305 fil := fi.(hugofs.FileMetaInfo)
306
307 if files.IsContentFile(path) {
308 m.contentFiles = append(m.contentFiles, fil)
309 if !m.siteUsed {
310 m.siteUsed, err = b.usesSiteVar(path)
311 if err != nil {
312 return err
313 }
314 }
315 return nil
316 }
317
318 m.otherFiles = append(m.otherFiles, fil)
319
320 return nil
321 }
322
323 walkCfg := hugofs.WalkwayConfig{
324 WalkFn: walkFn,
325 Fs: b.archeTypeFs,
326 Root: b.archetypeFilename,
327 }
328
329 w := hugofs.NewWalkway(walkCfg)
330
331 if err := w.Walk(); err != nil {
332 return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFilename, err)
333 }
334
335 b.dirMap = m
336
337 return nil
338 }
339
340 func (b *contentBuilder) openInEditorIfConfigured(filename string) error {
341 editor := b.h.Cfg.GetString("newContentEditor")
342 if editor == "" {
343 return nil
344 }
345
346 editorExec := strings.Fields(editor)[0]
347 editorFlags := strings.Fields(editor)[1:]
348
349 var args []any
350 for _, editorFlag := range editorFlags {
351 args = append(args, editorFlag)
352 }
353 args = append(
354 args,
355 filename,
356 hexec.WithStdin(os.Stdin),
357 hexec.WithStderr(os.Stderr),
358 hexec.WithStdout(os.Stdout),
359 )
360
361 b.h.Log.Printf("Editing %q with %q ...\n", filename, editorExec)
362
363 cmd, err := b.h.Deps.ExecHelper.New(editorExec, args...)
364 if err != nil {
365 return err
366 }
367
368 return cmd.Run()
369 }
370
371 func (b *contentBuilder) usesSiteVar(filename string) (bool, error) {
372 if filename == "" {
373 return false, nil
374 }
375 bb, err := afero.ReadFile(b.archeTypeFs, filename)
376 if err != nil {
377 return false, fmt.Errorf("failed to open archetype file: %w", err)
378 }
379
380 return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil
381
382 }
383
384 type archetypeMap struct {
385 // These needs to be parsed and executed as Go templates.
386 contentFiles []hugofs.FileMetaInfo
387 // These are just copied to destination.
388 otherFiles []hugofs.FileMetaInfo
389 // If the templates needs a fully built site. This can potentially be
390 // expensive, so only do when needed.
391 siteUsed bool
392 }