123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- package media
- import (
- "bytes"
- "database/sql/driver"
- "encoding/json"
- "errors"
- "fmt"
- "image"
- "io"
- "mime/multipart"
- "os"
- "path"
- "path/filepath"
- "regexp"
- "strings"
- "text/template"
- "time"
- "github.com/gosimple/slug"
- "github.com/iancoleman/strcase"
- "github.com/jinzhu/inflection"
- "gorm.io/gorm"
- "gorm.io/gorm/schema"
- )
- // CropOption includes crop options
- type CropOption struct {
- X, Y, Width, Height int
- }
- // FileHeader is an interface, for matched values, when call its `Open` method will return `multipart.File`
- type FileHeader interface {
- Open() (multipart.File, error)
- }
- type fileWrapper struct {
- *os.File
- }
- func (fileWrapper *fileWrapper) Open() (multipart.File, error) {
- return fileWrapper.File, nil
- }
- // Base defined a base struct for storages
- type Base struct {
- FileName string
- Url string
- CropOptions map[string]*CropOption `json:",omitempty"`
- Delete bool `json:"-"`
- Crop bool `json:"-"`
- FileHeader FileHeader `json:"-"`
- Reader io.Reader `json:"-"`
- Options map[string]string `json:",omitempty"`
- cropped bool
- Width int `json:",omitempty"`
- Height int `json:",omitempty"`
- FileSizes map[string]int `json:",omitempty"`
- }
- // Scan scan files, crop options, db values into struct
- func (b *Base) Scan(data interface{}) (err error) {
- switch values := data.(type) {
- case *os.File:
- b.FileHeader = &fileWrapper{values}
- b.FileName = filepath.Base(values.Name())
- case *multipart.FileHeader:
- b.FileHeader, b.FileName = values, values.Filename
- case []*multipart.FileHeader:
- if len(values) > 0 {
- if file := values[0]; file.Size > 0 {
- b.FileHeader, b.FileName = file, file.Filename
- b.Delete = false
- }
- }
- case []byte:
- if string(values) != "" {
- if err = json.Unmarshal(values, b); err == nil {
- var options struct {
- Crop bool
- Delete bool
- }
- if err = json.Unmarshal(values, &options); err == nil {
- if options.Crop {
- b.Crop = true
- }
- if options.Delete {
- b.Delete = true
- }
- }
- }
- }
- case string:
- return b.Scan([]byte(values))
- case []string:
- for _, str := range values {
- if err := b.Scan(str); err != nil {
- return err
- }
- }
- default:
- err = errors.New("unsupported driver -> Scan pair for MediaLibrary")
- }
- // If image is deleted, then clean up all values, for serialized fields
- if b.Delete {
- b.Url = ""
- b.FileName = ""
- b.CropOptions = nil
- }
- return
- }
- // Value return struct's Value
- func (b Base) Value() (driver.Value, error) {
- if b.Delete {
- return nil, nil
- }
- results, err := json.Marshal(b)
- return string(results), err
- }
- func (b Base) Ext() string {
- return strings.ToLower(path.Ext(b.Url))
- }
- // URL return file's url with given style
- func (b Base) URL(styles ...string) string {
- if b.Url != "" && len(styles) > 0 {
- ext := path.Ext(b.Url)
- return fmt.Sprintf("%v.%v%v", strings.TrimSuffix(b.Url, ext), styles[0], ext)
- }
- return b.Url
- }
- // String return file's url
- func (b Base) String() string {
- return b.URL()
- }
- // GetFileName get file's name
- func (b Base) GetFileName() string {
- if b.FileName != "" {
- return b.FileName
- }
- if b.Url != "" {
- return filepath.Base(b.Url)
- }
- return ""
- }
- // GetFileHeader get file's header, this value only exists when saving files
- func (b Base) GetFileHeader() FileHeader {
- return b.FileHeader
- }
- // GetURLTemplate get url template
- func (b Base) GetURLTemplate(option *Option) (path string) {
- if path = option.Get("URL"); path == "" {
- path = "/system/{{class}}/{{primary_key}}/{{column}}/{{filename_with_hash}}"
- }
- return
- }
- var urlReplacer = regexp.MustCompile("(\\s|\\+)+")
- func getFuncMap(db *gorm.DB, field *schema.Field, filename string) template.FuncMap {
- hash := func() string { return strings.Replace(time.Now().Format("20060102150405.000000"), ".", "", -1) }
- shortHash := func() string { return time.Now().Format("20060102150405") }
- return template.FuncMap{
- "class": func() string { return inflection.Plural(strcase.ToSnake(field.Schema.ModelType.Name())) },
- "primary_key": func() string {
- ppf := db.Statement.Schema.PrioritizedPrimaryField
- if ppf != nil {
- return fmt.Sprintf("%v", ppf.ReflectValueOf(db.Statement.Context, db.Statement.ReflectValue))
- }
- return "0"
- },
- "column": func() string { return strings.ToLower(field.Name) },
- "filename": func() string { return filename },
- "basename": func() string { return strings.TrimSuffix(path.Base(filename), path.Ext(filename)) },
- "hash": hash,
- "short_hash": shortHash,
- "filename_with_hash": func() string {
- return urlReplacer.ReplaceAllString(fmt.Sprintf("%s.%v%v", slug.Make(strings.TrimSuffix(path.Base(filename), path.Ext(filename))), hash(), path.Ext(filename)), "-")
- },
- "filename_with_short_hash": func() string {
- return urlReplacer.ReplaceAllString(fmt.Sprintf("%s.%v%v", slug.Make(strings.TrimSuffix(path.Base(filename), path.Ext(filename))), shortHash(), path.Ext(filename)), "-")
- },
- "extension": func() string { return strings.TrimPrefix(path.Ext(filename), ".") },
- }
- }
- // GetURL get default URL for a model based on its options
- func (b Base) GetURL(option *Option, db *gorm.DB, field *schema.Field, templater URLTemplater) string {
- if path := templater.GetURLTemplate(option); path != "" {
- tmpl := template.New("").Funcs(getFuncMap(db, field, b.GetFileName()))
- if tmpl, err := tmpl.Parse(path); err == nil {
- var result = bytes.NewBufferString("")
- if err := tmpl.Execute(result, db.Statement.Dest); err == nil {
- return result.String()
- }
- }
- }
- return ""
- }
- // Cropped mark the image to be cropped
- func (b *Base) Cropped(values ...bool) (result bool) {
- result = b.cropped
- for _, value := range values {
- b.cropped = value
- }
- return result
- }
- // NeedCrop return the file needs to be cropped or not
- func (b *Base) NeedCrop() bool {
- return b.Crop
- }
- // GetCropOption get crop options
- func (b *Base) GetCropOption(name string) *image.Rectangle {
- if cropOption := b.CropOptions[strings.Split(name, "@")[0]]; cropOption != nil {
- return &image.Rectangle{
- Min: image.Point{X: cropOption.X, Y: cropOption.Y},
- Max: image.Point{X: cropOption.X + cropOption.Width, Y: cropOption.Y + cropOption.Height},
- }
- }
- return nil
- }
- // GetFileSizes get file sizes
- func (b *Base) GetFileSizes() map[string]int {
- if b.FileSizes != nil {
- return b.FileSizes
- }
- return make(map[string]int)
- }
- // Retrieve retrieve file content with url
- func (b Base) Retrieve(url string) (*os.File, error) {
- return nil, errors.New("not implemented")
- }
- // GetSizes get configured sizes, it will be used to crop images accordingly
- func (b Base) GetSizes() map[string]*Size {
- return map[string]*Size{}
- }
- // IsImage return if it is an image
- func (b Base) IsImage() bool {
- return IsImageFormat(b.URL())
- }
- func (b Base) IsVideo() bool {
- return IsVideoFormat(b.URL())
- }
- func (b Base) IsSVG() bool {
- return IsSVGFormat(b.URL())
- }
|