feat: add image management functionality
- Implemented image folder repository with CRUD operations. - Added image reference repository for managing image references to articles. - Created image repository for handling image assets, including listing, inserting, updating, and deleting images. - Introduced image usage repository to track storage usage and quotas for tenants. - Added SQL queries for image assets, folders, references, and usage. - Developed image handler for HTTP endpoints to manage images and folders. - Created database migration scripts for image-related tables and structures.
This commit is contained in:
@@ -80,6 +80,12 @@ type RabbitMQConfig struct {
|
||||
TemplateAssistDLX string `mapstructure:"template_assist_dlx"`
|
||||
TemplateAssistDLQ string `mapstructure:"template_assist_dlq"`
|
||||
TemplateAssistDLQRouteKey string `mapstructure:"template_assist_dlq_routing_key"`
|
||||
ImageAssetExchange string `mapstructure:"image_asset_exchange"`
|
||||
ImageAssetRoutingKey string `mapstructure:"image_asset_routing_key"`
|
||||
ImageAssetQueue string `mapstructure:"image_asset_queue"`
|
||||
ImageAssetDLX string `mapstructure:"image_asset_dlx"`
|
||||
ImageAssetDLQ string `mapstructure:"image_asset_dlq"`
|
||||
ImageAssetDLQRouteKey string `mapstructure:"image_asset_dlq_routing_key"`
|
||||
ConsumerPrefetch int `mapstructure:"consumer_prefetch"`
|
||||
PublishChannelPoolSize int `mapstructure:"publish_channel_pool_size"`
|
||||
}
|
||||
@@ -337,6 +343,24 @@ func normalizeRabbitMQConfig(cfg *RabbitMQConfig) {
|
||||
if strings.TrimSpace(cfg.TemplateAssistDLQRouteKey) == "" {
|
||||
cfg.TemplateAssistDLQRouteKey = "template.assist.run.dlq"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ImageAssetExchange) == "" {
|
||||
cfg.ImageAssetExchange = "image.asset"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ImageAssetRoutingKey) == "" {
|
||||
cfg.ImageAssetRoutingKey = "image.asset.finalize"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ImageAssetQueue) == "" {
|
||||
cfg.ImageAssetQueue = "image.asset.finalize"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ImageAssetDLX) == "" {
|
||||
cfg.ImageAssetDLX = "image.asset.dlx"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ImageAssetDLQ) == "" {
|
||||
cfg.ImageAssetDLQ = "image.asset.finalize.dlq"
|
||||
}
|
||||
if strings.TrimSpace(cfg.ImageAssetDLQRouteKey) == "" {
|
||||
cfg.ImageAssetDLQRouteKey = "image.asset.finalize.dlq"
|
||||
}
|
||||
if cfg.PublishChannelPoolSize <= 0 {
|
||||
cfg.PublishChannelPoolSize = 16
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@ func (c *Client) PublishTemplateAssistTask(ctx context.Context, body []byte) err
|
||||
return c.publish(ctx, c.cfg.TemplateAssistExchange, c.cfg.TemplateAssistRoutingKey, body)
|
||||
}
|
||||
|
||||
func (c *Client) PublishImageAssetTask(ctx context.Context, body []byte) error {
|
||||
return c.publish(ctx, c.cfg.ImageAssetExchange, c.cfg.ImageAssetRoutingKey, body)
|
||||
}
|
||||
|
||||
func (c *Client) publish(ctx context.Context, exchange, routingKey string, body []byte) error {
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
conn, ch, pooled, err := c.acquirePublishChannel()
|
||||
@@ -176,6 +180,10 @@ func (c *Client) ConsumeTemplateAssistTask(consumerName string) (<-chan amqp.Del
|
||||
return c.consume(consumerName, c.cfg.TemplateAssistQueue)
|
||||
}
|
||||
|
||||
func (c *Client) ConsumeImageAssetTask(consumerName string) (<-chan amqp.Delivery, *amqp.Channel, error) {
|
||||
return c.consume(consumerName, c.cfg.ImageAssetQueue)
|
||||
}
|
||||
|
||||
func (c *Client) consume(consumerName, queue string) (<-chan amqp.Delivery, *amqp.Channel, error) {
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
conn, ch, err := c.openChannel()
|
||||
@@ -554,6 +562,18 @@ func (c *Client) ensureTopology(conn *amqp.Connection) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.ensureWorkQueue(
|
||||
ch,
|
||||
c.cfg.ImageAssetExchange,
|
||||
c.cfg.ImageAssetRoutingKey,
|
||||
c.cfg.ImageAssetQueue,
|
||||
c.cfg.ImageAssetDLX,
|
||||
c.cfg.ImageAssetDLQ,
|
||||
c.cfg.ImageAssetDLQRouteKey,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,36 @@ func (c *aliyunClient) GetBytes(ctx context.Context, objectKey string) ([]byte,
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *aliyunClient) Exists(ctx context.Context, objectKey string) (bool, error) {
|
||||
if err := c.Validate(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.TrimSpace(objectKey) == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
bucket, err := c.client.Bucket(c.bucket)
|
||||
if err != nil {
|
||||
c.logError(ctx, "aliyun get bucket failed",
|
||||
zap.String("bucket", c.bucket),
|
||||
zap.String("object_key", objectKey),
|
||||
zap.Error(err),
|
||||
)
|
||||
return false, fmt.Errorf("get aliyun bucket: %w", err)
|
||||
}
|
||||
|
||||
exists, err := bucket.IsObjectExist(objectKey)
|
||||
if err != nil {
|
||||
c.logError(ctx, "aliyun stat object failed",
|
||||
zap.String("bucket", c.bucket),
|
||||
zap.String("object_key", objectKey),
|
||||
zap.Error(err),
|
||||
)
|
||||
return false, fmt.Errorf("stat aliyun object: %w", err)
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func (c *aliyunClient) Delete(ctx context.Context, objectKey string) error {
|
||||
if err := c.Validate(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -17,6 +17,7 @@ type Client interface {
|
||||
Validate() error
|
||||
PutBytes(ctx context.Context, objectKey string, content []byte, contentType string) error
|
||||
GetBytes(ctx context.Context, objectKey string) ([]byte, error)
|
||||
Exists(ctx context.Context, objectKey string) (bool, error)
|
||||
Delete(ctx context.Context, objectKey string) error
|
||||
PublicURL(objectKey string) (string, error)
|
||||
}
|
||||
@@ -54,6 +55,10 @@ func (c disabledClient) GetBytes(context.Context, string) ([]byte, error) {
|
||||
return nil, c.Validate()
|
||||
}
|
||||
|
||||
func (c disabledClient) Exists(context.Context, string) (bool, error) {
|
||||
return false, c.Validate()
|
||||
}
|
||||
|
||||
func (c disabledClient) Delete(context.Context, string) error {
|
||||
return c.Validate()
|
||||
}
|
||||
|
||||
@@ -135,6 +135,32 @@ func (c *minioClient) GetBytes(ctx context.Context, objectKey string) ([]byte, e
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *minioClient) Exists(ctx context.Context, objectKey string) (bool, error) {
|
||||
if err := c.Validate(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.TrimSpace(objectKey) == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, err := c.client.StatObject(ctx, c.bucket, objectKey, minio.StatObjectOptions{})
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
responseErr := minio.ToErrorResponse(err)
|
||||
if responseErr.StatusCode == 404 || responseErr.Code == "NoSuchKey" || responseErr.Code == "NoSuchObject" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
c.logError(ctx, "minio stat object failed",
|
||||
zap.String("bucket", c.bucket),
|
||||
zap.String("object_key", objectKey),
|
||||
zap.Error(err),
|
||||
)
|
||||
return false, fmt.Errorf("stat minio object: %w", err)
|
||||
}
|
||||
|
||||
func (c *minioClient) Delete(ctx context.Context, objectKey string) error {
|
||||
if err := c.Validate(); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user