fix: hydrate meeting note file metadata

This commit is contained in:
richjohnrambo
2026-09-18 16:26:06 +08:00
parent dacac13f88
commit ef2967b056
5 changed files with 200 additions and 28 deletions
+138 -26
View File
@@ -3,9 +3,13 @@ package store
import (
"context"
"database/sql"
"encoding/base32"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"mime"
"path/filepath"
"strings"
"sync"
@@ -14,6 +18,7 @@ import (
"git.118im.com/im_team/im_workbench/internal/config"
mysqlDriver "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/xtea"
)
type Store struct {
@@ -23,6 +28,32 @@ type Store struct {
tenantStoreMu sync.Mutex
}
var (
uidCipherMu sync.RWMutex
uidCipher *xtea.Cipher
)
func init() {
if err := SetUIDKey("dkXxWLwqUNRfYmdXRzOkZw=="); err != nil {
panic(err)
}
}
func SetUIDKey(encoded string) error {
key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
if err != nil {
return fmt.Errorf("invalid uid key: %w", err)
}
cipher, err := xtea.NewCipher(key)
if err != nil {
return fmt.Errorf("invalid uid key: %w", err)
}
uidCipherMu.Lock()
uidCipher = cipher
uidCipherMu.Unlock()
return nil
}
func Open(cfg config.MySQLConfig) (*Store, error) {
st, err := openDSN(cfg.DSN)
if err != nil {
@@ -603,19 +634,8 @@ func (s *Store) CreateMeetingNoteFile(ctx context.Context, userID int64, meeting
}
now := time.Now().UTC().Round(time.Millisecond)
fileID := strings.TrimSpace(input.FileID)
fileName, mimeType, fileSize, fileETag := normalizeMeetingNoteFileName(input.FileName, fileID), strings.TrimSpace(input.MIMEType), int64(0), ""
if input.FileSize != nil && *input.FileSize > 0 {
fileSize = *input.FileSize
}
var uploadMime string
var uploadSize int64
_ = s.db.QueryRowxContext(ctx, `SELECT IFNULL(mimetype,''),IFNULL(size,0),IFNULL(etag,'') FROM im_fileuploads WHERE id=? LIMIT 1`, fileID).Scan(&uploadMime, &uploadSize, &fileETag)
if mimeType == "" {
mimeType = uploadMime
}
if fileSize <= 0 {
fileSize = uploadSize
}
meta := s.fileUploadMetadata(ctx, fileID)
fileName, mimeType, fileSize, fileETag := normalizeMeetingNoteFileMetadata(input, meta, fileID)
_, err = s.db.ExecContext(ctx, `INSERT INTO im_meeting_note_files(note_file_id,meeting_id,file_id,uploader_id,file_name,mime_type,file_size,file_etag,status,preview_status,preview_mode,preview_file_id,version,idempotency_key,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,?,'active','processing','native','',1,?,?,?)`, noteFileID, meetingID, fileID, userID, fileName, mimeType, fileSize, fileETag, idempotencyKey, now, now)
if isDuplicate(err) && idempotencyKey != "" {
@@ -646,19 +666,8 @@ func (s *Store) ReplaceMeetingNoteFile(ctx context.Context, userID int64, meetin
return nil, ErrForbidden
}
fileID := strings.TrimSpace(input.FileID)
fileName, mimeType, fileSize, fileETag := normalizeMeetingNoteFileName(input.FileName, fileID), strings.TrimSpace(input.MIMEType), int64(0), ""
if input.FileSize != nil && *input.FileSize > 0 {
fileSize = *input.FileSize
}
var uploadMime string
var uploadSize int64
_ = s.db.QueryRowxContext(ctx, `SELECT IFNULL(mimetype,''),IFNULL(size,0),IFNULL(etag,'') FROM im_fileuploads WHERE id=? LIMIT 1`, fileID).Scan(&uploadMime, &uploadSize, &fileETag)
if mimeType == "" {
mimeType = uploadMime
}
if fileSize <= 0 {
fileSize = uploadSize
}
meta := s.fileUploadMetadata(ctx, fileID)
fileName, mimeType, fileSize, fileETag := normalizeMeetingNoteFileMetadata(input, meta, fileID)
res, err := s.db.ExecContext(ctx, `UPDATE im_meeting_note_files SET file_id=?,file_name=?,mime_type=?,file_size=?,file_etag=?,preview_status='processing',preview_file_id='',version=version+1,idempotency_key=?,updated_at=?
WHERE meeting_id=? AND note_file_id=? AND status='active' AND version=?`,
fileID, fileName, mimeType, fileSize, fileETag, idempotencyKey, time.Now().UTC().Round(time.Millisecond), meetingID, noteFileID, version)
@@ -671,6 +680,76 @@ func (s *Store) ReplaceMeetingNoteFile(ctx context.Context, userID int64, meetin
return s.meetingNoteFile(ctx, meetingID, noteFileID)
}
type fileUploadMetadata struct {
MIMEType string `db:"mimetype"`
FileSize int64 `db:"size"`
FileETag string `db:"etag"`
Location string `db:"location"`
}
func (s *Store) fileUploadMetadata(ctx context.Context, fileID string) fileUploadMetadata {
var meta fileUploadMetadata
decodedID, storageName := parseUploadFileID(fileID)
if decodedID <= 0 && storageName == "" {
return meta
}
if decodedID > 0 {
err := s.db.GetContext(ctx, &meta, `SELECT IFNULL(mimetype,'') AS mimetype,IFNULL(size,0) AS size,IFNULL(etag,'') AS etag,IFNULL(location,'') AS location FROM im_fileuploads WHERE id=? LIMIT 1`, decodedID)
if err == nil {
return meta
}
}
if storageName == "" {
return meta
}
_ = s.db.GetContext(ctx, &meta, `SELECT IFNULL(mimetype,'') AS mimetype,IFNULL(size,0) AS size,IFNULL(etag,'') AS etag,IFNULL(location,'') AS location
FROM im_fileuploads WHERE location=? OR location LIKE ? ORDER BY createdat DESC LIMIT 1`, storageName, "%/"+storageName)
return meta
}
func parseUploadFileID(fileID string) (int64, string) {
name := strings.TrimSpace(fileID)
if dot := strings.LastIndexByte(name, '.'); dot > 0 {
ext := name[dot+1:]
if len(ext) == 0 || len(ext) > 16 {
return 0, ""
}
for _, r := range ext {
if (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && (r < '0' || r > '9') {
return 0, ""
}
}
name = name[:dot]
}
decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(name)
if err != nil || len(decoded) < 8 {
return 0, ""
}
storageData := make([]byte, 8)
binary.LittleEndian.PutUint64(storageData, binary.LittleEndian.Uint64(decoded[:8]))
storageName := strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(storageData))
plain := make([]byte, 8)
uidCipherMu.RLock()
cipher := uidCipher
uidCipherMu.RUnlock()
cipher.Decrypt(plain, decoded[:8])
return int64(binary.LittleEndian.Uint64(plain)), storageName
}
func normalizeMeetingNoteFileMetadata(input MeetingNoteFileInput, meta fileUploadMetadata, fileID string) (string, string, int64, string) {
mimeType := firstNonEmpty(input.MIMEType, meta.MIMEType)
fileSize := int64(0)
if input.FileSize != nil && *input.FileSize > 0 {
fileSize = *input.FileSize
} else if meta.FileSize > 0 {
fileSize = meta.FileSize
}
fileETag := firstNonEmpty(input.FileETag, meta.FileETag)
fileName := normalizeMeetingNoteFileName(firstNonEmpty(input.FileName, fileNameFromLocation(meta.Location), fallbackFileName(fileID, mimeType)), fileID)
return fileName, strings.TrimSpace(mimeType), fileSize, strings.TrimSpace(fileETag)
}
func normalizeMeetingNoteFileName(fileName, fileID string) string {
fileName = strings.TrimSpace(strings.ReplaceAll(fileName, "\\", "/"))
fileName = filepath.Base(fileName)
@@ -689,6 +768,39 @@ func normalizeMeetingNoteFileName(fileName, fileID string) string {
return string(runes[:255-len(extRunes)]) + ext
}
func fileNameFromLocation(location string) string {
name := strings.TrimSpace(strings.ReplaceAll(location, "\\", "/"))
name = filepath.Base(name)
if name == "." || name == "/" {
return ""
}
if filepath.Ext(name) == "" {
return ""
}
return name
}
func fallbackFileName(fileID, mimeType string) string {
fileID = strings.TrimSpace(fileID)
if filepath.Ext(fileID) != "" || strings.TrimSpace(mimeType) == "" {
return fileID
}
exts, _ := mime.ExtensionsByType(strings.TrimSpace(mimeType))
if len(exts) == 0 {
return fileID
}
return fileID + strings.ToLower(exts[0])
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
func (s *Store) DeleteMeetingNoteFile(ctx context.Context, userID int64, meetingID, noteFileID, idempotencyKey string, version int) (*MeetingNoteFile, error) {
meeting, err := s.Meeting(ctx, userID, meetingID)
if err != nil {