Fix Windows attachment path encoding issue by normalizing to forward slashes

Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-18 21:54:39 +00:00
parent 3d5545bf4d
commit 41c0ccf628
2 changed files with 79 additions and 2 deletions

View File

@@ -98,11 +98,32 @@ func ToItemAttachment(attachment *ent.Attachment) ItemAttachment {
} }
func (r *AttachmentRepo) path(gid uuid.UUID, hash string) string { func (r *AttachmentRepo) path(gid uuid.UUID, hash string) string {
return filepath.Join(gid.String(), "documents", hash) // Always use forward slashes for consistency across platforms
// This ensures paths are stored in the database with forward slashes
return fmt.Sprintf("%s/documents/%s", gid.String(), hash)
} }
func (r *AttachmentRepo) fullPath(relativePath string) string { func (r *AttachmentRepo) fullPath(relativePath string) string {
return filepath.Join(r.storage.PrefixPath, relativePath) // Normalize path separators to forward slashes for blob storage
// The blob library expects forward slashes in keys regardless of OS
normalizedRelativePath := strings.ReplaceAll(relativePath, "\\", "/")
// Always use forward slashes when joining paths for blob storage
if r.storage.PrefixPath == "" {
return normalizedRelativePath
}
normalizedPrefix := strings.ReplaceAll(r.storage.PrefixPath, "\\", "/")
// Trim trailing slashes from prefix and leading slashes from relative path
// to avoid double slashes when joining
normalizedPrefix = strings.TrimSuffix(normalizedPrefix, "/")
normalizedRelativePath = strings.TrimPrefix(normalizedRelativePath, "/")
if normalizedPrefix == "" {
return normalizedRelativePath
}
return fmt.Sprintf("%s/%s", normalizedPrefix, normalizedRelativePath)
} }
func (r *AttachmentRepo) GetFullPath(relativePath string) string { func (r *AttachmentRepo) GetFullPath(relativePath string) string {

View File

@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent" "github.com/sysadminsmedia/homebox/backend/internal/data/ent"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
) )
func TestAttachmentRepo_Create(t *testing.T) { func TestAttachmentRepo_Create(t *testing.T) {
@@ -281,3 +282,58 @@ func TestAttachmentRepo_SettingPhotoPrimaryStillWorks(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.False(t, photo1.Primary, "Photo 1 should no longer be primary after setting Photo 2 as primary") assert.False(t, photo1.Primary, "Photo 1 should no longer be primary after setting Photo 2 as primary")
} }
func TestAttachmentRepo_PathNormalization(t *testing.T) {
// Test that paths always use forward slashes
repo := &AttachmentRepo{
storage: config.Storage{
PrefixPath: ".data",
},
}
testGUID := uuid.MustParse("eb6bf410-a1a8-478d-a803-ca3948368a0c")
testHash := "f295eb01-18a9-4631-a797-70bd9623edd4.png"
// Test path() method - should always return forward slashes
relativePath := repo.path(testGUID, testHash)
assert.Equal(t, "eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", relativePath)
assert.NotContains(t, relativePath, "\\", "path() should not contain backslashes")
// Test fullPath() with forward slash input (from database)
fullPath := repo.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
assert.Equal(t, ".data/eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPath)
assert.NotContains(t, fullPath, "\\", "fullPath() should not contain backslashes")
// Test fullPath() with backslash input (legacy Windows paths from old database)
fullPathWithBackslash := repo.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c\\documents\\f295eb01-18a9-4631-a797-70bd9623edd4.png")
assert.Equal(t, ".data/eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPathWithBackslash)
assert.NotContains(t, fullPathWithBackslash, "\\", "fullPath() should normalize backslashes to forward slashes")
// Test with Windows-style prefix path
repoWindows := &AttachmentRepo{
storage: config.Storage{
PrefixPath: ".data",
},
}
fullPathWindows := repoWindows.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
assert.NotContains(t, fullPathWindows, "\\", "fullPath() should normalize Windows paths")
// Test empty prefix
repoNoPrefix := &AttachmentRepo{
storage: config.Storage{
PrefixPath: "",
},
}
fullPathNoPrefix := repoNoPrefix.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
assert.Equal(t, "eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPathNoPrefix)
// Test with single slash prefix (like in tests)
repoSlashPrefix := &AttachmentRepo{
storage: config.Storage{
PrefixPath: "/",
},
}
fullPathSlashPrefix := repoSlashPrefix.fullPath("eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png")
assert.Equal(t, "eb6bf410-a1a8-478d-a803-ca3948368a0c/documents/f295eb01-18a9-4631-a797-70bd9623edd4.png", fullPathSlashPrefix)
assert.NotContains(t, fullPathSlashPrefix, "//", "fullPath() should not have double slashes")
}