Files
wehub-resource-sync e0e362d700
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

204 lines
5.3 KiB
Go

// Copyright 2025 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controller
import (
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/alibaba/opensandbox/execd/pkg/util/pathutil"
"github.com/alibaba/opensandbox/execd/pkg/web/model"
)
// DownloadFile serves a file for download with support for range requests
// and line-based reading via offset/limit query parameters.
func (c *FilesystemController) DownloadFile() {
rec := beginFilesystemMetric("download")
defer rec.Finish(c.basicController)
filePath := c.ctx.Query("path")
if filePath == "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeMissingQuery,
"missing query parameter 'path'",
)
return
}
resolvedFilePath, err := pathutil.ExpandPath(filePath)
if err != nil {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error resolving file path: %s. %v", filePath, err),
)
return
}
rawOffset := c.ctx.Query("offset")
rawLimit := c.ctx.Query("limit")
hasLineParams := rawOffset != "" || rawLimit != ""
rangeHeader := c.ctx.GetHeader("Range")
if hasLineParams && rangeHeader != "" {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
"line-based reading (offset/limit) and byte range (Range header) are mutually exclusive",
)
return
}
file, err := os.Open(resolvedFilePath)
if err != nil {
c.handleFileError(err)
return
}
defer file.Close()
if hasLineParams {
c.serveLineRange(file, rawOffset, rawLimit)
rec.MarkSuccess()
return
}
fileInfo, err := file.Stat()
if err != nil {
c.RespondError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error getting file stat info: %s. %v", resolvedFilePath, err),
)
return
}
c.ctx.Header("Content-Type", "application/octet-stream")
c.ctx.Header("Content-Disposition", formatContentDisposition(filepath.Base(resolvedFilePath)))
c.ctx.Header("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
if rangeHeader != "" {
ranges, err := ParseRange(rangeHeader, fileInfo.Size())
if err != nil {
c.RespondError(
http.StatusRequestedRangeNotSatisfiable,
model.ErrorCodeUnknown,
)
return
}
if len(ranges) > 0 {
r := ranges[0]
c.ctx.Status(http.StatusPartialContent)
c.ctx.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, fileInfo.Size()))
c.ctx.Header("Content-Length", strconv.FormatInt(r.length, 10))
_, _ = file.Seek(r.start, io.SeekStart)
_, _ = io.CopyN(c.ctx.Writer, file, r.length)
rec.MarkSuccess()
return
}
}
rec.MarkSuccess()
http.ServeContent(c.ctx.Writer, c.ctx.Request, filepath.Base(resolvedFilePath), fileInfo.ModTime(), file)
}
// serveLineRange reads lines from file starting at a 1-based offset and
// returns up to limit lines as text/plain.
func (c *FilesystemController) serveLineRange(file *os.File, rawOffset, rawLimit string) {
offset := int64(1)
if rawOffset != "" {
parsed, err := strconv.ParseInt(rawOffset, 10, 64)
if err != nil || parsed < 1 {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
fmt.Sprintf("invalid query parameter 'offset': %s", rawOffset),
)
return
}
offset = parsed
}
limit := int64(-1)
if rawLimit != "" {
parsed, err := strconv.ParseInt(rawLimit, 10, 64)
if err != nil || parsed < 1 {
c.RespondError(
http.StatusBadRequest,
model.ErrorCodeInvalidRequest,
fmt.Sprintf("invalid query parameter 'limit': %s", rawLimit),
)
return
}
limit = parsed
}
c.ctx.Header("Content-Type", "text/plain; charset=utf-8")
c.ctx.Status(http.StatusOK)
reader := bufio.NewReader(file)
var lineNum int64
var written int64
for {
line, err := reader.ReadBytes('\n')
if len(line) > 0 {
line = bytes.TrimRight(line, "\r\n")
lineNum++
if lineNum >= offset {
if written > 0 {
_, _ = c.ctx.Writer.Write([]byte("\n"))
}
_, _ = c.ctx.Writer.Write(line)
written++
if limit >= 0 && written >= limit {
break
}
}
}
if err != nil {
break
}
}
}
// formatContentDisposition formats the Content-Disposition header value with proper
// encoding for non-ASCII filenames according to RFC 6266 and RFC 5987.
func formatContentDisposition(filename string) string {
// Check if filename contains non-ASCII characters
needsEncoding := false
for _, r := range filename {
if r > 127 {
needsEncoding = true
break
}
}
if !needsEncoding {
return "attachment; filename=\"" + filename + "\""
}
// Use RFC 5987 encoding for non-ASCII filenames
// Format: attachment; filename="fallback"; filename*=UTF-8''encoded_name
encodedFilename := url.PathEscape(filename)
return "attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename
}