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