Skip to content

Commit 5a8ecfb

Browse files
Bogayknadh
andauthored
Add support for proxying S3 media files through a custom path (#2863)
Similar to how custom paths are set for filesystem uploads, the `Custom public URL` field in S3 settings now accepts a `/relative/path`, which is then handled by listmonk, proxying files from S3 via it. This allows for S3 or S3-like backends to be not visible to the internet. Co-authored-by: Kailash Nadh <kailash@nadh.in>
1 parent 0d9e66a commit 5a8ecfb

File tree

5 files changed

+39
-9
lines changed

5 files changed

+39
-9
lines changed

cmd/init.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ func initMediaStore(ko *koanf.Koanf) media.Store {
722722
case "s3":
723723
var o s3.Opt
724724
ko.Unmarshal("upload.s3", &o)
725+
o.RootURL = ko.String("app.root_url")
725726

726727
up, err := s3.NewS3Store(o)
727728
if err != nil {
@@ -912,8 +913,16 @@ func initHTTPServer(cfg *Config, urlCfg *UrlConfig, i *i18n.I18n, fs stuffbin.Fi
912913
srv.GET("/admin/static/*", echo.WrapHandler(fSrv))
913914

914915
// Public (subscriber) facing media upload files.
915-
if ko.String("upload.provider") == "filesystem" && ko.String("upload.filesystem.upload_uri") != "" {
916-
srv.Static(ko.String("upload.filesystem.upload_uri"), ko.String("upload.filesystem.upload_path"))
916+
var (
917+
uploadProvider = ko.String("upload.provider")
918+
uploadFsURI = ko.String("upload.filesystem.upload_uri")
919+
publicURL = ko.String("upload.s3.public_url")
920+
)
921+
switch {
922+
case uploadProvider == "filesystem" && uploadFsURI != "":
923+
srv.Static(uploadFsURI, ko.String("upload.filesystem.upload_path"))
924+
case uploadProvider == "s3" && strings.HasPrefix(publicURL, "/"):
925+
srv.GET(path.Join(publicURL, "/:filepath"), app.ServeS3Media)
917926
}
918927

919928
// Register all HTTP handlers.

cmd/media.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,22 @@ func (a *App) DeleteMedia(c echo.Context) error {
191191
return c.JSON(http.StatusOK, okResp{true})
192192
}
193193

194+
// ServeS3Media serves media files stored in S3 when the public URL is a relative path.
195+
func (a *App) ServeS3Media(c echo.Context) error {
196+
key := c.Param("filepath")
197+
if key == "" {
198+
return echo.NewHTTPError(http.StatusBadRequest, "missing media file path")
199+
}
200+
201+
b, err := a.media.GetBlob(key)
202+
if err != nil {
203+
a.log.Printf("error fetching media from s3 %s: %v", key, err)
204+
return echo.NewHTTPError(http.StatusInternalServerError, "error fetching media")
205+
}
206+
207+
return c.Stream(http.StatusOK, http.DetectContentType(b), bytes.NewReader(b))
208+
}
209+
194210
// processImage reads the image file and returns thumbnail bytes and
195211
// the original image's width, and height.
196212
func processImage(file *multipart.FileHeader) (*bytes.Reader, int, int, error) {

frontend/src/views/settings/media.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@
102102
placeholder="https://s3.$region.amazonaws.com" :maxlength="200" expanded type="url"
103103
pattern="https?://.*" />
104104
</b-field>
105-
<b-field :label="$t('settings.media.s3.publicURL')" label-position="on-border" expanded>
106-
<b-input v-model="data['upload.s3.public_url']" :message="$t('settings.media.s3.publicURLHelp')"
107-
name="upload.s3.public_url" placeholder="https://files.yourdomain.com" :maxlength="200" type="url"
108-
pattern="https?://.*" />
105+
<b-field :label="$t('settings.media.s3.publicURL')" label-position="on-border" :message="$t('settings.media.s3.publicURLHelp')" expanded>
106+
<b-input v-model="data['upload.s3.public_url']"
107+
name="upload.s3.public_url" placeholder="https://files.yourdomain.com" :maxlength="200" type="string"
108+
pattern="(https?://.*|/.+)" />
109109
</b-field>
110110
</div>
111111
</div>

i18n/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,8 @@
466466
"settings.media.s3.bucketTypePrivate": "Private",
467467
"settings.media.s3.bucketTypePublic": "Public",
468468
"settings.media.s3.key": "AWS access key",
469-
"settings.media.s3.publicURL": "Custom public URL (optional)",
470-
"settings.media.s3.publicURLHelp": "Custom S3 domain to use for image links instead of the default S3 backend URL.",
469+
"settings.media.s3.publicURL": "Custom public URL or path (optional)",
470+
"settings.media.s3.publicURLHelp": "Custom URL (https://cdn.example.com) to use for image links, or a path starting with / (e.g., /uploads) to proxy files through listmonk.",
471471
"settings.media.s3.region": "Region",
472472
"settings.media.s3.secret": "AWS access secret",
473473
"settings.media.s3.uploadExpiry": "Upload expiry",

internal/media/providers/s3/s3.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Opt struct {
2323
BucketPath string `koanf:"bucket_path"`
2424
BucketType string `koanf:"bucket_type"`
2525
Expiry time.Duration `koanf:"expiry"`
26+
RootURL string `koanf:"root_url"`
2627
}
2728

2829
// Client implements `media.Store` for S3 provider
@@ -159,7 +160,11 @@ func (c *Client) makeBucketPath(name string) string {
159160

160161
func (c *Client) makeFileURL(name string) string {
161162
if c.opts.PublicURL != "" {
162-
return c.opts.PublicURL + "/" + c.makeBucketPath(name)
163+
prefix := c.opts.PublicURL
164+
if strings.HasPrefix(prefix, "/") {
165+
prefix = c.opts.RootURL + prefix
166+
}
167+
return prefix + "/" + c.makeBucketPath(name)
163168
}
164169

165170
return c.opts.URL + "/" + c.opts.Bucket + "/" + c.makeBucketPath(name)

0 commit comments

Comments
 (0)