Merge pull request #1255 from trheyi/main
Implement attachment wrapper parsing and Base64 conversion in attachm…
This commit is contained in:
commit
b139f6fc2e
4 changed files with 306 additions and 145 deletions
|
|
@ -20,6 +20,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/fs"
|
||||
"github.com/yaoapp/gou/model"
|
||||
"github.com/yaoapp/yao/attachment/local"
|
||||
"github.com/yaoapp/yao/attachment/s3"
|
||||
|
|
@ -41,6 +42,148 @@ type UploadChunk struct {
|
|||
TotalChunks int64
|
||||
}
|
||||
|
||||
// Parse parses an attachment wrapper string and returns uploader name and file ID
|
||||
// Format: __<uploader>://<fileID>
|
||||
// Example: __yao.attachment://ccd472d11feb96e03a3fc468f494045c
|
||||
// Returns (uploader, fileID, isWrapper)
|
||||
func Parse(value string) (string, string, bool) {
|
||||
if !strings.HasPrefix(value, "__") {
|
||||
return "", value, false
|
||||
}
|
||||
|
||||
// Exclude common protocols (ftp, http, https, etc.)
|
||||
excludedProtocols := []string{"__ftp://", "__http://", "__https://", "__ws://", "__wss://"}
|
||||
for _, protocol := range excludedProtocols {
|
||||
if strings.HasPrefix(value, protocol) {
|
||||
return "", value, false
|
||||
}
|
||||
}
|
||||
|
||||
// Split by ://
|
||||
parts := strings.SplitN(value, "://", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", value, false
|
||||
}
|
||||
|
||||
uploader := parts[0] // Keep the __ prefix as it's part of the manager name
|
||||
fileID := parts[1]
|
||||
|
||||
return uploader, fileID, true
|
||||
}
|
||||
|
||||
// Base64 processes a wrapper value and converts it to Base64 if it's an attachment wrapper
|
||||
// If the value is not a wrapper, it returns the original value
|
||||
// Special case: if value looks like a file path, it will try to read from fs data
|
||||
// Optional parameter dataURI: if true, returns data URI format (data:image/png;base64,...)
|
||||
func Base64(ctx context.Context, value string, dataURI ...bool) string {
|
||||
useDataURI := false
|
||||
if len(dataURI) > 0 {
|
||||
useDataURI = dataURI[0]
|
||||
}
|
||||
|
||||
uploader, fileID, isWrapper := Parse(value)
|
||||
if !isWrapper {
|
||||
// Try to read as file path from fs data
|
||||
if base64Data := readFilePathAsBase64(value, useDataURI); base64Data != "" {
|
||||
return base64Data
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Get the manager
|
||||
manager, exists := Managers[uploader]
|
||||
if !exists {
|
||||
return value
|
||||
}
|
||||
|
||||
// Get file info to determine content type
|
||||
var contentType string
|
||||
if useDataURI {
|
||||
fileInfo, err := manager.Info(ctx, fileID)
|
||||
if err == nil && fileInfo != nil {
|
||||
contentType = fileInfo.ContentType
|
||||
}
|
||||
}
|
||||
|
||||
// Read the file as Base64
|
||||
base64Data, err := manager.ReadBase64(ctx, fileID)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
|
||||
// Return with data URI prefix if requested
|
||||
if useDataURI && contentType != "" {
|
||||
return fmt.Sprintf("data:%s;base64,%s", contentType, base64Data)
|
||||
}
|
||||
|
||||
return base64Data
|
||||
}
|
||||
|
||||
// readFilePathAsBase64 reads a file from fs data and returns Base64 encoded content
|
||||
// Returns empty string if file doesn't exist or can't be read
|
||||
// If dataURI is true, returns data URI format with mime type detection
|
||||
func readFilePathAsBase64(path string, dataURI bool) string {
|
||||
// Check if path looks like a file path (contains / or \)
|
||||
if !strings.Contains(path, "/") && !strings.Contains(path, "\\") {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try to get fs data
|
||||
dataFS, err := fs.Get("data")
|
||||
if err != nil || dataFS == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
exists, err := dataFS.Exists(path)
|
||||
if err != nil || !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Read file content
|
||||
content, err := dataFS.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Encode to Base64
|
||||
base64Str := base64.StdEncoding.EncodeToString(content)
|
||||
|
||||
// Return with data URI prefix if requested
|
||||
if dataURI {
|
||||
// Detect content type from file extension or content
|
||||
contentType := detectContentType(path, content)
|
||||
if contentType != "" {
|
||||
return fmt.Sprintf("data:%s;base64,%s", contentType, base64Str)
|
||||
}
|
||||
}
|
||||
|
||||
return base64Str
|
||||
}
|
||||
|
||||
// detectContentType detects the MIME type from file path and content
|
||||
func detectContentType(path string, content []byte) string {
|
||||
// First try to get from file extension
|
||||
ext := filepath.Ext(path)
|
||||
if ext != "" {
|
||||
mimeType := mime.TypeByExtension(ext)
|
||||
if mimeType != "" {
|
||||
return mimeType
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to detecting from content (first 512 bytes)
|
||||
if len(content) > 0 {
|
||||
detectSize := len(content)
|
||||
if detectSize > 512 {
|
||||
detectSize = 512
|
||||
}
|
||||
return http.DetectContentType(content[:detectSize])
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetHeader gets the header from the file header and request header
|
||||
func GetHeader(requestHeader http.Header, fileHeader textproto.MIMEHeader, size int64) *FileHeader {
|
||||
|
||||
|
|
|
|||
284
data/bindata.go
284
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/yaoapp/kun/exception"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/kun/maps"
|
||||
"github.com/yaoapp/yao/attachment"
|
||||
"github.com/yaoapp/yao/messenger"
|
||||
messengertypes "github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
|
|
@ -1007,11 +1008,22 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
|||
}
|
||||
}
|
||||
|
||||
// Process team_logo if it's a wrapper - use Data URI format for direct display in img src
|
||||
teamLogo := toString(team["logo"])
|
||||
if teamLogo != "" {
|
||||
teamLogo = attachment.Base64(ctx, teamLogo, true)
|
||||
}
|
||||
|
||||
// Process inviter_info.picture if it's a wrapper - use Data URI format for direct display in img src
|
||||
if inviterInfo != nil && inviterInfo.Picture != "" {
|
||||
inviterInfo.Picture = attachment.Base64(ctx, inviterInfo.Picture, true)
|
||||
}
|
||||
|
||||
// Build public response (exclude sensitive data like IDs)
|
||||
publicResponse := &PublicInvitationResponse{
|
||||
InvitationID: toString(invitationData["invitation_id"]),
|
||||
TeamName: toString(team["name"]),
|
||||
TeamLogo: toString(team["logo"]),
|
||||
TeamLogo: teamLogo,
|
||||
TeamDescription: toString(team["description"]),
|
||||
RoleLabel: roleLabel,
|
||||
Status: toString(invitationData["status"]),
|
||||
|
|
|
|||
|
|
@ -356,9 +356,15 @@
|
|||
"indexes": [
|
||||
{
|
||||
"name": "idx_team_user_unique",
|
||||
"columns": ["team_id", "user_id", "invitation_id"],
|
||||
"columns": ["team_id", "user_id"],
|
||||
"type": "unique",
|
||||
"comment": "Unique constraint: one user can have only one membership per team, with unique invitation_id for pending invitations"
|
||||
"comment": "Unique constraint: one user can have only one membership per team"
|
||||
},
|
||||
{
|
||||
"name": "idx_team_invitation_unique",
|
||||
"columns": ["team_id", "invitation_id"],
|
||||
"type": "unique",
|
||||
"comment": "Unique constraint: one invitation_id per team (for pending invitations)"
|
||||
},
|
||||
{
|
||||
"name": "idx_team_email",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue