base.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. package media
  2. import (
  3. "bytes"
  4. "database/sql/driver"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "image"
  9. "io"
  10. "mime/multipart"
  11. "os"
  12. "path"
  13. "path/filepath"
  14. "regexp"
  15. "strings"
  16. "text/template"
  17. "time"
  18. "github.com/gosimple/slug"
  19. "github.com/iancoleman/strcase"
  20. "github.com/jinzhu/inflection"
  21. "gorm.io/gorm"
  22. "gorm.io/gorm/schema"
  23. )
  24. // CropOption includes crop options
  25. type CropOption struct {
  26. X, Y, Width, Height int
  27. }
  28. // FileHeader is an interface, for matched values, when call its `Open` method will return `multipart.File`
  29. type FileHeader interface {
  30. Open() (multipart.File, error)
  31. }
  32. type fileWrapper struct {
  33. *os.File
  34. }
  35. func (fileWrapper *fileWrapper) Open() (multipart.File, error) {
  36. return fileWrapper.File, nil
  37. }
  38. // Base defined a base struct for storages
  39. type Base struct {
  40. FileName string
  41. Url string
  42. CropOptions map[string]*CropOption `json:",omitempty"`
  43. Delete bool `json:"-"`
  44. Crop bool `json:"-"`
  45. FileHeader FileHeader `json:"-"`
  46. Reader io.Reader `json:"-"`
  47. Options map[string]string `json:",omitempty"`
  48. cropped bool
  49. Width int `json:",omitempty"`
  50. Height int `json:",omitempty"`
  51. FileSizes map[string]int `json:",omitempty"`
  52. }
  53. // Scan scan files, crop options, db values into struct
  54. func (b *Base) Scan(data interface{}) (err error) {
  55. switch values := data.(type) {
  56. case *os.File:
  57. b.FileHeader = &fileWrapper{values}
  58. b.FileName = filepath.Base(values.Name())
  59. case *multipart.FileHeader:
  60. b.FileHeader, b.FileName = values, values.Filename
  61. case []*multipart.FileHeader:
  62. if len(values) > 0 {
  63. if file := values[0]; file.Size > 0 {
  64. b.FileHeader, b.FileName = file, file.Filename
  65. b.Delete = false
  66. }
  67. }
  68. case []byte:
  69. if string(values) != "" {
  70. if err = json.Unmarshal(values, b); err == nil {
  71. var options struct {
  72. Crop bool
  73. Delete bool
  74. }
  75. if err = json.Unmarshal(values, &options); err == nil {
  76. if options.Crop {
  77. b.Crop = true
  78. }
  79. if options.Delete {
  80. b.Delete = true
  81. }
  82. }
  83. }
  84. }
  85. case string:
  86. return b.Scan([]byte(values))
  87. case []string:
  88. for _, str := range values {
  89. if err := b.Scan(str); err != nil {
  90. return err
  91. }
  92. }
  93. default:
  94. err = errors.New("unsupported driver -> Scan pair for MediaLibrary")
  95. }
  96. // If image is deleted, then clean up all values, for serialized fields
  97. if b.Delete {
  98. b.Url = ""
  99. b.FileName = ""
  100. b.CropOptions = nil
  101. }
  102. return
  103. }
  104. // Value return struct's Value
  105. func (b Base) Value() (driver.Value, error) {
  106. if b.Delete {
  107. return nil, nil
  108. }
  109. results, err := json.Marshal(b)
  110. return string(results), err
  111. }
  112. func (b Base) Ext() string {
  113. return strings.ToLower(path.Ext(b.Url))
  114. }
  115. // URL return file's url with given style
  116. func (b Base) URL(styles ...string) string {
  117. if b.Url != "" && len(styles) > 0 {
  118. ext := path.Ext(b.Url)
  119. return fmt.Sprintf("%v.%v%v", strings.TrimSuffix(b.Url, ext), styles[0], ext)
  120. }
  121. return b.Url
  122. }
  123. // String return file's url
  124. func (b Base) String() string {
  125. return b.URL()
  126. }
  127. // GetFileName get file's name
  128. func (b Base) GetFileName() string {
  129. if b.FileName != "" {
  130. return b.FileName
  131. }
  132. if b.Url != "" {
  133. return filepath.Base(b.Url)
  134. }
  135. return ""
  136. }
  137. // GetFileHeader get file's header, this value only exists when saving files
  138. func (b Base) GetFileHeader() FileHeader {
  139. return b.FileHeader
  140. }
  141. // GetURLTemplate get url template
  142. func (b Base) GetURLTemplate(option *Option) (path string) {
  143. if path = option.Get("URL"); path == "" {
  144. path = "/system/{{class}}/{{primary_key}}/{{column}}/{{filename_with_hash}}"
  145. }
  146. return
  147. }
  148. var urlReplacer = regexp.MustCompile("(\\s|\\+)+")
  149. func getFuncMap(db *gorm.DB, field *schema.Field, filename string) template.FuncMap {
  150. hash := func() string { return strings.Replace(time.Now().Format("20060102150405.000000"), ".", "", -1) }
  151. shortHash := func() string { return time.Now().Format("20060102150405") }
  152. return template.FuncMap{
  153. "class": func() string { return inflection.Plural(strcase.ToSnake(field.Schema.ModelType.Name())) },
  154. "primary_key": func() string {
  155. ppf := db.Statement.Schema.PrioritizedPrimaryField
  156. if ppf != nil {
  157. return fmt.Sprintf("%v", ppf.ReflectValueOf(db.Statement.Context, db.Statement.ReflectValue))
  158. }
  159. return "0"
  160. },
  161. "column": func() string { return strings.ToLower(field.Name) },
  162. "filename": func() string { return filename },
  163. "basename": func() string { return strings.TrimSuffix(path.Base(filename), path.Ext(filename)) },
  164. "hash": hash,
  165. "short_hash": shortHash,
  166. "filename_with_hash": func() string {
  167. return urlReplacer.ReplaceAllString(fmt.Sprintf("%s.%v%v", slug.Make(strings.TrimSuffix(path.Base(filename), path.Ext(filename))), hash(), path.Ext(filename)), "-")
  168. },
  169. "filename_with_short_hash": func() string {
  170. return urlReplacer.ReplaceAllString(fmt.Sprintf("%s.%v%v", slug.Make(strings.TrimSuffix(path.Base(filename), path.Ext(filename))), shortHash(), path.Ext(filename)), "-")
  171. },
  172. "extension": func() string { return strings.TrimPrefix(path.Ext(filename), ".") },
  173. }
  174. }
  175. // GetURL get default URL for a model based on its options
  176. func (b Base) GetURL(option *Option, db *gorm.DB, field *schema.Field, templater URLTemplater) string {
  177. if path := templater.GetURLTemplate(option); path != "" {
  178. tmpl := template.New("").Funcs(getFuncMap(db, field, b.GetFileName()))
  179. if tmpl, err := tmpl.Parse(path); err == nil {
  180. var result = bytes.NewBufferString("")
  181. if err := tmpl.Execute(result, db.Statement.Dest); err == nil {
  182. return result.String()
  183. }
  184. }
  185. }
  186. return ""
  187. }
  188. // Cropped mark the image to be cropped
  189. func (b *Base) Cropped(values ...bool) (result bool) {
  190. result = b.cropped
  191. for _, value := range values {
  192. b.cropped = value
  193. }
  194. return result
  195. }
  196. // NeedCrop return the file needs to be cropped or not
  197. func (b *Base) NeedCrop() bool {
  198. return b.Crop
  199. }
  200. // GetCropOption get crop options
  201. func (b *Base) GetCropOption(name string) *image.Rectangle {
  202. if cropOption := b.CropOptions[strings.Split(name, "@")[0]]; cropOption != nil {
  203. return &image.Rectangle{
  204. Min: image.Point{X: cropOption.X, Y: cropOption.Y},
  205. Max: image.Point{X: cropOption.X + cropOption.Width, Y: cropOption.Y + cropOption.Height},
  206. }
  207. }
  208. return nil
  209. }
  210. // GetFileSizes get file sizes
  211. func (b *Base) GetFileSizes() map[string]int {
  212. if b.FileSizes != nil {
  213. return b.FileSizes
  214. }
  215. return make(map[string]int)
  216. }
  217. // Retrieve retrieve file content with url
  218. func (b Base) Retrieve(url string) (*os.File, error) {
  219. return nil, errors.New("not implemented")
  220. }
  221. // GetSizes get configured sizes, it will be used to crop images accordingly
  222. func (b Base) GetSizes() map[string]*Size {
  223. return map[string]*Size{}
  224. }
  225. // IsImage return if it is an image
  226. func (b Base) IsImage() bool {
  227. return IsImageFormat(b.URL())
  228. }
  229. func (b Base) IsVideo() bool {
  230. return IsVideoFormat(b.URL())
  231. }
  232. func (b Base) IsSVG() bool {
  233. return IsSVGFormat(b.URL())
  234. }