255 lines
7.6 KiB
Go
255 lines
7.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base32"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.118im.com/im_team/im_workbench/internal/auth"
|
|
"git.118im.com/im_team/im_workbench/internal/managerconfig"
|
|
"git.118im.com/im_team/im_workbench/internal/store"
|
|
aliyunoss "github.com/aliyun/aliyun-oss-go-sdk/oss"
|
|
)
|
|
|
|
const aiTableExportDocxMIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
|
|
type aiExportedFile struct {
|
|
FileID string
|
|
FileName string
|
|
DownloadURL string
|
|
}
|
|
|
|
type ossUploadConfig struct {
|
|
Enabled bool
|
|
Endpoint string
|
|
Bucket string
|
|
AccessKeyID string
|
|
AccessKeySecret string
|
|
ServeURL string
|
|
CacheControl string
|
|
}
|
|
|
|
func (a *API) uploadAITableAnalysisExport(ctx context.Context, r *http.Request, st *store.Store, p *auth.Principal, item *store.AITableAnalysis) (*aiExportedFile, error) {
|
|
cfg, err := a.ossUploadConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := aiTableAnalysisDocx(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fileID := newFileUploadID()
|
|
publicID, storageID := formatFileUploadID(fileID)
|
|
createdAt := time.Now().UTC()
|
|
objectKey := path.Join("tenants", strconv.FormatInt(p.TenantID, 10), createdAt.Format("2006"), createdAt.Format("01"), storageID)
|
|
client, err := aliyunoss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bucket, err := client.Bucket(cfg.Bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
options := []aliyunoss.Option{aliyunoss.ContentType(aiTableExportDocxMIME)}
|
|
if cfg.CacheControl != "" {
|
|
options = append(options, aliyunoss.CacheControl(cfg.CacheControl))
|
|
}
|
|
if err := bucket.PutObject(objectKey, bytes.NewReader(data), options...); err != nil {
|
|
return nil, err
|
|
}
|
|
etag := ""
|
|
if meta, err := bucket.GetObjectDetailedMeta(objectKey); err == nil {
|
|
etag = strings.Trim(meta.Get("ETag"), `"`)
|
|
}
|
|
fileName := aiTableExportFileName(item)
|
|
fileURLName := publicID + ".docx"
|
|
if err := st.SaveCompletedFileUpload(ctx, store.CompletedFileUpload{
|
|
ID: fileID, UserID: p.UserID, MIMEType: aiTableExportDocxMIME, Size: int64(len(data)),
|
|
ETag: etag, Location: objectKey, CreatedAt: createdAt,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := st.UpdateAITableAnalysisExportFileID(ctx, p.UserID, item.RecordID, fileURLName); err != nil {
|
|
return nil, err
|
|
}
|
|
return &aiExportedFile{
|
|
FileID: fileURLName,
|
|
FileName: fileName,
|
|
DownloadURL: publicFileDownloadURL(r, cfg.ServeURL, fileURLName),
|
|
}, nil
|
|
}
|
|
|
|
func downloadWorkbenchFileToTemp(r *http.Request, fileID, fileType string) (string, func(), error) {
|
|
fileID = strings.TrimSpace(fileID)
|
|
if fileID == "" {
|
|
return "", func() {}, fmt.Errorf("empty file id")
|
|
}
|
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, absoluteURL(r, "/v0/file/s/"+url.PathEscape(fileID)), nil)
|
|
if err != nil {
|
|
return "", func() {}, err
|
|
}
|
|
copyHeader(req, r, "Authorization")
|
|
copyHeader(req, r, "X-MeetingIM-APIKey")
|
|
copyHeader(req, r, "X-Tenant-Code")
|
|
copyHeader(req, r, "X-IM-Schema")
|
|
copyHeader(req, r, "X-Request-ID")
|
|
if auth := strings.TrimSpace(r.Header.Get("X-MeetingIM-Auth")); auth != "" {
|
|
req.Header.Set("X-MeetingIM-Auth", auth)
|
|
} else if token := bearerTokenFromHeader(r.Header.Get("Authorization")); token != "" {
|
|
req.Header.Set("X-MeetingIM-Auth", "token "+token)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", func() {}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return "", func() {}, fmt.Errorf("file service returned %d", resp.StatusCode)
|
|
}
|
|
return responseBodyToTemp(resp.Body, fileType)
|
|
}
|
|
|
|
func (a *API) downloadOSSObjectToTemp(ctx context.Context, objectKey, fileType string) (string, func(), error) {
|
|
objectKey = strings.Trim(strings.TrimSpace(objectKey), "/")
|
|
if objectKey == "" {
|
|
return "", func() {}, fmt.Errorf("empty object key")
|
|
}
|
|
cfg, err := a.ossUploadConfig(ctx)
|
|
if err != nil {
|
|
return "", func() {}, err
|
|
}
|
|
client, err := aliyunoss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
|
|
if err != nil {
|
|
return "", func() {}, err
|
|
}
|
|
bucket, err := client.Bucket(cfg.Bucket)
|
|
if err != nil {
|
|
return "", func() {}, err
|
|
}
|
|
reader, err := bucket.GetObject(objectKey)
|
|
if err != nil {
|
|
return "", func() {}, err
|
|
}
|
|
defer reader.Close()
|
|
return responseBodyToTemp(reader, fileType)
|
|
}
|
|
|
|
func responseBodyToTemp(reader io.Reader, fileType string) (string, func(), error) {
|
|
suffix := "." + strings.TrimLeft(strings.ToLower(strings.TrimSpace(fileType)), ".")
|
|
if suffix == "." {
|
|
suffix = ".table"
|
|
}
|
|
tmp, err := os.CreateTemp("", "im-workbench-ai-table-*"+suffix)
|
|
if err != nil {
|
|
return "", func() {}, err
|
|
}
|
|
tmpPath := tmp.Name()
|
|
cleanup := func() { _ = os.Remove(tmpPath) }
|
|
if _, err := io.Copy(tmp, reader); err != nil {
|
|
_ = tmp.Close()
|
|
cleanup()
|
|
return "", func() {}, err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
cleanup()
|
|
return "", func() {}, err
|
|
}
|
|
return tmpPath, cleanup, nil
|
|
}
|
|
|
|
func copyHeader(dst *http.Request, src *http.Request, key string) {
|
|
if value := strings.TrimSpace(src.Header.Get(key)); value != "" {
|
|
dst.Header.Set(key, value)
|
|
}
|
|
}
|
|
|
|
func bearerTokenFromHeader(value string) string {
|
|
token, ok := strings.CutPrefix(strings.TrimSpace(value), "Bearer ")
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(token)
|
|
}
|
|
|
|
func (a *API) ossUploadConfig(ctx context.Context) (ossUploadConfig, error) {
|
|
values, err := managerconfig.New(a.manager).ThirdPartyConfig(ctx, "im_third_party",
|
|
"oss_enabled",
|
|
"oss_endpoint",
|
|
"oss_bucket",
|
|
"oss_access_key_id",
|
|
"oss_access_key_secret",
|
|
"oss_serve_url",
|
|
"oss_cache_control",
|
|
)
|
|
if err != nil {
|
|
return ossUploadConfig{}, err
|
|
}
|
|
cfg := ossUploadConfig{
|
|
Enabled: parseConfigBool(values["oss_enabled"]),
|
|
Endpoint: strings.TrimSpace(values["oss_endpoint"]),
|
|
Bucket: strings.TrimSpace(values["oss_bucket"]),
|
|
AccessKeyID: strings.TrimSpace(values["oss_access_key_id"]),
|
|
AccessKeySecret: strings.TrimSpace(values["oss_access_key_secret"]),
|
|
ServeURL: strings.TrimSpace(values["oss_serve_url"]),
|
|
CacheControl: strings.TrimSpace(values["oss_cache_control"]),
|
|
}
|
|
if cfg.ServeURL == "" {
|
|
cfg.ServeURL = "/v0/file/s/"
|
|
}
|
|
if !cfg.Enabled || cfg.Endpoint == "" || cfg.Bucket == "" || cfg.AccessKeyID == "" || cfg.AccessKeySecret == "" {
|
|
return ossUploadConfig{}, fmt.Errorf("oss config incomplete")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func parseConfigBool(value string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "1", "true", "yes", "on", "enabled":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func newFileUploadID() int64 {
|
|
var random [8]byte
|
|
if _, err := rand.Read(random[:]); err != nil {
|
|
return time.Now().UnixNano()
|
|
}
|
|
suffix := int64(binary.LittleEndian.Uint64(random[:]) & ((1 << 20) - 1))
|
|
return (time.Now().UnixMilli() << 20) | suffix
|
|
}
|
|
|
|
func formatFileUploadID(id int64) (publicID string, storageID string) {
|
|
encrypted := encryptUID(id)
|
|
publicID = base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(encrypted)
|
|
storageID = strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(encrypted))
|
|
return publicID, storageID
|
|
}
|
|
|
|
func publicFileDownloadURL(r *http.Request, serveURL, fileName string) string {
|
|
serveURL = strings.TrimSpace(serveURL)
|
|
if strings.HasPrefix(serveURL, "http://") || strings.HasPrefix(serveURL, "https://") {
|
|
return strings.TrimRight(serveURL, "/") + "/" + fileName
|
|
}
|
|
if serveURL == "" {
|
|
serveURL = "/v0/file/s/"
|
|
}
|
|
if !strings.HasPrefix(serveURL, "/") {
|
|
serveURL = "/" + serveURL
|
|
}
|
|
return absoluteURL(r, strings.TrimRight(serveURL, "/")+"/"+fileName)
|
|
}
|