feat: attach material files over MCP as base64
- storeMaterialFile: the one attach.Store call of the management API,
shared by the multipart route and the new inline route
- POST /api/v1/bm/materials/{id}/files/inline: JSON {name, mime?,
content_base64}, at most 3000000 bytes decoded, body.limit 4300000
- bm_manage_attach_material_file and bm_manage_detach_material_file,
both warning that material files are public to anyone with the link
This commit is contained in:
@@ -122,10 +122,6 @@ func (h *Handlers) storeParts(ctx context.Context, db *gorm.DB, bucket *blob.Buc
|
||||
var keys []string
|
||||
errRefused := errors.New("refused")
|
||||
err := lagoon.Transaction(ctx, db, func(_ context.Context, tx *gorm.DB) error {
|
||||
var have int64
|
||||
if err := materialFiles(tx.Model(&attach.File{}), m).Count(&have).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
added := 0
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
@@ -139,32 +135,18 @@ func (h *Handlers) storeParts(ctx context.Context, db *gorm.DB, bucket *blob.Buc
|
||||
part.Close()
|
||||
continue
|
||||
}
|
||||
if int(have)+added >= MaterialFilesMax {
|
||||
part.Close()
|
||||
*refusal = "A material holds at most " + strconv.Itoa(MaterialFilesMax) + " files."
|
||||
return errRefused
|
||||
}
|
||||
f, err := attach.Store(ctx, tx, bucket, attach.Upload{FileName: part.FileName(), Body: part, Public: true}, fileLimits)
|
||||
f, why, err := storeMaterialFile(ctx, tx, bucket, m, part.FileName(), part, fileLimits)
|
||||
part.Close()
|
||||
switch {
|
||||
case errors.Is(err, attach.ErrTooLarge):
|
||||
*refusal = part.FileName() + ": the file is larger than 50 MB."
|
||||
return errRefused
|
||||
case errors.Is(err, attach.ErrFileType), errors.Is(err, attach.ErrMIMEType):
|
||||
*refusal = part.FileName() + ": this file type is not allowed."
|
||||
return errRefused
|
||||
case err != nil:
|
||||
return err
|
||||
if f != nil {
|
||||
keys = append(keys, attach.BlobKeys(*f)...)
|
||||
}
|
||||
keys = append(keys, attach.BlobKeys(*f)...)
|
||||
err = tx.Model(&attach.File{}).Where("id = ?", f.ID).Updates(map[string]any{
|
||||
"attachment_type": m.MorphName(),
|
||||
"attachment_id": strconv.FormatUint(uint64(m.ID), 10),
|
||||
"field": models.MaterialFilesField,
|
||||
}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if why != "" {
|
||||
*refusal = why
|
||||
return errRefused
|
||||
}
|
||||
added++
|
||||
}
|
||||
if added == 0 {
|
||||
@@ -179,6 +161,48 @@ func (h *Handlers) storeParts(ctx context.Context, db *gorm.DB, bucket *blob.Buc
|
||||
return keys, err
|
||||
}
|
||||
|
||||
// storeMaterialFile stores one file through attach.Store as a public file of
|
||||
// m and binds it to the material, inside tx. It is the only way the
|
||||
// management API writes a material file: the multipart route, the inline
|
||||
// (base64) route and the signed upload all call it. The material's
|
||||
// MaterialFilesMax cap and attach's extension, MIME and size checks (lim)
|
||||
// apply. A refusal is returned as a text for the answer, with no error; the
|
||||
// returned file, when not nil, has blobs the caller deletes when tx does
|
||||
// not commit.
|
||||
func storeMaterialFile(ctx context.Context, tx *gorm.DB, bucket *blob.Bucket, m models.Material, name string, body io.Reader, lim attach.Limits) (*attach.File, string, error) {
|
||||
var have int64
|
||||
if err := materialFiles(tx.Model(&attach.File{}), m).Count(&have).Error; err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if have >= MaterialFilesMax {
|
||||
return nil, "A material holds at most " + strconv.Itoa(MaterialFilesMax) + " files.", nil
|
||||
}
|
||||
f, err := attach.Store(ctx, tx, bucket, attach.Upload{FileName: name, Body: body, Public: true}, lim)
|
||||
switch {
|
||||
case errors.Is(err, attach.ErrTooLarge):
|
||||
return nil, name + ": the file is larger than " + sizeMB(lim.MaxBytes) + ".", nil
|
||||
case errors.Is(err, attach.ErrFileType), errors.Is(err, attach.ErrMIMEType):
|
||||
return nil, name + ": this file type is not allowed.", nil
|
||||
case err != nil:
|
||||
return nil, "", err
|
||||
}
|
||||
err = tx.Model(&attach.File{}).Where("id = ?", f.ID).Updates(map[string]any{
|
||||
"attachment_type": m.MorphName(),
|
||||
"attachment_id": strconv.FormatUint(uint64(m.ID), 10),
|
||||
"field": models.MaterialFilesField,
|
||||
}).Error
|
||||
return f, "", err
|
||||
}
|
||||
|
||||
// sizeMB names a byte limit in the refusal texts: 50 MB for 50 MiB, 3 MB
|
||||
// for 3000000 bytes.
|
||||
func sizeMB(n int64) string {
|
||||
if n%(1<<20) == 0 {
|
||||
return strconv.FormatInt(n>>20, 10) + " MB"
|
||||
}
|
||||
return strconv.FormatInt(n/1_000_000, 10) + " MB"
|
||||
}
|
||||
|
||||
// DetachMaterialFile answers DELETE /materials/{id}/files/{file}: the file
|
||||
// row goes, then its blob and thumbnails. A file of another material is a
|
||||
// 404.
|
||||
|
||||
Reference in New Issue
Block a user