func (s *Storage) DownloadAction() *fire.Action {
	return fire.A("blaze/Storage.DownloadAction", []string{"HEAD", "GET"}, 0, func(ctx *fire.Context) error {
		if ctx.Store != nil && ctx.Store != s.store {
			return xo.F("stores must be identical")
		}
		key := ctx.HTTPRequest.URL.Query().Get("key")
		if key == "" {
			ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
			return nil
		}
		dl := ctx.HTTPRequest.URL.Query().Get("dl") == "1"
		download, file, err := s.Download(ctx, key)
		if err != nil {
			return err
		}
		binding := s.register.Get(file.Binding)
		if binding == nil {
			return xo.F("missing binding")
		}
		contentDisposition := "inline"
		if dl {
			filename := file.Name
			if binding.Filename != "" {
				filename = binding.Filename
			} else if filename == "" {
				filename = file.ID().Hex()
			}
			contentDisposition = fmt.Sprintf(`attachment; filename="%s"`, filename)
		}
		ctx.ResponseWriter.Header().Set("Content-Type", file.Type)
		ctx.ResponseWriter.Header().Set("Content-Length", strconv.FormatInt(file.Size, 10))
		ctx.ResponseWriter.Header().Set("Content-Disposition", contentDisposition)
		ctx.ResponseWriter.Header().Del("Content-Security-Policy")
		http.ServeContent(ctx.ResponseWriter, ctx.HTTPRequest, "", file.Updated, download)
		return nil
	})
}
DownloadAction returns an action that allows downloading files using view
keys. This action is usually publicly accessible.
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
}
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
}
func (s *Storage) ClaimLink(ctx context.Context, link *Link, binding string, owner coal.ID) error {
	if !coal.HasTransaction(ctx) {
		return xo.F("missing transaction for claim")
	}
	if !link.File.IsZero() {
		return xo.F("existing claimed filed")
	}
	if link.ClaimKey == "" {
		return xo.F("missing claim key")
	}
	file, err := s.ClaimFile(ctx, link.ClaimKey, binding, owner)
	if err != nil {
		return err
	}
	*link = Link{
		Ref:      link.Ref,
		File:     file.ID(),
		FileName: file.Name,
		FileType: file.Type,
		FileSize: file.Size,
	}
	return nil
}
func (s *Storage) ClaimFile(ctx context.Context, claimKey, binding string, owner coal.ID) (*File, error) {
	ctx, span := xo.Trace(ctx, "blaze/Storage.ClaimFile")
	defer span.End()
	bnd := s.register.Get(binding)
	if bnd == nil {
		return nil, xo.F("unknown binding: %s", binding)
	}
	if owner.IsZero() {
		return nil, xo.F("missing owner")
	}
	var key ClaimKey
	err := s.notary.Verify(&key, claimKey)
	if err != nil {
		return nil, err
	}
	if bnd.Limit > 0 && key.Size > bnd.Limit {
		return nil, xo.F("too big")
	}
	if len(bnd.Types) > 0 && !stick.Contains(bnd.Types, key.Type) {
		return nil, xo.F("unsupported type: %s", key.Type)
	}
	var file File
	found, err := s.store.M(&File{}).UpdateFirst(ctx, &file, bson.M{
		"_id":   key.File,
		"State": Uploaded,
	}, bson.M{
		"$set": bson.M{
			"State":   Claimed,
			"Updated": time.Now(),
			"Binding": binding,
			"Owner":   owner,
		},
	}, nil, false)
	if err != nil {
		return nil, err
	} else if !found {
		return nil, xo.F("unable to claim file")
	}
	return &file, nil
}
func (s *Storage) Release(ctx context.Context, model coal.Model, field string) error {
	value := stick.MustGet(model, field)
	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.ReleaseLink(ctx, link)
	if err != nil {
		return err
	}
	switch value.(type) {
	case Link:
		stick.MustSet(model, field, Link{})
	case *Link:
		stick.MustSet(model, field, nil)
	}
	return nil
}
func (s *Storage) ReleaseLink(ctx context.Context, link *Link) error {
	file := link.File
	if file.IsZero() {
		return xo.F("invalid file id")
	}
	if !coal.HasTransaction(ctx) {
		return xo.F("missing transaction for release")
	}
	err := s.ReleaseFile(ctx, file)
	if err != nil {
		return err
	}
	return nil
}
func (s *Storage) ReleaseFile(ctx context.Context, file coal.ID) error {
	ctx, span := xo.Trace(ctx, "blaze/Storage.ReleaseFile")
	defer span.End()
	found, err := s.store.M(&File{}).UpdateFirst(ctx, nil, bson.M{
		"_id":   file,
		"State": Claimed,
	}, bson.M{
		"$set": bson.M{
			"State":   Released,
			"Updated": time.Now(),
			"Binding": "",
			"Owner":   nil,
		},
	}, nil, false)
	if err != nil {
		return err
	} else if !found {
		return xo.F("unable to release file")
	}
	return nil
}
func (s *Storage) Modifier(fields ...string) *fire.Callback {
	return fire.C("blaze/Storage.Modifier", fire.Modifier, fire.Only(fire.Create|fire.Update|fire.Delete), func(ctx *fire.Context) error {
		if ctx.Store != s.store {
			return xo.F("stores must be identical")
		}
		if len(fields) == 0 {
			fields = collectFields(ctx.Controller.Model)
		}
		owner := ctx.Model.ID()
		for _, field := range fields {
			value := stick.MustGet(ctx.Model, field)
			var oldValue interface{}
			if ctx.Original != nil {
				oldValue = stick.MustGet(ctx.Original, field)
			}
			binding := s.register.Lookup(ctx.Model, field)
			if binding == nil {
				return xo.F("missing binding")
			}
			switch value := value.(type) {
			case Link:
				var oldLink *Link
				if oldValue != nil {
					l := oldValue.(Link)
					oldLink = &l
				}
				newLink := &value
				if ctx.Operation == fire.Delete {
					oldLink = newLink
					newLink = nil
				}
				err := s.modifyLink(ctx, newLink, oldLink, binding.Name, owner)
				if err != nil {
					return xo.WF(err, field)
				}
				stick.MustSet(ctx.Model, field, value)
			case *Link:
				var oldLink *Link
				if oldValue != nil {
					oldLink = oldValue.(*Link)
				}
				newLink := value
				if ctx.Operation == fire.Delete {
					oldLink = newLink
					newLink = nil
				}
				err := s.modifyLink(ctx, newLink, oldLink, binding.Name, owner)
				if err != nil {
					return xo.WF(err, field)
				}
				stick.MustSet(ctx.Model, field, value)
			case Links:
				var oldLinks Links
				if oldValue != nil {
					oldLinks = oldValue.(Links)
				}
				newLinks := value
				if ctx.Operation == fire.Delete {
					oldLinks = newLinks
					newLinks = nil
				}
				err := s.modifyLinks(ctx, newLinks, oldLinks, binding.Name, owner)
				if err != nil {
					return xo.WF(err, field)
				}
				stick.MustSet(ctx.Model, field, value)
			default:
				return xo.F("%s: unsupported type: %T", field, value)
			}
		}
		return nil
	})
}
func (s *Storage) modifyLink(ctx context.Context, newLink, oldLink *Link, binding string, owner coal.ID) error {
	added := oldLink == nil && newLink != nil
	updated := oldLink != nil && newLink != nil && newLink.ClaimKey != ""
	deleted := oldLink != nil && newLink == nil
	if updated || deleted {
		err := s.ReleaseLink(ctx, oldLink)
		if err != nil {
			return err
		}
	}
	if added || updated {
		newLink.File = coal.ID{}
		err := s.ClaimLink(ctx, newLink, binding, owner)
		if err != nil {
			return err
		}
	}
	return nil
}
func (s *Storage) modifyLinks(ctx context.Context, newLinks, oldLinks Links, binding string, owner coal.ID) error {
	newMap := make(map[string]*Link, len(newLinks))
	newRefs := make([]string, 0, len(newLinks))
	for i, link := range newLinks {
		if link.Ref == "" {
			return xo.F("missing reference")
		}
		newMap[link.Ref] = &newLinks[i]
		newRefs = append(newRefs, link.Ref)
	}
	oldMap := make(map[string]*Link, len(oldLinks))
	oldRefs := make([]string, 0, len(oldLinks))
	for i, link := range oldLinks {
		if link.Ref == "" {
			return xo.F("missing reference")
		}
		oldMap[link.Ref] = &oldLinks[i]
		oldRefs = append(oldRefs, link.Ref)
	}
	added := stick.Subtract(newRefs, oldRefs)
	kept := stick.Intersect(newRefs, oldRefs)
	deleted := stick.Subtract(oldRefs, newRefs)
	updated := make([]string, 0, len(kept))
	for _, ref := range kept {
		if newMap[ref].ClaimKey != "" {
			updated = append(updated, ref)
		}
	}
	for _, ref := range stick.Union(updated, deleted) {
		err := s.ReleaseLink(ctx, oldMap[ref])
		if err != nil {
			return err
		}
	}
	for _, ref := range stick.Union(added, updated) {
		link := newMap[ref]
		link.File = coal.ID{}
		err := s.ClaimLink(ctx, link, binding, owner)
		if err != nil {
			return err
		}
	}
	return nil
}
func (s *Storage) Decorate(link *Link) error {
	if link == nil || link.File.IsZero() {
		return nil
	}
	viewKey, err := s.notary.Issue(&ViewKey{
		File: link.File,
	})
	if err != nil {
		return err
	}
	link.ViewKey = viewKey
	link.Name = link.FileName
	link.Type = link.FileType
	link.Size = link.FileSize
	return nil
}
func (s *Storage) Decorator(fields ...string) *fire.Callback {
	return fire.C("blaze/Storage.Decorator", fire.Decorator, fire.All(), func(ctx *fire.Context) error {
		if len(fields) == 0 {
			fields = collectFields(ctx.Controller.Model)
		}
		if ctx.Model != nil {
			err := s.decorateModel(ctx.Model, fields)
			if err != nil {
				return err
			}
		}
		for _, model := range ctx.Models {
			err := s.decorateModel(model, fields)
			if err != nil {
				return err
			}
		}
		return nil
	})
}
func (s *Storage) decorateModel(model coal.Model, fields []string) error {
	if len(fields) == 0 {
		fields = collectFields(model)
	}
	for _, field := range fields {
		value := stick.MustGet(model, field)
		var err error
		switch value := value.(type) {
		case Link:
			err = s.Decorate(&value)
			if err != nil {
				return err
			}
			stick.MustSet(model, field, value)
		case *Link:
			err = s.Decorate(value)
			if err != nil {
				return err
			}
		case Links:
			for i := range value {
				err = s.Decorate(&value[i])
				if err != nil {
					return err
				}
			}
		}
	}
	return nil
}
func (s *Storage) Download(ctx context.Context, viewKey string) (Download, *File, error) {
	ctx, span := xo.Trace(ctx, "blaze/Storage.Download")
	defer span.End()
	var key ViewKey
	err := s.notary.Verify(&key, viewKey)
	if err != nil {
		return nil, nil, err
	}
	download, file, err := s.DownloadFile(ctx, key.File)
	if err != nil {
		return nil, nil, err
	}
	return download, file, nil
}
func (s *Storage) DownloadFile(ctx context.Context, id coal.ID) (Download, *File, error) {
	var file File
	found, err := s.store.M(&File{}).FindFirst(ctx, &file, bson.M{
		"_id":   id,
		"State": Claimed,
	}, nil, 0, false)
	if err != nil {
		return nil, nil, err
	} else if !found {
		return nil, nil, xo.F("missing file")
	}
	download, err := s.service.Download(ctx, file.Handle)
	if err != nil {
		return nil, nil, xo.W(err)
	}
	return download, &file, nil
}