From cb74b0d817f9e216f097df13f417f93cfe32e0fb Mon Sep 17 00:00:00 2001 From: Tigor Hutasuhut Date: Tue, 4 Jun 2024 00:15:07 +0700 Subject: [PATCH] wip: prepare to implement server-sent events for image download progress --- api/events/events.go | 18 ++++++ api/events/image_download_start.go | 56 +++++++++++++++++++ server/routes/events/download_simple.go | 46 +++++++++++++++ server/routes/events/events.go | 17 ++++++ server/routes/events/trace.go | 5 ++ .../components/progress/image_download.templ | 23 ++++++++ 6 files changed, 165 insertions(+) create mode 100644 api/events/events.go create mode 100644 api/events/image_download_start.go create mode 100644 server/routes/events/download_simple.go create mode 100644 server/routes/events/events.go create mode 100644 server/routes/events/trace.go create mode 100644 views/components/progress/image_download.templ diff --git a/api/events/events.go b/api/events/events.go new file mode 100644 index 0000000..b2d8a7d --- /dev/null +++ b/api/events/events.go @@ -0,0 +1,18 @@ +package events + +import ( + "io" + + "github.com/a-h/templ" +) + +type Event interface { + templ.Component + // Event returns the event name + Event() string + // SerializeTo writes the event data to the writer. + // + // SerializeTo must not write multiple linebreaks (single linebreak is fine) + // in succession to the writer since it will mess up SSE events. + SerializeTo(w io.Writer) error +} diff --git a/api/events/image_download_start.go b/api/events/image_download_start.go new file mode 100644 index 0000000..6d8f0b0 --- /dev/null +++ b/api/events/image_download_start.go @@ -0,0 +1,56 @@ +package events + +import ( + "context" + "encoding/json" + "io" +) + +type ImageDownloadEvent string + +const ( + ImageDownloadStart ImageDownloadEvent = "image.download.start" + ImageDownloadEnd ImageDownloadEvent = "image.download.end" + ImageDownloadError ImageDownloadEvent = "image.download.error" + ImageDownloadProgress ImageDownloadEvent = "image.download.progress" +) + +type ImageDownload struct { + EventKind ImageDownloadEvent `json:"event"` + ImageURL string `json:"image_url"` + ImageHeight int64 `json:"image_height"` + ImageWidth int64 `json:"image_width"` + ContentLength int64 `json:"content_length"` + Downloaded int64 `json:"downloaded"` + Subreddit string `json:"subreddit"` + PostURL string `json:"post_url"` + PostName string `json:"post_name"` + PostTitle string `json:"post_title"` + Error error `json:"error"` +} + +// Render the template. +func (im ImageDownload) Render(ctx context.Context, w io.Writer) error { + panic("not implemented") // TODO: Implement +} + +// Event returns the event name +func (im ImageDownload) Event() string { + return "image.download" +} + +// SerializeTo writes the event data to the writer. +// +// SerializeTo must not write multiple linebreaks (single linebreak is fine) +// in succession to the writer since it will mess up SSE events. +func (im ImageDownload) SerializeTo(w io.Writer) error { + return json.NewEncoder(w).Encode(im) +} + +type ImageDownloadSubreddit struct { + ImageDownload +} + +func (im ImageDownloadSubreddit) Event() string { + return string(im.EventKind) + "." + im.Subreddit +} diff --git a/server/routes/events/download_simple.go b/server/routes/events/download_simple.go new file mode 100644 index 0000000..d784f3b --- /dev/null +++ b/server/routes/events/download_simple.go @@ -0,0 +1,46 @@ +package events + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/tigorlazuardi/redmage/pkg/log" +) + +func (handler *Handler) SimpleDownloadEvent(rw http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "*Routes.EventsAPI") + defer span.End() + + flush, ok := rw.(http.Flusher) + if !ok { + rw.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(rw).Encode(map[string]string{"error": "response writer does not support streaming"}) + return + } + + log.New(ctx).Info("new simple event stream connection", "user_agent", r.UserAgent()) + + rw.Header().Set("Content-Type", "text/event-stream") + rw.Header().Set("Cache-Control", "no-cache") + rw.Header().Set("Connection", "keep-alive") + rw.WriteHeader(200) + flush.Flush() + + ev, close := handler.Subscribe() + defer close() + + for { + select { + case <-r.Context().Done(): + log.New(ctx).Info("simple event stream connection closed", "user_agent", r.UserAgent()) + return + case event := <-ev: + msg := event.Event() + if _, err := fmt.Fprintf(rw, "event: %s\ndata: %s\n\n", msg, msg); err != nil { + return + } + flush.Flush() + } + } +} diff --git a/server/routes/events/events.go b/server/routes/events/events.go new file mode 100644 index 0000000..b37cf10 --- /dev/null +++ b/server/routes/events/events.go @@ -0,0 +1,17 @@ +package events + +import ( + "github.com/teivah/broadcast" + "github.com/tigorlazuardi/redmage/api/events" + "github.com/tigorlazuardi/redmage/config" +) + +type Handler struct { + Config *config.Config + Broadcast *broadcast.Relay[events.Event] +} + +func (handler *Handler) Subscribe() (<-chan events.Event, func()) { + listener := handler.Broadcast.Listener(10) + return listener.Ch(), listener.Close +} diff --git a/server/routes/events/trace.go b/server/routes/events/trace.go new file mode 100644 index 0000000..d572eb0 --- /dev/null +++ b/server/routes/events/trace.go @@ -0,0 +1,5 @@ +package events + +import "go.opentelemetry.io/otel" + +var tracer = otel.Tracer("server/routes/events") diff --git a/views/components/progress/image_download.templ b/views/components/progress/image_download.templ new file mode 100644 index 0000000..ad6e053 --- /dev/null +++ b/views/components/progress/image_download.templ @@ -0,0 +1,23 @@ +package progress + +type ImageDownloadStartData struct { + ID string + Subreddit string + PostURL string + PostName string + PostTitle string +} + +templ ImageDownloadStart(data ImageDownloadStartData) { +} + +type ImageDownloadEndData struct { + ID string + Subreddit string + PostURL string + PostName string + PostTitle string +} + +templ ImageDownloadEnd(data ImageDownloadEndData) { +}