From fad20e095940bd75117e1d2990212ab934f94c77 Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Sat, 16 Jul 2022 14:22:58 -0700 Subject: [PATCH 1/7] WIP basic server --- .gitignore | 3 +- html/base.html | 109 +++++++++++++++++++++++++++++++++++++++++++++++++ html/home.html | 37 +++++++++++++++++ meh.go | 7 ++++ server.go | 95 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 html/base.html create mode 100644 html/home.html create mode 100644 server.go diff --git a/.gitignore b/.gitignore index ff2845b..f767391 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,10 @@ # Local testing data data/ out/ +tmp/ # Builds build/ -# OS +# OS stuff .DS_Store \ No newline at end of file diff --git a/html/base.html b/html/base.html new file mode 100644 index 0000000..31e4283 --- /dev/null +++ b/html/base.html @@ -0,0 +1,109 @@ + + + + + + {{.Title}} + + + + +
+
+ {{template "page" .}} +
+ +
+ + Help |  + Source Code |  + Privacy + + + + This website isn't affiliated with Medium (website) or A Medium Corporation (company) + +
+
+ + \ No newline at end of file diff --git a/html/home.html b/html/home.html new file mode 100644 index 0000000..8e89f66 --- /dev/null +++ b/html/home.html @@ -0,0 +1,37 @@ +{{define "page"}} +

+ From the people who brought you the original Medium export tool but never made it produce anything but semi-random HTML +

+ +
+ Medium Export Helper: Upload your Medium export archive and we’ll convert it into JSON +
+ +
+
+ +
+ +
+ + +
+ +
+ +
    +
  • +  (by default Medium doesn’t include images in their export but we can download them for you) +
  • + +
  • + +
  • +
