Skip to content

Commit 1c207b5

Browse files
authored
fix(go): return correct CSV file names (#828)
1 parent f7bacfb commit 1c207b5

File tree

3 files changed

+31
-12
lines changed

3 files changed

+31
-12
lines changed

pkg/api/rendercsv.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import (
55
"errors"
66
"fmt"
77
"log/slog"
8+
"mime"
89
"net/http"
10+
"path"
11+
"path/filepath"
912
"strconv"
1013
"strings"
1114
"time"
@@ -79,7 +82,7 @@ func HandleGetRenderCSV(browser *service.BrowserService) http.Handler {
7982
attribute.String("renderKeyDomain", domain))
8083

8184
start := time.Now()
82-
contents, err := browser.RenderCSV(ctx, url, renderKey, domain, acceptLanguage)
85+
contents, fileName, err := browser.RenderCSV(ctx, url, renderKey, domain, acceptLanguage)
8386
if err != nil {
8487
MetricRenderCSVDuration.WithLabelValues("error").Observe(time.Since(start).Seconds())
8588
span.SetStatus(codes.Error, "csv rendering failed")
@@ -97,8 +100,24 @@ func HandleGetRenderCSV(browser *service.BrowserService) http.Handler {
97100
MetricRenderCSVDuration.WithLabelValues("success").Observe(time.Since(start).Seconds())
98101
span.SetStatus(codes.Ok, "csv rendered successfully")
99102

103+
requestedFilePath := r.URL.Query().Get("filePath")
104+
requestedFilePath = filepath.ToSlash(requestedFilePath)
105+
requestedFilePath = strings.TrimPrefix(requestedFilePath, "/")
106+
requestedFilePath = path.Base(requestedFilePath)
107+
if requestedFilePath == "." || requestedFilePath == "/" || requestedFilePath == "" {
108+
requestedFilePath = fileName
109+
}
110+
if requestedFilePath == "" {
111+
requestedFilePath = "data.csv"
112+
}
113+
if !strings.HasSuffix(strings.ToLower(requestedFilePath), ".csv") {
114+
requestedFilePath += ".csv"
115+
}
116+
100117
w.Header().Set("Content-Type", "text/csv")
101-
w.Header().Set("Content-Disposition", `attachment; filename="data.csv"`)
118+
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{
119+
"filename": requestedFilePath,
120+
}))
102121
_, _ = w.Write(contents)
103122
})
104123
}

pkg/service/browser.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -279,19 +279,19 @@ func (s *BrowserService) Render(ctx context.Context, url string, printer Printer
279279
// You may be thinking: what the hell are we doing? Why are we using a browser for this?
280280
// The CSV endpoint just returns HTML. The actual query is done by the browser, and then a script _in the webpage_ downloads it as a CSV file.
281281
// This SHOULD be replaced at some point, such that the Grafana server does all the work; this is just not acceptable behaviour...
282-
func (s *BrowserService) RenderCSV(ctx context.Context, url, renderKey, domain, acceptLanguage string) ([]byte, error) {
282+
func (s *BrowserService) RenderCSV(ctx context.Context, url, renderKey, domain, acceptLanguage string) ([]byte, string, error) {
283283
tracer := tracer(ctx)
284284
ctx, span := tracer.Start(ctx, "BrowserService.RenderCSV")
285285
defer span.End()
286286
start := time.Now()
287287

288288
if url == "" {
289-
return nil, fmt.Errorf("url must not be empty")
289+
return nil, "", fmt.Errorf("url must not be empty")
290290
}
291291

292292
allocatorOptions, err := s.createAllocatorOptions(s.cfg)
293293
if err != nil {
294-
return nil, fmt.Errorf("failed to create allocator options: %w", err)
294+
return nil, "", fmt.Errorf("failed to create allocator options: %w", err)
295295
}
296296
allocatorCtx, cancelAllocator := chromedp.NewExecAllocator(ctx, allocatorOptions...)
297297
defer cancelAllocator()
@@ -301,7 +301,7 @@ func (s *BrowserService) RenderCSV(ctx context.Context, url, renderKey, domain,
301301

302302
tmpDir, err := os.MkdirTemp("", "gir-csv-*")
303303
if err != nil {
304-
return nil, fmt.Errorf("failed to create temporary directory: %w", err)
304+
return nil, "", fmt.Errorf("failed to create temporary directory: %w", err)
305305
}
306306
defer func() {
307307
if err := os.RemoveAll(tmpDir); err != nil {
@@ -337,15 +337,15 @@ func (s *BrowserService) RenderCSV(ctx context.Context, url, renderKey, domain,
337337
MetricBrowserInstancesActive.Inc()
338338
defer MetricBrowserInstancesActive.Dec()
339339
if err := chromedp.Run(browserCtx, actions...); err != nil {
340-
return nil, fmt.Errorf("failed to run browser: %w", err)
340+
return nil, "", fmt.Errorf("failed to run browser: %w", err)
341341
}
342342
span.AddEvent("actions completed")
343343

344344
// Wait for the file to be downloaded.
345345
var entries []os.DirEntry
346346
for {
347347
if err := ctx.Err(); err != nil {
348-
return nil, err
348+
return nil, "", err
349349
}
350350

351351
entries, err = os.ReadDir(tmpDir)
@@ -355,7 +355,7 @@ func (s *BrowserService) RenderCSV(ctx context.Context, url, renderKey, domain,
355355

356356
select {
357357
case <-ctx.Done():
358-
return nil, ctx.Err()
358+
return nil, "", ctx.Err()
359359
case <-time.After(100 * time.Millisecond):
360360
// try again
361361
}
@@ -364,11 +364,11 @@ func (s *BrowserService) RenderCSV(ctx context.Context, url, renderKey, domain,
364364

365365
fileContents, err := os.ReadFile(filepath.Join(tmpDir, entries[0].Name()))
366366
if err != nil {
367-
return nil, fmt.Errorf("failed to read temporary file: %w", err)
367+
return nil, "", fmt.Errorf("failed to read temporary file: %w", err)
368368
}
369369

370370
MetricBrowserRenderCSVDuration.Observe(time.Since(start).Seconds())
371-
return fileContents, nil
371+
return fileContents, filepath.Base(entries[0].Name()), nil
372372
}
373373

374374
func (s *BrowserService) createAllocatorOptions(cfg config.BrowserConfig) ([]chromedp.ExecAllocatorOption, error) {

tests/acceptance/rendering_grafana_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func TestRenderingGrafana(t *testing.T) {
114114
contentDisposition := resp.Header.Get("Content-Disposition")
115115
_, params, err := mime.ParseMediaType(contentDisposition)
116116
require.NoError(t, err, "could not parse Content-Disposition header")
117-
require.NotEmpty(t, params["filename"], "no filename in Content-Disposition header")
117+
require.Regexp(t, `^Prometheus_ 1-data-.*\.csv$`, params["filename"])
118118

119119
body := ReadBody(t, resp.Body)
120120
const fixture = "render-prometheus-defaults.csv"

0 commit comments

Comments
 (0)