Repository: gohugoio/hugo (language: go)

Issue:
False path warning / resources possibly written twice to /public in some cases
See https://discourse.gohugo.io/t/duplicate-target-paths/52788/5


Discussion / hints:
FYI...

With only 1 file (`foo.txt`) in the assets directory, this template...

```text
A: {{ (resources.Get "foo.txt").RelPermalink }}
B: {{ (resources.GetMatch "foo.txt").RelPermalink }}
C: {{ (index (resources.Match "foo.txt") 0).RelPermalink }}
D: {{ (index (resources.ByType "text") 0).RelPermalink }}
```

...triggers this warning:

```
WARN  Duplicate target paths: /foo.txt (4)
```

But this template doesn't trigger any warnings:

```text
A: {{ (resources.Get "foo.txt").RelPermalink }}
B: {{ (resources.Get "foo.txt").RelPermalink }}
C: {{ (resources.Get "foo.txt").RelPermalink }}
D: {{ (resources.Get "foo.txt").RelPermalink }}
```

I tried to create an integration test but I couldn't trigger the warning.

<details>
<summary>integration test</summary>

```go
func TestFoo(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
printPathWarnings = true
-- assets/foo.txt --
foo
-- layouts/index.html --
A: {{ (resources.Get "foo.txt").RelPermalink }}
B: {{ (resources.GetMatch "foo.txt").RelPermalink }}
C: {{ (index (resources.Match "foo.txt") 0).RelPermalink }}
D: {{ (index (resources.ByType "text") 0).RelPermalink }}
-- layouts/unused/single.html --
{{ .Title }}
`
	b := hugolib.Test(t, files)

	b.AssertFileContent("public/index.html",
		"A: /foo.txt",
		"B: /foo.txt",
		"C: /foo.txt",
		"D: /foo.txt",
	)
	b.AssertLogContains("WARN  Duplicate target paths: /foo.txt (4)")
}
```
</details>
<br>



Fix patch:
diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go
index 02e541a05b1..38849317492 100644
--- a/hugofs/rootmapping_fs.go
+++ b/hugofs/rootmapping_fs.go
@@ -311,12 +311,13 @@ func (fs *RootMappingFs) Open(name string) (afero.File, error) {
 
 // Stat returns the os.FileInfo structure describing a given file.  If there is
 // an error, it will be of type *os.PathError.
+// If multiple roots are found, the last one will be used.
 func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) {
 	fis, err := fs.doStat(name)
 	if err != nil {
 		return nil, err
 	}
-	return fis[0], nil
+	return fis[len(fis)-1], nil
 }
 
 type ComponentPath struct {
diff --git a/resources/resource_cache.go b/resources/resource_cache.go
index a3ba9aa260d..898cd4c3169 100644
--- a/resources/resource_cache.go
+++ b/resources/resource_cache.go
@@ -36,6 +36,11 @@ func newResourceCache(rs *Spec, memCache *dynacache.Cache) *ResourceCache {
 			"/res1",
 			dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40},
 		),
+		cacheResourceFile: dynacache.GetOrCreatePartition[string, resource.Resource](
+			memCache,
+			"/res2",
+			dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40},
+		),
 		CacheResourceRemote: dynacache.GetOrCreatePartition[string, resource.Resource](
 			memCache,
 			"/resr",
@@ -58,6 +63,7 @@ type ResourceCache struct {
 	sync.RWMutex
 
 	cacheResource               *dynacache.Partition[string, resource.Resource]
+	cacheResourceFile           *dynacache.Partition[string, resource.Resource]
 	CacheResourceRemote         *dynacache.Partition[string, resource.Resource]
 	cacheResources              *dynacache.Partition[string, resource.Resources]
 	cacheResourceTransformation *dynacache.Partition[string, *resourceAdapterInner]
@@ -79,6 +85,12 @@ func (c *ResourceCache) GetOrCreate(key string, f func() (resource.Resource, err
 	})
 }
 
+func (c *ResourceCache) GetOrCreateFile(key string, f func() (resource.Resource, error)) (resource.Resource, error) {
+	return c.cacheResourceFile.GetOrCreate(key, func(key string) (resource.Resource, error) {
+		return f()
+	})
+}
+
 func (c *ResourceCache) GetOrCreateResources(key string, f func() (resource.Resources, error)) (resource.Resources, error) {
 	return c.cacheResources.GetOrCreate(key, func(key string) (resource.Resources, error) {
 		return f()
diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go
index 7dd26f4c0f7..581c0a8541d 100644
--- a/resources/resource_factories/create/create.go
+++ b/resources/resource_factories/create/create.go
@@ -143,19 +143,7 @@ func (c *Client) Get(pathname string) (resource.Resource, error) {
 			return nil, err
 		}
 
-		meta := fi.(hugofs.FileMetaInfo).Meta()
-		pi := meta.PathInfo
-
-		return c.rs.NewResource(resources.ResourceSourceDescriptor{
-			LazyPublish: true,
-			OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
-				return c.rs.BaseFs.Assets.Fs.Open(filename)
-			},
-			Path:                 pi,
-			GroupIdentity:        pi,
-			TargetPath:           pathname,
-			SourceFilenameOrPath: meta.Filename,
-		})
+		return c.getOrCreateFileResource(fi.(hugofs.FileMetaInfo))
 	})
 }
 
@@ -181,6 +169,23 @@ func (c *Client) GetMatch(pattern string) (resource.Resource, error) {
 	return res[0], err
 }
 
+func (c *Client) getOrCreateFileResource(info hugofs.FileMetaInfo) (resource.Resource, error) {
+	meta := info.Meta()
+	return c.rs.ResourceCache.GetOrCreateFile(filepath.ToSlash(meta.Filename), func() (resource.Resource, error) {
+		return c.rs.NewResource(resources.ResourceSourceDescriptor{
+			LazyPublish: true,
+			OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
+				return meta.Open()
+			},
+			NameNormalized:       meta.PathInfo.Path(),
+			NameOriginal:         meta.PathInfo.Unnormalized().Path(),
+			GroupIdentity:        meta.PathInfo,
+			TargetPath:           meta.PathInfo.Unnormalized().Path(),
+			SourceFilenameOrPath: meta.Filename,
+		})
+	})
+}
+
 func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource) bool, firstOnly bool) (resource.Resources, error) {
 	pattern = glob.NormalizePath(pattern)
 	partitions := glob.FilterGlobParts(strings.Split(pattern, "/"))
@@ -191,19 +196,7 @@ func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource)
 		var res resource.Resources
 
 		handle := func(info hugofs.FileMetaInfo) (bool, error) {
-			meta := info.Meta()
-
-			r, err := c.rs.NewResource(resources.ResourceSourceDescriptor{
-				LazyPublish: true,
-				OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
-					return meta.Open()
-				},
-				NameNormalized:       meta.PathInfo.Path(),
-				NameOriginal:         meta.PathInfo.Unnormalized().Path(),
-				GroupIdentity:        meta.PathInfo,
-				TargetPath:           meta.PathInfo.Unnormalized().Path(),
-				SourceFilenameOrPath: meta.Filename,
-			})
+			r, err := c.getOrCreateFileResource(info)
 			if err != nil {
 				return true, err
 			}


Test patch:
diff --git a/testscripts/commands/hugo__path-warnings_issue13164.txt b/testscripts/commands/hugo__path-warnings_issue13164.txt
new file mode 100644
index 00000000000..1342c287a61
--- /dev/null
+++ b/testscripts/commands/hugo__path-warnings_issue13164.txt
@@ -0,0 +1,15 @@
+hugo --printPathWarnings
+
+! stderr 'Duplicate target paths'
+
+-- hugo.toml --
+disableKinds = ['page','rss','section','sitemap','taxonomy','term']
+-- assets/foo.txt --
+foo
+-- layouts/index.html --
+A: {{ (resources.Get "foo.txt").RelPermalink }}
+B: {{ (resources.GetMatch "foo.txt").RelPermalink }}
+C: {{ (index (resources.Match "foo.txt") 0).RelPermalink }}
+D: {{ (index (resources.ByType "text") 0).RelPermalink }}
+-- layouts/unused/single.html --
+{{ .Title }}