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  }