func (s *Storage) Claim(ctx context.Context, model coal.Model, field string) error {
	value := stick.MustGet(model, field)
	binding := s.register.Lookup(model, field)
	if binding == nil {
		return xo.F("missing binding")
	}
	var link *Link
	switch value := value.(type) {
	case Link:
		link = &value
	case *Link:
		link = value
	}
	if link == nil {
		return xo.F("missing link")
	}
	err := s.ClaimLink(ctx, link, binding.Name, model.ID())
	if err != nil {
		return err
	}
	switch value.(type) {
	case Link:
		stick.MustSet(model, field, *link)
	case *Link:
		stick.MustSet(model, field, link)
	}
	return nil
}
Claim will claim the link at the field on the provided model. The claimed
link must be persisted in the same transaction as the claim to ensure
consistency.
package blaze
import (
	"context"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"path"
	"strconv"
	"strings"
	"time"
	"github.com/256dpi/serve"
	"github.com/256dpi/xo"
	"go.mongodb.org/mongo-driver/bson"
	"github.com/256dpi/fire"
	"github.com/256dpi/fire/axe"
	"github.com/256dpi/fire/coal"
	"github.com/256dpi/fire/heat"
	"github.com/256dpi/fire/stick"
)
type Storage struct {
	store    *coal.Store
	notary   *heat.Notary
	service  Service
	register *Register
}
func NewStorage(store *coal.Store, notary *heat.Notary, service Service, register *Register) *Storage {
	return &Storage{
		store:    store,
		notary:   notary,
		service:  service,
		register: register,
	}
}
func (s *Storage) Upload(ctx context.Context, name, mediaType string, cb func(Upload) (int64, error)) (string, *File, error) {
	ctx, span := xo.Trace(ctx, "blaze/Storage.Upload")
	span.Tag("type", mediaType)
	defer span.End()
	if coal.HasTransaction(ctx) {
		return "", nil, xo.F("unexpected transaction for upload")
	}
	if len(name) > maxFileNameLength {
		return "", nil, xo.SF("file name too long")
	}
	if mediaType == "" {
		if name != "" {
			mediaType = serve.MimeTypeByExtension(path.Ext(name), false)
		}
		if mediaType == "" {
			mediaType = "application/octet-stream"
		}
	}
	handle, err := s.service.Prepare(ctx)
	if err != nil {
		return "", nil, xo.W(err)
	}
	file := &File{
		Base:    coal.B(),
		State:   Uploading,
		Updated: time.Now(),
		Name:    name,
		Type:    mediaType,
		Handle:  handle,
	}
	err = file.Validate()
	if err != nil {
		return "", nil, err
	}
	err = s.store.M(file).Insert(ctx, file)
	if err != nil {
		return "", nil, err
	}
	upload, err := s.service.Upload(ctx, handle, name, mediaType)
	if err != nil {
		return "", nil, xo.W(err)
	}
	size, err := cb(upload)
	if err != nil {
		return "", nil, xo.W(err)
	}
	now := time.Now()
	file.State = Uploaded
	file.Updated = now
	file.Size = size
	err = file.Validate()
	if err != nil {
		return "", nil, err
	}
	_, err = s.store.M(file).Update(ctx, nil, file.ID(), bson.M{
		"$set": bson.M{
			"State":   Uploaded,
			"Updated": now,
			"Size":    size,
		},
	}, false)
	if err != nil {
		return "", nil, err
	}
	claimKey, err := s.notary.Issue(&ClaimKey{
		File: file.ID(),
		Size: file.Size,
		Name: file.Name,
		Type: file.Type,
	})
	if err != nil {
		return "", nil, err
	}
	return claimKey, file, nil
}
func (s *Storage) UploadAction(limit int64) *fire.Action {
	if limit == 0 {
		limit = serve.MustByteSize("8M")
	}
	return fire.A("blaze/Storage.UploadAction", []string{"POST"}, limit, func(ctx *fire.Context) error {
		if ctx.Store != nil && ctx.Store != s.store {
			return xo.F("stores must be identical")
		}
		rawContentType := ctx.HTTPRequest.Header.Get("Content-Type")
		contentType, ctParams, err := mime.ParseMediaType(rawContentType)
		if rawContentType != "" && err != nil {
			ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
			return nil
		}
		contentLength := ctx.HTTPRequest.ContentLength
		if contentLength != -1 && contentLength > limit {
			ctx.ResponseWriter.WriteHeader(http.StatusRequestEntityTooLarge)
			return nil
		}
		var keys []string
		if contentType == "multipart/form-data" {
			keys, err = s.uploadMultipart(ctx, ctParams["boundary"])
		} else {
			keys, err = s.uploadBody(ctx, contentType)
		}
		if err != nil && strings.HasSuffix(err.Error(), serve.ErrBodyLimitExceeded.Error()) {
			ctx.ResponseWriter.WriteHeader(http.StatusRequestEntityTooLarge)
			return nil
		} else if err != nil {
			return err
		}
		return ctx.Respond(stick.Map{
			"keys": keys,
		})
	})
}
func (s *Storage) uploadBody(ctx *fire.Context, mediaType string) ([]string, error) {
	filename := ""
	if ctx.HTTPRequest.Header.Get("Content-Disposition") != "" {
		disposition, params, err := mime.ParseMediaType(ctx.HTTPRequest.Header.Get("Content-Disposition"))
		if err != nil {
			return nil, err
		}
		if disposition != "" && disposition != "attachment" {
			return nil, xo.SF("expected attachment content disposition")
		}
		filename = params["filename"]
	}
	claimKey, _, err := s.Upload(ctx, filename, mediaType, func(upload Upload) (int64, error) {
		return UploadFrom(upload, ctx.HTTPRequest.Body)
	})
	if err != nil {
		return nil, err
	}
	return []string{claimKey}, nil
}
func (s *Storage) uploadMultipart(ctx *fire.Context, boundary string) ([]string, error) {
	reader := multipart.NewReader(ctx.HTTPRequest.Body, boundary)
	part, err := reader.NextPart()
	if err != nil && err != io.EOF {
		return nil, xo.W(err)
	}
	var claimKeys []string
	for part != nil {
		contentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
		if err != nil {
			return nil, xo.W(err)
		}
		claimKey, _, err := s.Upload(ctx, part.FileName(), contentType, func(upload Upload) (int64, error) {
			return UploadFrom(upload, part)
		})
		if err != nil {
			return nil, err
		}
		claimKeys = append(claimKeys, claimKey)
		part, err = reader.NextPart()
		if err != nil && err != io.EOF {
			return nil, xo.W(err)
		}
	}
	return claimKeys, nil
}