mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-06 21:52:54 +08:00
* feat(mcp-server): support base64 ak/sk * fix(mcp-agent): try to fix route of default-mcp-tools
605 lines
18 KiB
Go
605 lines
18 KiB
Go
// Copyright 2019 Yunion
|
||
//
|
||
// 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 handler
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/csv"
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/360EntSecGroup-Skylar/excelize"
|
||
"golang.org/x/sync/errgroup"
|
||
|
||
"yunion.io/x/jsonutils"
|
||
"yunion.io/x/log"
|
||
"yunion.io/x/pkg/appctx"
|
||
"yunion.io/x/pkg/util/httputils"
|
||
"yunion.io/x/pkg/utils"
|
||
|
||
"yunion.io/x/onecloud/pkg/apigateway/options"
|
||
"yunion.io/x/onecloud/pkg/appsrv"
|
||
"yunion.io/x/onecloud/pkg/httperrors"
|
||
"yunion.io/x/onecloud/pkg/mcclient"
|
||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
|
||
"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
|
||
"yunion.io/x/onecloud/pkg/mcclient/modules/identity"
|
||
)
|
||
|
||
const contentTypeSpreadsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||
|
||
const (
|
||
HOST_MAC = "*MAC地址"
|
||
HOST_NAME = "*名称"
|
||
HOST_IPMI_ADDR = "*IPMI地址"
|
||
HOST_IPMI_USERNAME = "*IPMI用户名"
|
||
HOST_IPMI_PASSWORD = "*IPMI密码"
|
||
HOST_MNG_IP_ADDR = "*管理口IP地址"
|
||
HOST_IPMI_ADDR_OPTIONAL = "IPMI地址"
|
||
HOST_IPMI_USERNAME_OPTIONAL = "IPMI用户名"
|
||
HOST_IPMI_PASSWORD_OPTIONAL = "IPMI密码"
|
||
HOST_MNG_IP_ADDR_OPTIONAL = "管理口IP地址"
|
||
HOST_MNG_MAC_ADDR_OPTIONAL = "管理口MAC地址"
|
||
)
|
||
|
||
const (
|
||
BATCH_USER_REGISTER_QUANTITY_LIMITATION = 1000
|
||
BATCH_HOST_REGISTER_QUANTITY_LIMITATION = 1000
|
||
)
|
||
|
||
var (
|
||
BatchHostRegisterTemplate = []string{HOST_MAC, HOST_NAME, HOST_IPMI_ADDR_OPTIONAL, HOST_IPMI_USERNAME_OPTIONAL, HOST_IPMI_PASSWORD_OPTIONAL}
|
||
BatchHostISORegisterTemplate = []string{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_IP_ADDR}
|
||
BatchHostPXERegisterTemplate = []string{HOST_NAME, HOST_IPMI_ADDR, HOST_IPMI_USERNAME, HOST_IPMI_PASSWORD, HOST_MNG_MAC_ADDR_OPTIONAL, HOST_MNG_IP_ADDR_OPTIONAL}
|
||
)
|
||
|
||
func FetchSession(ctx context.Context, r *http.Request) *mcclient.ClientSession {
|
||
token := AppContextToken(ctx)
|
||
session := auth.GetSession(ctx, token, FetchRegion(r))
|
||
return session
|
||
}
|
||
|
||
type MiscHandler struct {
|
||
prefix string
|
||
}
|
||
|
||
func NewMiscHandler(prefix string) *MiscHandler {
|
||
return &MiscHandler{prefix}
|
||
}
|
||
|
||
func (h *MiscHandler) GetPrefix() string {
|
||
return h.prefix
|
||
}
|
||
|
||
func (h *MiscHandler) Bind(app *appsrv.Application) {
|
||
prefix := h.prefix
|
||
|
||
uploader := UploadHandlerInfo(POST, prefix+"uploads", FetchAuthToken(h.PostUploads))
|
||
app.AddHandler3(uploader)
|
||
app.AddHandler(GET, prefix+"downloads/<template_id>", FetchAuthToken(h.getDownloadsHandler))
|
||
// download vm image by url (no auth token required). token 有效期24小时
|
||
imageDownloadByUrl := uploadHandlerInfo(GET, prefix+"imageutils/image/<image_name>", imageDownloadByUrlHandler)
|
||
app.AddHandler3(imageDownloadByUrl)
|
||
// fetch vm image download url or download large file directly with query parameter <direct=true>
|
||
imageDownloader := uploadHandlerInfo("GET", prefix+"imageutils/download/<image_id>", FetchAuthToken(imageDownloadHandler))
|
||
app.AddHandler3(imageDownloader)
|
||
imageUploader := uploadHandlerInfo("POST", prefix+"imageutils/upload", FetchAuthToken(imageUploadHandler))
|
||
app.AddHandler3(imageUploader)
|
||
s3upload := uploadHandlerInfo(POST, prefix+"s3uploads", FetchAuthToken(h.postS3UploadHandler))
|
||
app.AddHandler3(s3upload)
|
||
|
||
// mcp agent chat stream
|
||
chatStream := chatHandlerInfo("POST", prefix+"mcp_agents/<id>/chat-stream", FetchAuthToken(mcpAgentChatStreamHandler))
|
||
app.AddHandler3(chatStream)
|
||
// mcp agent default chat stream (uses agent with default_agent=true)
|
||
defaultChatStream := chatHandlerInfo("POST", prefix+"mcp_agents/default/chat-stream", FetchAuthToken(mcpAgentDefaultChatStreamHandler))
|
||
app.AddHandler3(defaultChatStream)
|
||
|
||
// syslog webservice handlers
|
||
app.AddHandler(POST, prefix+"syslog/token", handleSyslogWebServiceToken)
|
||
app.AddHandler(POST, prefix+"syslog/message", handleSyslogWebServiceMessage)
|
||
// service settings
|
||
app.AddHandler(GET, prefix+"service_settings", h.getServiceSettings)
|
||
|
||
// mcp servers config
|
||
app.AddHandler(GET, prefix+"mcp-servers-config", mcpServersConfigHandler)
|
||
// mcp agent default MCP server tools (options.MCPServerURL only, no mcp_agent entry)
|
||
app.AddHandler(GET, prefix+"default-mcp-tools", FetchAuthToken(mcpAgentDefaultToolsHandler))
|
||
}
|
||
|
||
func UploadHandlerInfo(method, prefix string, handler func(context.Context, http.ResponseWriter, *http.Request)) *appsrv.SHandlerInfo {
|
||
hi := appsrv.SHandlerInfo{}
|
||
hi.SetMethod(method)
|
||
hi.SetPath(prefix)
|
||
hi.SetHandler(handler)
|
||
hi.SetProcessTimeout(6 * time.Hour)
|
||
hi.SetWorkerManager(GetUploaderWorker())
|
||
return &hi
|
||
}
|
||
|
||
func (mh *MiscHandler) PostUploads(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
// 10 MB
|
||
var maxMemory int64 = 10 << 20
|
||
e := req.ParseMultipartForm(maxMemory)
|
||
if e != nil {
|
||
httperrors.InvalidInputError(ctx, w, "invalid form")
|
||
return
|
||
}
|
||
|
||
params := req.MultipartForm.Value
|
||
|
||
actions, ok := params["action"]
|
||
if !ok || len(actions) == 0 || len(actions[0]) == 0 {
|
||
err := httperrors.NewInputParameterError("Missing parameter %s", "action")
|
||
httperrors.JsonClientError(ctx, w, err)
|
||
return
|
||
}
|
||
|
||
switch actions[0] {
|
||
// 主机批量注册
|
||
case "BatchHostRegister":
|
||
mh.DoBatchHostRegister(ctx, w, req)
|
||
return
|
||
// 用户批量注册
|
||
case "BatchUserRegister":
|
||
mh.DoBatchUserRegister(ctx, w, req)
|
||
return
|
||
default:
|
||
err := httperrors.NewInputParameterError("Unsupported action %s", actions[0])
|
||
httperrors.JsonClientError(ctx, w, err)
|
||
return
|
||
}
|
||
}
|
||
|
||
func (mh *MiscHandler) DoBatchHostRegister(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
files := req.MultipartForm.File
|
||
|
||
hostfiles, ok := files["hosts"]
|
||
if !ok || len(hostfiles) == 0 || hostfiles[0] == nil {
|
||
e := httperrors.NewInputParameterError("Missing parameter %s", "hosts")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
fileHeader := hostfiles[0].Header
|
||
contentType := fileHeader.Get("Content-Type")
|
||
if contentType != contentTypeSpreadsheet {
|
||
e := httperrors.NewInputParameterError("Wrong content type %s, want %s", contentType, contentTypeSpreadsheet)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
file, err := hostfiles[0].Open()
|
||
defer file.Close()
|
||
if err != nil {
|
||
log.Errorf("%s", err.Error())
|
||
e := httperrors.NewInternalServerError("can't open file")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
xlsx, err := excelize.OpenReader(file)
|
||
if err != nil {
|
||
log.Errorf("%s", err.Error())
|
||
e := httperrors.NewInternalServerError("can't parse file")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
rows := xlsx.GetRows("hosts")
|
||
if len(rows) == 0 {
|
||
e := httperrors.NewGeneralError(fmt.Errorf("empty file content"))
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
// check header line
|
||
titlesOk := false
|
||
for _, t := range [][]string{BatchHostRegisterTemplate, BatchHostISORegisterTemplate, BatchHostPXERegisterTemplate} {
|
||
if len(t) == len(rows[0]) {
|
||
for _, title := range rows[0] {
|
||
if !utils.IsInStringArray(title, t) {
|
||
break
|
||
}
|
||
}
|
||
|
||
titlesOk = true
|
||
}
|
||
}
|
||
|
||
if !titlesOk {
|
||
httperrors.InputParameterError(ctx, w, "template file is invalid. please check.")
|
||
return
|
||
}
|
||
|
||
paramKeys := []string{}
|
||
i1 := -1
|
||
i2 := -1
|
||
for i, title := range rows[0] {
|
||
switch title {
|
||
case HOST_MAC, HOST_MNG_MAC_ADDR_OPTIONAL:
|
||
paramKeys = append(paramKeys, "access_mac")
|
||
case HOST_NAME:
|
||
paramKeys = append(paramKeys, "name")
|
||
case HOST_IPMI_ADDR, HOST_IPMI_ADDR_OPTIONAL:
|
||
i1 = i
|
||
paramKeys = append(paramKeys, "ipmi_ip_addr")
|
||
case HOST_IPMI_USERNAME, HOST_IPMI_USERNAME_OPTIONAL:
|
||
paramKeys = append(paramKeys, "ipmi_username")
|
||
case HOST_IPMI_PASSWORD, HOST_IPMI_PASSWORD_OPTIONAL:
|
||
paramKeys = append(paramKeys, "ipmi_password")
|
||
case HOST_MNG_IP_ADDR, HOST_MNG_IP_ADDR_OPTIONAL:
|
||
i2 = i
|
||
paramKeys = append(paramKeys, "access_ip")
|
||
default:
|
||
e := httperrors.NewInternalServerError("empty file content")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
}
|
||
|
||
// skipped header row
|
||
if len(rows) > BATCH_HOST_REGISTER_QUANTITY_LIMITATION {
|
||
e := httperrors.NewInputParameterError("beyond limitation. excel file rows must less than %d", BATCH_HOST_REGISTER_QUANTITY_LIMITATION)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
ips := []string{}
|
||
hosts := bytes.Buffer{}
|
||
for idx, row := range rows[1:] {
|
||
rowStr := strings.Join(row, "")
|
||
if len(rowStr) == 0 {
|
||
log.Warningf("empty row: %d, skipping it", idx+1)
|
||
continue
|
||
}
|
||
var e *httputils.JSONClientError
|
||
if i1 >= 0 && len(row[i1]) > 0 {
|
||
i1Ip := fmt.Sprintf("%d-%s", i1, row[i1])
|
||
if utils.IsInStringArray(i1Ip, ips) {
|
||
e = httperrors.NewDuplicateIdError("ip", row[i1])
|
||
} else {
|
||
ips = append(ips, i1Ip)
|
||
}
|
||
}
|
||
|
||
if i2 >= 0 && len(row[i2]) > 0 {
|
||
i2Ip := fmt.Sprintf("%d-%s", i2, row[i2])
|
||
if utils.IsInStringArray(i2Ip, ips) {
|
||
e = httperrors.NewDuplicateIdError("ip", row[i2])
|
||
} else {
|
||
ips = append(ips, i2Ip)
|
||
}
|
||
}
|
||
|
||
if e != nil {
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
hosts.WriteString(strings.Join(row, ",") + "\n")
|
||
}
|
||
|
||
params := jsonutils.NewDict()
|
||
s := FetchSession(ctx, req)
|
||
params.Set("hosts", jsonutils.NewString(hosts.String()))
|
||
|
||
// extra params
|
||
for k, values := range req.MultipartForm.Value {
|
||
if len(values) > 0 && k != "action" {
|
||
params.Set(k, jsonutils.NewString(values[0]))
|
||
}
|
||
}
|
||
|
||
submitResult, err := compute.Hosts.BatchRegister(s, paramKeys, params)
|
||
if err != nil {
|
||
e := httperrors.NewGeneralError(err)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
w.WriteHeader(207)
|
||
appsrv.SendJSON(w, modulebase.SubmitResults2JSON(submitResult))
|
||
}
|
||
|
||
func (mh *MiscHandler) DoBatchUserRegister(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
adminS := auth.GetAdminSession(ctx, FetchRegion(req))
|
||
s := FetchSession(ctx, req)
|
||
files := req.MultipartForm.File
|
||
|
||
userfiles, ok := files["users"]
|
||
if !ok || len(userfiles) == 0 || userfiles[0] == nil {
|
||
e := httperrors.NewInputParameterError("Missing parameter %s", "users")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
fileHeader := userfiles[0].Header
|
||
contentType := fileHeader.Get("Content-Type")
|
||
if contentType != contentTypeSpreadsheet {
|
||
e := httperrors.NewInputParameterError("Wrong content type %s, want %s", contentType, contentTypeSpreadsheet)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
file, err := userfiles[0].Open()
|
||
defer file.Close()
|
||
if err != nil {
|
||
log.Errorf("%s", err.Error())
|
||
e := httperrors.NewInternalServerError("can't open file")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
xlsx, err := excelize.OpenReader(file)
|
||
if err != nil {
|
||
log.Errorf("%s", err.Error())
|
||
e := httperrors.NewInternalServerError("can't parse file")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
// skipped header row
|
||
rows := xlsx.GetRows("users")
|
||
if len(rows) <= 1 {
|
||
e := httperrors.NewInputParameterError("empty file content")
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
} else if len(rows) > BATCH_USER_REGISTER_QUANTITY_LIMITATION {
|
||
e := httperrors.NewInputParameterError("beyond limitation.excel file rows must less than %d", BATCH_USER_REGISTER_QUANTITY_LIMITATION)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
users := []jsonutils.JSONObject{}
|
||
names := map[string]bool{}
|
||
domains := map[string]string{}
|
||
for i, row := range rows[1:] {
|
||
rowIdx := i + 2
|
||
name := row[0]
|
||
password := row[1]
|
||
displayname := row[2]
|
||
domain := row[3]
|
||
allowWebConsole := strings.ToLower(row[4])
|
||
|
||
// 忽略空白行
|
||
rowStr := strings.Join(row, "")
|
||
if len(strings.TrimSpace(rowStr)) == 0 {
|
||
continue
|
||
}
|
||
|
||
if len(name) == 0 {
|
||
e := httperrors.NewClientError("row %d name is empty", rowIdx)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
if len(displayname) == 0 {
|
||
displayname = name
|
||
}
|
||
|
||
if len(password) == 0 {
|
||
e := httperrors.NewClientError("row %d password is empty", rowIdx)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
domainId, ok := domains[domain]
|
||
if !ok {
|
||
if len(domain) == 0 {
|
||
e := httperrors.NewClientError("row %d domain is empty", rowIdx)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
id, err := identity.Domains.GetId(adminS, domain, nil)
|
||
if err != nil {
|
||
httperrors.JsonClientError(ctx, w, httperrors.NewGeneralError(err))
|
||
return
|
||
}
|
||
|
||
domainId = id
|
||
domains[domain] = id
|
||
}
|
||
|
||
if _, ok := names[name+"/"+domainId]; ok {
|
||
e := httperrors.NewClientError("row %d duplicate name %s", rowIdx, name)
|
||
httperrors.JsonClientError(ctx, w, e)
|
||
return
|
||
} else {
|
||
names[name+"/"+domainId] = true
|
||
params := jsonutils.NewDict()
|
||
params.Set("domain_id", jsonutils.NewString(domainId))
|
||
_, err := identity.UsersV3.Get(s, name, params)
|
||
if err == nil {
|
||
continue
|
||
}
|
||
}
|
||
|
||
user := jsonutils.NewDict()
|
||
user.Add(jsonutils.NewString(name), "name")
|
||
user.Add(jsonutils.NewString(displayname), "displayname")
|
||
user.Add(jsonutils.NewString(domainId), "domain_id")
|
||
if len(password) > 0 {
|
||
user.Add(jsonutils.NewString(password), "password")
|
||
user.Add(jsonutils.JSONTrue, "skip_password_complexity_check")
|
||
}
|
||
|
||
if allowWebConsole == "true" || allowWebConsole == "1" {
|
||
user.Add(jsonutils.JSONTrue, "allow_web_console")
|
||
} else {
|
||
user.Add(jsonutils.JSONFalse, "allow_web_console")
|
||
}
|
||
|
||
users = append(users, user)
|
||
}
|
||
|
||
// batch create
|
||
var userG errgroup.Group
|
||
for i := range users {
|
||
user := users[i]
|
||
userG.Go(func() error {
|
||
_, err := identity.UsersV3.Create(s, user)
|
||
return err
|
||
})
|
||
}
|
||
|
||
if err := userG.Wait(); err != nil {
|
||
e := httperrors.NewGeneralError(err)
|
||
httperrors.GeneralServerError(ctx, w, e)
|
||
return
|
||
}
|
||
|
||
appsrv.SendJSON(w, jsonutils.NewDict())
|
||
}
|
||
|
||
func (mh *MiscHandler) getDownloadsHandler(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
params := appctx.AppContextParams(ctx)
|
||
template, ok := params["<template_id>"]
|
||
if !ok || len(template) == 0 {
|
||
httperrors.MissingParameterError(ctx, w, "template_id")
|
||
return
|
||
}
|
||
|
||
var err error
|
||
var content bytes.Buffer
|
||
switch template {
|
||
case "BatchHostRegister":
|
||
records := [][]string{BatchHostRegisterTemplate}
|
||
content, err = writeXlsx("hosts", records)
|
||
case "BatchHostISORegister":
|
||
records := [][]string{BatchHostISORegisterTemplate}
|
||
content, err = writeXlsx("hosts", records)
|
||
case "BatchHostPXERegister":
|
||
records := [][]string{BatchHostPXERegisterTemplate}
|
||
content, err = writeXlsx("hosts", records)
|
||
case "BatchUserRegister":
|
||
records := [][]string{{"*用户名(user)", "*用户密码(password)", "*显示名(displayname)", "*部门/域(domain)", "*是否登录控制台(allow_web_console:true、false)"}}
|
||
content, err = writeXlsx("users", records)
|
||
case "BatchProjectRegister":
|
||
var titles []string
|
||
if options.Options.NonDefaultDomainProjects {
|
||
titles = []string{"项目名称", "域", "配额"}
|
||
} else {
|
||
titles = []string{"项目名称", "域"}
|
||
}
|
||
|
||
records := [][]string{titles}
|
||
content, err = writeXlsx("projects", records)
|
||
default:
|
||
httperrors.InputParameterError(ctx, w, "template not found %s", template)
|
||
return
|
||
}
|
||
|
||
if err != nil {
|
||
httperrors.InternalServerError(ctx, w, "internal server error")
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||
w.Header().Set("Content-Disposition", "Attachment; filename=template.xlsx")
|
||
w.Write(content.Bytes())
|
||
return
|
||
}
|
||
|
||
func (mh *MiscHandler) postS3UploadHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||
reader, e := r.MultipartReader()
|
||
if e != nil {
|
||
log.Debugf("postS3UploadHandler.MultipartReader %s", e)
|
||
httperrors.InvalidInputError(ctx, w, "invalid form")
|
||
return
|
||
}
|
||
|
||
p, f, e := readImageForm(reader)
|
||
if e != nil {
|
||
log.Debugf("postS3UploadHandler.readImageForm %s", e)
|
||
httperrors.InvalidInputError(ctx, w, "invalid form")
|
||
return
|
||
}
|
||
|
||
bucket_id, ok := p["bucket_id"]
|
||
if !ok {
|
||
httperrors.MissingParameterError(ctx, w, "bucket_id")
|
||
return
|
||
}
|
||
|
||
key, ok := p["key"]
|
||
if !ok {
|
||
httperrors.MissingParameterError(ctx, w, "key")
|
||
return
|
||
}
|
||
|
||
_content_length, ok := p["content_length"]
|
||
if !ok {
|
||
httperrors.MissingParameterError(ctx, w, "content_length")
|
||
return
|
||
}
|
||
|
||
content_length, e := strconv.ParseInt(_content_length, 10, 64)
|
||
if e != nil {
|
||
httperrors.InvalidInputError(ctx, w, "invalid content_length %s", _content_length)
|
||
return
|
||
}
|
||
|
||
storage_class, _ := p["storage_class"]
|
||
acl, _ := p["acl"]
|
||
|
||
token := AppContextToken(ctx)
|
||
s := auth.GetSession(ctx, token, FetchRegion(r))
|
||
|
||
meta := http.Header{}
|
||
meta.Set("Content-Type", "application/octet-stream")
|
||
e = compute.Buckets.Upload(s, bucket_id, key, f, content_length, storage_class, acl, meta)
|
||
if e != nil {
|
||
httperrors.GeneralServerError(ctx, w, e)
|
||
return
|
||
}
|
||
appsrv.SendJSON(w, jsonutils.NewDict())
|
||
}
|
||
|
||
func writeCsv(records [][]string) (bytes.Buffer, error) {
|
||
var content bytes.Buffer
|
||
content.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM, 防止office打开后中文乱码
|
||
writer := csv.NewWriter(&content)
|
||
writer.WriteAll(records)
|
||
if err := writer.Error(); err != nil {
|
||
log.Errorf("error writing csv:%s", err.Error())
|
||
return content, err
|
||
}
|
||
|
||
return content, nil
|
||
}
|
||
|
||
func writeXlsx(sheetName string, records [][]string) (bytes.Buffer, error) {
|
||
var content bytes.Buffer
|
||
xlsx := excelize.NewFile()
|
||
xlsx.SetSheetName("Sheet1", sheetName)
|
||
index := xlsx.GetSheetIndex(sheetName)
|
||
for i, record := range records {
|
||
xlsx.SetSheetRow(sheetName, fmt.Sprintf("A%d", i+1), &record)
|
||
}
|
||
xlsx.SetActiveSheet(index)
|
||
if err := xlsx.Write(&content); err != nil {
|
||
log.Errorf("error writing xlsx:%s", err.Error())
|
||
return content, err
|
||
}
|
||
return content, nil
|
||
}
|