Fix group file metadata and access
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+28
-1
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user