More image type support for thumbnails (#814)

This commit is contained in:
Matt
2025-06-26 10:19:34 -04:00
committed by GitHub
parent 09cccc63a7
commit 4861a8537f
23 changed files with 408 additions and 25 deletions

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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"
},

View File

@@ -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"
},

View File

@@ -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:

View File

@@ -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

View File

@@ -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=

View File

@@ -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()
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
},

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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"),
}
}

View File

@@ -0,0 +1,3 @@
-- +goose Up
ALTER TABLE public.attachments ADD COLUMN mime_type VARCHAR DEFAULT 'application/octet-stream';

View File

@@ -0,0 +1,3 @@
-- +goose Up
ALTER TABLE attachments ADD COLUMN mime_type TEXT DEFAULT 'application/octet-stream';

View File

@@ -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 {

View File

@@ -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"
},

View File

@@ -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:

View File

@@ -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;

View File

@@ -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>