diff --git a/backend/internal/data/repo/repo_item_attachments.go b/backend/internal/data/repo/repo_item_attachments.go index 0cd2feed..f82a926e 100644 --- a/backend/internal/data/repo/repo_item_attachments.go +++ b/backend/internal/data/repo/repo_item_attachments.go @@ -98,11 +98,32 @@ func ToItemAttachment(attachment *ent.Attachment) ItemAttachment { } 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 { - 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 { diff --git a/backend/internal/data/repo/repo_item_attachments_test.go b/backend/internal/data/repo/repo_item_attachments_test.go index 0963a160..cbd839e5 100644 --- a/backend/internal/data/repo/repo_item_attachments_test.go +++ b/backend/internal/data/repo/repo_item_attachments_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/sysadminsmedia/homebox/backend/internal/data/ent" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment" + "github.com/sysadminsmedia/homebox/backend/internal/sys/config" ) func TestAttachmentRepo_Create(t *testing.T) { @@ -281,3 +282,58 @@ func TestAttachmentRepo_SettingPhotoPrimaryStillWorks(t *testing.T) { require.NoError(t, err) 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") +}