github.com/gohugoio/hugo@v0.88.1/create/content.go (about) 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 "io" 20 "os" 21 "path/filepath" 22 "strings" 23 24 "github.com/gohugoio/hugo/common/paths" 25 26 "github.com/pkg/errors" 27 28 "github.com/gohugoio/hugo/common/hexec" 29 "github.com/gohugoio/hugo/hugofs/files" 30 31 "github.com/gohugoio/hugo/hugofs" 32 33 "github.com/gohugoio/hugo/helpers" 34 "github.com/gohugoio/hugo/hugolib" 35 "github.com/spf13/afero" 36 jww "github.com/spf13/jwalterweatherman" 37 ) 38 39 // NewContent creates a new content file in the content directory based upon the 40 // given kind, which is used to lookup an archetype. 41 func NewContent( 42 sites *hugolib.HugoSites, kind, targetPath string) error { 43 targetPath = filepath.Clean(targetPath) 44 ext := paths.Ext(targetPath) 45 ps := sites.PathSpec 46 archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs 47 sourceFs := ps.Fs.Source 48 49 jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext) 50 51 archetypeFilename, isDir := findArchetype(ps, kind, ext) 52 contentPath, s := resolveContentPath(sites, sourceFs, targetPath) 53 54 if isDir { 55 56 langFs, err := hugofs.NewLanguageFs(sites.LanguageSet(), archetypeFs) 57 if err != nil { 58 return err 59 } 60 61 cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename) 62 if err != nil { 63 return err 64 } 65 66 if cm.siteUsed { 67 if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { 68 return err 69 } 70 } 71 72 name := filepath.Base(targetPath) 73 return newContentFromDir(archetypeFilename, sites, sourceFs, cm, name, contentPath) 74 } 75 76 // Building the sites can be expensive, so only do it if really needed. 77 siteUsed := false 78 79 if archetypeFilename != "" { 80 81 var err error 82 siteUsed, err = usesSiteVar(archetypeFs, archetypeFilename) 83 if err != nil { 84 return err 85 } 86 } 87 88 if siteUsed { 89 if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { 90 return err 91 } 92 } 93 94 content, err := executeArcheTypeAsTemplate(s, "", kind, targetPath, archetypeFilename) 95 if err != nil { 96 return err 97 } 98 99 if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil { 100 return err 101 } 102 103 jww.FEEDBACK.Println(contentPath, "created") 104 105 editor := s.Cfg.GetString("newContentEditor") 106 if editor != "" { 107 jww.FEEDBACK.Printf("Editing %s with %q ...\n", targetPath, editor) 108 109 editorCmd := append(strings.Fields(editor), contentPath) 110 cmd, err := hexec.SafeCommand(editorCmd[0], editorCmd[1:]...) 111 if err != nil { 112 return err 113 } 114 cmd.Stdin = os.Stdin 115 cmd.Stdout = os.Stdout 116 cmd.Stderr = os.Stderr 117 118 return cmd.Run() 119 } 120 121 return nil 122 } 123 124 func targetSite(sites *hugolib.HugoSites, fi hugofs.FileMetaInfo) *hugolib.Site { 125 for _, s := range sites.Sites { 126 if fi.Meta().Lang == s.Language().Lang { 127 return s 128 } 129 } 130 return sites.Sites[0] 131 } 132 133 func newContentFromDir( 134 archetypeDir string, 135 sites *hugolib.HugoSites, 136 targetFs afero.Fs, 137 cm archetypeMap, name, targetPath string) error { 138 for _, f := range cm.otherFiles { 139 meta := f.Meta() 140 filename := meta.Path 141 // Just copy the file to destination. 142 in, err := meta.Open() 143 if err != nil { 144 return errors.Wrap(err, "failed to open non-content file") 145 } 146 147 targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir)) 148 149 targetDir := filepath.Dir(targetFilename) 150 if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) { 151 return errors.Wrapf(err, "failed to create target directory for %s:", targetDir) 152 } 153 154 out, err := targetFs.Create(targetFilename) 155 if err != nil { 156 return err 157 } 158 159 _, err = io.Copy(out, in) 160 if err != nil { 161 return err 162 } 163 164 in.Close() 165 out.Close() 166 } 167 168 for _, f := range cm.contentFiles { 169 filename := f.Meta().Path 170 s := targetSite(sites, f) 171 targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir)) 172 173 content, err := executeArcheTypeAsTemplate(s, name, archetypeDir, targetFilename, filename) 174 if err != nil { 175 return errors.Wrap(err, "failed to execute archetype template") 176 } 177 178 if err := helpers.SafeWriteToDisk(targetFilename, bytes.NewReader(content), targetFs); err != nil { 179 return errors.Wrap(err, "failed to save results") 180 } 181 } 182 183 jww.FEEDBACK.Println(targetPath, "created") 184 185 return nil 186 } 187 188 type archetypeMap struct { 189 // These needs to be parsed and executed as Go templates. 190 contentFiles []hugofs.FileMetaInfo 191 // These are just copied to destination. 192 otherFiles []hugofs.FileMetaInfo 193 // If the templates needs a fully built site. This can potentially be 194 // expensive, so only do when needed. 195 siteUsed bool 196 } 197 198 func mapArcheTypeDir( 199 ps *helpers.PathSpec, 200 fs afero.Fs, 201 archetypeDir string) (archetypeMap, error) { 202 var m archetypeMap 203 204 walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { 205 if err != nil { 206 return err 207 } 208 209 if fi.IsDir() { 210 return nil 211 } 212 213 fil := fi.(hugofs.FileMetaInfo) 214 215 if files.IsContentFile(path) { 216 m.contentFiles = append(m.contentFiles, fil) 217 if !m.siteUsed { 218 m.siteUsed, err = usesSiteVar(fs, path) 219 if err != nil { 220 return err 221 } 222 } 223 return nil 224 } 225 226 m.otherFiles = append(m.otherFiles, fil) 227 228 return nil 229 } 230 231 walkCfg := hugofs.WalkwayConfig{ 232 WalkFn: walkFn, 233 Fs: fs, 234 Root: archetypeDir, 235 } 236 237 w := hugofs.NewWalkway(walkCfg) 238 239 if err := w.Walk(); err != nil { 240 return m, errors.Wrapf(err, "failed to walk archetype dir %q", archetypeDir) 241 } 242 243 return m, nil 244 } 245 246 func usesSiteVar(fs afero.Fs, filename string) (bool, error) { 247 f, err := fs.Open(filename) 248 if err != nil { 249 return false, errors.Wrap(err, "failed to open archetype file") 250 } 251 defer f.Close() 252 return helpers.ReaderContains(f, []byte(".Site")), nil 253 } 254 255 // Resolve the target content path. 256 func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string) (string, *hugolib.Site) { 257 targetDir := filepath.Dir(targetPath) 258 first := sites.Sites[0] 259 260 var ( 261 s *hugolib.Site 262 siteContentDir string 263 ) 264 265 // Try the filename: my-post.en.md 266 for _, ss := range sites.Sites { 267 if strings.Contains(targetPath, "."+ss.Language().Lang+".") { 268 s = ss 269 break 270 } 271 } 272 273 var dirLang string 274 275 for _, dir := range sites.BaseFs.Content.Dirs { 276 meta := dir.Meta() 277 contentDir := meta.Filename 278 279 if !strings.HasSuffix(contentDir, helpers.FilePathSeparator) { 280 contentDir += helpers.FilePathSeparator 281 } 282 283 if strings.HasPrefix(targetPath, contentDir) { 284 siteContentDir = contentDir 285 dirLang = meta.Lang 286 break 287 } 288 } 289 290 if s == nil && dirLang != "" { 291 for _, ss := range sites.Sites { 292 if ss.Lang() == dirLang { 293 s = ss 294 break 295 } 296 } 297 } 298 299 if s == nil { 300 s = first 301 } 302 303 if targetDir != "" && targetDir != "." { 304 exists, _ := helpers.Exists(targetDir, fs) 305 306 if exists { 307 return targetPath, s 308 } 309 } 310 311 if siteContentDir == "" { 312 } 313 314 if siteContentDir != "" { 315 pp := filepath.Join(siteContentDir, strings.TrimPrefix(targetPath, siteContentDir)) 316 return s.PathSpec.AbsPathify(pp), s 317 } else { 318 var contentDir string 319 for _, dir := range sites.BaseFs.Content.Dirs { 320 contentDir = dir.Meta().Filename 321 if dir.Meta().Lang == s.Lang() { 322 break 323 } 324 } 325 return s.PathSpec.AbsPathify(filepath.Join(contentDir, targetPath)), s 326 } 327 } 328 329 // FindArchetype takes a given kind/archetype of content and returns the path 330 // to the archetype in the archetype filesystem, blank if none found. 331 func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string, isDir bool) { 332 fs := ps.BaseFs.Archetypes.Fs 333 334 var pathsToCheck []string 335 336 if kind != "" { 337 pathsToCheck = append(pathsToCheck, kind+ext) 338 } 339 pathsToCheck = append(pathsToCheck, "default"+ext, "default") 340 341 for _, p := range pathsToCheck { 342 fi, err := fs.Stat(p) 343 if err == nil { 344 return p, fi.IsDir() 345 } 346 } 347 348 return "", false 349 }