feat(preview): add archive listing and browser support
Introduces the ability to browse and preview the contents of archive files directly within the web interface.
Changes include:
- Added a new API endpoint `GET /d/{boxID}/archive/{fileID}` to fetch archive listings.
- Implemented on-demand archive listing generation in the backend.
- Updated the frontend preview component to support rendering and navigating archive contents.
This commit is contained in:
@@ -135,6 +135,7 @@ func (a *App) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /d/{boxID}/f/{fileID}/og-image.jpg", a.FileOGImage)
|
||||
mux.HandleFunc("GET /d/{boxID}/thumb/{fileID}", a.Thumbnail)
|
||||
mux.HandleFunc("GET /d/{boxID}/scene/{fileID}", a.VideoScenesPreview)
|
||||
mux.HandleFunc("GET /d/{boxID}/archive/{fileID}", a.ArchiveListing)
|
||||
mux.HandleFunc("GET /d/{boxID}/og-image.jpg", a.BoxOGImage)
|
||||
mux.HandleFunc("GET /robots.txt", a.RobotsTxt)
|
||||
mux.HandleFunc("GET /sitemap.xml", a.SitemapXML)
|
||||
|
||||
@@ -48,8 +48,10 @@ type fileView struct {
|
||||
DownloadURL string
|
||||
ThumbnailURL string
|
||||
SceneURL string
|
||||
ArchiveURL string
|
||||
HasThumbnail bool
|
||||
HasScene bool
|
||||
HasArchive bool
|
||||
IconURL string
|
||||
IconRetroURL string
|
||||
ReactURL string
|
||||
@@ -384,6 +386,51 @@ func (a *App) VideoScenesPreview(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, file.ID+"-scenes.jpg", object.ModTime, readSeekCloser(object.Body))
|
||||
}
|
||||
|
||||
func (a *App) ArchiveListing(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Robots-Tag", "noindex, nofollow, noarchive")
|
||||
box, file, ok := a.loadFileForRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !jobs.NeedsArchiveListing(file) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if a.uploadService.IsProtected(box) && box.Obfuscate && !a.isBoxUnlocked(r, box) {
|
||||
http.Error(w, "password required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ToLower(filepath.Ext(file.ArchiveListing)) != ".json" {
|
||||
if listing := a.generateMissingArchiveListingForRequest(r, box, file); listing != "" {
|
||||
file.ArchiveListing = listing
|
||||
file.ArchiveListingObjectKey = ""
|
||||
}
|
||||
}
|
||||
|
||||
object, err := a.uploadService.OpenArchiveListingObject(r.Context(), box, file)
|
||||
if err != nil {
|
||||
if listing := a.generateMissingArchiveListingForRequest(r, box, file); listing != "" {
|
||||
file.ArchiveListing = listing
|
||||
file.ArchiveListingObjectKey = ""
|
||||
object, err = a.uploadService.OpenArchiveListingObject(r.Context(), box, file)
|
||||
if err == nil {
|
||||
defer object.Body.Close()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
||||
http.ServeContent(w, r, file.ID+"-archive.json", object.ModTime, readSeekCloser(object.Body))
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(w, "archive preview unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer object.Body.Close()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
|
||||
http.ServeContent(w, r, file.ID+"-archive.json", object.ModTime, readSeekCloser(object.Body))
|
||||
}
|
||||
|
||||
func (a *App) generateMissingThumbnailForRequest(r *http.Request, box services.Box, file services.File) string {
|
||||
if file.Thumbnail != "" || !jobs.NeedsThumbnail(file) {
|
||||
return ""
|
||||
@@ -432,6 +479,31 @@ func (a *App) generateMissingVideoScenesForRequest(r *http.Request, box services
|
||||
return scene
|
||||
}
|
||||
|
||||
func (a *App) generateMissingArchiveListingForRequest(r *http.Request, box services.Box, file services.File) string {
|
||||
if strings.ToLower(filepath.Ext(file.ArchiveListing)) == ".json" || !jobs.NeedsArchiveListing(file) {
|
||||
return ""
|
||||
}
|
||||
listing, err := jobs.GenerateArchiveListingForFile(a.uploadService, box, file)
|
||||
if err != nil || listing == "" {
|
||||
if err != nil {
|
||||
a.logger.Warn("on-demand archive listing generation failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4108, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
for i := range box.Files {
|
||||
if box.Files[i].ID == file.ID {
|
||||
box.Files[i].ArchiveListing = listing
|
||||
box.Files[i].ArchiveListingObjectKey = ""
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := a.uploadService.SaveBox(box); err != nil {
|
||||
a.logger.Warn("on-demand archive listing metadata save failed", withRequestLogAttrs(r, "source", "thumbnail", "severity", "warn", "code", 4109, "box_id", box.ID, "file_id", file.ID, "error", err.Error())...)
|
||||
return ""
|
||||
}
|
||||
return listing
|
||||
}
|
||||
|
||||
// servePlaceholderThumbnail serves the fallback image with no-store so the
|
||||
// browser re-requests on the next load and picks up the real thumbnail as soon
|
||||
// as it has been generated.
|
||||
@@ -577,8 +649,10 @@ func (a *App) fileViewWithReactions(box services.Box, file services.File, reacti
|
||||
DownloadURL: fmt.Sprintf("/d/%s/f/%s/download", box.ID, file.ID),
|
||||
ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID),
|
||||
SceneURL: fmt.Sprintf("/d/%s/scene/%s", box.ID, file.ID),
|
||||
ArchiveURL: fmt.Sprintf("/d/%s/archive/%s", box.ID, file.ID),
|
||||
HasThumbnail: file.Thumbnail != "" || jobs.NeedsThumbnail(file),
|
||||
HasScene: file.SceneThumbnail != "" || jobs.NeedsVideoScenes(file),
|
||||
HasArchive: file.ArchiveListing != "" || jobs.NeedsArchiveListing(file),
|
||||
IconURL: fileIconURL("standard", icon.Standard),
|
||||
IconRetroURL: fileIconURL("retro", icon.Retro),
|
||||
ReactURL: fmt.Sprintf("/d/%s/f/%s/react", box.ID, file.ID),
|
||||
|
||||
@@ -24,6 +24,7 @@ Disallow: /d/*/f/*/download
|
||||
Disallow: /d/*/zip
|
||||
Disallow: /d/*/thumb/
|
||||
Disallow: /d/*/scene/
|
||||
Disallow: /d/*/archive/
|
||||
Disallow: /d/*/og-image.jpg
|
||||
Disallow: /d/*/unlock
|
||||
Disallow: /d/*/manage/
|
||||
|
||||
Reference in New Issue
Block a user