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)
|
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 (
|
OR EXISTS (
|
||||||
SELECT 1 FROM im_meeting_note_files AS mnf
|
SELECT 1 FROM im_meeting_note_files AS mnf
|
||||||
INNER JOIN im_meetings AS mtg
|
INNER JOIN im_meetings AS mtg
|
||||||
@@ -5205,6 +5212,7 @@ func (a *adapter) FileUserHasAccess(fid string, uid t.Uid) (bool, error) {
|
|||||||
)
|
)
|
||||||
) LIMIT 1`,
|
) LIMIT 1`,
|
||||||
store.DecodeUid(fileID), t.UploadCompleted, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid),
|
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))
|
fid, t.MeetingStateCanceled, store.DecodeUid(uid), store.DecodeUid(uid), store.DecodeUid(uid))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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)
|
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`,
|
) 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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -933,6 +933,7 @@ func (a *adapter) TenantGroupFileCreate(id t.TenantID, file *t.GroupFile) (*t.Gr
|
|||||||
if file.IdempotencyKey == "" {
|
if file.IdempotencyKey == "" {
|
||||||
file.IdempotencyKey = file.GroupFileID
|
file.IdempotencyKey = file.GroupFileID
|
||||||
}
|
}
|
||||||
|
a.enrichGroupFileFromUpload(id, file)
|
||||||
now := t.TimeNow()
|
now := t.TimeNow()
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
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
|
_, 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)
|
(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(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
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.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)
|
file.FileName, file.MIMEType, file.FileType, file.FileSize, file.Status, file.IdempotencyKey, now, now)
|
||||||
if err != nil && !isDupe(err) {
|
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)
|
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) {
|
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 {
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||||
return nil, err
|
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) {
|
if !decodeIMJSONBody(wrt, req, &body) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileID := strings.TrimSpace(body.FileID)
|
fileID := normalizeIMUploadedFileID(body.FileID)
|
||||||
if fileID == "" {
|
if fileID == "" {
|
||||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "file_id"})
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "file_id"})
|
||||||
return
|
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 {
|
func parseIMLimit(raw string, fallback, max int) int {
|
||||||
limit := fallback
|
limit := fallback
|
||||||
if raw != "" {
|
if raw != "" {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"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,
|
TenantID: 7, User: types.Uid(42).String(), Topic: "grpAlpha", ModeWant: types.ModeCFull, ModeGiven: types.ModeCFull,
|
||||||
}, nil).Times(3)
|
}, nil).Times(3)
|
||||||
|
|
||||||
|
fileID := types.Uid(101).String()
|
||||||
createReq := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/files",
|
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("Authorization", "Bearer token-1")
|
||||||
createReq.Header.Set("Idempotency-Key", "gfile-1")
|
createReq.Header.Set("Idempotency-Key", "gfile-1")
|
||||||
createRec := httptest.NewRecorder()
|
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())
|
t.Fatalf("create status: got %d body=%s", createRec.Code, createRec.Body.String())
|
||||||
}
|
}
|
||||||
created := decodeIMHTTPResponse(t, createRec).Data.(map[string]any)
|
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)
|
t.Fatalf("unexpected create response: %#v", created)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -67,6 +68,9 @@ func enqueueKafkaFileAttachmentsBind(ctx context.Context, tenantID types.TenantI
|
|||||||
SeqID: msg.SeqId,
|
SeqID: msg.SeqId,
|
||||||
UserID: types.ParseUid(msg.From).UserId(),
|
UserID: types.ParseUid(msg.From).UserId(),
|
||||||
FileType: lifecycleFileTypeFromMessageHead(msg.Head),
|
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,
|
OccurredAt: msg.CreatedAt,
|
||||||
}
|
}
|
||||||
return enqueueKafkaFileLifecycle(ctx, tenantID, kafkaFileLifecycleAttachmentsBind, fmt.Sprintf("%s:%d", msg.Topic, msg.SeqId), payload)
|
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,
|
SourceTopic: payload.Topic,
|
||||||
SourceSeq: payload.SeqID,
|
SourceSeq: payload.SeqID,
|
||||||
UploaderID: uploader,
|
UploaderID: uploader,
|
||||||
|
FileName: payload.FileName,
|
||||||
|
MIMEType: payload.MIMEType,
|
||||||
FileType: lifecycleFileType(payload.FileType),
|
FileType: lifecycleFileType(payload.FileType),
|
||||||
|
FileSize: payload.FileSize,
|
||||||
IdempotencyKey: fmt.Sprintf("%s:%d:%s", payload.Topic, payload.SeqID, fid.String()),
|
IdempotencyKey: fmt.Sprintf("%s:%d:%s", payload.Topic, payload.SeqID, fid.String()),
|
||||||
})
|
})
|
||||||
if err != nil && !errors.Is(err, types.ErrDuplicate) {
|
if err != nil && !errors.Is(err, types.ErrDuplicate) {
|
||||||
@@ -183,6 +190,58 @@ func lifecycleHeaderString(head types.KVMap, key string) string {
|
|||||||
return strings.TrimSpace(str)
|
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 {
|
func lifecycleCanonicalMessageType(raw string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||||
case "audio":
|
case "audio":
|
||||||
|
|||||||
Reference in New Issue
Block a user