From f4408abeae8ac46dad17045bda1835bf1108f662 Mon Sep 17 00:00:00 2001 From: Evan Buss Date: Fri, 12 Jul 2024 19:36:08 +0000 Subject: [PATCH] refactor: dry conversion code --- convert/convert.go | 7 +-- convert/kepub.go | 31 +++++++--- convert/mobi.go | 26 ++++---- server.go | 150 ++++++++++++++++++++++++--------------------- 4 files changed, 116 insertions(+), 98 deletions(-) diff --git a/convert/convert.go b/convert/convert.go index ce7baa3..041f584 100644 --- a/convert/convert.go +++ b/convert/convert.go @@ -1,14 +1,9 @@ package convert -const ( - MOBI_MIME = "application/x-mobipocket-ebook" - EPUB_MIME = "application/epub+zip" -) - type Converter interface { // Whether or not the converter is available // Usually based on the availability of the underlying tool Available() bool // Convert the input file to the output file - Convert(input string, output string) error + Convert(input string) (string, error) } diff --git a/convert/kepub.go b/convert/kepub.go index e8a2a6b..18698b7 100644 --- a/convert/kepub.go +++ b/convert/kepub.go @@ -1,29 +1,40 @@ package convert import ( + "fmt" "os/exec" + "path/filepath" + "strings" "sync" ) type KepubConverter struct { - mutex sync.Mutex + mutex sync.Mutex + available bool + availableOnce sync.Once } func (kc *KepubConverter) Available() bool { - path, err := exec.LookPath("kepubify") - if err != nil { - return false - } - return path != "" + kc.availableOnce.Do(func() { + fmt.Println("TEST") + path, err := exec.LookPath("kepubify") + kc.available = err == nil && path != "" + }) + + return kc.available } -func (kc *KepubConverter) Convert(input string, output string) error { +func (kc *KepubConverter) Convert(input string) (string, error) { kc.mutex.Lock() defer kc.mutex.Unlock() - cmd := exec.Command("kepubify", "-v", "-u", "-o", output, input) + + dir := filepath.Dir(input) + kepubFile := filepath.Join(dir, strings.Replace(filepath.Base(input), ".epub", ".kepub.epub", 1)) + + cmd := exec.Command("kepubify", "-v", "-u", "-o", kepubFile, input) if err := cmd.Run(); err != nil { - return err + return "", err } - return nil + return kepubFile, nil } diff --git a/convert/mobi.go b/convert/mobi.go index d098c86..13acbd9 100644 --- a/convert/mobi.go +++ b/convert/mobi.go @@ -5,34 +5,38 @@ import ( "fmt" "os/exec" "path/filepath" + "strings" "sync" ) type MobiConverter struct { - mutex sync.Mutex + mutex sync.Mutex + available bool + availableOnce sync.Once } func (mc *MobiConverter) Available() bool { - path, err := exec.LookPath("kindlegen") - if err != nil { - return false - } - return path != "" + mc.availableOnce.Do(func() { + path, err := exec.LookPath("kindlegen") + mc.available = err == nil && path != "" + }) + return mc.available } -func (mc *MobiConverter) Convert(input string, output string) error { +func (mc *MobiConverter) Convert(input string) (string, error) { mc.mutex.Lock() defer mc.mutex.Unlock() // KindleGen doesn't allow the input file to be in a different directory // So set the working directory to the input file. outDir, _ := filepath.Abs(filepath.Dir(input)) + mobiFile := filepath.Join(outDir, strings.Replace(filepath.Base(input), ".epub", ".mobi", 1)) // And remove the directory from file paths cmd := exec.Command("kindlegen", filepath.Base(input), "-dont_append_source", "-c1", "-o", - filepath.Base(output), + filepath.Base(mobiFile), ) cmd.Dir = outDir @@ -45,13 +49,13 @@ func (mc *MobiConverter) Convert(input string, output string) error { if exiterr, ok := err.(*exec.ExitError); ok { if exiterr.ExitCode() != 1 { fmt.Println(fmt.Sprint(err) + ": " + out.String() + ":" + stderr.String()) - return err + return "", err } } else { fmt.Println(fmt.Sprint(err) + ": " + out.String() + ":" + stderr.String()) - return err + return "", err } } - return nil + return mobiFile, nil } diff --git a/server.go b/server.go index 3c6c145..c53391c 100644 --- a/server.go +++ b/server.go @@ -18,13 +18,23 @@ import ( "github.com/evan-buss/opds-proxy/opds" ) -type Middleware func(http.HandlerFunc) http.HandlerFunc - type Server struct { addr string router *http.ServeMux } +const ( + MOBI_MIME = "application/x-mobipocket-ebook" + EPUB_MIME = "application/epub+zip" + ATOM_MIME = "application/atom+xml" +) + +var ( + _ = mime.AddExtensionType(".epub", EPUB_MIME) + _ = mime.AddExtensionType(".kepub.epub", EPUB_MIME) + _ = mime.AddExtensionType(".mobi", MOBI_MIME) +) + func NewServer(config *config) *Server { router := http.NewServeMux() router.HandleFunc("GET /{$}", handleHome(config.Feeds)) @@ -56,7 +66,7 @@ func handleHome(feeds []feedConfig) http.HandlerFunc { } } -func handleFeed(dir string) http.HandlerFunc { +func handleFeed(outputDir string) http.HandlerFunc { kepubConverter := &convert.KepubConverter{} mobiConverter := &convert.MobiConverter{} @@ -93,7 +103,7 @@ func handleFeed(dir string) http.HandlerFunc { handleError(r, w, "Failed to parse content type", err) } - if mimeType == "application/atom+xml" { + if mimeType == ATOM_MIME { feed, err := opds.ParseFeed(resp.Body) if err != nil { handleError(r, w, "Failed to parse feed", err) @@ -105,82 +115,41 @@ func handleFeed(dir string) http.HandlerFunc { Feed: feed, } - err = html.Feed(w, feedParams, partial(r)) - if err != nil { + if err = html.Feed(w, feedParams, partial(r)); err != nil { handleError(r, w, "Failed to render feed", err) return } } - if mimeType != convert.EPUB_MIME { - for k, v := range resp.Header { - w.Header()[k] = v - } - - io.Copy(w, resp.Body) - return + var converter convert.Converter + if strings.Contains(r.UserAgent(), "Kobo") && kepubConverter.Available() { + converter = kepubConverter + } else if strings.Contains(r.UserAgent(), "Kindle") && mobiConverter.Available() { + converter = mobiConverter } - if strings.Contains(r.Header.Get("User-Agent"), "Kobo") && kepubConverter.Available() { - epubFile := filepath.Join(dir, parseFileName(resp)) - downloadFile(epubFile, resp) - - kepubFile := filepath.Join(dir, strings.Replace(parseFileName(resp), ".epub", ".kepub.epub", 1)) - kepubConverter.Convert(epubFile, kepubFile) - if err != nil { - handleError(r, w, "Failed to convert to kepub", err) - } - - outFile, _ := os.Open(kepubFile) - defer outFile.Close() - - outInfo, _ := outFile.Stat() - - w.Header().Set("Content-Length", fmt.Sprintf("%d", outInfo.Size())) - w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filepath.Base(kepubFile)})) - w.Header().Set("Content-Type", convert.EPUB_MIME) - - io.Copy(w, outFile) - - os.Remove(epubFile) - os.Remove(kepubFile) - + if mimeType != EPUB_MIME || converter == nil { + forwardResponse(w, resp) return } - if strings.Contains(r.Header.Get("User-Agent"), "Kindle") && mobiConverter.Available() { - epubFile := filepath.Join(dir, parseFileName(resp)) - downloadFile(epubFile, resp) - - mobiFile := filepath.Join(dir, strings.Replace(parseFileName(resp), ".epub", ".mobi", 1)) - err := mobiConverter.Convert(epubFile, mobiFile) - if err != nil { - handleError(r, w, "Failed to convert to mobi", err) - return - } - - outFile, _ := os.Open(mobiFile) - defer outFile.Close() - - outInfo, _ := outFile.Stat() - - w.Header().Set("Content-Length", fmt.Sprintf("%d", outInfo.Size())) - w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filepath.Base(mobiFile)})) - w.Header().Set("Content-Type", convert.MOBI_MIME) - - io.Copy(w, outFile) + filename, err := parseFileName(resp) + if err != nil { + handleError(r, w, "Failed to parse file name", err) + } - os.Remove(epubFile) - os.Remove(mobiFile) + epubFile := filepath.Join(outputDir, filename) + downloadFile(epubFile, resp) + defer os.Remove(epubFile) - return + outputFile, err := converter.Convert(epubFile) + if err != nil { + handleError(r, w, "Failed to convert epub", err) } - for k, v := range resp.Header { - w.Header()[k] = v + if err = sendConvertedFile(w, outputFile); err != nil { + handleError(r, w, "Failed to send converted file", err) } - - io.Copy(w, resp.Body) } } @@ -211,25 +180,64 @@ func partial(req *http.Request) string { return req.URL.Query().Get("partial") } -func downloadFile(path string, resp *http.Response) { +func downloadFile(path string, resp *http.Response) error { file, err := os.Create(path) if err != nil { - log.Fatal(err) + return err } defer file.Close() _, err = io.Copy(file, resp.Body) if err != nil { - log.Fatal(err) + return err } + + return nil } -func parseFileName(resp *http.Response) string { +func parseFileName(resp *http.Response) (string, error) { contentDisposition := resp.Header.Get("Content-Disposition") _, params, err := mime.ParseMediaType(contentDisposition) if err != nil { - log.Fatal(err) + return "", err + } + + return params["filename"], nil +} + +func forwardResponse(w http.ResponseWriter, resp *http.Response) { + for k, v := range resp.Header { + w.Header()[k] = v + } + + io.Copy(w, resp.Body) +} + +func sendConvertedFile(w http.ResponseWriter, filePath string) error { + defer os.Remove(filePath) + file, err := os.Open(filePath) + if err != nil { + return err + } + + info, err := file.Stat() + if err != nil { + return err + } + + w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size())) + w.Header().Set("Content-Disposition", + mime.FormatMediaType( + "attachment", + map[string]string{"filename": filepath.Base(filePath)}, + ), + ) + w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(filePath))) + + _, err = io.Copy(w, file) + if err != nil { + return err } - return params["filename"] + return nil }