|
package types |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
|
|
"monica-proxy/internal/config" |
|
"monica-proxy/internal/utils" |
|
"net/http" |
|
"strings" |
|
"sync" |
|
"time" |
|
|
|
"github.com/cespare/xxhash/v2" |
|
"github.com/google/uuid" |
|
) |
|
|
|
const MaxFileSize = 10 * 1024 * 1024 |
|
|
|
var imageCache sync.Map |
|
|
|
|
|
func sampleAndHash(data string) string { |
|
|
|
if len(data) <= 1024 { |
|
return fmt.Sprintf("%x", xxhash.Sum64String(data)) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
var samples []string |
|
samples = append(samples, data[:256]) |
|
mid := len(data) / 2 |
|
samples = append(samples, data[mid-128:mid+128]) |
|
samples = append(samples, data[len(data)-256:]) |
|
|
|
|
|
return fmt.Sprintf("%x", xxhash.Sum64String(strings.Join(samples, ""))) |
|
} |
|
|
|
|
|
func UploadBase64Image(ctx context.Context, base64Data string) (*FileInfo, error) { |
|
|
|
cacheKey := sampleAndHash(base64Data) |
|
|
|
|
|
if value, exists := imageCache.Load(cacheKey); exists { |
|
return value.(*FileInfo), nil |
|
} |
|
|
|
|
|
|
|
parts := strings.Split(base64Data, ",") |
|
if len(parts) != 2 { |
|
return nil, fmt.Errorf("invalid base64 image format") |
|
} |
|
|
|
|
|
mimeType := strings.TrimSuffix(strings.TrimPrefix(parts[0], "data:"), ";base64") |
|
if !strings.HasPrefix(mimeType, "image/") { |
|
return nil, fmt.Errorf("invalid image mime type: %s", mimeType) |
|
} |
|
|
|
|
|
imageData, err := utils.Base64Decode(parts[1]) |
|
if err != nil { |
|
return nil, fmt.Errorf("decode base64 failed: %v", err) |
|
} |
|
|
|
|
|
fileInfo, err := validateImageBytes(imageData, mimeType) |
|
if err != nil { |
|
return nil, fmt.Errorf("validate image failed: %v", err) |
|
} |
|
|
|
|
|
|
|
preSignReq := &PreSignRequest{ |
|
FilenameList: []string{fileInfo.FileName}, |
|
Module: ImageModule, |
|
Location: ImageLocation, |
|
ObjID: uuid.New().String(), |
|
} |
|
|
|
var preSignResp PreSignResponse |
|
_, err = utils.RestyDefaultClient.R(). |
|
SetContext(ctx). |
|
SetHeader("cookie", config.MonicaConfig.MonicaCookie). |
|
SetBody(preSignReq). |
|
SetResult(&preSignResp). |
|
Post(PreSignURL) |
|
|
|
if err != nil { |
|
return nil, fmt.Errorf("get pre-sign url failed: %v", err) |
|
} |
|
|
|
if len(preSignResp.Data.PreSignURLList) == 0 || len(preSignResp.Data.ObjectURLList) == 0 { |
|
return nil, fmt.Errorf("no pre-sign url or object url returned") |
|
} |
|
|
|
|
|
|
|
_, err = utils.RestyDefaultClient.R(). |
|
SetContext(ctx). |
|
SetHeader("Content-Type", fileInfo.FileType). |
|
SetBody(imageData). |
|
Put(preSignResp.Data.PreSignURLList[0]) |
|
|
|
if err != nil { |
|
return nil, fmt.Errorf("upload file failed: %v", err) |
|
} |
|
|
|
|
|
fileInfo.ObjectURL = preSignResp.Data.ObjectURLList[0] |
|
uploadReq := &FileUploadRequest{ |
|
Data: []FileInfo{*fileInfo}, |
|
} |
|
|
|
var uploadResp FileUploadResponse |
|
_, err = utils.RestyDefaultClient.R(). |
|
SetContext(ctx). |
|
SetHeader("cookie", config.MonicaConfig.MonicaCookie). |
|
SetBody(uploadReq). |
|
SetResult(&uploadResp). |
|
Post(FileUploadURL) |
|
|
|
if err != nil { |
|
return nil, fmt.Errorf("create file object failed: %v", err) |
|
} |
|
|
|
if len(uploadResp.Data.Items) > 0 { |
|
fileInfo.FileName = uploadResp.Data.Items[0].FileName |
|
fileInfo.FileType = uploadResp.Data.Items[0].FileType |
|
fileInfo.FileSize = uploadResp.Data.Items[0].FileSize |
|
fileInfo.FileUID = uploadResp.Data.Items[0].FileUID |
|
fileInfo.FileExt = uploadResp.Data.Items[0].FileType |
|
fileInfo.FileTokens = uploadResp.Data.Items[0].FileTokens |
|
fileInfo.FileChunks = uploadResp.Data.Items[0].FileChunks |
|
} |
|
|
|
fileInfo.UseFullText = true |
|
fileInfo.FileURL = preSignResp.Data.CDNURLList[0] |
|
|
|
|
|
var batchResp FileBatchGetResponse |
|
reqMap := make(map[string][]string) |
|
reqMap["file_uids"] = []string{fileInfo.FileUID} |
|
var retryCount = 1 |
|
for { |
|
if retryCount > 5 { |
|
return nil, fmt.Errorf("retry limit exceeded") |
|
} |
|
_, err = utils.RestyDefaultClient.R(). |
|
SetContext(ctx). |
|
SetHeader("cookie", config.MonicaConfig.MonicaCookie). |
|
SetBody(reqMap). |
|
SetResult(&batchResp). |
|
Post(FileGetURL) |
|
if err != nil { |
|
return nil, fmt.Errorf("batch get file failed: %v", err) |
|
} |
|
if len(batchResp.Data.Items) > 0 && batchResp.Data.Items[0].FileChunks > 0 { |
|
break |
|
} else { |
|
retryCount++ |
|
} |
|
time.Sleep(1 * time.Second) |
|
} |
|
fileInfo.FileChunks = batchResp.Data.Items[0].FileChunks |
|
fileInfo.FileTokens = batchResp.Data.Items[0].FileTokens |
|
fileInfo.URL = "" |
|
fileInfo.ObjectURL = "" |
|
|
|
|
|
imageCache.Store(cacheKey, fileInfo) |
|
|
|
return fileInfo, nil |
|
} |
|
|
|
|
|
func validateImageBytes(imageData []byte, mimeType string) (*FileInfo, error) { |
|
if len(imageData) > MaxFileSize { |
|
return nil, fmt.Errorf("file size exceeds limit: %d > %d", len(imageData), MaxFileSize) |
|
} |
|
|
|
contentType := http.DetectContentType(imageData) |
|
if !SupportedImageTypes[contentType] { |
|
return nil, fmt.Errorf("unsupported image type: %s", contentType) |
|
} |
|
|
|
|
|
ext := ".png" |
|
switch mimeType { |
|
case "image/jpeg": |
|
ext = ".jpg" |
|
case "image/gif": |
|
ext = ".gif" |
|
case "image/webp": |
|
ext = ".webp" |
|
} |
|
|
|
fileName := fmt.Sprintf("%s%s", uuid.New().String(), ext) |
|
|
|
return &FileInfo{ |
|
FileName: fileName, |
|
FileSize: int64(len(imageData)), |
|
FileType: contentType, |
|
}, nil |
|
} |
|
|