8f4f8f3a14
Upload a file as an asset to an existing release. Completes the publishing workflow for extension .zip packages. Uses multipart form POST to Gitea's release assets endpoint. Safety: 100MB cap, requires absolute file_path, supports custom asset name (defaults to filename basename). Closes the gap in the release workflow: 1. create_release (existing) - creates the release 2. upload_release_attachment (NEW) - uploads the .zip/binary asset
192 lines
5.2 KiB
Go
192 lines
5.2 KiB
Go
// release_asset.go - upload_release_attachment
|
|
//
|
|
// מטרה: השלמת workflow הפצה - העלאת קובץ ZIP של extension ל-release ב-Gitea.
|
|
|
|
package repo
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
|
"gitea.com/gitea/gitea-mcp/pkg/log"
|
|
"gitea.com/gitea/gitea-mcp/pkg/params"
|
|
"gitea.com/gitea/gitea-mcp/pkg/to"
|
|
|
|
mcpContext "gitea.com/gitea/gitea-mcp/pkg/context"
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
const (
|
|
UploadReleaseAttachmentToolName = "upload_release_attachment"
|
|
|
|
// MaxAttachmentSizeBytes — תקרה לגודל attachment (100MB).
|
|
MaxAttachmentSizeBytes = 100 * 1024 * 1024
|
|
)
|
|
|
|
var UploadReleaseAttachmentTool = mcp.NewTool(
|
|
UploadReleaseAttachmentToolName,
|
|
mcp.WithDescription(
|
|
"Upload a file as an asset to an existing release. "+
|
|
"Essential for publishing extension .zip packages. "+
|
|
"The file is read from the local filesystem and uploaded via multipart form.",
|
|
),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
|
|
mcp.WithNumber("release_id", mcp.Required(), mcp.Description("release ID")),
|
|
mcp.WithString("file_path", mcp.Required(),
|
|
mcp.Description("absolute path to the local file to upload")),
|
|
mcp.WithString("name",
|
|
mcp.Description("filename in the release (defaults to basename of file_path)")),
|
|
)
|
|
|
|
func init() {
|
|
Tool.RegisterWrite(server.ServerTool{
|
|
Tool: UploadReleaseAttachmentTool,
|
|
Handler: UploadReleaseAttachmentFn,
|
|
})
|
|
}
|
|
|
|
// ReleaseAsset — תוצאה מ-Gitea API.
|
|
type ReleaseAsset struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Size int64 `json:"size"`
|
|
DownloadCount int64 `json:"download_count"`
|
|
BrowserDownloadURL string `json:"browser_download_url"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
func UploadReleaseAttachmentFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called UploadReleaseAttachmentFn")
|
|
args := req.GetArguments()
|
|
|
|
owner, err := params.GetString(args, "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
repo, err := params.GetString(args, "repo")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
releaseID, err := params.GetIndex(args, "release_id")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
filePath, err := params.GetString(args, "file_path")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
name, _ := args["name"].(string)
|
|
|
|
// אבטחה: דרוש absolute path
|
|
if !filepath.IsAbs(filePath) {
|
|
return to.ErrorResult(fmt.Errorf("file_path must be absolute"))
|
|
}
|
|
|
|
fileInfo, err := os.Stat(filePath)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("stat file: %w", err))
|
|
}
|
|
if fileInfo.Size() > MaxAttachmentSizeBytes {
|
|
return to.ErrorResult(fmt.Errorf(
|
|
"file too large (%d bytes > max %d)",
|
|
fileInfo.Size(), MaxAttachmentSizeBytes,
|
|
))
|
|
}
|
|
if name == "" {
|
|
name = filepath.Base(filePath)
|
|
}
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("open file: %w", err))
|
|
}
|
|
defer file.Close()
|
|
|
|
// בניית multipart form
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
part, err := writer.CreateFormFile("attachment", name)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("create form file: %w", err))
|
|
}
|
|
if _, err := io.Copy(part, file); err != nil {
|
|
return to.ErrorResult(fmt.Errorf("copy file content: %w", err))
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return to.ErrorResult(fmt.Errorf("close writer: %w", err))
|
|
}
|
|
|
|
host := flag.Host
|
|
if host == "" {
|
|
return to.ErrorResult(fmt.Errorf("gitea host not configured"))
|
|
}
|
|
apiURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/%d/assets",
|
|
host,
|
|
url.PathEscape(owner),
|
|
url.PathEscape(repo),
|
|
releaseID,
|
|
)
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, &body)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("create request: %w", err))
|
|
}
|
|
|
|
// אימות - token מ-context או flag
|
|
token := flag.Token
|
|
if t, ok := ctx.Value(mcpContext.TokenContextKey).(string); ok && t != "" {
|
|
token = t
|
|
}
|
|
if token != "" {
|
|
httpReq.Header.Set("Authorization", "token "+token)
|
|
}
|
|
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
|
|
httpReq.Header.Set("Accept", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(httpReq)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("upload request: %w", err))
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return to.ErrorResult(fmt.Errorf(
|
|
"upload failed (status %d): %s",
|
|
resp.StatusCode, string(respBody),
|
|
))
|
|
}
|
|
|
|
var asset ReleaseAsset
|
|
if err := json.Unmarshal(respBody, &asset); err != nil {
|
|
return to.ErrorResult(fmt.Errorf("parse response: %w", err))
|
|
}
|
|
|
|
msg := fmt.Sprintf(
|
|
"Asset uploaded successfully:\n"+
|
|
" Asset ID: %d\n"+
|
|
" Name: %s\n"+
|
|
" Size: %s\n"+
|
|
" Download URL: %s\n"+
|
|
" Created at: %s\n",
|
|
asset.ID,
|
|
asset.Name,
|
|
formatBytes(asset.Size),
|
|
asset.BrowserDownloadURL,
|
|
asset.CreatedAt,
|
|
)
|
|
return to.TextResult(msg)
|
|
}
|