| 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 |
| } |
|
|