Skip to content

Commit 74afd80

Browse files
committed
fix: set correct Content-Type when downloading images
Fixes #414 Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
1 parent 8372fe8 commit 74afd80

5 files changed

Lines changed: 88 additions & 7 deletions

File tree

internal/asset/cache/s3/s3.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"go.uber.org/zap"
2020

2121
"github.com/siderolabs/image-factory/internal/asset/cache"
22+
"github.com/siderolabs/image-factory/internal/mime"
2223
)
2324

2425
const (
@@ -173,6 +174,7 @@ func (c *Cache) Put(ctx context.Context, profileID string, asset cache.BootAsset
173174
}
174175

175176
stat, err := c.s3cli.PutObject(ctx, c.bucketName, key, data, asset.Size(), minio.PutObjectOptions{
177+
ContentType: mime.ContentType(filename),
176178
ContentDisposition: fmt.Sprintf(`attachment; filename="%s"`, filename),
177179
})
178180
if err != nil {
@@ -212,6 +214,7 @@ func (c *Cache) Put(ctx context.Context, profileID string, asset cache.BootAsset
212214
func hasRequiredMetadata(metadata http.Header) bool {
213215
for _, key := range []string{
214216
"Content-Disposition",
217+
"Content-Type",
215218
} {
216219
if metadata.Get(key) == "" {
217220
return false

internal/frontend/http/image.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ import (
88
"context"
99
"fmt"
1010
"io"
11-
"mime"
1211
"net/http"
13-
"path/filepath"
1412
"strconv"
1513
"strings"
1614

@@ -19,6 +17,7 @@ import (
1917
"go.uber.org/zap"
2018

2119
"github.com/siderolabs/image-factory/internal/asset/cache"
20+
"github.com/siderolabs/image-factory/internal/mime"
2221
"github.com/siderolabs/image-factory/internal/profile"
2322
)
2423

@@ -83,11 +82,7 @@ func (f *Frontend) handleImage(ctx context.Context, w http.ResponseWriter, r *ht
8382
}
8483

8584
w.Header().Set("Content-Length", strconv.FormatInt(asset.Size(), 10))
86-
87-
if ext := filepath.Ext(path); ext != "" {
88-
w.Header().Set("Content-Type", mime.TypeByExtension(ext))
89-
}
90-
85+
w.Header().Set("Content-Type", mime.ContentType(path))
9186
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
9287
w.WriteHeader(http.StatusOK)
9388

internal/integration/download_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/siderolabs/gen/xtesting/must"
2525
"github.com/siderolabs/go-blockdevice/v2/blkid"
2626
"github.com/siderolabs/go-pointer"
27+
"github.com/siderolabs/image-factory/internal/mime"
2728
"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
2829
"github.com/stretchr/testify/assert"
2930
"github.com/stretchr/testify/require"
@@ -157,6 +158,7 @@ func downloadAssetAndMatchSize(ctx context.Context, t *testing.T, baseURL string
157158

158159
require.Equal(t, http.StatusOK, resp.StatusCode)
159160
require.Contains(t, resp.Header, "Content-Disposition")
161+
assert.Equal(t, mime.ContentType(path), resp.Header.Get("Content-Type"))
160162

161163
size := matchSizeAndType(t, body, fileType, expectedSize)
162164

internal/mime/mime.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
// Package mime provides MIME type detection based on file extensions.
6+
package mime
7+
8+
import (
9+
"mime"
10+
"path/filepath"
11+
)
12+
13+
// ContentType returns the MIME type for a given filename based on its extension.
14+
//
15+
// If the extension is unknown, it returns "application/octet-stream".
16+
func ContentType(filename string) string {
17+
ext := filepath.Ext(filename)
18+
19+
var mimeType string
20+
21+
switch ext {
22+
case ".efi":
23+
// see https://www.iana.org/assignments/media-types/media-types.xhtml
24+
mimeType = "application/efi"
25+
case ".iso":
26+
mimeType = "application/x-iso9660-image"
27+
case ".xz":
28+
mimeType = "application/x-xz"
29+
case ".gz":
30+
mimeType = "application/gzip"
31+
case ".qcow2":
32+
mimeType = "application/x-qemu-disk"
33+
default:
34+
// no match
35+
if ext != "" {
36+
mimeType = mime.TypeByExtension(ext)
37+
}
38+
39+
if mimeType == "" {
40+
mimeType = "application/octet-stream"
41+
}
42+
}
43+
44+
return mimeType
45+
}

internal/mime/mime_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package mime_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
12+
"github.com/siderolabs/image-factory/internal/mime"
13+
)
14+
15+
func TestContentType(t *testing.T) {
16+
t.Parallel()
17+
18+
for _, test := range []struct {
19+
filename string
20+
expected string
21+
}{
22+
{"file.efi", "application/efi"},
23+
{"file.unknown", "application/octet-stream"},
24+
{"file.txt", "text/plain; charset=utf-8"},
25+
{"file.iso", "application/x-iso9660-image"},
26+
{"file.tar.gz", "application/gzip"},
27+
{"file.qcow2", "application/x-qemu-disk"},
28+
{"file.xz", "application/x-xz"},
29+
} {
30+
t.Run(test.filename, func(t *testing.T) {
31+
t.Parallel()
32+
33+
assert.Equal(t, test.expected, mime.ContentType(test.filename))
34+
})
35+
}
36+
}

0 commit comments

Comments
 (0)