Skip to content

Commit

Permalink
iss #69: download books in web-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
maizy committed May 7, 2021
1 parent 246444b commit c3191d6
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 42 deletions.
4 changes: 2 additions & 2 deletions fb2_scanner/archives.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "github.com/gabriel-vasile/mimetype"
type SupportedArchive string

const (
Zip SupportedArchive = "zip"
ZipArchive SupportedArchive = "zip"
)

func archivePtr(archive SupportedArchive) *SupportedArchive {
Expand All @@ -16,7 +16,7 @@ func DetectSupportedArchive(path string) *SupportedArchive {
if mime, err := mimetype.DetectFile(path); err == nil {
switch mime.String() {
case "application/zip":
return archivePtr(Zip)
return archivePtr(ZipArchive)
default:
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion fb2_scanner/fs_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (d *DirectoryTarget) RId() resource.RId {
}

func (d *DirectoryTarget) Type() TargetType {
return FsDir
return FsDirTargetType
}

func (d *DirectoryTarget) GetUUID() *string {
Expand Down
2 changes: 1 addition & 1 deletion fb2_scanner/fs_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func (f *FileTarget) RId() resource.RId {
}

func (f *FileTarget) Type() TargetType {
return FsFile
return FsFileTargetType
}

func (f *FileTarget) GetUUID() *string {
Expand Down
25 changes: 25 additions & 0 deletions fb2_scanner/resource/rid.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package resource
import (
"errors"
"net/url"
"path"
)

type Q struct {
Expand All @@ -18,10 +19,34 @@ type RId struct {
Query []Q
}

const SubPathKey = "p"

func (u *RId) String() string {
return EncodeRId(u.Scheme, u.Path, u.Query)
}

func (u *RId) SubPath() string {
if u.Query != nil {
for _, pair := range u.Query {
if pair.Key == SubPathKey {
return pair.Value
}
}
}
return ""
}

func (u *RId) ResourceBaseName() string {
objectPath := u.Path
if subPath := u.SubPath(); subPath != "" {
objectPath = subPath
}
if objectPath != "" {
return path.Base(objectPath)
}
return ""
}

func EncodeRId(scheme, path string, query []Q) string {
internalUrl := url.URL{Scheme: scheme, Path: path}
urlQ := url.Values{}
Expand Down
66 changes: 55 additions & 11 deletions fb2_scanner/resource/rid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,53 @@ type args struct {
}

var tests = []struct {
name string
args args
full string
name string
args args
full string
baseName string
}{
{"full", args{"file", "/path/to/book.fb2", []Q{{"reload", "true"}}}, "file:///path/to/book.fb2?reload=true"},
{"only scheme", args{scheme: "special"}, "special:"},
{"scheme and path", args{scheme: "dir", path: "/path/to/dir"}, "dir:///path/to/dir"},
{"with space in path", args{scheme: "dir", path: "/path/to/dir with spaces"},
"dir:///path/to/dir%20with%20spaces"},
{"with unicode in path", args{scheme: "dir", path: "/unicode/path/привет"},
"dir:///unicode/path/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82"},
{"repeated args", args{scheme: "special", query: []Q{{"k", "z"}, {"k", "a"}}}, "special:?k=z&k=a"},
{
"full",
args{"file", "/path/to/book.fb2", []Q{{"reload", "true"}}},
"file:///path/to/book.fb2?reload=true",
"book.fb2",
},
{
"only scheme",
args{scheme: "special"},
"special:",
"",
},
{
"scheme and path",
args{scheme: "dir", path: "/path/to/dir"},
"dir:///path/to/dir",
"dir",
},
{
"with space in path",
args{scheme: "dir", path: "/path/to/dir with spaces"},
"dir:///path/to/dir%20with%20spaces",
"dir with spaces",
},
{
"with unicode in path",
args{scheme: "dir", path: "/unicode/path/привет"},
"dir:///unicode/path/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82",
"привет",
},
{
"repeated args",
args{scheme: "special", query: []Q{{"k", "z"}, {"k", "a"}}},
"special:?k=z&k=a",
"",
},
{
"with subpath",
args{scheme: "zip", path: "/path/to/archive.zip", query: []Q{{SubPathKey, "path/to/file.fb2"}}},
"zip:///path/to/archive.zip?p=path%2Fto%2Ffile.fb2",
"file.fb2",
},
}

func TestEncodeRId(t *testing.T) {
Expand All @@ -46,6 +81,15 @@ func TestRId_String(t *testing.T) {
}
}

func TestRId_BaseName(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := (&RId{tt.args.scheme, tt.args.path, tt.args.query}).ResourceBaseName()
assert.Equal(t, tt.baseName, got)
})
}
}

func TestDecodeRId(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
63 changes: 61 additions & 2 deletions fb2_scanner/sources.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
package fb2_scanner

import (
"archive/zip"
"bufio"
"fmt"
"io"
"os"

"dev.maizy.ru/ponylib/fb2_scanner/resource"
)

type FileSource struct {
Path string
}

const (
FileSourceType = "file"
ZipSourceType = "zip"
)

var nop = func() {}

func (s *FileSource) RId() resource.RId {
return resource.RId{Scheme: "file", Path: s.Path}
return resource.RId{Scheme: FileSourceType, Path: s.Path}
}

type ZipArchiveFileSource struct {
Expand All @@ -18,5 +31,51 @@ type ZipArchiveFileSource struct {
}

func (z *ZipArchiveFileSource) RId() resource.RId {
return resource.RId{Scheme: "zip", Path: z.Path, Query: []resource.Q{{"p", z.FilePath}}}
return resource.RId{Scheme: ZipSourceType, Path: z.Path, Query: []resource.Q{{resource.SubPathKey, z.FilePath}}}
}

func OpenResource(RId resource.RId) (*io.Reader, int64, func(), error) {
switch RId.Scheme {
case FileSourceType:
file, err := os.Open(RId.Path)
if err != nil {
return nil, 0, nop, fmt.Errorf("unable to read file for %s: %w", RId, err)
}
stat, err := file.Stat()
if err != nil {
return nil, 0, nop, fmt.Errorf("unable to get file size for %s: %w", RId, err)
}
var reader io.Reader = bufio.NewReader(file)
closeF := func() {
_ = file.Close()
}
return &reader, stat.Size(), closeF, nil

case ZipSourceType:
zipFile, err := zip.OpenReader(RId.Path)
if err != nil {
return nil, 0, nop, fmt.Errorf("unable to open zip archive for %s: %w", RId, err)
}
subPath := RId.SubPath()
file, err := zipFile.Open(subPath)
if err != nil {
_ = zipFile.Close()
return nil, 0, nop, fmt.Errorf("unable to open file %s in zip archive for %s: %w", subPath, RId, err)
}
stat, err := file.Stat()
if err != nil {
return nil, 0, nop,
fmt.Errorf("unable to get file size for %s in zip archive for %s: %w", subPath, RId, err)
}
var reader io.Reader = file
closeF := func() {
_ = file.Close()
_ = zipFile.Close()
}
return &reader, stat.Size(), closeF, nil

default:
return nil, 0, nop, fmt.Errorf("unexpected resource type %s for %s", RId.Scheme, RId)
}

}
8 changes: 4 additions & 4 deletions fb2_scanner/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
type TargetType string

const (
FsDir TargetType = "Directory"
FsFile = "File"
ZipArchive = "Zip Archive"
FsDirTargetType TargetType = "Directory"
FsFileTargetType = "File"
ZipArchiveTargetType = "Zip Archive"
//GzFile = "Gzip File"
)

Expand Down Expand Up @@ -43,7 +43,7 @@ func NewTargetFromEntryPath(entry string, withUUID bool) (ScanTarget, error) {
case mode.IsRegular():
mayBeArchive := DetectSupportedArchive(entry)
switch {
case mayBeArchive != nil && *mayBeArchive == Zip:
case mayBeArchive != nil && *mayBeArchive == ZipArchive:
target = &ZipArchiveTarget{entry, targetUUID}
default:
target = &FileTarget{entry, targetUUID}
Expand Down
2 changes: 1 addition & 1 deletion fb2_scanner/zip.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (z *ZipArchiveTarget) RId() resource.RId {
}

func (z *ZipArchiveTarget) Type() TargetType {
return ZipArchive
return ZipArchiveTargetType
}

func (z *ZipArchiveTarget) GetUUID() *string {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.6.1
github.com/ugorji/go v1.2.5 // indirect
github.com/vfaronov/httpheader v0.1.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
golang.org/x/text v0.3.6 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ github.com/ugorji/go v1.2.5/go.mod h1:gat2tIT8KJG8TVI8yv77nEO/KYT6dV7JE1gfUa8Xul
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.5 h1:8WobZKAk18Msm2CothY2jnztY56YVY8kF1oQrj21iis=
github.com/ugorji/go/codec v1.2.5/go.mod h1:QPxoTbPKSEAlAHPYt02++xp/en9B/wUdwFCz+hj5caA=
github.com/vfaronov/httpheader v0.1.0 h1:VdzetvOKRoQVHjSrXcIOwCV6JG5BCAW9rjbVbFPBmb0=
github.com/vfaronov/httpheader v0.1.0/go.mod h1:ZBxgbYu6nbN5V9Ptd1yYUUan0voD0O8nZLXHyxLgoLE=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
Expand Down
41 changes: 41 additions & 0 deletions ponylib_app/db/book.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package db

import (
"context"
"fmt"

"github.com/jackc/pgx/v4/pgxpool"

"dev.maizy.ru/ponylib/fb2_parser"
"dev.maizy.ru/ponylib/fb2_scanner/resource"
)

type BookResult struct {
UUID string
RId resource.RId
Metadata fb2_parser.Fb2Metadata
}

func GetBook(conn *pgxpool.Pool, uuid string) (*BookResult, error) {
var sqlQuery = "select id, rid, metadata from book where id = $1 limit 1"
rows, err := conn.Query(context.Background(), sqlQuery, uuid)
if err != nil {
return nil, fmt.Errorf("unable to get book by uuid='%s': %w", uuid, err)
}
defer rows.Close()

var uuidFromDb string
var rawRId string
var bookMetadata fb2_parser.Fb2Metadata
rows.Next()
if err := rows.Scan(&uuidFromDb, &rawRId, &bookMetadata); err != nil {
return nil, fmt.Errorf("book with uuid='%s' not found", uuid)
}

bookRId, err := resource.DecodeRId(rawRId)
if err != nil {
return nil, fmt.Errorf("unable to deserialize RId for book with uuid='%s': %w", uuid, err)
}

return &BookResult{uuidFromDb, *bookRId, bookMetadata}, nil
}
29 changes: 29 additions & 0 deletions ponylib_app/web/books.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import (

"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/vfaronov/httpheader"

"dev.maizy.ru/ponylib/fb2_scanner"
"dev.maizy.ru/ponylib/internal/pagination"
"dev.maizy.ru/ponylib/internal/u"
"dev.maizy.ru/ponylib/ponylib_app/db"
"dev.maizy.ru/ponylib/ponylib_app/search"
)

Expand Down Expand Up @@ -71,3 +74,29 @@ func BuildBooksHandler(conn *pgxpool.Pool) func(c *gin.Context) {
}))
}
}

func BuildDownloadBookHandler(conn *pgxpool.Pool) func(c *gin.Context) {
return func(c *gin.Context) {
uuid := c.Param("uuid")
book, err := db.GetBook(conn, uuid)
if err != nil {
returnError(c, "Book not found", http.StatusNotFound)
return
}

reader, size, closeF, err := fb2_scanner.OpenResource(book.RId)
if closeF != nil {
defer closeF()
}
if err != nil {
returnError(c, "Book is missing", http.StatusNotFound)
return
}

header := http.Header{}
httpheader.SetContentDisposition(header, "attachment", book.RId.ResourceBaseName(), nil)
c.Header("Content-Disposition", header.Get("Content-Disposition"))

c.DataFromReader(http.StatusOK, size, "text/xml; charset=utf-8", *reader, nil)
}
}
Loading

0 comments on commit c3191d6

Please sign in to comment.