Implement attachment wrapper parsing and Base64 conversion in attachment manager
- Added `Parse` function to handle attachment wrapper strings, extracting uploader name and file ID. - Introduced `Base64` function to convert attachment wrappers to Base64 format, with optional data URI support. - Enhanced `readFilePathAsBase64` to read files from the filesystem and return Base64 encoded content. - Updated `teamInvitationGetPublic` to process team logos and inviter pictures as Base64 for direct display in the response.
This commit is contained in:
parent
538f76e317
commit
0ee38a1cb3
4 changed files with 306 additions and 145 deletions
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/fs"
|
||||||
"github.com/yaoapp/gou/model"
|
"github.com/yaoapp/gou/model"
|
||||||
"github.com/yaoapp/yao/attachment/local"
|
"github.com/yaoapp/yao/attachment/local"
|
||||||
"github.com/yaoapp/yao/attachment/s3"
|
"github.com/yaoapp/yao/attachment/s3"
|
||||||
|
|
@ -41,6 +42,148 @@ type UploadChunk struct {
|
||||||
TotalChunks int64
|
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
|
// GetHeader gets the header from the file header and request header
|
||||||
func GetHeader(requestHeader http.Header, fileHeader textproto.MIMEHeader, size int64) *FileHeader {
|
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/exception"
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/kun/maps"
|
"github.com/yaoapp/kun/maps"
|
||||||
|
"github.com/yaoapp/yao/attachment"
|
||||||
"github.com/yaoapp/yao/messenger"
|
"github.com/yaoapp/yao/messenger"
|
||||||
messengertypes "github.com/yaoapp/yao/messenger/types"
|
messengertypes "github.com/yaoapp/yao/messenger/types"
|
||||||
"github.com/yaoapp/yao/openapi/oauth"
|
"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)
|
// Build public response (exclude sensitive data like IDs)
|
||||||
publicResponse := &PublicInvitationResponse{
|
publicResponse := &PublicInvitationResponse{
|
||||||
InvitationID: toString(invitationData["invitation_id"]),
|
InvitationID: toString(invitationData["invitation_id"]),
|
||||||
TeamName: toString(team["name"]),
|
TeamName: toString(team["name"]),
|
||||||
TeamLogo: toString(team["logo"]),
|
TeamLogo: teamLogo,
|
||||||
TeamDescription: toString(team["description"]),
|
TeamDescription: toString(team["description"]),
|
||||||
RoleLabel: roleLabel,
|
RoleLabel: roleLabel,
|
||||||
Status: toString(invitationData["status"]),
|
Status: toString(invitationData["status"]),
|
||||||
|
|
|
||||||
|
|
@ -356,9 +356,15 @@
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{
|
{
|
||||||
"name": "idx_team_user_unique",
|
"name": "idx_team_user_unique",
|
||||||
"columns": ["team_id", "user_id", "invitation_id"],
|
"columns": ["team_id", "user_id"],
|
||||||
"type": "unique",
|
"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",
|
"name": "idx_team_email",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue