fix: upload ai table exports to oss
This commit is contained in:
@@ -443,11 +443,15 @@ func (a *API) aiTableAnalysisResource(w http.ResponseWriter, r *http.Request, p
|
||||
writeStoreError(w, r, err)
|
||||
return
|
||||
}
|
||||
fileName := aiTableExportFileName(item)
|
||||
file, err := a.uploadAITableAnalysisExport(r.Context(), r, st, p, item)
|
||||
if err != nil {
|
||||
writeError(w, r, http.StatusServiceUnavailable, 30053, reasonAIProviderFailed, "ai export storage failed", nil)
|
||||
return
|
||||
}
|
||||
writeSuccess(w, r, map[string]any{
|
||||
"file_id": "ai_export_" + recordID,
|
||||
"file_name": fileName,
|
||||
"download_url": absoluteURL(r, "/v1/workbench/ai/table-analyses/"+recordID+"/export/download?format=docx"),
|
||||
"file_id": file.FileID,
|
||||
"file_name": file.FileName,
|
||||
"download_url": file.DownloadURL,
|
||||
"expires_at": time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339Nano),
|
||||
})
|
||||
return
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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 (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)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ type API struct {
|
||||
auth *auth.Authenticator
|
||||
events events.Publisher
|
||||
authCfg config.AuthConfig
|
||||
manager config.ManagerConfig
|
||||
ai *ai.Service
|
||||
}
|
||||
|
||||
@@ -34,11 +35,11 @@ const (
|
||||
maxMailboxContentLength = 5000
|
||||
)
|
||||
|
||||
func New(st *store.Store, authenticator *auth.Authenticator, publisher events.Publisher, authCfg config.AuthConfig, aiService *ai.Service) *API {
|
||||
func New(st *store.Store, authenticator *auth.Authenticator, publisher events.Publisher, authCfg config.AuthConfig, managerCfg config.ManagerConfig, aiService *ai.Service) *API {
|
||||
if publisher == nil {
|
||||
publisher = events.NoopPublisher{}
|
||||
}
|
||||
return &API{store: st, auth: authenticator, events: publisher, authCfg: authCfg, ai: aiService}
|
||||
return &API{store: st, auth: authenticator, events: publisher, authCfg: authCfg, manager: managerCfg, ai: aiService}
|
||||
}
|
||||
|
||||
func (a *API) Register(mux *http.ServeMux) {
|
||||
|
||||
Reference in New Issue
Block a user