Skip to content

Commit

Permalink
Add Download Handler (#72)
Browse files Browse the repository at this point in the history
Signed-off-by: Russell Troxel <[email protected]>
  • Loading branch information
rtrox authored Jan 20, 2024
1 parent e84d58c commit 41e2f5e
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 4 deletions.
8 changes: 4 additions & 4 deletions internal/calibre/tasks/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,8 @@ func registerBookFile(ctx context.Context, client *ent.Client, bookID ksuid.ID,
return nil
}

filetype := filetype.FromExtension(ext)
if filetype == 0 {
ft := filetype.FromExtension(ext)
if ft == filetype.Unknown {
return errors.New("unknown file type")
}
info, err := file.Info()
Expand All @@ -402,10 +402,10 @@ func registerBookFile(ctx context.Context, client *ent.Client, bookID ksuid.ID,
}
size := info.Size()
_, err = client.BookFile.Create().
SetName(file.Name()).
SetName(nameWithoutExt).
SetPath(path).
SetSize(size).
SetFormat(bookfile.Format(filetype.String())).
SetFormat(bookfile.Format(ft.String())).
SetBookID(bookID).
Save(ctx)
if err != nil {
Expand Down
9 changes: 9 additions & 0 deletions internal/ent/schema/filetype/filetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ func (f FileType) String() string {
return ""
}

func (f FileType) Extension() string {
switch f {
case KEPUB:
return ".kepub.epub"
default:
return "." + strings.ToLower(f.String())
}
}

func FromString(s string) FileType {
switch s {
case EPUB.String():
Expand Down
78 changes: 78 additions & 0 deletions internal/handler/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package handler

import (
"fmt"
"lybbrio/internal/ent"
"lybbrio/internal/ent/book"
"lybbrio/internal/ent/bookfile"
"lybbrio/internal/ent/schema/ksuid"
"mime"
"net/http"
"net/url"
"strings"

"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/rs/zerolog/log"
)

func DownloadRoutes(client *ent.Client) http.Handler {
r := chi.NewRouter()
r.Get("/{bookID}/{book_format}", Download(client))
return r
}

func Download(client *ent.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Info().Str("path", r.URL.Path).Msg("Download")
ctx := r.Context()
bookID := chi.URLParam(r, "bookID")
bookFormat := chi.URLParam(r, "book_format")
log.Info().
Str("bookID", bookID).
Str("book_format", bookFormat).
Msg("Params")
if bookID == "" || bookFormat == "" {

status := http.StatusBadRequest
w.WriteHeader(status)
render.JSON(w, r, map[string]string{
"error": http.StatusText(status),
})
return
}

bookFile, err := client.BookFile.Query().
Where(
bookfile.And(
bookfile.HasBookWith(book.ID(ksuid.ID(bookID))),
bookfile.FormatEQ(bookfile.Format(strings.ToUpper(bookFormat))),
),
).
First(ctx)
if err != nil {
// TODO: Convert if Format isn't available?
status := http.StatusNotFound
if !ent.IsNotFound(err) {
log.Error().Err(err).Msg("Failed to query book file")
status = http.StatusInternalServerError
}
w.WriteHeader(status)
render.JSON(w, r, map[string]string{
"error": http.StatusText(status),
})
return
}
mtype := mime.TypeByExtension("." + strings.ToLower(bookFile.Format.String()))
if mtype == "" {
mtype = "application/octet-stream"
}
w.Header().Set("Content-Type", mtype)
dispo := fmt.Sprintf("attachment; filename=%s; filename*=UTF-8''%s",
url.QueryEscape(bookFile.Name),
url.QueryEscape(bookFile.Name),
)
w.Header().Set("Content-Disposition", dispo)
http.ServeFile(w, r, bookFile.Path)
}
}
138 changes: 138 additions & 0 deletions internal/handler/download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package handler

import (
"context"
"lybbrio/internal/db"
"lybbrio/internal/ent"
"net/http"
"net/http/httptest"
"testing"

"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/require"

"lybbrio/internal/ent/schema/permissions"
"lybbrio/internal/viewer"
)

type testContext struct {
client *ent.Client
teardown func()
adminCtx context.Context
}

func (t testContext) Teardown() {
t.client.Close()
t.teardown()
}

func setupHandlerTest(t *testing.T, testName string, teardown ...func()) testContext {
var ret testContext

ret.client = db.OpenTest(t, testName)

ret.teardown = func() {
ret.client.Close()
}

ret.adminCtx = viewer.NewSystemAdminContext(context.Background())

return ret
}

func addContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := viewer.NewContext(r.Context(), "usr_asdf", permissions.NewPermissions())
next.ServeHTTP(w, r.WithContext(ctx))
})
}

func TestDownload(t *testing.T) {
t.Parallel()
require := require.New(t)
tc := setupHandlerTest(t, "TestDownload")
defer tc.Teardown()

r := chi.NewRouter()
r.Use(addContextMiddleware)
r.Mount("/", DownloadRoutes(tc.client))
server := httptest.NewServer(r)
defer server.Close()

book, err := tc.client.Book.Create().
SetTitle("Test Book").
SetSort("Test Book").
SetPath("/testdata/").Save(tc.adminCtx)
require.NoError(err)

bookFile, err := tc.client.BookFile.Create().
SetBook(book).
SetName("Twenty Thousand Leagues under t - Jules Verne").
SetPath("testdata/Twenty Thousand Leagues under t - Jules Verne.epub").
SetSize(369814).
SetFormat("EPUB").
Save(tc.adminCtx)
require.NoError(err)

url := server.URL + "/" + book.ID.String() + "/" + bookFile.Format.String()
resp, err := server.Client().Get(url)
require.NoError(err)
require.Equal(200, resp.StatusCode)
require.Equal("application/epub+zip", resp.Header.Get("Content-Type"))
require.Equal(
"attachment; filename=Twenty+Thousand+Leagues+under+t+-+Jules+Verne; "+
"filename*=UTF-8''Twenty+Thousand+Leagues+under+t+-+Jules+Verne",
resp.Header.Get("Content-Disposition"),
)
require.Equal("369814", resp.Header.Get("Content-Length"))
}

func TestDownloadErrors(t *testing.T) {
tests := []struct {
name string
bookID string
bookFormat string
wantCode int
}{
{
name: "Missing bookID",
bookFormat: "EPUB",
wantCode: http.StatusBadRequest,
},
{
name: "Missing book_format",
bookID: "asdf",
wantCode: http.StatusNotFound,
},
{
name: "Nonexistent bookID",
bookID: "asdf",
bookFormat: "EPUB",
wantCode: http.StatusNotFound,
},
{
name: "Invalid book_format",
bookID: "asdf",
bookFormat: "asdf",
wantCode: http.StatusNotFound,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
tc := setupHandlerTest(t, tt.name)
defer tc.Teardown()

r := chi.NewRouter()
r.Use(addContextMiddleware)
r.Mount("/", DownloadRoutes(tc.client))
server := httptest.NewServer(r)
defer server.Close()

url := server.URL + "/" + tt.bookID + "/" + tt.bookFormat
resp, err := server.Client().Get(url)
require.NoError(t, err)
require.Equal(t, tt.wantCode, resp.StatusCode)
})
}
}

0 comments on commit 41e2f5e

Please sign in to comment.