Add text content storage functionality to attachment manager

- Implemented `GetText` and `SaveText` methods in the attachment manager to handle storing and retrieving parsed text content from files.
- Enhanced the `manager_test.go` with comprehensive tests for various scenarios, including saving, updating, and retrieving text, as well as handling non-existent file IDs.
- Updated the README.md to document the new text content storage features, providing examples for saving and retrieving parsed text.
- Modified the attachment model to include a new `content` field for storing text content, improving the overall functionality of the attachment management system.
This commit is contained in:
Max 2025-12-03 18:18:46 +08:00
parent 60fef5744a
commit 4170fbd13b
6 changed files with 477 additions and 142 deletions

View file

@ -659,6 +659,103 @@ case "upload_failed":
} }
``` ```
## Text Content Storage
The attachment package supports storing parsed text content extracted from files (e.g., from PDFs, Word documents, or image OCR). This is useful for building search indexes or providing text-based previews.
### Saving Parsed Text Content
Use `SaveText` to store the extracted text content for a file:
```go
// Upload a PDF file
file, err := manager.Upload(ctx, fileHeader, reader, option)
if err != nil {
return err
}
// Extract text from the PDF (using your preferred library)
parsedText := extractTextFromPDF(file.ID)
// Save the parsed text to the attachment record
err = manager.SaveText(ctx, file.ID, parsedText)
if err != nil {
return fmt.Errorf("failed to save text content: %w", err)
}
```
### Retrieving Parsed Text Content
Use `GetText` to retrieve the stored text content:
```go
// Get the parsed text content
text, err := manager.GetText(ctx, file.ID)
if err != nil {
return fmt.Errorf("failed to get text content: %w", err)
}
if text == "" {
fmt.Println("No text content available for this file")
} else {
fmt.Printf("Text content (%d characters): %s\n", len(text), text)
}
```
### Example: Complete Text Processing Workflow
```go
// 1. Upload file
file, err := manager.Upload(ctx, fileHeader, reader, option)
if err != nil {
return err
}
// 2. Process file based on content type
var parsedText string
switch {
case strings.HasPrefix(file.ContentType, "image/"):
// Use OCR to extract text from image
parsedText, err = performOCR(file.ID)
case file.ContentType == "application/pdf":
// Extract text from PDF
parsedText, err = extractPDFText(file.ID)
case strings.Contains(file.ContentType, "wordprocessingml"):
// Extract text from Word document
parsedText, err = extractWordText(file.ID)
}
if err != nil {
return fmt.Errorf("failed to extract text: %w", err)
}
// 3. Save the extracted text
if parsedText != "" {
err = manager.SaveText(ctx, file.ID, parsedText)
if err != nil {
return fmt.Errorf("failed to save text: %w", err)
}
}
// 4. Later, retrieve the text for search or display
savedText, err := manager.GetText(ctx, file.ID)
if err != nil {
return err
}
fmt.Printf("Retrieved text: %s\n", savedText)
```
### Text Content Features
- **Storage**: Text content is stored in the `content` field (longText type) of the attachment record
- **Size**: Supports very large text content (up to 4GB with longText type)
- **Update**: Text content can be updated at any time using `SaveText`
- **Clear**: Set text to empty string to clear the content
- **Retrieval**: Returns empty string if no text content has been saved
#### `RegisterDefault(name string) (*Manager, error)` #### `RegisterDefault(name string) (*Manager, error)`
Registers a default attachment manager with sensible defaults for common file types. Registers a default attachment manager with sensible defaults for common file types.

View file

@ -1322,3 +1322,73 @@ func (manager Manager) getStoragePathFromDatabase(ctx context.Context, fileID st
return "", fmt.Errorf("invalid storage path for file ID: %s", fileID) return "", fmt.Errorf("invalid storage path for file ID: %s", fileID)
} }
// GetText retrieves the parsed text content for a file by its ID
// Returns the text content stored in the 'content' field of the attachment
func (manager Manager) GetText(ctx context.Context, fileID string) (string, error) {
m := model.Select("__yao.attachment")
records, err := m.Get(model.QueryParam{
Select: []interface{}{"content"},
Wheres: []model.QueryWhere{
{Column: "file_id", Value: fileID},
},
Limit: 1,
})
if err != nil {
return "", fmt.Errorf("failed to query text content: %w", err)
}
if len(records) == 0 {
return "", fmt.Errorf("file not found: %s", fileID)
}
// Handle content field - it may be nil, string, or other types
if content, ok := records[0]["content"].(string); ok {
return content, nil
}
// If content is nil or not a string, return empty string
return "", nil
}
// SaveText saves the parsed text content for a file by its ID
// Updates the 'content' field in the attachment record
func (manager Manager) SaveText(ctx context.Context, fileID string, text string) error {
m := model.Select("__yao.attachment")
// Check if record exists first
records, err := m.Get(model.QueryParam{
Select: []interface{}{"file_id"},
Wheres: []model.QueryWhere{
{Column: "file_id", Value: fileID},
},
Limit: 1,
})
if err != nil {
return fmt.Errorf("failed to check file existence: %w", err)
}
if len(records) == 0 {
return fmt.Errorf("file not found: %s", fileID)
}
// Update the content field
updateData := map[string]interface{}{
"content": text,
}
_, err = m.UpdateWhere(model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "file_id", Value: fileID},
},
}, updateData)
if err != nil {
return fmt.Errorf("failed to save text content: %w", err)
}
return nil
}

View file

