diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index 8b43747..4d4f9d3 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -5187,6 +5187,13 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) { AND d.low<=m.seqid AND (d.hi=0 OR d.hi>m.seqid) ) ) + OR EXISTS ( + SELECT 1 FROM im_group_files AS gf + INNER JOIN im_subscriptions AS s + ON s.topic=gf.topic AND s.userid=? + WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%')) AND gf.status='active' + AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0 + ) OR EXISTS ( SELECT 1 FROM im_meeting_note_files AS mnf INNER JOIN im_meetings AS mtg @@ -5205,6 +5212,7 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) { ) ) LIMIT 1`, store.DecodeUid(fileID), t.UploadCompleted, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), + store.DecodeUid(uid), fid, fid, fid, t.MeetingStateCanceled, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid)) if err != nil { return false, err diff --git a/server/db/mysql/tenant_adapter.go b/server/db/mysql/tenant_adapter.go index 97ae6f3..44afedb 100644 --- a/server/db/mysql/tenant_adapter.go +++ b/server/db/mysql/tenant_adapter.go @@ -7843,8 +7843,16 @@ func (a *adapter) TenantFileUserHasAccess(id t.TenantID, fid string, uid t.Uid) AND d.low<=m.seqid AND (d.hi=0 OR d.hi>m.seqid) ) ) + OR EXISTS ( + SELECT 1 FROM im_group_files AS gf + INNER JOIN im_subscriptions AS s + ON s.tenant_id=fu.tenant_id AND s.topic=gf.topic AND s.userid=? + WHERE (gf.file_id=? OR gf.file_id LIKE CONCAT(?,'.%')) AND gf.status='active' + AND s.deletedat IS NULL AND INSTR(s.modewant,'R')>0 AND INSTR(s.modegiven,'R')>0 + ) ) LIMIT 1`, - id, decodeUID(fileID), t.UploadCompleted, decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid)) + id, decodeUID(fileID), t.UploadCompleted, decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid), decodeUID(uid), + decodeUID(uid), fid, fid) if err != nil { return false, err } diff --git a/server/db/mysql/tenant_groups.go b/server/db/mysql/tenant_groups.go index 3dd1e71..99d70a3 100644 --- a/server/db/mysql/tenant_groups.go +++ b/server/db/mysql/tenant_groups.go @@ -933,6 +933,7 @@ func (a *adapter) TenantGroupFileCreate(id t.TenantID, file *t.GroupFile) (*t.Gr if file.IdempotencyKey == "" { file.IdempotencyKey = file.GroupFileID } + a.enrichGroupFileFromUpload(id, file) now := t.TimeNow() ctx, cancel := a.getContext() if cancel != nil { @@ -941,7 +942,12 @@ func (a *adapter) TenantGroupFileCreate(id t.TenantID, file *t.GroupFile) (*t.Gr _, err := a.db.ExecContext(ctx, `INSERT INTO im_group_files (group_file_id,topic,file_id,source_topic,source_seq,uploader_id,file_name,mime_type,file_type,file_size,status,idempotency_key,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?) - ON DUPLICATE KEY UPDATE updated_at=updated_at`, + ON DUPLICATE KEY UPDATE + file_name=IF(file_name='', VALUES(file_name), file_name), + mime_type=IF(mime_type='', VALUES(mime_type), mime_type), + file_size=IF(file_size=0, VALUES(file_size), file_size), + file_type=IF(file_type='' OR file_type='file', VALUES(file_type), file_type), + updated_at=updated_at`, file.GroupFileID, file.Topic, file.FileID, file.SourceTopic, file.SourceSeq, decodeUID(file.UploaderID), file.FileName, file.MIMEType, file.FileType, file.FileSize, file.Status, file.IdempotencyKey, now, now) if err != nil && !isDupe(err) { @@ -950,6 +956,32 @@ func (a *adapter) TenantGroupFileCreate(id t.TenantID, file *t.GroupFile) (*t.Gr return a.groupFileByQuery(ctx, "SELECT * FROM im_group_files WHERE topic=? AND file_id=?", file.Topic, file.FileID) } +func (a *adapter) enrichGroupFileFromUpload(id t.TenantID, file *t.GroupFile) { + if file == nil || t.ParseUid(file.FileID).IsZero() { + return + } + fd, err := a.TenantFileGet(id, file.FileID) + if err != nil || fd == nil { + if file.FileName == "" { + file.FileName = file.FileID + } + return + } + if file.MIMEType == "" { + file.MIMEType = strings.TrimSpace(fd.MimeType) + } + if file.FileSize <= 0 { + file.FileSize = fd.Size + } + if file.FileName == "" { + if original := strings.TrimSpace(fd.OriginalName); original != "" { + file.FileName = original + } else { + file.FileName = file.FileID + } + } +} + func (a *adapter) TenantGroupFileDelete(id t.TenantID, topic, groupFileID string, actor t.Uid, canManage bool, now time.Time) (*t.GroupFile, error) { if tenantAdp, err := a.routeGroupTenant(id); err != nil { return nil, err diff --git a/server/im_http.go b/server/im_http.go index 1b934f7..8f1adde 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -5220,7 +5220,7 @@ func serveIMGroupFiles(wrt http.ResponseWriter, req *http.Request, topic, tail s if !decodeIMJSONBody(wrt, req, &body) { return } - fileID := strings.TrimSpace(body.FileID) + fileID := normalizeIMUploadedFileID(body.FileID) if fileID == "" { writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "file_id"}) return @@ -6817,6 +6817,33 @@ func normalizeIMFileType(raw string) string { } } +func normalizeIMUploadedFileID(raw string) string { + value := strings.TrimSpace(raw) + if value == "" { + return "" + } + if cut, _, ok := strings.Cut(value, "?"); ok { + value = cut + } + if cut, _, ok := strings.Cut(value, "#"); ok { + value = cut + } + if strings.Contains(value, "/") && store.Store != nil && store.Store.GetMediaHandler() != nil { + if fid := store.Store.GetMediaHandler().GetIdFromUrl(value); !fid.IsZero() { + return fid.String() + } + } + if fid := types.ParseUid(value); !fid.IsZero() { + return fid.String() + } + if dot := strings.IndexByte(value, '.'); dot > 0 { + if fid := types.ParseUid(value[:dot]); !fid.IsZero() { + return fid.String() + } + } + return "" +} + func parseIMLimit(raw string, fallback, max int) int { limit := fallback if raw != "" { diff --git a/server/im_http_test.go b/server/im_http_test.go index 8dde05f..581a813 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" + "fmt" "io" "mime/multipart" "net/http" @@ -4501,8 +4502,9 @@ func TestServeIMGroupFiles(t *testing.T) { TenantID: 7, User: types.Uid(42).String(), Topic: "grpAlpha", ModeWant: types.ModeCFull, ModeGiven: types.ModeCFull, }, nil).Times(3) + fileID := types.Uid(101).String() createReq := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/files", - strings.NewReader(`{"file_id":"file1","file_name":"方案.pdf","file_type":"document","file_size":123}`)) + strings.NewReader(fmt.Sprintf(`{"file_id":"%s.pdf","file_name":"方案.pdf","file_type":"document","file_size":123}`, fileID))) createReq.Header.Set("Authorization", "Bearer token-1") createReq.Header.Set("Idempotency-Key", "gfile-1") createRec := httptest.NewRecorder() @@ -4511,7 +4513,7 @@ func TestServeIMGroupFiles(t *testing.T) { t.Fatalf("create status: got %d body=%s", createRec.Code, createRec.Body.String()) } created := decodeIMHTTPResponse(t, createRec).Data.(map[string]any) - if created["file_id"] != "file1" || created["file_type"] != "document" { + if created["file_id"] != fileID || created["file_type"] != "document" { t.Fatalf("unexpected create response: %#v", created) } diff --git a/server/kafka_file_lifecycle.go b/server/kafka_file_lifecycle.go index 2eed661..09dad45 100644 --- a/server/kafka_file_lifecycle.go +++ b/server/kafka_file_lifecycle.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "fmt" "strings" @@ -67,6 +68,9 @@ func enqueueKafkaFileAttachmentsBind(ctx context.Context, tenantID types.TenantI SeqID: msg.SeqId, UserID: types.ParseUid(msg.From).UserId(), FileType: lifecycleFileTypeFromMessageHead(msg.Head), + FileName: lifecycleContentString(msg.Content, "name", "file_name"), + MIMEType: lifecycleContentString(msg.Content, "mime", "mime_type"), + FileSize: lifecycleContentInt64(msg.Content, "size", "file_size"), OccurredAt: msg.CreatedAt, } return enqueueKafkaFileLifecycle(ctx, tenantID, kafkaFileLifecycleAttachmentsBind, fmt.Sprintf("%s:%d", msg.Topic, msg.SeqId), payload) @@ -141,7 +145,10 @@ func executeKafkaFileAttachmentsBind(tenantID types.TenantID, payload kafkaFileL SourceTopic: payload.Topic, SourceSeq: payload.SeqID, UploaderID: uploader, + FileName: payload.FileName, + MIMEType: payload.MIMEType, FileType: lifecycleFileType(payload.FileType), + FileSize: payload.FileSize, IdempotencyKey: fmt.Sprintf("%s:%d:%s", payload.Topic, payload.SeqID, fid.String()), }) if err != nil && !errors.Is(err, types.ErrDuplicate) { @@ -183,6 +190,58 @@ func lifecycleHeaderString(head types.KVMap, key string) string { return strings.TrimSpace(str) } +func lifecycleContentString(content any, keys ...string) string { + values, ok := lifecycleContentMap(content) + if !ok { + return "" + } + for _, key := range keys { + if value, ok := values[key]; ok { + if str, ok := value.(string); ok { + if str = strings.TrimSpace(str); str != "" { + return str + } + } + } + } + return "" +} + +func lifecycleContentInt64(content any, keys ...string) int64 { + values, ok := lifecycleContentMap(content) + if !ok { + return 0 + } + for _, key := range keys { + if value, ok := values[key]; ok { + switch typed := value.(type) { + case int: + return int64(typed) + case int64: + return typed + case float64: + return int64(typed) + case json.Number: + if parsed, err := typed.Int64(); err == nil { + return parsed + } + } + } + } + return 0 +} + +func lifecycleContentMap(content any) (map[string]any, bool) { + switch typed := content.(type) { + case map[string]any: + return typed, true + case types.KVMap: + return map[string]any(typed), true + default: + return nil, false + } +} + func lifecycleCanonicalMessageType(raw string) string { switch strings.ToLower(strings.TrimSpace(raw)) { case "audio":