316 lines
8.6 KiB
Go
316 lines
8.6 KiB
Go
package cast
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"fonchain-fiee/pkg/utils"
|
||
"io"
|
||
"mime/multipart"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
const (
|
||
// Ayrshare API 配置
|
||
ayrshareAPIBaseURL = "https://api.ayrshare.com/api"
|
||
maxSmallFileSize = 30 * 1024 * 1024 // 30MB
|
||
apiKey = "208CBE9F-8E4F426A-990A0184-2C6287B9"
|
||
)
|
||
|
||
// UploadMediaResponse 上传媒体文件响应
|
||
type UploadMediaResponse struct {
|
||
ID string `json:"id"`
|
||
URL string `json:"url"`
|
||
FileName string `json:"fileName"`
|
||
Description string `json:"description"`
|
||
IsAs bool `json:"isAs"`
|
||
}
|
||
|
||
// UploadURLResponse 获取上传URL响应
|
||
type UploadURLResponse struct {
|
||
AccessURL string `json:"accessUrl"`
|
||
ContentType string `json:"contentType"`
|
||
UploadURL string `json:"uploadUrl"`
|
||
}
|
||
|
||
// VerifyURLResponse 验证URL响应
|
||
type VerifyURLResponse struct {
|
||
Status string `json:"status"`
|
||
StatusCode int `json:"statusCode"`
|
||
ContentType string `json:"contentType"`
|
||
}
|
||
|
||
// UploadMediaByURL 根据文件大小自动选择上传方式
|
||
// fileURL: 文件的URL链接
|
||
// apiKey: Ayrshare API Key
|
||
// fileName: 文件名(可选)
|
||
// description: 文件描述(可选)
|
||
func UploadMediaByURL(ctx context.Context, fileURL, fileName, description string) (*UploadMediaResponse, error) {
|
||
if fileURL == "" {
|
||
return nil, errors.New("文件URL不能为空")
|
||
}
|
||
ok, err := VerifyMediaURL(ctx, fileURL)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if ok {
|
||
return &UploadMediaResponse{
|
||
ID: "",
|
||
URL: fileURL,
|
||
FileName: "",
|
||
Description: "",
|
||
IsAs: false,
|
||
}, nil
|
||
}
|
||
// 下载文件到临时目录
|
||
tempFile, fileSize, err := downloadFile(ctx, fileURL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("下载文件失败: %v", err)
|
||
}
|
||
defer os.Remove(tempFile) // 清理临时文件
|
||
|
||
zap.L().Info("文件下载完成", zap.String("tempFile", tempFile), zap.Int64("fileSize", fileSize))
|
||
|
||
// 如果没有提供文件名,从URL中提取
|
||
if fileName == "" {
|
||
fileName = filepath.Base(fileURL)
|
||
}
|
||
|
||
// 根据文件大小选择上传方式
|
||
if fileSize < maxSmallFileSize {
|
||
zap.L().Info("使用小文件上传方式", zap.Int64("fileSize", fileSize))
|
||
return uploadSmallMedia(ctx, tempFile, apiKey, fileName, description)
|
||
}
|
||
zap.L().Info("使用大文件上传方式", zap.Int64("fileSize", fileSize))
|
||
return uploadLargeMedia(ctx, tempFile, apiKey, fileName, description)
|
||
}
|
||
|
||
// downloadFile 下载文件到临时目录
|
||
func downloadFile(ctx context.Context, fileURL string) (string, int64, error) {
|
||
req, err := http.NewRequestWithContext(ctx, "GET", fileURL, nil)
|
||
if err != nil {
|
||
return "", 0, err
|
||
}
|
||
|
||
client := &http.Client{Timeout: 10 * time.Minute}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return "", 0, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return "", 0, fmt.Errorf("下载文件失败,HTTP状态码: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 创建临时文件
|
||
tempFile, err := os.CreateTemp("", "ayrshare_upload_*")
|
||
if err != nil {
|
||
return "", 0, err
|
||
}
|
||
defer tempFile.Close()
|
||
|
||
// 复制内容到临时文件
|
||
fileSize, err := io.Copy(tempFile, resp.Body)
|
||
if err != nil {
|
||
os.Remove(tempFile.Name())
|
||
return "", 0, err
|
||
}
|
||
|
||
return tempFile.Name(), fileSize, nil
|
||
}
|
||
|
||
// uploadSmallMedia 上传小于30MB的文件(使用multipart form-data)
|
||
func uploadSmallMedia(ctx context.Context, filePath, apiKey, fileName, description string) (*UploadMediaResponse, error) {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开文件失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
// 创建multipart form-data
|
||
body := &bytes.Buffer{}
|
||
writer := multipart.NewWriter(body)
|
||
|
||
// 添加文件
|
||
part, err := writer.CreateFormFile("file", fileName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if _, err := io.Copy(part, file); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 添加其他字段
|
||
if fileName != "" {
|
||
_ = writer.WriteField("fileName", fileName)
|
||
}
|
||
if description != "" {
|
||
_ = writer.WriteField("description", description)
|
||
}
|
||
|
||
if err := writer.Close(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 发送请求
|
||
url := fmt.Sprintf("%s/media/upload", ayrshareAPIBaseURL)
|
||
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||
|
||
client := &http.Client{Timeout: 10 * time.Minute}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||
}
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
zap.L().Error("上传小文件失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
|
||
return nil, fmt.Errorf("上传失败: HTTP %d, %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
var result UploadMediaResponse
|
||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||
}
|
||
|
||
zap.L().Info("小文件上传成功", zap.Any("response", result))
|
||
return &result, nil
|
||
}
|
||
|
||
// uploadLargeMedia 上传大于30MB的文件(使用presigned URL)
|
||
func uploadLargeMedia(ctx context.Context, filePath, apiKey, fileName, description string) (*UploadMediaResponse, error) {
|
||
// 获取文件的content type
|
||
ext := filepath.Ext(fileName)
|
||
if len(ext) > 0 {
|
||
ext = ext[1:] // 去掉点号
|
||
}
|
||
|
||
// Step 1: 获取上传URL
|
||
uploadURLResp, err := getUploadURL(ctx, apiKey, fileName, ext)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取上传URL失败: %v", err)
|
||
}
|
||
|
||
zap.L().Info("获取上传URL成功", zap.Any("uploadURLResp", uploadURLResp))
|
||
|
||
// Step 2: 上传文件到presigned URL
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开文件失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
req, err := http.NewRequestWithContext(ctx, "PUT", uploadURLResp.UploadURL, file)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", uploadURLResp.ContentType)
|
||
|
||
client := &http.Client{Timeout: 30 * time.Minute}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("上传文件失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
respBody, _ := io.ReadAll(resp.Body)
|
||
zap.L().Error("上传大文件失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
|
||
return nil, fmt.Errorf("上传失败: HTTP %d", resp.StatusCode)
|
||
}
|
||
|
||
zap.L().Info("大文件上传成功")
|
||
|
||
// 返回结果
|
||
return &UploadMediaResponse{
|
||
URL: uploadURLResp.AccessURL,
|
||
FileName: fileName,
|
||
Description: description,
|
||
}, nil
|
||
}
|
||
|
||
// getUploadURL 获取大文件上传的presigned URL
|
||
func getUploadURL(ctx context.Context, apiKey, fileName, contentType string) (*UploadURLResponse, error) {
|
||
url := fmt.Sprintf("%s/media/uploadUrl?fileName=%s", ayrshareAPIBaseURL, fileName)
|
||
if contentType != "" {
|
||
url = fmt.Sprintf("%s&contentType=%s", url, contentType)
|
||
}
|
||
|
||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||
|
||
client := &http.Client{Timeout: 30 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
zap.L().Error("获取上传URL失败", zap.Int("statusCode", resp.StatusCode), zap.String("response", string(respBody)))
|
||
return nil, fmt.Errorf("获取上传URL失败: HTTP %d, %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
var result UploadURLResponse
|
||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
// VerifyMediaURL 验证媒体URL是否有效
|
||
func VerifyMediaURL(ctx context.Context, mediaURL string) (bool, error) {
|
||
url := fmt.Sprintf("%s/media/urlExists", ayrshareAPIBaseURL)
|
||
requestBody := map[string]string{
|
||
"mediaUrl": mediaURL,
|
||
}
|
||
jsonData, _ := json.Marshal(requestBody)
|
||
code, postBody, err := utils.PostBytesHeader(url, map[string]interface{}{
|
||
"Content-Type": "application/json",
|
||
"Authorization": "Bearer " + apiKey,
|
||
}, jsonData)
|
||
if err != nil {
|
||
zap.L().Error("VerifyMediaURL 提交异常", zap.Error(err))
|
||
return false, errors.New("验证链接提交异常")
|
||
}
|
||
if code != http.StatusOK {
|
||
return false, nil
|
||
}
|
||
var result *VerifyURLResponse
|
||
_ = json.Unmarshal(postBody, &result)
|
||
if result.StatusCode != http.StatusOK || result.Status != "success" {
|
||
return false, nil
|
||
}
|
||
return true, nil
|
||
}
|