commands: Preserve non-content files in convert output

When running `hugo convert` with `--output`, copy the content tree first so non-content bundle resources are kept in the destination, then overwrite converted content files.

Also avoid recursive self-copy when the output path points inside the content tree by skipping output directories during copy.

Fixes #4621
This commit is contained in:
Alexandre Vaz
2026-03-23 13:49:13 -03:00
committed by GitHub
parent d88a29e002
commit c4fb61d9df
2 changed files with 77 additions and 3 deletions
+58
View File
@@ -18,10 +18,12 @@ import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"time" "time"
"github.com/bep/simplecobra" "github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
@@ -200,6 +202,56 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
return nil return nil
} }
func (c *convertCommand) copyContentDirsForOutput(pagesBackedByFile page.Pages) error {
contentDirs := make(map[string]bool)
for _, p := range pagesBackedByFile {
filename := p.File().Filename()
contentDir := strings.TrimSuffix(filename, p.File().Path())
if contentDir == filename {
continue
}
contentDirs[filepath.Clean(contentDir)] = true
}
var contentDirList []string
for contentDir := range contentDirs {
contentDirList = append(contentDirList, contentDir)
}
slices.Sort(contentDirList)
outputDirAbs, err := filepath.Abs(c.outputDir)
if err != nil {
return fmt.Errorf("failed to resolve output path %q: %w", c.outputDir, err)
}
for _, contentDir := range contentDirList {
outputContentDirAbs := filepath.Join(outputDirAbs, filepath.Base(contentDir))
skipDirs := make(map[string]bool)
relToOutputDir, err := filepath.Rel(contentDir, outputDirAbs)
if err == nil && relToOutputDir != ".." && !strings.HasPrefix(relToOutputDir, ".."+string(filepath.Separator)) {
skipDirs[filepath.Clean(outputDirAbs)] = true
}
relToOutputContentDir, err := filepath.Rel(contentDir, outputContentDirAbs)
if err == nil && relToOutputContentDir != ".." && !strings.HasPrefix(relToOutputContentDir, ".."+string(filepath.Separator)) {
skipDirs[filepath.Clean(outputContentDirAbs)] = true
}
var shouldCopy func(filename string) bool
if len(skipDirs) > 0 {
shouldCopy = func(filename string) bool {
return !skipDirs[filepath.Clean(filename)]
}
}
if err := hugio.CopyDir(hugofs.Os, contentDir, outputContentDirAbs, shouldCopy); err != nil {
return fmt.Errorf("failed to copy %q to %q: %w", contentDir, outputContentDirAbs, err)
}
}
return nil
}
func (c *convertCommand) convertContents(format metadecoders.Format) error { func (c *convertCommand) convertContents(format metadecoders.Format) error {
if c.outputDir == "" && !c.unsafe { if c.outputDir == "" && !c.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path") return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
@@ -219,6 +271,12 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
pagesBackedByFile = append(pagesBackedByFile, p) pagesBackedByFile = append(pagesBackedByFile, p)
} }
if c.outputDir != "" {
if err := c.copyContentDirsForOutput(pagesBackedByFile); err != nil {
return err
}
}
site.Log.Println("processing", len(pagesBackedByFile), "content files") site.Log.Println("processing", len(pagesBackedByFile), "content files")
for _, p := range site.AllPages() { for _, p := range site.AllPages() {
if err := c.convertAndSavePage(p, site, format); err != nil { if err := c.convertAndSavePage(p, site, format); err != nil {
+19 -3
View File
@@ -10,14 +10,21 @@ hugo convert toYAML -h
stdout 'to use YAML for the front matter' stdout 'to use YAML for the front matter'
hugo convert toJSON -o myjsoncontent hugo convert toJSON -o myjsoncontent
stdout 'processing 3 content files' stdout 'processing 4 content files'
grep '^{' myjsoncontent/content/mytoml.md grep '^{' myjsoncontent/content/mytoml.md
grep '^{' myjsoncontent/content/myjson.md grep '^{' myjsoncontent/content/myjson.md
grep '^{' myjsoncontent/content/myyaml.md grep '^{' myjsoncontent/content/myyaml.md
grep '^{' myjsoncontent/content/bundle/index.md
exists myjsoncontent/content/bundle/data.txt
exists myjsoncontent/content/bundle/nested/asset.dat
hugo convert toYAML -o myyamlcontent hugo convert toYAML -o myyamlcontent
stdout 'processing 3 content files' stdout 'processing 4 content files'
exists myyamlcontent/content/bundle/data.txt
exists myyamlcontent/content/bundle/nested/asset.dat
hugo convert toTOML -o mytomlcontent hugo convert toTOML -o mytomlcontent
stdout 'processing 3 content files' stdout 'processing 4 content files'
exists mytomlcontent/content/bundle/data.txt
exists mytomlcontent/content/bundle/nested/asset.dat
@@ -40,3 +47,12 @@ JSON content
title: YAML title: YAML
--- ---
YAML content YAML content
-- content/bundle/index.md --
---
title: Bundle
---
Bundle content
-- content/bundle/data.txt --
bundle resource
-- content/bundle/nested/asset.dat --
nested resource