Enhance Attachment Package with Process API and Initialization
- Added an initialization step in the Load function to register attachment processes, improving the setup for file management. - Expanded the README to include detailed documentation on the new Process API, outlining available processes, permission models, and usage examples for better developer guidance.
This commit is contained in:
parent
6fa808c875
commit
042066e6b3
4 changed files with 1949 additions and 0 deletions
|
|
@ -790,3 +790,252 @@ fmt.Printf("Retrieved text: %s\n", savedText)
|
|||
#### `RegisterDefault(name string) (*Manager, error)`
|
||||
|
||||
Registers a default attachment manager with sensible defaults for common file types.
|
||||
|
||||
## Process API
|
||||
|
||||
The attachment package provides a set of Yao Process APIs for file management with built-in permission support.
|
||||
|
||||
### Available Processes
|
||||
|
||||
| Process | Description |
|
||||
|---------|-------------|
|
||||
| `attachment.Save` | Save a file from base64 data URI |
|
||||
| `attachment.Read` | Read file content as base64 data URI |
|
||||
| `attachment.Info` | Get file metadata |
|
||||
| `attachment.List` | List files with pagination and filtering |
|
||||
| `attachment.Delete` | Delete a file |
|
||||
| `attachment.Exists` | Check if file exists |
|
||||
| `attachment.URL` | Get file URL |
|
||||
| `attachment.SaveText` | Save parsed text content for a file |
|
||||
| `attachment.GetText` | Get parsed text content for a file |
|
||||
|
||||
### Permission Model
|
||||
|
||||
The Process API integrates with Yao's `process.Authorized` mechanism:
|
||||
|
||||
- **Authorized Info**: Reads `UserID`, `TeamID`, `TenantID` from `process.Authorized` (set by OAuth guard)
|
||||
- **Auto Permission Storage**: On save, automatically stores `__yao_created_by`, `__yao_team_id`, `__yao_tenant_id` from `process.Authorized`
|
||||
- **Data Constraints**: Respects `Constraints.OwnerOnly` and `Constraints.TeamOnly` from ACL enforcement
|
||||
- **Owner Access**: When `OwnerOnly` is set, only file creator (`__yao_created_by`) can access their files
|
||||
- **Team Access**: When `TeamOnly` is set, team members can access files with `share: "team"`
|
||||
- **Public Access**: Files with `public: true` are readable by everyone regardless of constraints
|
||||
- **No Constraints**: If no constraints are set, all authenticated users can access all files
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### JavaScript (Yao Scripts)
|
||||
|
||||
```javascript
|
||||
// Save a file from base64 data URI
|
||||
const file = Process("attachment.Save", "default",
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...",
|
||||
"photo.png",
|
||||
{ share: "team" }
|
||||
);
|
||||
console.log("Saved file ID:", file.file_id);
|
||||
|
||||
// Save text file
|
||||
const textFile = Process("attachment.Save", "default",
|
||||
"data:text/plain;base64,SGVsbG8gV29ybGQh",
|
||||
"hello.txt"
|
||||
);
|
||||
|
||||
// Read file content as data URI
|
||||
const dataURI = Process("attachment.Read", "default", file.file_id);
|
||||
// Returns: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."
|
||||
|
||||
// Get file info
|
||||
const info = Process("attachment.Info", "default", file.file_id);
|
||||
|
||||
// List files with pagination
|
||||
const result = Process("attachment.List", "default", {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
filters: { status: "uploaded", content_type: "image/*" },
|
||||
order_by: "created_at desc"
|
||||
});
|
||||
|
||||
// Check if file exists
|
||||
const exists = Process("attachment.Exists", "default", file.file_id);
|
||||
|
||||
// Get file URL
|
||||
const url = Process("attachment.URL", "default", file.file_id);
|
||||
|
||||
// Save parsed text content (e.g., OCR result, PDF text)
|
||||
Process("attachment.SaveText", "default", file.file_id, "Extracted text content...");
|
||||
|
||||
// Get text content (preview by default)
|
||||
const preview = Process("attachment.GetText", "default", file.file_id);
|
||||
|
||||
// Get full text content
|
||||
const fullText = Process("attachment.GetText", "default", file.file_id, true);
|
||||
|
||||
// Delete file
|
||||
Process("attachment.Delete", "default", file.file_id);
|
||||
```
|
||||
|
||||
#### Flow DSL
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Save Image",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "save",
|
||||
"process": "attachment.Save",
|
||||
"args": [
|
||||
"default",
|
||||
"{{$in.dataURI}}",
|
||||
"{{$in.filename}}",
|
||||
{ "share": "team" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"output": "{{$res.save}}"
|
||||
}
|
||||
```
|
||||
|
||||
### Process Reference
|
||||
|
||||
#### `attachment.Save`
|
||||
|
||||
Save a file from base64 data URI. Automatically parses content type from data URI header and stores permission fields from `process.Authorized`.
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `content` (string) - Base64 data URI (e.g., `"data:image/png;base64,xxxx"`) or plain base64
|
||||
3. `filename` (string, optional) - Original filename (auto-generated if not provided)
|
||||
4. `option` (map, optional) - Upload options:
|
||||
- `groups` ([]string) - Directory groups for organization
|
||||
- `gzip` (bool) - Enable gzip compression
|
||||
- `compress_image` (bool) - Enable image compression
|
||||
- `compress_size` (int) - Target image size in pixels
|
||||
- `public` (bool) - Make file publicly accessible
|
||||
- `share` (string) - Share scope: "private" or "team"
|
||||
|
||||
**Returns:** `*File` - Saved file information
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// With data URI (auto-detect content type)
|
||||
Process("attachment.Save", "default", "data:image/png;base64,iVBORw0KGgo...", "photo.png")
|
||||
|
||||
// With plain base64 (defaults to application/octet-stream)
|
||||
Process("attachment.Save", "default", "SGVsbG8gV29ybGQh", "hello.txt")
|
||||
|
||||
// With options
|
||||
Process("attachment.Save", "default", "data:application/pdf;base64,...", "doc.pdf", {
|
||||
groups: ["documents"],
|
||||
share: "team",
|
||||
public: false
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.Read`
|
||||
|
||||
Read file content as base64 data URI.
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `fileID` (string) - The file ID
|
||||
|
||||
**Returns:** `string` - Base64 data URI (e.g., `"data:image/png;base64,xxxx"`)
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
const dataURI = Process("attachment.Read", "default", "abc123")
|
||||
// Returns: "data:image/png;base64,iVBORw0KGgo..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.Info`
|
||||
|
||||
Get file metadata.
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `fileID` (string) - The file ID
|
||||
|
||||
**Returns:** `*File` - File metadata
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.List`
|
||||
|
||||
List files with pagination and filtering.
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `option` (map, optional) - List options:
|
||||
- `page` (int) - Page number (default: 1)
|
||||
- `page_size` (int) - Items per page (default: 20, max: 100)
|
||||
- `filters` (map) - Filter conditions (e.g., `{"status": "uploaded"}`)
|
||||
- `order_by` (string) - Sort order (e.g., "created_at desc")
|
||||
- `select` ([]string) - Fields to return
|
||||
|
||||
**Returns:** `*ListResult` - Paginated file list
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.Delete`
|
||||
|
||||
Delete a file. Requires write permission (owner only).
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `fileID` (string) - The file ID
|
||||
|
||||
**Returns:** `bool` - Success status
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.Exists`
|
||||
|
||||
Check if a file exists.
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `fileID` (string) - The file ID
|
||||
|
||||
**Returns:** `bool` - Whether file exists
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.URL`
|
||||
|
||||
Get the URL of a file.
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `fileID` (string) - The file ID
|
||||
|
||||
**Returns:** `string` - File URL
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.SaveText`
|
||||
|
||||
Save parsed text content for a file (e.g., OCR result, PDF extracted text).
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `fileID` (string) - The file ID
|
||||
3. `text` (string) - Text content to save
|
||||
|
||||
**Returns:** `bool` - Success status
|
||||
|
||||
---
|
||||
|
||||
#### `attachment.GetText`
|
||||
|
||||
Get parsed text content for a file.
|
||||
|
||||
**Arguments:**
|
||||
1. `uploaderID` (string) - The uploader/manager ID
|
||||
2. `fileID` (string) - The file ID
|
||||
3. `fullContent` (bool, optional) - Whether to get full content (default: false, returns preview)
|
||||
|
||||
**Returns:** `string` - Text content
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ var systemUploaders = map[string]string{
|
|||
|
||||
// Load load uploaders
|
||||
func Load(cfg config.Config) error {
|
||||
// Register attachment processes
|
||||
Init()
|
||||
|
||||
messages := []string{}
|
||||
|
||||
// Load system uploaders
|
||||
|
|
|
|||
653
attachment/process.go
Normal file
653
attachment/process.go
Normal file
|
|
@ -0,0 +1,653 @@
|
|||
package attachment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/yaoapp/gou/model"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/kun/any"
|
||||
"github.com/yaoapp/kun/maps"
|
||||
)
|
||||
|
||||
// Init registers all attachment processes
|
||||
func Init() {
|
||||
process.RegisterGroup("attachment", map[string]process.Handler{
|
||||
"Save": processSave,
|
||||
"Read": processRead,
|
||||
"Info": processInfo,
|
||||
"List": processList,
|
||||
"Delete": processDelete,
|
||||
"Exists": processExists,
|
||||
"URL": processURL,
|
||||
"SaveText": processSaveText,
|
||||
"GetText": processGetText,
|
||||
})
|
||||
}
|
||||
|
||||
// processSave saves a file from base64 data URI
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - content: string - base64 data URI (e.g., "data:image/png;base64,xxxx") or plain base64
|
||||
// - filename: string (optional) - original filename
|
||||
// - option: map (optional) - upload options (groups, gzip, compress_image, public, share)
|
||||
//
|
||||
// Returns: *File - uploaded file info
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// Process("attachment.Save", "default", "data:image/png;base64,iVBORw0KGgo...", "photo.png")
|
||||
// Process("attachment.Save", "default", "data:text/plain;base64,SGVsbG8=", "hello.txt", {"share": "team"})
|
||||
func processSave(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(2)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
content := p.ArgsString(1)
|
||||
|
||||
// Get manager
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
// Parse data URI and decode content
|
||||
contentType, data, err := parseDataURI(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse content: %v", err)
|
||||
}
|
||||
|
||||
// Get filename from args or generate from content type
|
||||
filename := ""
|
||||
if p.NumOfArgs() > 2 {
|
||||
filename = p.ArgsString(2)
|
||||
}
|
||||
if filename == "" {
|
||||
filename = generateFilename(contentType)
|
||||
}
|
||||
|
||||
// Create file header
|
||||
header := createFileHeader(filename, contentType, int64(len(data)))
|
||||
|
||||
// Create upload options
|
||||
option := createUploadOption(p, filename)
|
||||
|
||||
// Upload
|
||||
ctx := context.Background()
|
||||
file, err := manager.Upload(ctx, header, strings.NewReader(string(data)), option)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save file: %v", err)
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
// processRead reads file content as base64 data URI
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - fileID: string - the file ID
|
||||
//
|
||||
// Returns: string - base64 data URI (e.g., "data:image/png;base64,xxxx")
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// const dataURI = Process("attachment.Read", "default", "abc123")
|
||||
func processRead(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(2)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
fileID := p.ArgsString(1)
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get file info for content type and permission check
|
||||
fileInfo, err := manager.Info(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %v", err)
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if err := checkFilePermission(p, fileInfo, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read content as base64
|
||||
base64Data, err := manager.ReadBase64(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %v", err)
|
||||
}
|
||||
|
||||
// Return as data URI
|
||||
return fmt.Sprintf("data:%s;base64,%s", fileInfo.ContentType, base64Data)
|
||||
}
|
||||
|
||||
// processInfo gets file information
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - fileID: string - the file ID
|
||||
//
|
||||
// Returns: *File - file info
|
||||
func processInfo(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(2)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
fileID := p.ArgsString(1)
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
fileInfo, err := manager.Info(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %v", err)
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if err := checkFilePermission(p, fileInfo, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fileInfo
|
||||
}
|
||||
|
||||
// processList lists files with pagination and filtering
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - option: map (optional) - list options (page, page_size, filters, order_by, select)
|
||||
//
|
||||
// Returns: *ListResult - paginated file list
|
||||
func processList(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(1)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
// Parse list options
|
||||
listOption := ListOption{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
if p.NumOfArgs() > 1 {
|
||||
optionRaw := p.ArgsMap(1)
|
||||
option := maps.MapOf(optionRaw).Dot()
|
||||
|
||||
if page := any.Of(option.Get("page")).CInt(); page > 0 {
|
||||
listOption.Page = page
|
||||
}
|
||||
if pageSize := any.Of(option.Get("page_size")).CInt(); pageSize > 0 && pageSize <= 100 {
|
||||
listOption.PageSize = pageSize
|
||||
}
|
||||
if filters, ok := option.Get("filters").(map[string]interface{}); ok {
|
||||
listOption.Filters = filters
|
||||
}
|
||||
if orderBy, ok := option.Get("order_by").(string); ok {
|
||||
listOption.OrderBy = orderBy
|
||||
}
|
||||
if selectFields, ok := option.Get("select").([]interface{}); ok {
|
||||
for _, field := range selectFields {
|
||||
if f, ok := field.(string); ok {
|
||||
listOption.Select = append(listOption.Select, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always filter by uploader
|
||||
if listOption.Filters == nil {
|
||||
listOption.Filters = make(map[string]interface{})
|
||||
}
|
||||
listOption.Filters["uploader"] = uploaderID
|
||||
|
||||
// Add permission-based filtering
|
||||
listOption.Wheres = append(listOption.Wheres, model.QueryWhere{
|
||||
Column: "uploader",
|
||||
Value: uploaderID,
|
||||
})
|
||||
listOption.Wheres = append(listOption.Wheres, buildPermissionWheres(p)...)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := manager.List(ctx, listOption)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list files: %v", err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// processDelete deletes a file
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - fileID: string - the file ID
|
||||
//
|
||||
// Returns: bool - success
|
||||
func processDelete(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(2)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
fileID := p.ArgsString(1)
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get file info first
|
||||
fileInfo, err := manager.Info(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %v", err)
|
||||
}
|
||||
|
||||
// Check write permission
|
||||
if err := checkFilePermission(p, fileInfo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete file
|
||||
if err := manager.Delete(ctx, fileID); err != nil {
|
||||
return fmt.Errorf("failed to delete file: %v", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// processExists checks if file exists
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - fileID: string - the file ID
|
||||
//
|
||||
// Returns: bool
|
||||
func processExists(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(2)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
fileID := p.ArgsString(1)
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
return manager.Exists(ctx, fileID)
|
||||
}
|
||||
|
||||
// processURL gets file URL
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - fileID: string - the file ID
|
||||
//
|
||||
// Returns: string - file URL
|
||||
func processURL(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(2)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
fileID := p.ArgsString(1)
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get file info for permission check
|
||||
fileInfo, err := manager.Info(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %v", err)
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if err := checkFilePermission(p, fileInfo, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return manager.storage.URL(ctx, fileID)
|
||||
}
|
||||
|
||||
// processSaveText saves parsed text content for a file
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - fileID: string - the file ID
|
||||
// - text: string - the text content to save
|
||||
//
|
||||
// Returns: bool - success
|
||||
func processSaveText(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(3)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
fileID := p.ArgsString(1)
|
||||
text := p.ArgsString(2)
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get file info first to check write permission
|
||||
fileInfo, err := manager.Info(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %v", err)
|
||||
}
|
||||
|
||||
// Check write permission
|
||||
if err := checkFilePermission(p, fileInfo, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := manager.SaveText(ctx, fileID, text); err != nil {
|
||||
return fmt.Errorf("failed to save text: %v", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// processGetText gets parsed text content for a file
|
||||
// Args:
|
||||
// - uploaderID: string - the uploader/manager ID
|
||||
// - fileID: string - the file ID
|
||||
// - fullContent: bool (optional) - whether to get full content (default: false, returns preview)
|
||||
//
|
||||
// Returns: string - text content
|
||||
func processGetText(p *process.Process) interface{} {
|
||||
p.ValidateArgNums(2)
|
||||
|
||||
uploaderID := p.ArgsString(0)
|
||||
fileID := p.ArgsString(1)
|
||||
|
||||
fullContent := false
|
||||
if p.NumOfArgs() > 2 {
|
||||
fullContent = p.ArgsBool(2)
|
||||
}
|
||||
|
||||
manager, exists := Managers[uploaderID]
|
||||
if !exists {
|
||||
return fmt.Errorf("uploader not found: %s", uploaderID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get file info for permission check
|
||||
fileInfo, err := manager.Info(ctx, fileID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %v", err)
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if err := checkFilePermission(p, fileInfo, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
text, err := manager.GetText(ctx, fileID, fullContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get text: %v", err)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// ============ Helper Functions ============
|
||||
|
||||
// parseDataURI parses a data URI or plain base64 string
|
||||
// Returns content type, decoded data, and error
|
||||
func parseDataURI(content string) (string, []byte, error) {
|
||||
contentType := "application/octet-stream"
|
||||
|
||||
// Handle data URI format: data:image/png;base64,xxxxx
|
||||
if strings.HasPrefix(content, "data:") {
|
||||
// Split by comma to get the data part
|
||||
parts := strings.SplitN(content, ",", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", nil, fmt.Errorf("invalid data URI format")
|
||||
}
|
||||
|
||||
// Parse the header: data:image/png;base64
|
||||
header := parts[0]
|
||||
content = parts[1]
|
||||
|
||||
// Extract content type from header
|
||||
header = strings.TrimPrefix(header, "data:")
|
||||
headerParts := strings.Split(header, ";")
|
||||
if len(headerParts) > 0 && headerParts[0] != "" {
|
||||
contentType = headerParts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Decode base64
|
||||
data, err := base64.StdEncoding.DecodeString(content)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to decode base64: %v", err)
|
||||
}
|
||||
|
||||
return contentType, data, nil
|
||||
}
|
||||
|
||||
// generateFilename generates a filename based on content type
|
||||
func generateFilename(contentType string) string {
|
||||
// Get extension from content type
|
||||
exts, err := mime.ExtensionsByType(contentType)
|
||||
if err == nil && len(exts) > 0 {
|
||||
return "file" + exts[0]
|
||||
}
|
||||
|
||||
// Fallback for common types
|
||||
switch contentType {
|
||||
case "image/png":
|
||||
return "file.png"
|
||||
case "image/jpeg":
|
||||
return "file.jpg"
|
||||
case "image/gif":
|
||||
return "file.gif"
|
||||
case "image/webp":
|
||||
return "file.webp"
|
||||
case "text/plain":
|
||||
return "file.txt"
|
||||
case "application/pdf":
|
||||
return "file.pdf"
|
||||
case "application/json":
|
||||
return "file.json"
|
||||
default:
|
||||
return "file.bin"
|
||||
}
|
||||
}
|
||||
|
||||
// createUploadOption creates UploadOption from process args
|
||||
func createUploadOption(p *process.Process, filename string) UploadOption {
|
||||
option := UploadOption{
|
||||
OriginalFilename: filename,
|
||||
}
|
||||
|
||||
// Parse option from fourth argument if provided
|
||||
if p.NumOfArgs() > 3 {
|
||||
optionRaw := p.ArgsMap(3)
|
||||
optionMap := maps.MapOf(optionRaw).Dot()
|
||||
|
||||
// Groups
|
||||
if groups, ok := optionMap.Get("groups").([]interface{}); ok {
|
||||
for _, g := range groups {
|
||||
if gs, ok := g.(string); ok {
|
||||
option.Groups = append(option.Groups, gs)
|
||||
}
|
||||
}
|
||||
} else if groupsStr, ok := optionMap.Get("groups").(string); ok {
|
||||
option.Groups = strings.Split(groupsStr, ",")
|
||||
for i := range option.Groups {
|
||||
option.Groups[i] = strings.TrimSpace(option.Groups[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Gzip
|
||||
if gzip, ok := optionMap.Get("gzip").(bool); ok {
|
||||
option.Gzip = gzip
|
||||
}
|
||||
|
||||
// Compress image
|
||||
if compress, ok := optionMap.Get("compress_image").(bool); ok {
|
||||
option.CompressImage = compress
|
||||
}
|
||||
if size := any.Of(optionMap.Get("compress_size")).CInt(); size > 0 {
|
||||
option.CompressSize = size
|
||||
}
|
||||
|
||||
// Public/Share
|
||||
if public, ok := optionMap.Get("public").(bool); ok {
|
||||
option.Public = public
|
||||
}
|
||||
if share, ok := optionMap.Get("share").(string); ok {
|
||||
option.Share = share
|
||||
}
|
||||
}
|
||||
|
||||
// Set permission fields from process.Authorized
|
||||
if p.Authorized != nil {
|
||||
option.YaoCreatedBy = p.Authorized.UserID
|
||||
option.YaoTeamID = p.Authorized.TeamID
|
||||
option.YaoTenantID = p.Authorized.TenantID
|
||||
}
|
||||
|
||||
return option
|
||||
}
|
||||
|
||||
// createFileHeader creates a FileHeader from parameters
|
||||
func createFileHeader(filename, contentType string, size int64) *FileHeader {
|
||||
header := &multipart.FileHeader{
|
||||
Filename: filename,
|
||||
Size: size,
|
||||
Header: make(textproto.MIMEHeader),
|
||||
}
|
||||
header.Header.Set("Content-Type", contentType)
|
||||
|
||||
// Set extension from filename
|
||||
if ext := filepath.Ext(filename); ext != "" {
|
||||
header.Header.Set("Content-Extension", ext)
|
||||
}
|
||||
|
||||
return &FileHeader{FileHeader: header}
|
||||
}
|
||||
|
||||
// checkFilePermission checks if user has permission to access the file
|
||||
// readable: true for read permission, false for write permission
|
||||
func checkFilePermission(p *process.Process, fileInfo *File, readable bool) error {
|
||||
auth := p.Authorized
|
||||
|
||||
// No auth info - allow access (for non-authenticated operations)
|
||||
if auth == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// No constraints - allow access
|
||||
if !auth.Constraints.TeamOnly && !auth.Constraints.OwnerOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Public files are readable by everyone
|
||||
if readable && fileInfo.Public {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Combined Team and Owner permission validation
|
||||
if auth.Constraints.TeamOnly && auth.Constraints.OwnerOnly {
|
||||
if fileInfo.YaoCreatedBy == auth.UserID && fileInfo.YaoTeamID == auth.TeamID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Owner only permission validation
|
||||
if auth.Constraints.OwnerOnly {
|
||||
if fileInfo.YaoCreatedBy != "" && fileInfo.YaoCreatedBy == auth.UserID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Team only permission validation
|
||||
if auth.Constraints.TeamOnly {
|
||||
switch fileInfo.Share {
|
||||
case "team":
|
||||
if fileInfo.YaoTeamID == auth.TeamID {
|
||||
return nil
|
||||
}
|
||||
case "private":
|
||||
if fileInfo.YaoCreatedBy == auth.UserID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("forbidden: no permission to access file")
|
||||
}
|
||||
|
||||
// buildPermissionWheres builds where clauses for permission filtering
|
||||
func buildPermissionWheres(p *process.Process) []model.QueryWhere {
|
||||
auth := p.Authorized
|
||||
if auth == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// No constraints - no additional filtering needed
|
||||
if !auth.Constraints.TeamOnly && !auth.Constraints.OwnerOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
var wheres []model.QueryWhere
|
||||
|
||||
// Team only - User can access:
|
||||
// 1. Public files (public = true)
|
||||
// 2. Files in their team where:
|
||||
// - They uploaded the file (__yao_created_by matches)
|
||||
// - OR the file is shared with team (share = "team")
|
||||
if auth.Constraints.TeamOnly && auth.TeamID != "" {
|
||||
wheres = append(wheres, model.QueryWhere{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "public", Value: true, Method: "orwhere"},
|
||||
{Wheres: []model.QueryWhere{
|
||||
{Column: "__yao_team_id", Value: auth.TeamID},
|
||||
{Wheres: []model.QueryWhere{
|
||||
{Column: "__yao_created_by", Value: auth.UserID},
|
||||
{Column: "share", Value: "team", Method: "orwhere"},
|
||||
}},
|
||||
}, Method: "orwhere"},
|
||||
},
|
||||
})
|
||||
return wheres
|
||||
}
|
||||
|
||||
// Owner only - User can access:
|
||||
// 1. Public files (public = true)
|
||||
// 2. Files they uploaded where:
|
||||
// - __yao_team_id is null (not team files)
|
||||
// - __yao_created_by matches their user ID
|
||||
if auth.Constraints.OwnerOnly && auth.UserID != "" {
|
||||
wheres = append(wheres, model.QueryWhere{
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "public", Value: true, Method: "orwhere"},
|
||||
{Wheres: []model.QueryWhere{
|
||||
{Column: "__yao_team_id", OP: "null"},
|
||||
{Column: "__yao_created_by", Value: auth.UserID},
|
||||
}, Method: "orwhere"},
|
||||
},
|
||||
})
|
||||
return wheres
|
||||
}
|
||||
|
||||
return wheres
|
||||
}
|
||||
1044
attachment/process_test.go
Normal file
1044
attachment/process_test.go
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue