feat: implement relative path storage and URL resolution for media files

- Add libraryService dependency to CollectionHandler and OPDSHandler for centralized path resolution
- Create internal/utils/mediaurl.go with ResolveMediaURL() function as single source of truth
- Update GetMediaItem and ListMediaItems handlers to return resolved URLs in API responses
- Update collection handlers (GetCollection, TestRules, PreviewCollection) to use resolved cover URLs
- Update progress handler (GetAllProgress) to use resolved cover URLs
- Add library_id to GetCollectionItems SQL query to enable URL resolution
- Refactor media scanner to store relative paths instead of absolute filesystem paths
- Add ResolveMediaPath() to LibraryService for resolving relative paths to absolute paths
- Add ServeFile endpoint at /uploads/library-:id/* for authenticated file serving
- Add MimeTypes map to library_service.go for consistent MIME type handling
- Update DownloadBook handler to use resolved filesystem paths
- Add getRelativePath() helper to MediaScanner for converting absolute to relative paths
- Use strings.EqualFold for case-insensitive path comparisons in zip extraction

This change enables the application to work with relative paths stored in the
database, making it portable across different server environments while
maintaining backward compatibility with existing absolute paths.
This commit is contained in:
2026-02-27 16:51:44 -05:00
parent 123ab0c966
commit 209e9f2a3c
13 changed files with 300 additions and 99 deletions
+28 -31
View File
@@ -741,29 +741,12 @@ coverPath := h.resolveCoverURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
**File**: `internal/handlers/media.go`
The current implementation returns raw database rows directly. We need to convert them to API-safe responses with resolved URLs.
**Option A: Quick fix** - Modify the response before returning (lines 609, 620, 639)
For `ListMediaItems` (around line 609 and 620), add a helper to convert each item:
Add to imports:
```go
// Add this function somewhere in media.go
func resolveMediaItemCoverAndFile(item database.ListMediaItemsRow) database.ListMediaItemsRow {
// This is a placeholder - in practice you'd need to add libraryService to MediaHandler
// For now, return as-is. Full implementation requires adding libraryService dependency.
return item
}
"bookhoard/internal/utils"
```
**Note**: The `ListMediaItems` and `GetMediaItem` functions currently return raw database rows. To properly resolve URLs, you would need to either:
1. **Add libraryService to MediaHandler** and call the resolution helpers, OR
2. **Create a separate response struct** that converts pgtype.Text to resolved URLs
For this implementation, the recommended approach is:
**Modify GetMediaItem** (line 639):
**GetMediaItem** - Find where it returns the response (around line 770):
**Current code**:
```go
@@ -773,17 +756,23 @@ return c.JSON(http.StatusOK, item)
**New code**:
```go
return c.JSON(http.StatusOK, map[string]interface{}{
"id": uuid.UUID(item.ID.Bytes).String(),
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
"title": item.Title,
"author": textToString(item.Author),
"cover_image_path": mh.ResolveCoverURL(item.LibraryID, item.CoverImagePath),
"file_path": mh.ResolveFileURL(item.LibraryID, item.FilePath),
"id": uuid.UUID(item.ID.Bytes).String(),
"library_id": uuid.UUID(item.LibraryID.Bytes).String(),
"title": item.Title,
"author": textToString(item.Author),
"cover_image_path": utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
"file_path": utils.ResolveMediaURL(item.LibraryID, item.FilePath),
"file_size": item.FileSize,
"mime_type": textToString(item.MimeType),
// ... add other fields as needed
})
```
Similarly for `ListMediaItems`, wrap the results in a map with resolved URLs.
**ListMediaItems** - Find where it returns items (around line 609):
Wrap each item in the response with resolved URLs. The exact implementation depends on how ListMediaItems currently returns data - you may need to build a custom response map similar to GetMediaItem.
**Note**: Unlike collections.go and progress.go where we added helper methods to the handler, here we use the utils package function directly since we've consolidated URL resolution into utils.
---
@@ -811,14 +800,22 @@ The backend now returns full URLs like `/uploads/library-{id}/path/to/cover.jpg`
| File | Changes |
|------|---------|
| `internal/handlers/media.go` | Add `ResolveCoverURL()`, `ResolveFileURL()`, `resolveMediaURL()` helpers |
| `internal/handlers/collections.go` | Add `libraryService` to struct and constructor; add `resolveCoverURL()`, `resolveFileURL()` helpers; update lines 193-201, 620-641, 910-919 |
| `internal/handlers/progress.go` | Add `resolveCoverURL()` to Handler (in commonhandlers.go); update lines 286-289, 357-360 |
| `internal/handlers/media.go` | Update `GetMediaItem` to return resolved URLs in response map |
| `internal/utils/mediaurl.go` | Create with `ResolveMediaURL()` function for URL resolution (one source of truth) |
| `internal/handlers/media.go` | Update GetMediaItem and ListMediaItems to use `utils.ResolveMediaURL()` for resolved URLs in responses |
| `internal/handlers/collections.go` | Use `utils.ResolveMediaURL()` in GetCollection, TestRules, PreviewCollection; update lines 193-201, 620-641, 910-919 |
| `internal/handlers/progress.go` | Use `utils.ResolveMediaURL()` in GetAllProgress; update lines 286-289, 357-360 |
| `web/src/bookshelf.ts` | Remove `/covers/` prefix from cover image URL |
---
### Additional Plan Updates Needed
| Item | Status |
|------|--------|
| Add `mi.library_id` to GetCollectionItems SQL query | Needs to be done before implementing Step 2 in collections.go |
| Create `internal/utils/mediaurl.go` | Needs to be created before implementing URL resolution |
| Update callers to use utils package | Replace h.resolveCoverURL/resolveFileURL with utils.ResolveMediaURL |
## Phase 8: Backward Compatibility
Handle existing absolute paths in database: