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/
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"image"
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -112,7 +115,8 @@ func generateMissingThumbnailsForBox(uploadService *services.UploadService, logg
|
||||
file := &box.Files[i]
|
||||
needsPrimary := file.Thumbnail == "" && needsThumbnail(*file)
|
||||
needsScenes := file.SceneThumbnail == "" && needsVideoScenes(*file)
|
||||
if !needsPrimary && !needsScenes {
|
||||
needsArchive := !archiveListingCurrent(*file) && needsArchiveListing(*file)
|
||||
if !needsPrimary && !needsScenes && !needsArchive {
|
||||
continue
|
||||
}
|
||||
result.Scanned++
|
||||
@@ -144,6 +148,21 @@ func generateMissingThumbnailsForBox(uploadService *services.UploadService, logg
|
||||
result.Generated++
|
||||
}
|
||||
}
|
||||
|
||||
if needsArchive {
|
||||
archiveListing, err := generateArchiveListing(uploadService, box, *file)
|
||||
if err != nil {
|
||||
logger.Warn("archive listing generation failed", "source", "thumbnail", "severity", "warn", "code", 4107, "file_id", file.ID, "error", err.Error())
|
||||
result.Failed++
|
||||
} else if archiveListing == "" {
|
||||
result.Failed++
|
||||
} else {
|
||||
file.ArchiveListing = archiveListing
|
||||
file.ArchiveListingObjectKey = ""
|
||||
changed = true
|
||||
result.Generated++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
@@ -170,6 +189,10 @@ func NeedsVideoScenes(file services.File) bool {
|
||||
return needsVideoScenes(file)
|
||||
}
|
||||
|
||||
func NeedsArchiveListing(file services.File) bool {
|
||||
return needsArchiveListing(file)
|
||||
}
|
||||
|
||||
func GenerateThumbnailForFile(uploadService *services.UploadService, box services.Box, file services.File) (string, error) {
|
||||
return generateThumbnail(uploadService, box, file)
|
||||
}
|
||||
@@ -178,6 +201,10 @@ func GenerateVideoScenesForFile(uploadService *services.UploadService, box servi
|
||||
return generateVideoScenesThumbnail(uploadService, box, file)
|
||||
}
|
||||
|
||||
func GenerateArchiveListingForFile(uploadService *services.UploadService, box services.Box, file services.File) (string, error) {
|
||||
return generateArchiveListing(uploadService, box, file)
|
||||
}
|
||||
|
||||
func generateThumbnail(uploadService *services.UploadService, box services.Box, file services.File) (string, error) {
|
||||
thumbnailName := "@thumb@" + file.ID + ".jpg"
|
||||
object, err := uploadService.OpenFileObject(context.Background(), box, file)
|
||||
@@ -232,6 +259,25 @@ func generateVideoScenesThumbnail(uploadService *services.UploadService, box ser
|
||||
return sceneName, err
|
||||
}
|
||||
|
||||
func generateArchiveListing(uploadService *services.UploadService, box services.Box, file services.File) (string, error) {
|
||||
if !needsArchiveListing(file) {
|
||||
return "", nil
|
||||
}
|
||||
listingName := "@archive@" + file.ID + ".json"
|
||||
object, err := uploadService.OpenFileObject(context.Background(), box, file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer object.Body.Close()
|
||||
|
||||
data, err := createArchiveListing(file, object.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = uploadService.PutThumbnailObject(context.Background(), box, listingName, bytes.NewReader(data), int64(len(data)), "application/json")
|
||||
return listingName, err
|
||||
}
|
||||
|
||||
func isTextThumbnailCandidate(file services.File) bool {
|
||||
contentType := strings.ToLower(strings.TrimSpace(file.ContentType))
|
||||
if i := strings.IndexByte(contentType, ';'); i >= 0 {
|
||||
@@ -253,6 +299,219 @@ func isTextThumbnailCandidate(file services.File) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func needsArchiveListing(file services.File) bool {
|
||||
contentType := strings.ToLower(strings.TrimSpace(file.ContentType))
|
||||
if i := strings.IndexByte(contentType, ';'); i >= 0 {
|
||||
contentType = strings.TrimSpace(contentType[:i])
|
||||
}
|
||||
switch contentType {
|
||||
case "application/zip", "application/x-zip-compressed", "application/java-archive", "application/vnd.android.package-archive", "application/epub+zip":
|
||||
return true
|
||||
}
|
||||
ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(file.Name)), ".")
|
||||
switch ext {
|
||||
case "zip", "jar", "war", "ear", "apk", "epub", "docx", "xlsx", "pptx":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func archiveListingCurrent(file services.File) bool {
|
||||
return strings.ToLower(filepath.Ext(file.ArchiveListing)) == ".json"
|
||||
}
|
||||
|
||||
type archiveTreeNode struct {
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size,omitempty"`
|
||||
Dir bool `json:"dir"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Children map[string]*archiveTreeNode `json:"-"`
|
||||
Items []*archiveTreeNode `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
type archiveListingData struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
FileCount int `json:"fileCount"`
|
||||
FolderCount int `json:"folderCount"`
|
||||
UncompressedSize uint64 `json:"uncompressedSize"`
|
||||
Root *archiveTreeNode `json:"root"`
|
||||
}
|
||||
|
||||
func createArchiveListing(file services.File, source io.Reader) ([]byte, error) {
|
||||
sourceFile, err := os.CreateTemp("", "warpbox-archive-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(sourceFile.Name())
|
||||
if _, err := io.Copy(sourceFile, source); err != nil {
|
||||
sourceFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := sourceFile.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
archive, err := zip.OpenReader(sourceFile.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
root := &archiveTreeNode{Name: ".", Dir: true, Children: map[string]*archiveTreeNode{}}
|
||||
var totalSize uint64
|
||||
var fileCount int
|
||||
var dirCount int
|
||||
for _, entry := range archive.File {
|
||||
name := strings.Trim(entry.Name, "/")
|
||||
if name == "" || strings.HasPrefix(name, "__MACOSX/") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(name, "/")
|
||||
node := root
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if node.Children == nil {
|
||||
node.Children = map[string]*archiveTreeNode{}
|
||||
}
|
||||
child, ok := node.Children[part]
|
||||
if !ok {
|
||||
child = &archiveTreeNode{Name: part, Dir: i < len(parts)-1 || entry.FileInfo().IsDir(), Children: map[string]*archiveTreeNode{}}
|
||||
node.Children[part] = child
|
||||
if child.Dir {
|
||||
dirCount++
|
||||
}
|
||||
}
|
||||
node = child
|
||||
}
|
||||
if !entry.FileInfo().IsDir() {
|
||||
node.Dir = false
|
||||
node.Size = entry.UncompressedSize64
|
||||
totalSize += entry.UncompressedSize64
|
||||
fileCount++
|
||||
}
|
||||
}
|
||||
|
||||
finalizeArchiveTree(root)
|
||||
data := archiveListingData{
|
||||
Name: file.Name,
|
||||
Type: archiveLabel(file),
|
||||
FileCount: fileCount,
|
||||
FolderCount: dirCount,
|
||||
UncompressedSize: totalSize,
|
||||
Root: root,
|
||||
}
|
||||
return json.MarshalIndent(data, "", " ")
|
||||
}
|
||||
|
||||
func finalizeArchiveTree(node *archiveTreeNode) {
|
||||
node.Items = sortedArchiveChildren(node)
|
||||
for _, child := range node.Items {
|
||||
if child.Dir {
|
||||
child.Icon = "folder"
|
||||
finalizeArchiveTree(child)
|
||||
} else {
|
||||
child.Icon = archiveFileIconName(child.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeArchiveTree(out *strings.Builder, node *archiveTreeNode, prefix string) {
|
||||
children := sortedArchiveChildren(node)
|
||||
for i, child := range children {
|
||||
last := i == len(children)-1
|
||||
branch := "|-- "
|
||||
nextPrefix := prefix + "| "
|
||||
if last {
|
||||
branch = "`-- "
|
||||
nextPrefix = prefix + " "
|
||||
}
|
||||
|
||||
out.WriteString(prefix)
|
||||
out.WriteString(branch)
|
||||
out.WriteString(archiveNodeLabel(child))
|
||||
out.WriteString("\n")
|
||||
if child.Dir {
|
||||
writeArchiveTree(out, child, nextPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sortedArchiveChildren(node *archiveTreeNode) []*archiveTreeNode {
|
||||
children := make([]*archiveTreeNode, 0, len(node.Children))
|
||||
for _, child := range node.Children {
|
||||
children = append(children, child)
|
||||
}
|
||||
sort.Slice(children, func(i, j int) bool {
|
||||
if children[i].Dir != children[j].Dir {
|
||||
return children[i].Dir
|
||||
}
|
||||
return strings.ToLower(children[i].Name) < strings.ToLower(children[j].Name)
|
||||
})
|
||||
return children
|
||||
}
|
||||
|
||||
func archiveNodeLabel(node *archiveTreeNode) string {
|
||||
if node.Dir {
|
||||
return "[DIR] " + node.Name + "/"
|
||||
}
|
||||
return archiveFileIcon(node.Name) + " " + node.Name + " (" + formatArchiveBytes(node.Size) + ")"
|
||||
}
|
||||
|
||||
func archiveFileIcon(name string) string {
|
||||
return "[" + strings.ToUpper(archiveFileIconName(name)) + "]"
|
||||
}
|
||||
|
||||
func archiveFileIconName(name string) string {
|
||||
switch strings.TrimPrefix(strings.ToLower(filepath.Ext(name)), ".") {
|
||||
case "jpg", "jpeg", "png", "gif", "webp", "avif", "svg":
|
||||
return "img"
|
||||
case "mp4", "mov", "webm", "mkv", "avi":
|
||||
return "vid"
|
||||
case "mp3", "wav", "flac", "ogg", "m4a":
|
||||
return "aud"
|
||||
case "md", "txt", "log", "csv":
|
||||
return "txt"
|
||||
case "html", "css", "js", "ts", "go", "rs", "py", "json", "xml", "yaml", "yml":
|
||||
return "code"
|
||||
case "zip", "jar", "war", "ear", "apk", "epub", "docx", "xlsx", "pptx":
|
||||
return "arc"
|
||||
default:
|
||||
return "file"
|
||||
}
|
||||
}
|
||||
|
||||
func archiveLabel(file services.File) string {
|
||||
ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(file.Name)), ".")
|
||||
if ext != "" {
|
||||
return strings.ToUpper(ext) + " archive"
|
||||
}
|
||||
if file.ContentType != "" {
|
||||
return file.ContentType
|
||||
}
|
||||
return "ZIP-compatible archive"
|
||||
}
|
||||
|
||||
func formatArchiveBytes(size uint64) string {
|
||||
const unit = 1024
|
||||
if size < unit {
|
||||
return fmt.Sprintf("%d B", size)
|
||||
}
|
||||
div := float64(unit)
|
||||
value := float64(size) / div
|
||||
units := []string{"KiB", "MiB", "GiB", "TiB"}
|
||||
for _, suffix := range units {
|
||||
if value < unit {
|
||||
return fmt.Sprintf("%.1f %s", value, suffix)
|
||||
}
|
||||
value /= div
|
||||
}
|
||||
return fmt.Sprintf("%.1f PiB", value)
|
||||
}
|
||||
|
||||
func createImageThumbnail(source io.Reader) ([]byte, error) {
|
||||
img, _, err := image.Decode(source)
|
||||
if err != nil {
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -129,9 +129,11 @@ type File struct {
|
||||
PreviewKind string `json:"previewKind"`
|
||||
Thumbnail string `json:"thumbnail,omitempty"`
|
||||
SceneThumbnail string `json:"sceneThumbnail,omitempty"`
|
||||
ArchiveListing string `json:"archiveListing,omitempty"`
|
||||
ObjectKey string `json:"objectKey,omitempty"`
|
||||
ThumbnailObjectKey string `json:"thumbnailObjectKey,omitempty"`
|
||||
SceneThumbnailObjectKey string `json:"sceneThumbnailObjectKey,omitempty"`
|
||||
ArchiveListingObjectKey string `json:"archiveListingObjectKey,omitempty"`
|
||||
Processing bool `json:"processing,omitempty"`
|
||||
ProcessingError string `json:"processingError,omitempty"`
|
||||
UploadedAt time.Time `json:"uploadedAt"`
|
||||
@@ -736,6 +738,9 @@ func (s *UploadService) RemoveFileFromBox(boxID, fileID string) (bool, error) {
|
||||
if key := s.SceneThumbnailObjectKey(box, file); key != "" {
|
||||
_ = backend.Delete(context.Background(), key)
|
||||
}
|
||||
if key := s.ArchiveListingObjectKey(box, file); key != "" {
|
||||
_ = backend.Delete(context.Background(), key)
|
||||
}
|
||||
}
|
||||
|
||||
box.Files = append(box.Files[:index], box.Files[index+1:]...)
|
||||
@@ -833,6 +838,16 @@ func (s *UploadService) SceneThumbnailObjectKey(box Box, file File) string {
|
||||
return boxObjectKey(box.ID, file.SceneThumbnail)
|
||||
}
|
||||
|
||||
func (s *UploadService) ArchiveListingObjectKey(box Box, file File) string {
|
||||
if file.ArchiveListingObjectKey != "" {
|
||||
return file.ArchiveListingObjectKey
|
||||
}
|
||||
if file.ArchiveListing == "" {
|
||||
return ""
|
||||
}
|
||||
return boxObjectKey(box.ID, file.ArchiveListing)
|
||||
}
|
||||
|
||||
func (s *UploadService) OpenFileObject(ctx context.Context, box Box, file File) (StorageObject, error) {
|
||||
if file.Processing {
|
||||
return StorageObject{}, fmt.Errorf("file is still processing")
|
||||
@@ -868,6 +883,18 @@ func (s *UploadService) OpenSceneThumbnailObject(ctx context.Context, box Box, f
|
||||
return backend.Get(ctx, key)
|
||||
}
|
||||
|
||||
func (s *UploadService) OpenArchiveListingObject(ctx context.Context, box Box, file File) (StorageObject, error) {
|
||||
key := s.ArchiveListingObjectKey(box, file)
|
||||
if key == "" {
|
||||
return StorageObject{}, os.ErrNotExist
|
||||
}
|
||||
backend, err := s.storage.Backend(s.BoxStorageBackendID(box))
|
||||
if err != nil {
|
||||
return StorageObject{}, err
|
||||
}
|
||||
return backend.Get(ctx, key)
|
||||
}
|
||||
|
||||
func (s *UploadService) PutThumbnailObject(ctx context.Context, box Box, name string, body io.Reader, size int64, contentType string) (string, error) {
|
||||
backend, err := s.storage.Backend(s.BoxStorageBackendID(box))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user