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:
2026-06-08 03:43:43 +03:00
parent f9755fa98f
commit cba416b238
9 changed files with 852 additions and 2 deletions

View File

@@ -1,7 +1,9 @@
package jobs
import (
"archive/zip"
"bytes"
"encoding/json"
"image"
"image/color"
"image/jpeg"
@@ -108,6 +110,51 @@ func TestRenderVideoScenesThumbnailReturnsLargeJPEG(t *testing.T) {
}
}
func TestCreateArchiveListingRendersZipTree(t *testing.T) {
var archive bytes.Buffer
writer := zip.NewWriter(&archive)
addZipTestFile(t, writer, "docs/readme.md", "hello")
addZipTestFile(t, writer, "src/main.go", "package main\n")
if err := writer.Close(); err != nil {
t.Fatalf("zip.Close returned error: %v", err)
}
data, err := createArchiveListing(services.File{Name: "bundle.zip", ContentType: "application/zip"}, bytes.NewReader(archive.Bytes()))
if err != nil {
t.Fatalf("createArchiveListing returned error: %v", err)
}
var listing archiveListingData
if err := json.Unmarshal(data, &listing); err != nil {
t.Fatalf("json.Unmarshal returned error: %v\n%s", err, string(data))
}
if listing.Name != "bundle.zip" || listing.FileCount != 2 || listing.FolderCount != 2 {
t.Fatalf("archive listing metadata = %+v", listing)
}
if listing.Root == nil || len(listing.Root.Items) != 2 {
t.Fatalf("archive listing root = %+v", listing.Root)
}
if listing.Root.Items[0].Name != "docs" || listing.Root.Items[0].Icon != "folder" {
t.Fatalf("first archive folder = %+v", listing.Root.Items[0])
}
if listing.Root.Items[0].Items[0].Name != "readme.md" || listing.Root.Items[0].Items[0].Icon != "txt" {
t.Fatalf("markdown archive file = %+v", listing.Root.Items[0].Items[0])
}
if listing.Root.Items[1].Items[0].Name != "main.go" || listing.Root.Items[1].Items[0].Icon != "code" {
t.Fatalf("go archive file = %+v", listing.Root.Items[1].Items[0])
}
}
func addZipTestFile(t *testing.T, writer *zip.Writer, name, body string) {
t.Helper()
file, err := writer.Create(name)
if err != nil {
t.Fatalf("zip.Create returned error: %v", err)
}
if _, err := file.Write([]byte(body)); err != nil {
t.Fatalf("zip file write returned error: %v", err)
}
}
func solidTestImage(c color.Color) image.Image {
img := image.NewRGBA(image.Rect(0, 0, 32, 24))
for y := 0; y < img.Bounds().Dy(); y++ {