feat: implement file upload handler and related functionality
- Added file upload handler to process both multipart and raw file uploads. - Implemented parsing logic for upload requests, including handling file metadata. - Introduced SaveFileDecodedInput structure for handling decoded file uploads. - Created unit tests for file upload parsing and validation. feat: add metadata retry configuration and functionality - Introduced MetadataRetryConfig to the application configuration. - Implemented MetadataRetryer to handle retrying metadata extraction for thoughts. - Added new tool for retrying failed metadata extractions. - Updated thought metadata structure to include status and timestamps for metadata processing. fix: enhance metadata normalization and error handling - Updated metadata normalization functions to track status and errors. - Improved handling of metadata extraction failures during thought updates and captures. - Ensured that metadata status is correctly set during various operations. refactor: streamline file saving logic in FilesTool - Refactored Save method in FilesTool to utilize new SaveDecoded method. - Simplified project and thought ID resolution logic during file saving.
This commit is contained in:
@@ -30,6 +30,15 @@ type SaveFileInput struct {
|
||||
Project string `json:"project,omitempty" jsonschema:"optional project name or id when saving outside a linked thought"`
|
||||
}
|
||||
|
||||
type SaveFileDecodedInput struct {
|
||||
Name string
|
||||
Content []byte
|
||||
MediaType string
|
||||
Kind string
|
||||
ThoughtID string
|
||||
Project string
|
||||
}
|
||||
|
||||
type SaveFileOutput struct {
|
||||
File thoughttypes.StoredFile `json:"file"`
|
||||
}
|
||||
@@ -59,11 +68,6 @@ func NewFilesTool(db *store.DB, sessions *session.ActiveProjects) *FilesTool {
|
||||
}
|
||||
|
||||
func (t *FilesTool) Save(ctx context.Context, req *mcp.CallToolRequest, in SaveFileInput) (*mcp.CallToolResult, SaveFileOutput, error) {
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
return nil, SaveFileOutput{}, errInvalidInput("name is required")
|
||||
}
|
||||
|
||||
contentBase64, mediaTypeFromDataURL := splitDataURL(strings.TrimSpace(in.ContentBase64))
|
||||
if contentBase64 == "" {
|
||||
return nil, SaveFileOutput{}, errInvalidInput("content_base64 is required")
|
||||
@@ -73,66 +77,18 @@ func (t *FilesTool) Save(ctx context.Context, req *mcp.CallToolRequest, in SaveF
|
||||
if err != nil {
|
||||
return nil, SaveFileOutput{}, errInvalidInput("content_base64 must be valid base64")
|
||||
}
|
||||
if len(content) == 0 {
|
||||
return nil, SaveFileOutput{}, errInvalidInput("decoded file content must not be empty")
|
||||
}
|
||||
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return nil, SaveFileOutput{}, err
|
||||
}
|
||||
|
||||
var thoughtID *uuid.UUID
|
||||
var projectID = projectIDPtr(project)
|
||||
if rawThoughtID := strings.TrimSpace(in.ThoughtID); rawThoughtID != "" {
|
||||
parsedThoughtID, err := parseUUID(rawThoughtID)
|
||||
if err != nil {
|
||||
return nil, SaveFileOutput{}, err
|
||||
}
|
||||
thought, err := t.store.GetThought(ctx, parsedThoughtID)
|
||||
if err != nil {
|
||||
return nil, SaveFileOutput{}, err
|
||||
}
|
||||
thoughtID = &parsedThoughtID
|
||||
projectID = thought.ProjectID
|
||||
if project != nil && thought.ProjectID != nil && *thought.ProjectID != project.ID {
|
||||
return nil, SaveFileOutput{}, errInvalidInput("project does not match the linked thought's project")
|
||||
}
|
||||
}
|
||||
|
||||
mediaType := normalizeMediaType(strings.TrimSpace(in.MediaType), mediaTypeFromDataURL, content)
|
||||
kind := normalizeFileKind(strings.TrimSpace(in.Kind), mediaType)
|
||||
sum := sha256.Sum256(content)
|
||||
|
||||
file := thoughttypes.StoredFile{
|
||||
Name: name,
|
||||
MediaType: mediaType,
|
||||
Kind: kind,
|
||||
Encoding: "base64",
|
||||
SizeBytes: int64(len(content)),
|
||||
SHA256: hex.EncodeToString(sum[:]),
|
||||
out, err := t.SaveDecoded(ctx, req, SaveFileDecodedInput{
|
||||
Name: in.Name,
|
||||
Content: content,
|
||||
ProjectID: projectID,
|
||||
}
|
||||
if thoughtID != nil {
|
||||
file.ThoughtID = thoughtID
|
||||
}
|
||||
|
||||
created, err := t.store.InsertStoredFile(ctx, file)
|
||||
MediaType: firstNonEmpty(strings.TrimSpace(in.MediaType), mediaTypeFromDataURL),
|
||||
Kind: in.Kind,
|
||||
ThoughtID: in.ThoughtID,
|
||||
Project: in.Project,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, SaveFileOutput{}, err
|
||||
}
|
||||
|
||||
if created.ThoughtID != nil {
|
||||
if err := t.store.AddThoughtAttachment(ctx, *created.ThoughtID, thoughtAttachmentFromFile(created)); err != nil {
|
||||
return nil, SaveFileOutput{}, err
|
||||
}
|
||||
}
|
||||
if created.ProjectID != nil {
|
||||
_ = t.store.TouchProject(ctx, *created.ProjectID)
|
||||
}
|
||||
|
||||
return nil, SaveFileOutput{File: created}, nil
|
||||
return nil, out, nil
|
||||
}
|
||||
|
||||
func (t *FilesTool) Load(ctx context.Context, _ *mcp.CallToolRequest, in LoadFileInput) (*mcp.CallToolResult, LoadFileOutput, error) {
|
||||
@@ -193,6 +149,73 @@ func (t *FilesTool) List(ctx context.Context, req *mcp.CallToolRequest, in ListF
|
||||
return nil, ListFilesOutput{Files: files}, nil
|
||||
}
|
||||
|
||||
func (t *FilesTool) SaveDecoded(ctx context.Context, req *mcp.CallToolRequest, in SaveFileDecodedInput) (SaveFileOutput, error) {
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
return SaveFileOutput{}, errInvalidInput("name is required")
|
||||
}
|
||||
if len(in.Content) == 0 {
|
||||
return SaveFileOutput{}, errInvalidInput("decoded file content must not be empty")
|
||||
}
|
||||
|
||||
project, err := resolveProject(ctx, t.store, t.sessions, req, in.Project, false)
|
||||
if err != nil {
|
||||
return SaveFileOutput{}, err
|
||||
}
|
||||
|
||||
var thoughtID *uuid.UUID
|
||||
var projectID = projectIDPtr(project)
|
||||
if rawThoughtID := strings.TrimSpace(in.ThoughtID); rawThoughtID != "" {
|
||||
parsedThoughtID, err := parseUUID(rawThoughtID)
|
||||
if err != nil {
|
||||
return SaveFileOutput{}, err
|
||||
}
|
||||
thought, err := t.store.GetThought(ctx, parsedThoughtID)
|
||||
if err != nil {
|
||||
return SaveFileOutput{}, err
|
||||
}
|
||||
thoughtID = &parsedThoughtID
|
||||
projectID = thought.ProjectID
|
||||
if project != nil && thought.ProjectID != nil && *thought.ProjectID != project.ID {
|
||||
return SaveFileOutput{}, errInvalidInput("project does not match the linked thought's project")
|
||||
}
|
||||
}
|
||||
|
||||
mediaType := normalizeMediaType(strings.TrimSpace(in.MediaType), "", in.Content)
|
||||
kind := normalizeFileKind(strings.TrimSpace(in.Kind), mediaType)
|
||||
sum := sha256.Sum256(in.Content)
|
||||
|
||||
file := thoughttypes.StoredFile{
|
||||
Name: name,
|
||||
MediaType: mediaType,
|
||||
Kind: kind,
|
||||
Encoding: "base64",
|
||||
SizeBytes: int64(len(in.Content)),
|
||||
SHA256: hex.EncodeToString(sum[:]),
|
||||
Content: in.Content,
|
||||
ProjectID: projectID,
|
||||
}
|
||||
if thoughtID != nil {
|
||||
file.ThoughtID = thoughtID
|
||||
}
|
||||
|
||||
created, err := t.store.InsertStoredFile(ctx, file)
|
||||
if err != nil {
|
||||
return SaveFileOutput{}, err
|
||||
}
|
||||
|
||||
if created.ThoughtID != nil {
|
||||
if err := t.store.AddThoughtAttachment(ctx, *created.ThoughtID, thoughtAttachmentFromFile(created)); err != nil {
|
||||
return SaveFileOutput{}, err
|
||||
}
|
||||
}
|
||||
if created.ProjectID != nil {
|
||||
_ = t.store.TouchProject(ctx, *created.ProjectID)
|
||||
}
|
||||
|
||||
return SaveFileOutput{File: created}, nil
|
||||
}
|
||||
|
||||
func thoughtAttachmentFromFile(file thoughttypes.StoredFile) thoughttypes.ThoughtAttachment {
|
||||
return thoughttypes.ThoughtAttachment{
|
||||
FileID: file.ID,
|
||||
@@ -238,6 +261,15 @@ func normalizeMediaType(explicit string, fromDataURL string, content []byte) str
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeFileKind(explicit string, mediaType string) string {
|
||||
if explicit != "" {
|
||||
return explicit
|
||||
|
||||
Reference in New Issue
Block a user