mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
More image type support for thumbnails (#814)
This commit is contained in:
@@ -82,7 +82,7 @@ func (ctrl *V1Controller) HandleItemAttachmentCreate() errchain.HandlerFunc {
|
||||
ext := filepath.Ext(attachmentName)
|
||||
|
||||
switch strings.ToLower(ext) {
|
||||
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff", ".avif", ".ico":
|
||||
case ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff", ".avif", ".ico", ".heic", ".jxl":
|
||||
attachmentType = attachment.TypePhoto.String()
|
||||
default:
|
||||
attachmentType = attachment.TypeAttachment.String()
|
||||
|
||||
@@ -309,7 +309,7 @@ func run(cfg *config.Config) error {
|
||||
}
|
||||
}))
|
||||
|
||||
runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
|
||||
go runner.AddFunc("create-thumbnails-subscription", func(ctx context.Context) error {
|
||||
pubsubString, err := utils.GenerateSubPubConn(cfg.Database.PubSubConnString, "thumbnails")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to generate pubsub connection string")
|
||||
@@ -347,7 +347,6 @@ func run(cfg *config.Config) error {
|
||||
log.Debug().Msg("received thumbnail generation request from pubsub topic")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to receive message from pubsub topic")
|
||||
return err
|
||||
}
|
||||
groupId, err := uuid.Parse(msg.Metadata["group_id"])
|
||||
if err != nil {
|
||||
@@ -355,8 +354,6 @@ func run(cfg *config.Config) error {
|
||||
Err(err).
|
||||
Str("group_id", msg.Metadata["group_id"]).
|
||||
Msg("failed to parse group ID from message metadata")
|
||||
msg.Nack()
|
||||
return err
|
||||
}
|
||||
attachmentId, err := uuid.Parse(msg.Metadata["attachment_id"])
|
||||
if err != nil {
|
||||
@@ -364,14 +361,10 @@ func run(cfg *config.Config) error {
|
||||
Err(err).
|
||||
Str("attachment_id", msg.Metadata["attachment_id"]).
|
||||
Msg("failed to parse attachment ID from message metadata")
|
||||
msg.Nack()
|
||||
return err
|
||||
}
|
||||
err = app.repos.Attachments.CreateThumbnail(ctx, groupId, attachmentId, msg.Metadata["title"], msg.Metadata["path"])
|
||||
if err != nil {
|
||||
msg.Nack()
|
||||
log.Err(err).Msg("failed to create thumbnail")
|
||||
return err
|
||||
}
|
||||
msg.Ack()
|
||||
}
|
||||
|
||||
@@ -2193,6 +2193,10 @@ const docTemplate = `{
|
||||
"description": "ID of the ent.",
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"description": "MimeType holds the value of the \"mime_type\" field.",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path holds the value of the \"path\" field.",
|
||||
"type": "string"
|
||||
@@ -3122,6 +3126,9 @@ const docTemplate = `{
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2191,6 +2191,10 @@
|
||||
"description": "ID of the ent.",
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"description": "MimeType holds the value of the \"mime_type\" field.",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path holds the value of the \"path\" field.",
|
||||
"type": "string"
|
||||
@@ -3120,6 +3124,9 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -55,6 +55,9 @@ definitions:
|
||||
id:
|
||||
description: ID of the ent.
|
||||
type: string
|
||||
mime_type:
|
||||
description: MimeType holds the value of the "mime_type" field.
|
||||
type: string
|
||||
path:
|
||||
description: Path holds the value of the "path" field.
|
||||
type: string
|
||||
@@ -684,6 +687,8 @@ definitions:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
mimeType:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
primary:
|
||||
|
||||
@@ -9,6 +9,8 @@ require (
|
||||
github.com/ardanlabs/conf/v3 v3.8.0
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/gen2brain/avif v0.4.4
|
||||
github.com/gen2brain/heic v0.4.5
|
||||
github.com/gen2brain/jpegxl v0.4.5
|
||||
github.com/gen2brain/webp v0.5.5
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
|
||||
@@ -183,6 +183,10 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
|
||||
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
|
||||
github.com/gen2brain/heic v0.4.5 h1:Cq3hPu6wwlTJNv2t48ro3oWje54h82Q5pALeCBNgaSk=
|
||||
github.com/gen2brain/heic v0.4.5/go.mod h1:ECnpqbqLu0qSje4KSNWUUDK47UPXPzl80T27GWGEL5I=
|
||||
github.com/gen2brain/jpegxl v0.4.5 h1:TWpVEn5xkIfsswzkjHBArd0Cc9AE0tbjBSoa0jDsrbo=
|
||||
github.com/gen2brain/jpegxl v0.4.5/go.mod h1:4kWYJ18xCEuO2vzocYdGpeqNJ990/Gjy3uLMg5TBN6I=
|
||||
github.com/gen2brain/webp v0.5.5 h1:MvQR75yIPU/9nSqYT5h13k4URaJK3gf9tgz/ksRbyEg=
|
||||
github.com/gen2brain/webp v0.5.5/go.mod h1:xOSMzp4aROt2KFW++9qcK/RBTOVC2S9tJG66ip/9Oc0=
|
||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
@@ -346,6 +350,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
@@ -369,6 +375,8 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/olahol/melody v1.2.1 h1:xdwRkzHxf+B0w4TKbGpUSSkV516ZucQZJIWLztOWICQ=
|
||||
github.com/olahol/melody v1.2.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
@@ -409,6 +417,10 @@ github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQU
|
||||
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
||||
@@ -31,6 +31,8 @@ type Attachment struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
// Path holds the value of the "path" field.
|
||||
Path string `json:"path,omitempty"`
|
||||
// MimeType holds the value of the "mime_type" field.
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the AttachmentQuery when eager-loading is set.
|
||||
Edges AttachmentEdges `json:"edges"`
|
||||
@@ -79,7 +81,7 @@ func (*Attachment) scanValues(columns []string) ([]any, error) {
|
||||
switch columns[i] {
|
||||
case attachment.FieldPrimary:
|
||||
values[i] = new(sql.NullBool)
|
||||
case attachment.FieldType, attachment.FieldTitle, attachment.FieldPath:
|
||||
case attachment.FieldType, attachment.FieldTitle, attachment.FieldPath, attachment.FieldMimeType:
|
||||
values[i] = new(sql.NullString)
|
||||
case attachment.FieldCreatedAt, attachment.FieldUpdatedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
@@ -146,6 +148,12 @@ func (a *Attachment) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
a.Path = value.String
|
||||
}
|
||||
case attachment.FieldMimeType:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field mime_type", values[i])
|
||||
} else if value.Valid {
|
||||
a.MimeType = value.String
|
||||
}
|
||||
case attachment.ForeignKeys[0]:
|
||||
if value, ok := values[i].(*sql.NullScanner); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field attachment_thumbnail", values[i])
|
||||
@@ -223,6 +231,9 @@ func (a *Attachment) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("path=")
|
||||
builder.WriteString(a.Path)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("mime_type=")
|
||||
builder.WriteString(a.MimeType)
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ const (
|
||||
FieldTitle = "title"
|
||||
// FieldPath holds the string denoting the path field in the database.
|
||||
FieldPath = "path"
|
||||
// FieldMimeType holds the string denoting the mime_type field in the database.
|
||||
FieldMimeType = "mime_type"
|
||||
// EdgeItem holds the string denoting the item edge name in mutations.
|
||||
EdgeItem = "item"
|
||||
// EdgeThumbnail holds the string denoting the thumbnail edge name in mutations.
|
||||
@@ -56,6 +58,7 @@ var Columns = []string{
|
||||
FieldPrimary,
|
||||
FieldTitle,
|
||||
FieldPath,
|
||||
FieldMimeType,
|
||||
}
|
||||
|
||||
// ForeignKeys holds the SQL foreign-keys that are owned by the "attachments"
|
||||
@@ -93,6 +96,8 @@ var (
|
||||
DefaultTitle string
|
||||
// DefaultPath holds the default value on creation for the "path" field.
|
||||
DefaultPath string
|
||||
// DefaultMimeType holds the default value on creation for the "mime_type" field.
|
||||
DefaultMimeType string
|
||||
// DefaultID holds the default value on creation for the "id" field.
|
||||
DefaultID func() uuid.UUID
|
||||
)
|
||||
@@ -165,6 +170,11 @@ func ByPath(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldPath, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByMimeType orders the results by the mime_type field.
|
||||
func ByMimeType(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldMimeType, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByItemField orders the results by item field.
|
||||
func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption {
|
||||
return func(s *sql.Selector) {
|
||||
|
||||
@@ -81,6 +81,11 @@ func Path(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldEQ(FieldPath, v))
|
||||
}
|
||||
|
||||
// MimeType applies equality check predicate on the "mime_type" field. It's identical to MimeTypeEQ.
|
||||
func MimeType(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldEQ(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||
func CreatedAtEQ(v time.Time) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldEQ(FieldCreatedAt, v))
|
||||
@@ -321,6 +326,71 @@ func PathContainsFold(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldContainsFold(FieldPath, v))
|
||||
}
|
||||
|
||||
// MimeTypeEQ applies the EQ predicate on the "mime_type" field.
|
||||
func MimeTypeEQ(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldEQ(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeNEQ applies the NEQ predicate on the "mime_type" field.
|
||||
func MimeTypeNEQ(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldNEQ(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeIn applies the In predicate on the "mime_type" field.
|
||||
func MimeTypeIn(vs ...string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldIn(FieldMimeType, vs...))
|
||||
}
|
||||
|
||||
// MimeTypeNotIn applies the NotIn predicate on the "mime_type" field.
|
||||
func MimeTypeNotIn(vs ...string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldNotIn(FieldMimeType, vs...))
|
||||
}
|
||||
|
||||
// MimeTypeGT applies the GT predicate on the "mime_type" field.
|
||||
func MimeTypeGT(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldGT(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeGTE applies the GTE predicate on the "mime_type" field.
|
||||
func MimeTypeGTE(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldGTE(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeLT applies the LT predicate on the "mime_type" field.
|
||||
func MimeTypeLT(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldLT(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeLTE applies the LTE predicate on the "mime_type" field.
|
||||
func MimeTypeLTE(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldLTE(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeContains applies the Contains predicate on the "mime_type" field.
|
||||
func MimeTypeContains(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldContains(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeHasPrefix applies the HasPrefix predicate on the "mime_type" field.
|
||||
func MimeTypeHasPrefix(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldHasPrefix(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeHasSuffix applies the HasSuffix predicate on the "mime_type" field.
|
||||
func MimeTypeHasSuffix(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldHasSuffix(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeEqualFold applies the EqualFold predicate on the "mime_type" field.
|
||||
func MimeTypeEqualFold(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldEqualFold(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// MimeTypeContainsFold applies the ContainsFold predicate on the "mime_type" field.
|
||||
func MimeTypeContainsFold(v string) predicate.Attachment {
|
||||
return predicate.Attachment(sql.FieldContainsFold(FieldMimeType, v))
|
||||
}
|
||||
|
||||
// HasItem applies the HasEdge predicate on the "item" edge.
|
||||
func HasItem() predicate.Attachment {
|
||||
return predicate.Attachment(func(s *sql.Selector) {
|
||||
|
||||
@@ -106,6 +106,20 @@ func (ac *AttachmentCreate) SetNillablePath(s *string) *AttachmentCreate {
|
||||
return ac
|
||||
}
|
||||
|
||||
// SetMimeType sets the "mime_type" field.
|
||||
func (ac *AttachmentCreate) SetMimeType(s string) *AttachmentCreate {
|
||||
ac.mutation.SetMimeType(s)
|
||||
return ac
|
||||
}
|
||||
|
||||
// SetNillableMimeType sets the "mime_type" field if the given value is not nil.
|
||||
func (ac *AttachmentCreate) SetNillableMimeType(s *string) *AttachmentCreate {
|
||||
if s != nil {
|
||||
ac.SetMimeType(*s)
|
||||
}
|
||||
return ac
|
||||
}
|
||||
|
||||
// SetID sets the "id" field.
|
||||
func (ac *AttachmentCreate) SetID(u uuid.UUID) *AttachmentCreate {
|
||||
ac.mutation.SetID(u)
|
||||
@@ -217,6 +231,10 @@ func (ac *AttachmentCreate) defaults() {
|
||||
v := attachment.DefaultPath
|
||||
ac.mutation.SetPath(v)
|
||||
}
|
||||
if _, ok := ac.mutation.MimeType(); !ok {
|
||||
v := attachment.DefaultMimeType
|
||||
ac.mutation.SetMimeType(v)
|
||||
}
|
||||
if _, ok := ac.mutation.ID(); !ok {
|
||||
v := attachment.DefaultID()
|
||||
ac.mutation.SetID(v)
|
||||
@@ -248,6 +266,9 @@ func (ac *AttachmentCreate) check() error {
|
||||
if _, ok := ac.mutation.Path(); !ok {
|
||||
return &ValidationError{Name: "path", err: errors.New(`ent: missing required field "Attachment.path"`)}
|
||||
}
|
||||
if _, ok := ac.mutation.MimeType(); !ok {
|
||||
return &ValidationError{Name: "mime_type", err: errors.New(`ent: missing required field "Attachment.mime_type"`)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -307,6 +328,10 @@ func (ac *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(attachment.FieldPath, field.TypeString, value)
|
||||
_node.Path = value
|
||||
}
|
||||
if value, ok := ac.mutation.MimeType(); ok {
|
||||
_spec.SetField(attachment.FieldMimeType, field.TypeString, value)
|
||||
_node.MimeType = value
|
||||
}
|
||||
if nodes := ac.mutation.ItemIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
|
||||
@@ -92,6 +92,20 @@ func (au *AttachmentUpdate) SetNillablePath(s *string) *AttachmentUpdate {
|
||||
return au
|
||||
}
|
||||
|
||||
// SetMimeType sets the "mime_type" field.
|
||||
func (au *AttachmentUpdate) SetMimeType(s string) *AttachmentUpdate {
|
||||
au.mutation.SetMimeType(s)
|
||||
return au
|
||||
}
|
||||
|
||||
// SetNillableMimeType sets the "mime_type" field if the given value is not nil.
|
||||
func (au *AttachmentUpdate) SetNillableMimeType(s *string) *AttachmentUpdate {
|
||||
if s != nil {
|
||||
au.SetMimeType(*s)
|
||||
}
|
||||
return au
|
||||
}
|
||||
|
||||
// SetItemID sets the "item" edge to the Item entity by ID.
|
||||
func (au *AttachmentUpdate) SetItemID(id uuid.UUID) *AttachmentUpdate {
|
||||
au.mutation.SetItemID(id)
|
||||
@@ -220,6 +234,9 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
||||
if value, ok := au.mutation.Path(); ok {
|
||||
_spec.SetField(attachment.FieldPath, field.TypeString, value)
|
||||
}
|
||||
if value, ok := au.mutation.MimeType(); ok {
|
||||
_spec.SetField(attachment.FieldMimeType, field.TypeString, value)
|
||||
}
|
||||
if au.mutation.ItemCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
@@ -360,6 +377,20 @@ func (auo *AttachmentUpdateOne) SetNillablePath(s *string) *AttachmentUpdateOne
|
||||
return auo
|
||||
}
|
||||
|
||||
// SetMimeType sets the "mime_type" field.
|
||||
func (auo *AttachmentUpdateOne) SetMimeType(s string) *AttachmentUpdateOne {
|
||||
auo.mutation.SetMimeType(s)
|
||||
return auo
|
||||
}
|
||||
|
||||
// SetNillableMimeType sets the "mime_type" field if the given value is not nil.
|
||||
func (auo *AttachmentUpdateOne) SetNillableMimeType(s *string) *AttachmentUpdateOne {
|
||||
if s != nil {
|
||||
auo.SetMimeType(*s)
|
||||
}
|
||||
return auo
|
||||
}
|
||||
|
||||
// SetItemID sets the "item" edge to the Item entity by ID.
|
||||
func (auo *AttachmentUpdateOne) SetItemID(id uuid.UUID) *AttachmentUpdateOne {
|
||||
auo.mutation.SetItemID(id)
|
||||
@@ -518,6 +549,9 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
|
||||
if value, ok := auo.mutation.Path(); ok {
|
||||
_spec.SetField(attachment.FieldPath, field.TypeString, value)
|
||||
}
|
||||
if value, ok := auo.mutation.MimeType(); ok {
|
||||
_spec.SetField(attachment.FieldMimeType, field.TypeString, value)
|
||||
}
|
||||
if auo.mutation.ItemCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.M2O,
|
||||
|
||||
@@ -17,6 +17,7 @@ var (
|
||||
{Name: "primary", Type: field.TypeBool, Default: false},
|
||||
{Name: "title", Type: field.TypeString, Default: ""},
|
||||
{Name: "path", Type: field.TypeString, Default: ""},
|
||||
{Name: "mime_type", Type: field.TypeString, Default: "application/octet-stream"},
|
||||
{Name: "attachment_thumbnail", Type: field.TypeUUID, Unique: true, Nullable: true},
|
||||
{Name: "item_attachments", Type: field.TypeUUID, Nullable: true},
|
||||
}
|
||||
@@ -28,13 +29,13 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "attachments_attachments_thumbnail",
|
||||
Columns: []*schema.Column{AttachmentsColumns[7]},
|
||||
Columns: []*schema.Column{AttachmentsColumns[8]},
|
||||
RefColumns: []*schema.Column{AttachmentsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "attachments_items_attachments",
|
||||
Columns: []*schema.Column{AttachmentsColumns[8]},
|
||||
Columns: []*schema.Column{AttachmentsColumns[9]},
|
||||
RefColumns: []*schema.Column{ItemsColumns[0]},
|
||||
OnDelete: schema.Cascade,
|
||||
},
|
||||
|
||||
@@ -62,6 +62,7 @@ type AttachmentMutation struct {
|
||||
primary *bool
|
||||
title *string
|
||||
_path *string
|
||||
mime_type *string
|
||||
clearedFields map[string]struct{}
|
||||
item *uuid.UUID
|
||||
cleareditem bool
|
||||
@@ -392,6 +393,42 @@ func (m *AttachmentMutation) ResetPath() {
|
||||
m._path = nil
|
||||
}
|
||||
|
||||
// SetMimeType sets the "mime_type" field.
|
||||
func (m *AttachmentMutation) SetMimeType(s string) {
|
||||
m.mime_type = &s
|
||||
}
|
||||
|
||||
// MimeType returns the value of the "mime_type" field in the mutation.
|
||||
func (m *AttachmentMutation) MimeType() (r string, exists bool) {
|
||||
v := m.mime_type
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldMimeType returns the old "mime_type" field's value of the Attachment entity.
|
||||
// If the Attachment object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *AttachmentMutation) OldMimeType(ctx context.Context) (v string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldMimeType is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldMimeType requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldMimeType: %w", err)
|
||||
}
|
||||
return oldValue.MimeType, nil
|
||||
}
|
||||
|
||||
// ResetMimeType resets all changes to the "mime_type" field.
|
||||
func (m *AttachmentMutation) ResetMimeType() {
|
||||
m.mime_type = nil
|
||||
}
|
||||
|
||||
// SetItemID sets the "item" edge to the Item entity by id.
|
||||
func (m *AttachmentMutation) SetItemID(id uuid.UUID) {
|
||||
m.item = &id
|
||||
@@ -504,7 +541,7 @@ func (m *AttachmentMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *AttachmentMutation) Fields() []string {
|
||||
fields := make([]string, 0, 6)
|
||||
fields := make([]string, 0, 7)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, attachment.FieldCreatedAt)
|
||||
}
|
||||
@@ -523,6 +560,9 @@ func (m *AttachmentMutation) Fields() []string {
|
||||
if m._path != nil {
|
||||
fields = append(fields, attachment.FieldPath)
|
||||
}
|
||||
if m.mime_type != nil {
|
||||
fields = append(fields, attachment.FieldMimeType)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -543,6 +583,8 @@ func (m *AttachmentMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.Title()
|
||||
case attachment.FieldPath:
|
||||
return m.Path()
|
||||
case attachment.FieldMimeType:
|
||||
return m.MimeType()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -564,6 +606,8 @@ func (m *AttachmentMutation) OldField(ctx context.Context, name string) (ent.Val
|
||||
return m.OldTitle(ctx)
|
||||
case attachment.FieldPath:
|
||||
return m.OldPath(ctx)
|
||||
case attachment.FieldMimeType:
|
||||
return m.OldMimeType(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown Attachment field %s", name)
|
||||
}
|
||||
@@ -615,6 +659,13 @@ func (m *AttachmentMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetPath(v)
|
||||
return nil
|
||||
case attachment.FieldMimeType:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetMimeType(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Attachment field %s", name)
|
||||
}
|
||||
@@ -682,6 +733,9 @@ func (m *AttachmentMutation) ResetField(name string) error {
|
||||
case attachment.FieldPath:
|
||||
m.ResetPath()
|
||||
return nil
|
||||
case attachment.FieldMimeType:
|
||||
m.ResetMimeType()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Attachment field %s", name)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ func init() {
|
||||
attachmentDescPath := attachmentFields[3].Descriptor()
|
||||
// attachment.DefaultPath holds the default value on creation for the path field.
|
||||
attachment.DefaultPath = attachmentDescPath.Default.(string)
|
||||
// attachmentDescMimeType is the schema descriptor for mime_type field.
|
||||
attachmentDescMimeType := attachmentFields[4].Descriptor()
|
||||
// attachment.DefaultMimeType holds the default value on creation for the mime_type field.
|
||||
attachment.DefaultMimeType = attachmentDescMimeType.Default.(string)
|
||||
// attachmentDescID is the schema descriptor for id field.
|
||||
attachmentDescID := attachmentMixinFields0[0].Descriptor()
|
||||
// attachment.DefaultID holds the default value on creation for the id field.
|
||||
|
||||
@@ -25,6 +25,7 @@ func (Attachment) Fields() []ent.Field {
|
||||
field.Bool("primary").Default(false),
|
||||
field.String("title").Default(""),
|
||||
field.String("path").Default(""),
|
||||
field.String("mime_type").Default("application/octet-stream"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE public.attachments ADD COLUMN mime_type VARCHAR DEFAULT 'application/octet-stream';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE attachments ADD COLUMN mime_type TEXT DEFAULT 'application/octet-stream';
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/zeebo/blake3"
|
||||
|
||||
"github.com/gen2brain/avif"
|
||||
"github.com/gen2brain/heic"
|
||||
"github.com/gen2brain/jpegxl"
|
||||
"github.com/gen2brain/webp"
|
||||
"golang.org/x/image/draw"
|
||||
"image"
|
||||
@@ -62,6 +64,7 @@ type (
|
||||
Primary bool `json:"primary"`
|
||||
Path string `json:"path"`
|
||||
Title string `json:"title"`
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
Thumbnail *ent.Attachment `json:"thumbnail,omitempty"`
|
||||
}
|
||||
|
||||
@@ -87,6 +90,8 @@ func ToItemAttachment(attachment *ent.Attachment) ItemAttachment {
|
||||
Primary: attachment.Primary,
|
||||
Path: attachment.Path,
|
||||
Title: attachment.Title,
|
||||
MimeType: attachment.MimeType,
|
||||
Thumbnail: attachment.QueryThumbnail().FirstX(context.Background()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +198,17 @@ func (r *AttachmentRepo) Create(ctx context.Context, itemID uuid.UUID, doc ItemC
|
||||
return nil, err
|
||||
}
|
||||
|
||||
limitedReader := io.LimitReader(doc.Content, 1024*128)
|
||||
file, err := io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to read file content")
|
||||
err = tx.Rollback()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
bldr = bldr.SetMimeType(http.DetectContentType(file[:min(512, len(file))]))
|
||||
bldr = bldr.SetPath(path)
|
||||
|
||||
attachmentDb, err := bldr.Save(ctx)
|
||||
@@ -416,6 +432,14 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
log.Debug().Msg("detecting content type of original file")
|
||||
contentType := http.DetectContentType(contentBytes[:min(512, len(contentBytes))])
|
||||
|
||||
if contentType == "application/octet-stream" {
|
||||
if strings.HasSuffix(title, ".heic") || strings.HasSuffix(title, ".heif") {
|
||||
contentType = "image/heic"
|
||||
} else if strings.HasSuffix(title, ".avif") {
|
||||
contentType = "image/avif"
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case isImageFile(contentType):
|
||||
log.Debug().Msg("creating thumbnail for image file")
|
||||
@@ -532,10 +556,88 @@ func (r *AttachmentRepo) CreateThumbnail(ctx context.Context, groupId, attachmen
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
case contentType == "image/heic" || contentType == "image/heif":
|
||||
log.Debug().Msg("creating thumbnail for heic file")
|
||||
img, err := heic.Decode(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to decode avif image")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
buf := new(bytes.Buffer)
|
||||
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
case contentType == "image/jxl":
|
||||
log.Debug().Msg("creating thumbnail for jpegxl file")
|
||||
img, err := jpegxl.Decode(bytes.NewReader(contentBytes))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to decode avif image")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, r.thumbnail.Width, r.thumbnail.Height))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Rect, img, img.Bounds(), draw.Over, nil)
|
||||
buf := new(bytes.Buffer)
|
||||
err = webp.Encode(buf, dst, webp.Options{Quality: 80, Lossless: false})
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
contentBytes := buf.Bytes()
|
||||
log.Debug().Msg("uploading thumbnail file")
|
||||
thumbnailFile, err := r.UploadFile(ctx, tx.Group.GetX(ctx, groupId), ItemCreateAttachment{
|
||||
Title: fmt.Sprintf("%s-thumb", title),
|
||||
Content: bytes.NewReader(contentBytes),
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to upload thumbnail file")
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Debug().Msg("setting thumbnail file path in attachment")
|
||||
att.SetPath(thumbnailFile)
|
||||
default:
|
||||
return fmt.Errorf("file type %s is not supported for thumbnail creation or document thumnails disabled", title)
|
||||
}
|
||||
|
||||
att.SetMimeType("image/webp")
|
||||
|
||||
log.Debug().Msg("saving thumbnail attachment to database")
|
||||
thumbnail, err := att.Save(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -2191,6 +2191,10 @@
|
||||
"description": "ID of the ent.",
|
||||
"type": "string"
|
||||
},
|
||||
"mime_type": {
|
||||
"description": "MimeType holds the value of the \"mime_type\" field.",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path holds the value of the \"path\" field.",
|
||||
"type": "string"
|
||||
@@ -3120,6 +3124,9 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -55,6 +55,9 @@ definitions:
|
||||
id:
|
||||
description: ID of the ent.
|
||||
type: string
|
||||
mime_type:
|
||||
description: MimeType holds the value of the "mime_type" field.
|
||||
type: string
|
||||
path:
|
||||
description: Path holds the value of the "path" field.
|
||||
type: string
|
||||
@@ -684,6 +687,8 @@ definitions:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
mimeType:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
primary:
|
||||
|
||||
@@ -69,6 +69,8 @@ export interface EntAttachment {
|
||||
edges: EntAttachmentEdges;
|
||||
/** ID of the ent. */
|
||||
id: string;
|
||||
/** MimeType holds the value of the "mime_type" field. */
|
||||
mime_type: string;
|
||||
/** Path holds the value of the "path" field. */
|
||||
path: string;
|
||||
/** Primary holds the value of the "primary" field. */
|
||||
@@ -474,6 +476,7 @@ export interface GroupUpdate {
|
||||
export interface ItemAttachment {
|
||||
createdAt: Date | string;
|
||||
id: string;
|
||||
mimeType: string;
|
||||
path: string;
|
||||
primary: boolean;
|
||||
thumbnail: EntAttachment;
|
||||
|
||||
@@ -99,17 +99,28 @@
|
||||
};
|
||||
|
||||
type Photo = {
|
||||
src: string;
|
||||
thumbnailSrc?: string;
|
||||
originalSrc: string;
|
||||
originalType?: string;
|
||||
};
|
||||
|
||||
const photos = computed<Photo[]>(() => {
|
||||
if (!item.value) {
|
||||
return [];
|
||||
}
|
||||
return (
|
||||
item.value?.attachments.reduce((acc, cur) => {
|
||||
item.value.attachments.reduce((acc, cur) => {
|
||||
if (cur.type === "photo") {
|
||||
acc.push({
|
||||
// @ts-expect-error - it's impossible for this to be null at this point
|
||||
src: api.authURL(`/items/${item.value.id}/attachments/${cur.id}`),
|
||||
});
|
||||
const photo: Photo = {
|
||||
originalSrc: api.authURL(`/items/${item.value!.id}/attachments/${cur.id}`),
|
||||
originalType: cur.mimeType,
|
||||
};
|
||||
if (cur.thumbnail) {
|
||||
photo.thumbnailSrc = api.authURL(`/items/${item.value!.id}/attachments/${cur.thumbnail.id}`);
|
||||
} else {
|
||||
photo.thumbnailSrc = photo.originalSrc; // fallback to itself if no thumbnail
|
||||
}
|
||||
acc.push(photo);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Photo[]) || []
|
||||
@@ -387,12 +398,14 @@
|
||||
return v;
|
||||
});
|
||||
|
||||
const dialoged = reactive({
|
||||
src: "",
|
||||
const dialoged = reactive<Photo>({
|
||||
originalSrc: "",
|
||||
});
|
||||
|
||||
function openImageDialog(img: Photo) {
|
||||
dialoged.src = img.src;
|
||||
dialoged.originalSrc = img.originalSrc;
|
||||
dialoged.originalType = img.originalType;
|
||||
dialoged.thumbnailSrc = img.thumbnailSrc;
|
||||
openDialog("item-image");
|
||||
}
|
||||
|
||||
@@ -533,8 +546,16 @@
|
||||
|
||||
<Dialog dialog-id="item-image">
|
||||
<DialogContent class="w-auto border-transparent bg-transparent p-0" disable-close>
|
||||
<img :src="dialoged.src" />
|
||||
<a :class="buttonVariants({ size: 'icon' })" :href="dialoged.src" download class="absolute right-11 top-1">
|
||||
<picture>
|
||||
<source :srcset="dialoged.originalSrc" :type="dialoged.originalType" />
|
||||
<img :src="dialoged.thumbnailSrc" alt="attachement image" />
|
||||
</picture>
|
||||
<a
|
||||
:class="buttonVariants({ size: 'icon' })"
|
||||
:href="dialoged.originalSrc"
|
||||
download
|
||||
class="absolute right-11 top-1"
|
||||
>
|
||||
<MdiDownload />
|
||||
</a>
|
||||
<Button size="icon" class="absolute right-1 top-1" @click="closeImageDialog">
|
||||
@@ -679,7 +700,10 @@
|
||||
<template #title> {{ $t("items.photos") }} </template>
|
||||
<div class="scroll-bg container mx-auto flex max-h-[500px] flex-wrap gap-2 overflow-y-scroll border-t p-4">
|
||||
<button v-for="(img, i) in photos" :key="i" @click="openImageDialog(img)">
|
||||
<img class="max-h-[200px] rounded" :src="img.src" />
|
||||
<picture>
|
||||
<source :srcset="img.originalSrc" :type="img.originalType" />
|
||||
<img class="max-h-[200px] rounded" :src="img.thumbnailSrc" alt="attachment image" />
|
||||
</picture>
|
||||
</button>
|
||||
</div>
|
||||
</BaseCard>
|
||||
|
||||
Reference in New Issue
Block a user