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 }