diff --git a/html/src/Pages/SharePage.tsx b/html/src/Pages/SharePage.tsx
index 42a2a5d..667390d 100644
--- a/html/src/Pages/SharePage.tsx
+++ b/html/src/Pages/SharePage.tsx
@@ -1,6 +1,6 @@
import { Anchor, Box, Button, Center, CopyButton, Group, Paper, rem, Stack, Text, Tooltip } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
-import { IconClock, IconFileZip, IconHelpHexagon, IconLink, IconMoodSad, IconUpload, IconX } from "@tabler/icons-react";
+import { IconClock, IconDownload, IconFileZip, IconHelpHexagon, IconLink, IconMoodSad, IconUpload, IconX } from "@tabler/icons-react";
import { useCallback, useEffect, useState } from "react";
import { H } from "../APIClient";
import { UploadQueue, QueueItem } from "../UploadQueue";
@@ -167,13 +167,18 @@ export function SharePage() {
<>
{/* Top of page copy button */}
-
- {({ copied, copy }) => (
-
-
-
- )}
-
+
+
+ {({ copied, copy }) => (
+
+
+
+ )}
+
+ {canDownload() && items.length + queueItems.filter((i) => i.failed === false && i.finished === true ).length > 0 &&
+
+ }
+
{/* Message */}
diff --git a/hupload/handlers.go b/hupload/handlers.go
index b77c5f3..808dfde 100644
--- a/hupload/handlers.go
+++ b/hupload/handlers.go
@@ -1,6 +1,7 @@
package main
import (
+ "archive/zip"
"bufio"
"encoding/json"
"errors"
@@ -8,6 +9,7 @@ import (
"io"
"log/slog"
"net/http"
+ "path"
"strconv"
"github.com/ybizeul/hupload/internal/storage"
@@ -371,6 +373,73 @@ func (h *Hupload) getItem(w http.ResponseWriter, r *http.Request) {
}
}
+// getVersion returns hupload version
+func (h *Hupload) downloadShare(w http.ResponseWriter, r *http.Request) {
+ shareName := r.PathValue("share")
+
+ share, err := h.Config.Storage.GetShare(r.Context(), shareName)
+ if err != nil {
+ slog.Error("getItem", slog.String("error", err.Error()))
+ switch {
+ case errors.Is(err, storage.ErrInvalidShareName):
+ writeError(w, http.StatusBadRequest, "invalid share name")
+ return
+ case errors.Is(err, storage.ErrShareNotFound):
+ writeError(w, http.StatusNotFound, "share not found")
+ return
+ }
+ writeError(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ user, _ := auth.AuthForRequest(r)
+
+ if user == "" && (share.Options.Exposure != "both" && share.Options.Exposure != "download") {
+ writeError(w, http.StatusUnauthorized, "unauthorized")
+ return
+ }
+
+ items, err := h.Config.Storage.ListShare(r.Context(), shareName)
+ if err != nil {
+ slog.Error("downloadShare", slog.String("error", err.Error()))
+ writeError(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ w.Header().Add("Content-Type", "application/zip")
+ w.Header().Add("Content-Disposition", "attachment")
+
+ zipWriter := zip.NewWriter(w)
+
+ for _, item := range items {
+ f, err := zipWriter.Create(path.Base(item.Path))
+ if err != nil {
+ slog.Error("downloadShare", slog.String("error", err.Error()))
+ writeError(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+ d, err := h.Config.Storage.GetItemData(r.Context(), shareName, path.Base(item.Path))
+ if err != nil {
+ slog.Error("downloadShare", slog.String("error", err.Error()))
+ writeError(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+ defer d.Close()
+ _, err = io.Copy(f, d)
+ if err != nil {
+ slog.Error("downloadShare", slog.String("error", err.Error()))
+ writeError(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+ err = zipWriter.Close()
+ if err != nil {
+ slog.Error("downloadShare", slog.String("error", err.Error()))
+ writeError(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+}
+
// postLogin returns the user name for the current session
func (h *Hupload) postLogin(w http.ResponseWriter, r *http.Request) {
user, _ := auth.AuthForRequest(r)
diff --git a/hupload/handlers_test.go b/hupload/handlers_test.go
index 9d66392..d68f526 100644
--- a/hupload/handlers_test.go
+++ b/hupload/handlers_test.go
@@ -691,6 +691,40 @@ func TestGetShare(t *testing.T) {
}
}
+func TestDownloadShare(t *testing.T) {
+ for name, cfg := range cfgs {
+ if !cfg.Enabled {
+ continue
+ }
+
+ shareName := "downloadshare"
+ h := getHupload(t, cfg.Config)
+
+ makeShare(t, h, shareName, storage.Options{Exposure: "download"})
+ t.Cleanup(func() {
+ _ = h.Config.Storage.DeleteShare(context.Background(), shareName)
+ })
+
+ makeItem(t, h, shareName, "newfile1.txt", 1*1024*1024)
+ makeItem(t, h, shareName, "newfile2.txt", 1*1024*1024)
+
+ t.Run(name, func(t *testing.T) {
+ t.Cleanup(func() { cfg.Cleanup(h) })
+ api := h.API
+
+ req := httptest.NewRequest("GET", path.Join("/d/", shareName), nil)
+
+ w := httptest.NewRecorder()
+
+ api.Mux.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
+ return
+ }
+ })
+ }
+}
func TestDeleteShare(t *testing.T) {
for name, cfg := range cfgs {
if !cfg.Enabled {
diff --git a/hupload/internal/storage/s3.go b/hupload/internal/storage/s3.go
index b434f6f..78e754a 100644
--- a/hupload/internal/storage/s3.go
+++ b/hupload/internal/storage/s3.go
@@ -381,15 +381,13 @@ func (b *S3Backend) ListShare(ctx context.Context, name string) ([]Item, error)
inputs := s3.HeadObjectInput{
Bucket: &b.Options.Bucket,
Key: item.Key,
-
- // ObjectAttributes: []types.ObjectAttributes{
- // types.ObjectAttributesObjectSize,
- // },
}
+
gOutput, err := b.Client.HeadObject(ctx, &inputs)
if err != nil {
return nil, err
}
+
item := &Item{
Path: *item.Key,
ItemInfo: ItemInfo{
diff --git a/hupload/server.go b/hupload/server.go
index 55fc6a5..c1b77e9 100644
--- a/hupload/server.go
+++ b/hupload/server.go
@@ -87,6 +87,7 @@ func (h *Hupload) setup() {
api.AddPublicRoute("POST /api/v1/shares/{share}/items/{item}", authenticator, h.postItem)
api.AddPublicRoute("DELETE /api/v1/shares/{share}/items/{item}", authenticator, h.deleteItem)
+ api.AddPublicRoute("GET /d/{share}", authenticator, h.downloadShare)
api.AddPublicRoute("GET /d/{share}/{item}", authenticator, h.getItem)
// Protected routes
@@ -94,7 +95,7 @@ func (h *Hupload) setup() {
api.AddRoute("GET /login", authenticator, h.postLogin)
api.AddRoute("POST /login", authenticator, h.postLogin)
- api.AddRoute("GET /api/v1/defaults", authenticator, h.getDefaults)
+ api.AddRoute("GET /api/v1/defaults", authenticator, h.getDefaults)
api.AddRoute("GET /api/v1/shares", authenticator, h.getShares)
api.AddRoute("POST /api/v1/shares", authenticator, h.postShare)
@@ -107,7 +108,7 @@ func (h *Hupload) setup() {
api.AddRoute("GET /api/v1/version", authenticator, h.getVersion)
- api.AddRoute("GET /api/v1/*", authenticator, func(w http.ResponseWriter, r *http.Request) {
+ api.AddRoute("GET /api/v1/*", authenticator, func(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "Error")
})
diff --git a/readme_images/share-dark.png b/readme_images/share-dark.png
index 8ada7f4..e141028 100644
Binary files a/readme_images/share-dark.png and b/readme_images/share-dark.png differ
diff --git a/readme_images/share-light.png b/readme_images/share-light.png
index 14e306f..2b6338c 100644
Binary files a/readme_images/share-light.png and b/readme_images/share-light.png differ