Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
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
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

235 lines
7.0 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 (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
"github.com/alibaba/opensandbox/execd/pkg/log"
"github.com/alibaba/opensandbox/execd/pkg/util/pathutil"
"github.com/alibaba/opensandbox/execd/pkg/web/model"
)
type uploadError struct {
status int
code model.ErrorCode
message string
}
func newUploadError(status int, code model.ErrorCode, message string) *uploadError {
return &uploadError{status: status, code: code, message: message}
}
// UploadFile uploads files with metadata to specified paths
func (c *FilesystemController) UploadFile() {
rec := beginFilesystemMetric("upload")
defer rec.Finish(c.basicController)
metadataParts, fileParts, uerr := c.parseUploadForm()
if uerr != nil {
c.RespondError(uerr.status, uerr.code, uerr.message)
return
}
for i := range metadataParts {
if uerr := c.processUploadPair(metadataParts[i], fileParts[i]); uerr != nil {
c.RespondError(uerr.status, uerr.code, uerr.message)
return
}
}
rec.MarkSuccess()
c.RespondSuccess(nil)
}
func (c *FilesystemController) parseUploadForm() ([]*multipart.FileHeader, []*multipart.FileHeader, *uploadError) {
return parseUploadForm(c.ctx)
}
func parseUploadForm(ctx *gin.Context) ([]*multipart.FileHeader, []*multipart.FileHeader, *uploadError) {
form, err := ctx.MultipartForm()
if err != nil || form == nil {
return nil, nil, newUploadError(http.StatusBadRequest, model.ErrorCodeInvalidFile, "multipart form is empty")
}
metadataParts := form.File["metadata"]
fileParts := form.File["file"]
if len(metadataParts) == 0 {
return nil, nil, newUploadError(http.StatusBadRequest, model.ErrorCodeInvalidFileMetadata, "metadata file is missing")
}
if len(fileParts) == 0 {
return nil, nil, newUploadError(http.StatusBadRequest, model.ErrorCodeInvalidFileContent, "file is missing")
}
if len(metadataParts) != len(fileParts) {
return nil, nil, newUploadError(
http.StatusBadRequest,
model.ErrorCodeInvalidFile,
fmt.Sprintf("metadata and file count mismatch: %d vs %d", len(metadataParts), len(fileParts)),
)
}
return metadataParts, fileParts, nil
}
func (c *FilesystemController) processUploadPair(metadataHeader, fileHeader *multipart.FileHeader) *uploadError {
meta, uerr := parseUploadMetadata(metadataHeader)
if uerr != nil {
return uerr
}
resolvedPath, uerr := resolveUploadTarget(meta.Path, meta.Permission)
if uerr != nil {
return uerr
}
if uerr := writeUploadFile(resolvedPath, fileHeader); uerr != nil {
return uerr
}
return applyUploadPermission(resolvedPath, meta.Permission)
}
func parseUploadMetadata(header *multipart.FileHeader) (*model.FileMetadata, *uploadError) {
metadataFile, err := header.Open()
if err != nil {
return nil, newUploadError(
http.StatusBadRequest,
model.ErrorCodeInvalidFileMetadata,
fmt.Sprintf("error opening metadata file. %v", err),
)
}
metaBytes, err := io.ReadAll(metadataFile)
metadataFile.Close()
if err != nil {
return nil, newUploadError(
http.StatusBadRequest,
model.ErrorCodeInvalidFileMetadata,
fmt.Sprintf("error reading metadata content. %v", err),
)
}
var meta model.FileMetadata
if err := json.Unmarshal(metaBytes, &meta); err != nil {
return nil, newUploadError(
http.StatusBadRequest,
model.ErrorCodeInvalidFileMetadata,
fmt.Sprintf("invalid metadata format. %v", err),
)
}
if meta.Path == "" {
return nil, newUploadError(http.StatusBadRequest, model.ErrorCodeInvalidFileMetadata, "metadata path is empty")
}
return &meta, nil
}
func resolveUploadTarget(targetPath string, perm model.Permission) (string, *uploadError) {
resolvedPath, err := pathutil.ExpandPath(targetPath)
if err != nil {
return "", newUploadError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error resolving target path %s. %v", targetPath, err),
)
}
targetDir := filepath.Dir(resolvedPath)
if err := MkdirAllWithOwnership(targetDir, os.ModePerm, perm.Owner, perm.Group); err != nil {
return "", newUploadError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error creating target directory %s. %v", targetDir, err),
)
}
return resolvedPath, nil
}
func writeUploadFile(resolvedPath string, fileHeader *multipart.FileHeader) *uploadError {
file, err := fileHeader.Open()
if err != nil {
return newUploadError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error opening file %s. %v", fileHeader.Filename, err),
)
}
defer file.Close()
dst, err := os.OpenFile(resolvedPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
return newUploadError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error opening destination file %s. %v", resolvedPath, err),
)
}
if _, err := io.Copy(dst, file); err != nil {
dst.Close()
return newUploadError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error copying file %s. %v", resolvedPath, err),
)
}
if err := dst.Sync(); err != nil {
log.Error("failed to sync target file: %v", err)
}
if err := dst.Close(); err != nil {
log.Error("failed to close target file: %v", err)
}
// fsync parent directory so the new dirent is durable and visible on
// weakly-coherent filesystems (virtio-fs, 9pfs, etc.). Best-effort:
// some filesystems return ENOTSUP for directory fsync.
targetDir := filepath.Dir(resolvedPath)
if d, err := os.Open(targetDir); err == nil {
if err := d.Sync(); err != nil {
log.Warn("failed to sync parent dir %s: %v", targetDir, err)
}
_ = d.Close()
}
return nil
}
// applyUploadPermission applies the metadata permission with one retry to
// absorb metadata-propagation delay on weakly-coherent filesystems
// (virtio-fs, 9pfs). ChmodFile always invokes chown under the hood, so a
// freshly-created dirent that has not yet propagated will surface as ENOENT
// here even though the file is fully written and synced.
func applyUploadPermission(resolvedPath string, permission model.Permission) *uploadError {
chmodErr := ChmodFile(resolvedPath, permission)
if chmodErr != nil {
time.Sleep(20 * time.Millisecond)
chmodErr = ChmodFile(resolvedPath, permission)
}
if chmodErr != nil {
return newUploadError(
http.StatusInternalServerError,
model.ErrorCodeRuntimeError,
fmt.Sprintf("error chmoding file %s. %v", resolvedPath, chmodErr),
)
}
return nil
}