+
+ +
+ +
+
+{{end}} \ No newline at end of file diff --git a/meh.go b/meh.go index 0663ad4..a64b1ca 100644 --- a/meh.go +++ b/meh.go @@ -21,6 +21,7 @@ var output *string var verbose *bool var withImages *bool var version *bool +var server *string var logger *log.Logger var logbuf bytes.Buffer @@ -28,6 +29,7 @@ func init() { dir = flag.String("dir", "", "path to the uncompressed medium archive") zip = flag.String("zip", "", "path to the compressed medium archive") output = flag.String("out", "", "output directory") + server = flag.String("server", "", "run web version of meh on provided address") verbose = flag.Bool("verbose", false, "whether to print logs to stdout") version = flag.Bool("version", false, "print version and exit") withImages = flag.Bool("withImages", false, "whether to download images from medium cdn") @@ -44,6 +46,11 @@ func run() error { return nil } + if *server != "" { + RunHTTPServer(*server) + return nil + } + if (*dir == "" && *zip == "") || *output == "" { flag.Usage() return nil diff --git a/server.go b/server.go new file mode 100644 index 0000000..25d973e --- /dev/null +++ b/server.go @@ -0,0 +1,95 @@ +package main + +import ( + "embed" + "fmt" + "html/template" + "io" + "log" + "net/http" + "os" + "path" + "time" +) + +var INBOUND_DIR string = "/Users/anton/Code/meh/tmp" + +//go:embed html +var templates embed.FS + +type pageMeta struct { + Title string +} + +func homepage(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/" { + http.NotFound(w, req) + return + } + + tmpl := template.Must(template.New("base.html").ParseFS(templates, "html/base.html", "html/home.html")) + err := tmpl.Execute(w, pageMeta{Title: "Medium Export Helper"}) + if err != nil { + panic("Can't execute template") + } +} + +func convert(w http.ResponseWriter, req *http.Request) { + err := req.ParseMultipartForm(32 << 20) + if err != nil { + fmt.Fprintf(w, "ParseForm() err: %v", err) + return + } + + uploads := req.MultipartForm.File["archive"] + if len(uploads) == 0 { + fmt.Fprintf(w, "no file was sent from the client") + return + } + + if len(uploads) > 1 { + fmt.Fprintf(w, "too many files were sent from the client") + return + } + + header := uploads[0] + file, err := header.Open() + if err != nil { + fmt.Fprintf(w, "FileHeader.Open() err: %v", err) + return + } + + defer file.Close() + + dest, err := os.Create(path.Join(INBOUND_DIR, header.Filename)) + if err != nil { + fmt.Fprintf(w, "Couldn't create dest file: %v", err) + return + } + + defer dest.Close() + _, err = io.Copy(dest, file) + if err != nil { + fmt.Fprintf(w, "io.Copy err: %v", err) + return + } + + fmt.Fprintf(w, "success! %s was uploaded", header.Filename) +} + +func RunHTTPServer(addr string) (s *http.Server) { + mux := http.NewServeMux() + mux.HandleFunc("/", homepage) + mux.HandleFunc("/convert", convert) + + s = &http.Server{ + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + IdleTimeout: 120 * time.Second, + Addr: addr, + Handler: mux, + } + + log.Print(s.ListenAndServe()) + return +} From d960f76b01485dca05eccc6f999f4eb6dd6564f9 Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Mon, 25 Jul 2022 09:17:47 -0700 Subject: [PATCH 2/7] WIP not so basic server anymore --- go.mod | 2 + go.sum | 2 + meh.go | 3 +- server.go | 95 ---------------------- server/convert.go | 72 +++++++++++++++++ server/errors.go | 13 +++ server/homepage.go | 14 ++++ {html => server/html}/base.html | 0 {html => server/html}/home.html | 0 server/server.go | 139 ++++++++++++++++++++++++++++++++ util/util.go | 2 +- 11 files changed, 245 insertions(+), 97 deletions(-) delete mode 100644 server.go create mode 100644 server/convert.go create mode 100644 server/errors.go create mode 100644 server/homepage.go rename {html => server/html}/base.html (100%) rename {html => server/html}/home.html (100%) create mode 100644 server/server.go diff --git a/go.mod b/go.mod index 4eb9b93..ead7933 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/valueof/meh go 1.18 require golang.org/x/net v0.0.0-20220403103023-749bd193bc2b + +require github.com/google/uuid v1.3.0 // indirect diff --git a/go.sum b/go.sum index 935083a..0d35800 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= diff --git a/meh.go b/meh.go index a64b1ca..a1d80e1 100644 --- a/meh.go +++ b/meh.go @@ -10,6 +10,7 @@ import ( "github.com/valueof/meh/formatters" "github.com/valueof/meh/parser" + http "github.com/valueof/meh/server" "github.com/valueof/meh/util" ) @@ -47,7 +48,7 @@ func run() error { } if *server != "" { - RunHTTPServer(*server) + http.RunHTTPServer(*server) return nil } diff --git a/server.go b/server.go deleted file mode 100644 index 25d973e..0000000 --- a/server.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "embed" - "fmt" - "html/template" - "io" - "log" - "net/http" - "os" - "path" - "time" -) - -var INBOUND_DIR string = "/Users/anton/Code/meh/tmp" - -//go:embed html -var templates embed.FS - -type pageMeta struct { - Title string -} - -func homepage(w http.ResponseWriter, req *http.Request) { - if req.URL.Path != "/" { - http.NotFound(w, req) - return - } - - tmpl := template.Must(template.New("base.html").ParseFS(templates, "html/base.html", "html/home.html")) - err := tmpl.Execute(w, pageMeta{Title: "Medium Export Helper"}) - if err != nil { - panic("Can't execute template") - } -} - -func convert(w http.ResponseWriter, req *http.Request) { - err := req.ParseMultipartForm(32 << 20) - if err != nil { - fmt.Fprintf(w, "ParseForm() err: %v", err) - return - } - - uploads := req.MultipartForm.File["archive"] - if len(uploads) == 0 { - fmt.Fprintf(w, "no file was sent from the client") - return - } - - if len(uploads) > 1 { - fmt.Fprintf(w, "too many files were sent from the client") - return - } - - header := uploads[0] - file, err := header.Open() - if err != nil { - fmt.Fprintf(w, "FileHeader.Open() err: %v", err) - return - } - - defer file.Close() - - dest, err := os.Create(path.Join(INBOUND_DIR, header.Filename)) - if err != nil { - fmt.Fprintf(w, "Couldn't create dest file: %v", err) - return - } - - defer dest.Close() - _, err = io.Copy(dest, file) - if err != nil { - fmt.Fprintf(w, "io.Copy err: %v", err) - return - } - - fmt.Fprintf(w, "success! %s was uploaded", header.Filename) -} - -func RunHTTPServer(addr string) (s *http.Server) { - mux := http.NewServeMux() - mux.HandleFunc("/", homepage) - mux.HandleFunc("/convert", convert) - - s = &http.Server{ - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, - IdleTimeout: 120 * time.Second, - Addr: addr, - Handler: mux, - } - - log.Print(s.ListenAndServe()) - return -} diff --git a/server/convert.go b/server/convert.go new file mode 100644 index 0000000..497138b --- /dev/null +++ b/server/convert.go @@ -0,0 +1,72 @@ +package server + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +func convert(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + logger := getLoggerFromContext(ctx) + rid := getRequestIDFromContext(ctx) + + err := r.ParseMultipartForm(32 << 20) + if err != nil { + logger.Printf("ParseForm() err: %v", err) + internalServerError(w, r) + return + } + + uploads := r.MultipartForm.File["archive"] + if len(uploads) == 0 { + logger.Printf("no file was sent from the client") + internalServerError(w, r) + return + } + + if len(uploads) > 1 { + logger.Printf("too many files were sent from the client") + internalServerError(w, r) + return + } + + header := uploads[0] + file, err := header.Open() + if err != nil { + logger.Printf("FileHeader.Open() err: %v", err) + internalServerError(w, r) + return + } + + defer file.Close() + + dest := filepath.Join(INBOUND_DIR, rid) + err = os.Mkdir(dest, 0700) + if err != nil { + logger.Printf("Failed to create holding directory %s: %v\n", dest, err) + internalServerError(w, r) + return + } + + upload, err := os.Create(filepath.Join(dest, header.Filename)) + if err != nil { + logger.Printf("Couldn't create dest file: %v", err) + internalServerError(w, r) + return + } + + defer upload.Close() + _, err = io.Copy(upload, file) + if err != nil { + logger.Printf("io.Copy err: %v", err) + internalServerError(w, r) + return + } + + // TODO: + // - Generate and show a receipt number + fmt.Fprintf(w, "success! %s was uploaded", header.Filename) +} diff --git a/server/errors.go b/server/errors.go new file mode 100644 index 0000000..7cf9239 --- /dev/null +++ b/server/errors.go @@ -0,0 +1,13 @@ +package server + +import ( + "net/http" +) + +func notFound(w http.ResponseWriter, r *http.Request) { + // TK +} + +func internalServerError(w http.ResponseWriter, r *http.Request) { + // TK +} diff --git a/server/homepage.go b/server/homepage.go new file mode 100644 index 0000000..8258020 --- /dev/null +++ b/server/homepage.go @@ -0,0 +1,14 @@ +package server + +import ( + "net/http" +) + +func homepage(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + notFound(w, r) + return + } + + render(w, r, "home.html", pageMeta{Title: "Medium Export Helper"}) +} diff --git a/html/base.html b/server/html/base.html similarity index 100% rename from html/base.html rename to server/html/base.html diff --git a/html/home.html b/server/html/home.html similarity index 100% rename from html/home.html rename to server/html/home.html diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..e555cce --- /dev/null +++ b/server/server.go @@ -0,0 +1,139 @@ +package server + +import ( + "context" + "embed" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "text/template" + "time" + + "github.com/google/uuid" +) + +type key int + +const ( + INBOUND_DIR string = "/var/tmp/mehserver" + REQUEST_ID_KEY key = 0 +) + +//go:embed html +var templates embed.FS + +type pageMeta struct { + Title string +} + +func render(w http.ResponseWriter, r *http.Request, name string, data any) { + ctx := r.Context() + logger := getLoggerFromContext(ctx) + rid := getRequestIDFromContext(ctx) + + t, err := template.New("base.html").ParseFS(templates, "html/base.html", "html/"+name) + if err != nil { + logger.Printf("ParseFS on html/%s failed with: %v", name, err) + fmt.Fprintf(w, "Internal Server Error (%s)", rid) + return + } + + err = t.Execute(w, data) + if err != nil { + logger.Printf("Execute on html/%s failed with %v", name, err) + fmt.Fprintf(w, "Internal Server Error (%s)", rid) + } +} + +func getRequestIDFromContext(ctx context.Context) (rid string) { + rid, ok := ctx.Value(REQUEST_ID_KEY).(string) + if !ok { + rid = "unknown" + } + return +} + +func getLoggerFromContext(ctx context.Context) (logger *log.Logger) { + rid := getRequestIDFromContext(ctx) + return log.New(os.Stdout, fmt.Sprintf("[%s]", rid), log.LstdFlags) +} + +func tracing(uuid func() string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rid := r.Header.Get("X-Request-Id") + if rid == "" { + rid = uuid() + } + + ctx := context.WithValue(r.Context(), REQUEST_ID_KEY, rid) + w.Header().Set("X-Request-Id", rid) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func logging(logger *log.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + rid := getRequestIDFromContext(r.Context()) + logger.Println(rid, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent()) + }() + + next.ServeHTTP(w, r) + }) + } +} + +func RunHTTPServer(addr string) { + logger := log.New(os.Stdout, "server: ", log.LstdFlags) + logger.Println("Server is starting...") + + logger.Println("Preparing directory to hold uploaded files") + err := os.MkdirAll(INBOUND_DIR, 0700) + if err != nil { + logger.Printf("Failed to create %s: %v", INBOUND_DIR, err) + logger.Fatalf("Can't proceed") + } + + router := http.NewServeMux() + router.HandleFunc("/", homepage) + router.HandleFunc("/convert", convert) + + s := &http.Server{ + ReadTimeout: 5 * time.Second, + WriteTimeout: 5 * time.Second, + IdleTimeout: 15 * time.Second, + Addr: addr, + Handler: tracing(uuid.NewString)(logging(logger)(router)), + } + + done := make(chan bool) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + + go func() { + <-quit + logger.Println("Shutting down...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + s.SetKeepAlivesEnabled(false) + if err := s.Shutdown(ctx); err != nil { + logger.Fatalf("Could not gracefully shutdown the server: %v\n", err) + } + close(done) + }() + + logger.Println("Server is ready at", addr) + if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Fatalf("Could not listen on %s: %v\n", addr, err) + } + + <-done + logger.Println("Goodbye, friend!") +} diff --git a/util/util.go b/util/util.go index 978e36f..933fcde 100644 --- a/util/util.go +++ b/util/util.go @@ -428,7 +428,7 @@ func UnzipArchive(src string, dest string) (err error) { if !f.FileInfo().IsDir() { p := filepath.Join(dest, f.Name) - os.MkdirAll(filepath.Dir(p), 0777) + os.MkdirAll(filepath.Dir(p), 0600) f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err From 3dfdee634d3a42db3a258c117149d52fc1dda15a Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Mon, 25 Jul 2022 22:28:02 -0700 Subject: [PATCH 3/7] added errors --- server/errors.go | 18 ++++++++++++++++-- server/html/404.html | 6 ++++++ server/html/500.html | 7 +++++++ server/html/base.html | 32 +++++++++++++++++++------------- server/html/home.html | 2 +- server/server.go | 3 ++- 6 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 server/html/404.html create mode 100644 server/html/500.html diff --git a/server/errors.go b/server/errors.go index 7cf9239..bac8e0e 100644 --- a/server/errors.go +++ b/server/errors.go @@ -5,9 +5,23 @@ import ( ) func notFound(w http.ResponseWriter, r *http.Request) { - // TK + data := pageMeta{} + data.Title = "[meh] Page Not Found" + data.SkipFooter = true + + render(w, r, "404.html", data) +} + +type internalServerErrorData struct { + RequestID string + pageMeta } func internalServerError(w http.ResponseWriter, r *http.Request) { - // TK + data := internalServerErrorData{} + data.Title = "[meh] Internal Server Error" + data.SkipFooter = true + data.RequestID = getRequestIDFromContext(r.Context()) + + render(w, r, "500.html", data) } diff --git a/server/html/404.html b/server/html/404.html new file mode 100644 index 0000000..4b2ee33 --- /dev/null +++ b/server/html/404.html @@ -0,0 +1,6 @@ +{{define "page"}} +
+

(´_`)

+

Page you’re looking for doesn't exist.

+
+{{end}} \ No newline at end of file diff --git a/server/html/500.html b/server/html/500.html new file mode 100644 index 0000000..214bce9 --- /dev/null +++ b/server/html/500.html @@ -0,0 +1,7 @@ +{{define "page"}} +
+

(✖╭╮✖)

+

Internal Server Error

+

You can file a bug and include this request ID: {{.RequestID}}

+
+{{end}} \ No newline at end of file diff --git a/server/html/base.html b/server/html/base.html index 31e4283..8b30093 100644 --- a/server/html/base.html +++ b/server/html/base.html @@ -35,11 +35,15 @@ .wrapper { display: flex; flex-direction: column; - justify-content: space-between; + justify-content: center; width: 80%; margin: 0 auto; } + .error { + text-align: center; + } + form { margin: 20px 0; padding: 20px; @@ -80,7 +84,6 @@ footer { display: flex; - margin-bottom: 50px; font-size: 80%; justify-content: space-between; } @@ -93,17 +96,20 @@ {{template "page" .}} -
- - Help |  - Source Code |  - Privacy - - - - This website isn't affiliated with Medium (website) or A Medium Corporation (company) - -
+ {{if .SkipFooter}} + {{else}} +
+ + Help |  + Source Code |  + Privacy + + + + This website isn't affiliated with Medium (website) or A Medium Corporation (company) + +
+ {{end}} \ No newline at end of file diff --git a/server/html/home.html b/server/html/home.html index 8e89f66..5ac5167 100644 --- a/server/html/home.html +++ b/server/html/home.html @@ -4,7 +4,7 @@

- Medium Export Helper: Upload your Medium export archive and we’ll convert it into JSON + Medium Export Helper: Convert you Medium archive into JSON!
diff --git a/server/server.go b/server/server.go index e555cce..da21be9 100644 --- a/server/server.go +++ b/server/server.go @@ -25,7 +25,8 @@ const ( var templates embed.FS type pageMeta struct { - Title string + Title string + SkipFooter bool } func render(w http.ResponseWriter, r *http.Request, name string, data any) { From b31d3208a0fd64addef918e7dcdb28224518c460 Mon Sep 17 00:00:00 2001 From: Anton Kovalyov Date: Tue, 26 Jul 2022 23:14:56 -0700 Subject: [PATCH 4/7] WIP upload & basic worker pool works now --- server/convert.go | 72 --------------- server/errors.go | 27 ------ server/handlers.go | 194 +++++++++++++++++++++++++++++++++++++++++ server/homepage.go | 14 --- server/html/base.html | 5 ++ server/html/fetch.html | 8 ++ server/html/home.html | 2 +- server/html/wait.html | 8 ++ server/server.go | 11 +-- server/tasks.go | 74 ++++++++++++++++ 10 files changed, 296 insertions(+), 119 deletions(-) delete mode 100644 server/convert.go delete mode 100644 server/errors.go create mode 100644 server/handlers.go delete mode 100644 server/homepage.go create mode 100644 server/html/fetch.html create mode 100644 server/html/wait.html create mode 100644 server/tasks.go diff --git a/server/convert.go b/server/convert.go deleted file mode 100644 index 497138b..0000000 --- a/server/convert.go +++ /dev/null @@ -1,72 +0,0 @@ -package server - -import ( - "fmt" - "io" - "net/http" - "os" - "path/filepath" -) - -func convert(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - logger := getLoggerFromContext(ctx) - rid := getRequestIDFromContext(ctx) - - err := r.ParseMultipartForm(32 << 20) - if err != nil { - logger.Printf("ParseForm() err: %v", err) - internalServerError(w, r) - return - } - - uploads := r.MultipartForm.File["archive"] - if len(uploads) == 0 { - logger.Printf("no file was sent from the client") - internalServerError(w, r) - return - } - - if len(uploads) > 1 { - logger.Printf("too many files were sent from the client") - internalServerError(w, r) - return - } - - header := uploads[0] - file, err := header.Open() - if err != nil { - logger.Printf("FileHeader.Open() err: %v", err) - internalServerError(w, r) - return - } - - defer file.Close() - - dest := filepath.Join(INBOUND_DIR, rid) - err = os.Mkdir(dest, 0700) - if err != nil { - logger.Printf("Failed to create holding directory %s: %v\n", dest, err) - internalServerError(w, r) - return - } - - upload, err := os.Create(filepath.Join(dest, header.Filename)) - if err != nil { - logger.Printf("Couldn't create dest file: %v", err) - internalServerError(w, r) - return - } - - defer upload.Close() - _, err = io.Copy(upload, file) - if err != nil { - logger.Printf("io.Copy err: %v", err) - internalServerError(w, r) - return - } - - // TODO: - // - Generate and show a receipt number - fmt.Fprintf(w, "success! %s was uploaded", header.Filename) -} diff --git a/server/errors.go b/server/errors.go deleted file mode 100644 index bac8e0e..0000000 --- a/server/errors.go +++ /dev/null @@ -1,27 +0,0 @@ -package server - -import ( - "net/http" -) - -func notFound(w http.ResponseWriter, r *http.Request) { - data := pageMeta{} - data.Title = "[meh] Page Not Found" - data.SkipFooter = true - - render(w, r, "404.html", data) -} - -type internalServerErrorData struct { - RequestID string - pageMeta -} - -func internalServerError(w http.ResponseWriter, r *http.Request) { - data := internalServerErrorData{} - data.Title = "[meh] Internal Server Error" - data.SkipFooter = true - data.RequestID = getRequestIDFromContext(r.Context()) - - render(w, r, "500.html", data) -} diff --git a/server/handlers.go b/server/handlers.go new file mode 100644 index 0000000..b00d3b7 --- /dev/null +++ b/server/handlers.go @@ -0,0 +1,194 @@ +package server + +import ( + "crypto/sha256" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +type pageMeta struct { + Title string + SkipFooter bool + Refresh string +} + +type internalServerErrorData struct { + RequestID string + pageMeta +} + +func notFound(w http.ResponseWriter, r *http.Request) { + data := pageMeta{} + data.Title = "[meh] Page Not Found" + data.SkipFooter = true + + render(w, r, "404.html", data) +} + +func internalServerError(w http.ResponseWriter, r *http.Request) { + data := internalServerErrorData{} + data.Title = "[meh] Internal Server Error" + data.SkipFooter = true + data.RequestID = getRequestIDFromContext(r.Context()) + + render(w, r, "500.html", data) +} + +func homepage(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + notFound(w, r) + return + } + + render(w, r, "home.html", pageMeta{Title: "Medium Export Helper"}) +} + +func upload(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + logger := getLoggerFromContext(ctx) + + err := r.ParseMultipartForm(32 << 20) + if err != nil { + logger.Printf("ParseForm() err: %v", err) + internalServerError(w, r) + return + } + + uploads := r.MultipartForm.File["archive"] + if len(uploads) == 0 { + logger.Printf("no file was sent from the client") + internalServerError(w, r) + return + } + + if len(uploads) > 1 { + logger.Printf("too many files were sent from the client") + internalServerError(w, r) + return + } + + header := uploads[0] + file, err := header.Open() + if err != nil { + logger.Printf("FileHeader.Open() err: %v", err) + internalServerError(w, r) + return + } + + defer file.Close() + + h := sha256.New() + io.TeeReader(file, h) + hashsum := fmt.Sprintf("%x", h.Sum(nil)) + + dest := filepath.Join(INBOUND_DIR, hashsum) + err = os.Mkdir(dest, 0700) + if err != nil && !errors.Is(err, os.ErrExist) { + logger.Printf("Failed to create holding directory %s: %v\n", dest, err) + internalServerError(w, r) + return + } + + dest = filepath.Join(dest, "upload.zip") + _, err = os.Stat(dest) + if err == nil { + // File already exists, check whether we need to reprocess it and redirect + if _, ok := tasks.Status(hashsum); !ok { + go unzipAndParse(hashsum, logger) + } + + url := fmt.Sprintf("/wait?h=%s", hashsum) + http.Redirect(w, r, url, http.StatusFound) + } else if errors.Is(err, os.ErrNotExist) { + // File doesn't exist, upload and send for processing + upload, err := os.Create(dest) + if err != nil { + logger.Printf("Couldn't create dest file: %v", err) + internalServerError(w, r) + return + } + + defer upload.Close() + _, err = io.Copy(upload, file) + if err != nil { + logger.Printf("io.Copy err: %v", err) + internalServerError(w, r) + return + } + + logger.Printf("Uploaded %s", dest) + + go unzipAndParse(hashsum, logger) + + url := fmt.Sprintf("/wait?h=%s", hashsum) + http.Redirect(w, r, url, http.StatusFound) + } else { + // Some other error, run around in panic + logger.Printf("os.Stat returned an unexpected error: %v\n", err) + internalServerError(w, r) + } +} + +func wait(w http.ResponseWriter, r *http.Request) { + logger := getLoggerFromContext(r.Context()) + + hashsum := r.URL.Query().Get("h") + if hashsum == "" { + notFound(w, r) + return + } + + if r.URL.Query().Has("dl") { + // TODO: use output file here + file, err := os.Open(filepath.Join(INBOUND_DIR, hashsum, "upload.zip")) + if err != nil { + logger.Printf("Couldn't read file for download: %v\n", err) + notFound(w, r) + return + } + + info, err := file.Stat() + if err != nil { + logger.Printf("file.Stat() returned an error: %v\n", err) + internalServerError(w, r) + return + } + + w.Header().Set("Content-Disposition", "attachment; filename=archive.zip") + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size())) + _, err = io.Copy(w, file) + if err != nil { + logger.Printf("io.Copy err: %v", err) + internalServerError(w, r) + return + } + + go cleanup(hashsum, logger) + return + } + + st, exists := tasks.Status(hashsum) + if !exists { + notFound(w, r) + return + } + + if st == TASK_DONE { + render(w, r, "fetch.html", pageMeta{ + Title: "[meh] Downloading...", + SkipFooter: true, + Refresh: fmt.Sprintf("0;url=/wait?h=%s&dl", hashsum), + }) + } + + render(w, r, "wait.html", pageMeta{ + Title: "[meh] Converting...", + SkipFooter: true, + Refresh: "10", + }) +} diff --git a/server/homepage.go b/server/homepage.go deleted file mode 100644 index 8258020..0000000 --- a/server/homepage.go +++ /dev/null @@ -1,14 +0,0 @@ -package server - -import ( - "net/http" -) - -func homepage(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - notFound(w, r) - return - } - - render(w, r, "home.html", pageMeta{Title: "Medium Export Helper"}) -} diff --git a/server/html/base.html b/server/html/base.html index 8b30093..1edcabe 100644 --- a/server/html/base.html +++ b/server/html/base.html @@ -4,6 +4,11 @@ {{.Title}} + + {{if .Refresh}} + + {{end}} +