Mateusz Urbanek 7c6d26184c
fix: set content-disposition on S3
S3 does not set proper Content-Disposition, and this header might be
ignored by e.g. browsers if set before redirect. We need to include it
when adding new object to the cache.

Additionally, if the object was previously cached, we need to ensure
that Content-Disposition header will be properly updated.

Signed-off-by: Mateusz Urbanek <mateusz.urbanek@siderolabs.com>
2025-08-12 08:55:45 +02:00

102 lines
2.4 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package http
import (
"context"
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strconv"
"strings"
"github.com/blang/semver/v4"
"github.com/julienschmidt/httprouter"
"go.uber.org/zap"
"github.com/siderolabs/image-factory/internal/asset/cache"
"github.com/siderolabs/image-factory/internal/profile"
)
// handleImage handles downloading of boot assets.
func (f *Frontend) handleImage(ctx context.Context, w http.ResponseWriter, r *http.Request, p httprouter.Params) error {
schematicID := p.ByName("schematic")
schematic, err := f.schematicFactory.Get(ctx, schematicID)
if err != nil {
return err
}
versionTag := p.ByName("version")
if !strings.HasPrefix(versionTag, "v") {
versionTag = "v" + versionTag
}
version, err := semver.Parse(versionTag[1:])
if err != nil {
return fmt.Errorf("error parsing version: %w", err)
}
path := p.ByName("path")
prof, err := profile.ParseFromPath(path, version.String())
if err != nil {
return fmt.Errorf("error parsing profile from path: %w", err)
}
prof, err = profile.EnhanceFromSchematic(ctx, prof, schematic, f.artifactsManager, f.secureBootService, versionTag)
if err != nil {
return fmt.Errorf("error enhancing profile from schematic: %w", err)
}
if err = prof.Validate(); err != nil {
return fmt.Errorf("error validating profile: %w", err)
}
asset, err := f.assetBuilder.Build(ctx, prof, version.String(), path)
if err != nil {
return err
}
if asset, ok := asset.(cache.RedirectableAsset); ok && r.Method != http.MethodHead {
var url string
url, err = asset.Redirect()
if err == nil {
http.Redirect(w, r, url, http.StatusFound)
return nil
}
f.logger.Warn("asset does not support redirection, serving directly", zap.Error(err))
}
w.Header().Set("Content-Length", strconv.FormatInt(asset.Size(), 10))
if ext := filepath.Ext(path); ext != "" {
w.Header().Set("Content-Type", mime.TypeByExtension(ext))
}
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, path))
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return nil
}
reader, err := asset.Reader()
if err != nil {
return err
}
defer reader.Close() //nolint:errcheck
_, err = io.Copy(w, reader)
return err
}