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:
parent
60fef5744a
commit
4170fbd13b
6 changed files with 477 additions and 142 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
284
data/bindata.go
284
data/bindata.go
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue