Skip to content

Commit

Permalink
new: add a button on share page to download zip archive
Browse files Browse the repository at this point in the history
  • Loading branch information
ybizeul authored Sep 13, 2024
1 parent 2a4a9db commit 03879be
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 14 deletions.
21 changes: 13 additions & 8 deletions html/src/Pages/SharePage.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -167,13 +167,18 @@ export function SharePage() {
<>
{/* Top of page copy button */}
<Box w="100%" ta="center">
<CopyButton value={window.location.protocol + '//' + window.location.host + '/' + share.name}>
{({ copied, copy }) => (
<Tooltip withArrow arrowOffset={10} arrowSize={4} label={copied?"Copied!":"Copy URL"}>
<Button mb="sm" justify="center" variant="outline" color={copied ? 'teal' : 'gray'} size="xs" onClick={copy}><IconLink style={{ width: '70%', height: '70%' }} stroke={1.5}/>{share.name}</Button>
</Tooltip>
)}
</CopyButton>
<Group gap="md" justify={"center"} w="100%" mb="sm">
<CopyButton value={window.location.protocol + '//' + window.location.host + '/' + share.name}>
{({ copied, copy }) => (
<Tooltip withArrow arrowOffset={10} arrowSize={4} label={copied?"Copied!":"Copy URL"}>
<Button justify="center" variant="outline" color={copied ? 'teal' : 'gray'} size="xs" onClick={copy}><IconLink style={{ width: '70%', height: '70%' }} stroke={1.5}/>{share.name}</Button>
</Tooltip>
)}
</CopyButton>
{canDownload() && items.length + queueItems.filter((i) => i.failed === false && i.finished === true ).length > 0 &&
<Button component="a" href={'/d/'+share.name} justify="center" variant="outline" size="xs"><IconDownload style={{ width: '70%', height: '70%' }} stroke={1.5}/>Download</Button>
}
</Group>
</Box>

{/* Message */}
Expand Down
69 changes: 69 additions & 0 deletions hupload/handlers.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package main

import (
"archive/zip"
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"path"
"strconv"

"github.com/ybizeul/hupload/internal/storage"
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions hupload/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 2 additions & 4 deletions hupload/internal/storage/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
5 changes: 3 additions & 2 deletions hupload/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,15 @@ 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

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)
Expand All @@ -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")
})

Expand Down
Binary file modified readme_images/share-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified readme_images/share-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 03879be

Please sign in to comment.