@ -1477,3 +1477,158 @@ func TestManagerLocalPath_ValidationFlow(t *testing.T) {
t.Logf("Warning: Failed to delete test file: %v", err) t.Logf("Warning: Failed to delete test file: %v", err)
} }
} }
func TestGetTextAndSaveText(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
manager, err := RegisterDefault("test-text-content")
if err != nil {
t.Fatalf("Failed to register manager: %v", err)
}
// Upload a test file
content := "This is a test file for text content storage"
reader := strings.NewReader(content)
fileHeader := &FileHeader{
FileHeader: &multipart.FileHeader{
Filename: "test-text.txt",
Size: int64(len(content)),
Header: make(map[string][]string),
},
}
fileHeader.Header.Set("Content-Type", "text/plain")
option := UploadOption{
Groups: []string{"test"},
OriginalFilename: "test-text.txt",
}
file, err := manager.Upload(context.Background(), fileHeader, reader, option)
if err != nil {
t.Fatalf("Failed to upload file: %v", err)
}
// Test 1: GetText on file without saved text (should return empty)
t.Run("GetTextEmpty", func(t *testing.T) {
text, err := manager.GetText(context.Background(), file.ID)
if err != nil {
t.Fatalf("Failed to get text: %v", err)
}
if text != "" {
t.Errorf("Expected empty text, got: %s", text)
}
})
// Test 2: SaveText and verify
t.Run("SaveTextAndVerify", func(t *testing.T) {
parsedText := "This is the parsed text content from the file. It could be extracted from PDF, Word, or image OCR."
err := manager.SaveText(context.Background(), file.ID, parsedText)
if err != nil {
t.Fatalf("Failed to save text: %v", err)
}
// Retrieve the saved text
retrievedText, err := manager.GetText(context.Background(), file.ID)
if err != nil {
t.Fatalf("Failed to get saved text: %v", err)
}
if retrievedText != parsedText {
t.Errorf("Text mismatch. Expected: %s, Got: %s", parsedText, retrievedText)
}
t.Logf("Successfully saved and retrieved text content (%d characters)", len(retrievedText))
})
// Test 3: Update existing text
t.Run("UpdateText", func(t *testing.T) {
updatedText := "This is the updated parsed text content with additional information."
err := manager.SaveText(context.Background(), file.ID, updatedText)
if err != nil {
t.Fatalf("Failed to update text: %v", err)
}
retrievedText, err := manager.GetText(context.Background(), file.ID)
if err != nil {
t.Fatalf("Failed to get updated text: %v", err)
}
if retrievedText != updatedText {
t.Errorf("Updated text mismatch. Expected: %s, Got: %s", updatedText, retrievedText)
}
})
// Test 4: Save long text content (simulating large document parsing)
t.Run("SaveLongText", func(t *testing.T) {
// Generate a large text content (10KB)
longText := strings.Repeat("This is a long text content that simulates parsing from a large document like PDF or Word. ", 100)
err := manager.SaveText(context.Background(), file.ID, longText)
if err != nil {
t.Fatalf("Failed to save long text: %v", err)
}
retrievedText, err := manager.GetText(context.Background(), file.ID)
if err != nil {
t.Fatalf("Failed to get long text: %v", err)
}
if retrievedText != longText {
t.Errorf("Long text mismatch. Expected length: %d, Got: %d", len(longText), len(retrievedText))
}
t.Logf("Successfully saved and retrieved long text content (%d characters)", len(retrievedText))
})
// Test 5: GetText with non-existent file ID
t.Run("GetTextNonExistent", func(t *testing.T) {
_, err := manager.GetText(context.Background(), "non-existent-id")
if err == nil {
t.Error("Expected error for non-existent file ID")
}
if !strings.Contains(err.Error(), "file not found") {
t.Errorf("Expected 'file not found' error, got: %s", err.Error())
}
})
// Test 6: SaveText with non-existent file ID
t.Run("SaveTextNonExistent", func(t *testing.T) {
err := manager.SaveText(context.Background(), "non-existent-id", "some text")
if err == nil {
t.Error("Expected error for non-existent file ID")
}
if !strings.Contains(err.Error(), "file not found") {
t.Errorf("Expected 'file not found' error, got: %s", err.Error())
}
})
// Test 7: Save empty text (clear content)
t.Run("SaveEmptyText", func(t *testing.T) {
err := manager.SaveText(context.Background(), file.ID, "")
if err != nil {
t.Fatalf("Failed to save empty text: %v", err)
}
retrievedText, err := manager.GetText(context.Background(), file.ID)
if err != nil {
t.Fatalf("Failed to get empty text: %v", err)
}
if retrievedText != "" {
t.Errorf("Expected empty text, got: %s", retrievedText)
}
})
// Clean up
err = manager.Delete(context.Background(), file.ID)
if err != nil {
t.Logf("Warning: Failed to delete test file: %v", err)
}
}

View file

@ -47,6 +47,12 @@ type FileManager interface {
// LocalPath gets the local path of the file // LocalPath gets the local path of the file
LocalPath(ctx context.Context, fileID string) (string, string, error) LocalPath(ctx context.Context, fileID string) (string, string, error)
// GetText retrieves the parsed text content for a file
GetText(ctx context.Context, fileID string) (string, error)
// SaveText saves the parsed text content for a file
SaveText(ctx context.Context, fileID string, text string) error
} }
// File the file // File the file

File diff suppressed because it is too large Load diff

View file

@ -45,6 +45,13 @@
"nullable": false, "nullable": false,
"index": true "index": true
}, },
{
"name": "content",
"type": "longText",
"label": "Content",
"comment": "Parsed text content from image, pdf, word and other file types",
"nullable": true
},
{ {
"name": "name", "name": "name",
"type": "string", "type": "string",