media

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: AGPL-3.0 Imports: 43 Imported by: 0

Documentation

Overview

Package media 提供附件与图片处理。

上传的文件经 Storage 接口落到本地目录或 S3 兼容对象存储;图片额外生成多档 WebP 缩略图。 存储位置由 storage 设置分组决定,可在运行时切换;S3 密钥只从环境变量读取,不入库。

Index

Constants

View Source
const (
	EnvS3AccessKey = "LUMO_S3_ACCESS_KEY"
	EnvS3SecretKey = "LUMO_S3_SECRET_KEY"
)

S3 凭据的兜底环境变量。

密钥的正规去处是后台「附件存储」里的两个字段(加密入库、接口不回传), 环境变量留给不便改后台的部署。两处都有时以后台为准。

View Source
const DriverLocal = "local"

DriverLocal 是本地文件系统驱动名。

View Source
const DriverS3 = "s3"

DriverS3 是 S3 兼容对象存储驱动名。

View Source
const GroupStorage = "storage"

GroupStorage 是附件存储设置分组。

View Source
const Name = "media"

Name 是模块名,也是迁移版本表的后缀(goose_db_version_media)。

View Source
const UploadsDirName = "uploads"

UploadsDirName 是本地驱动在工作目录下的子目录名(与 workdir.Subdirs 一致)。

View Source
const UploadsURLPrefix = "/" + UploadsDirName

UploadsURLPrefix 是本地驱动的对外访问前缀,由核心路由挂载静态文件服务。

Variables

View Source
var (
	// ErrNotFound 表示附件不存在。
	ErrNotFound = errors.New("附件不存在")
	// ErrKeyTaken 表示对象键已被占用。
	ErrKeyTaken = errors.New("对象键已被占用")
	// ErrInvalid 表示字段违反了数据库约束。
	ErrInvalid = errors.New("字段不合法")
)

错误哨兵。处理器据此映射为 404 / 409。

View Source
var DefaultThumbnails = []ThumbnailSpec{
	{Name: "thumb", MaxEdge: 320},
	{Name: "medium", MaxEdge: 768},
	{Name: "large", MaxEdge: 1600},
}

DefaultThumbnails 是默认的缩略图档位。

三档覆盖列表缩略图、正文内联图与全宽大图;原图长边不足某档时跳过该档, 绝不放大——放大只会得到更大的模糊文件。

View Source
var ErrInvalidKey = errors.New("对象键不合法")

ErrInvalidKey 表示对象键不合法。

View Source
var ErrMissingS3Credentials = fmt.Errorf(
	"没有对象存储访问密钥:请在「设置 → 附件存储」里填写,或设置环境变量 %s 与 %s",
	EnvS3AccessKey, EnvS3SecretKey)

ErrMissingS3Credentials 表示没有可用的 S3 凭据。

View Source
var ErrTooLarge = errors.New("文件超过允许的大小")

ErrTooLarge 表示上传内容超过服务端允许的大小。

Functions

func AllowedExtensions

func AllowedExtensions() []string

AllowedExtensions 返回全部允许的扩展名,按字典序,供接口文档与前端提示使用。

func S3Credentials

func S3Credentials() (accessKey, secretKey string, err error)

S3Credentials 从环境变量读取访问密钥,作为后台未配置时的兜底。

Types

type ErrContentMismatch

type ErrContentMismatch struct {
	Ext      string
	Declared string
	Detected string
}

ErrContentMismatch 表示文件内容与扩展名声明的类型不符。

func (*ErrContentMismatch) Error

func (e *ErrContentMismatch) Error() string

Error 实现 error。

type ErrUnsupportedType

type ErrUnsupportedType struct{ Ext string }

ErrUnsupportedType 表示扩展名不在白名单内。

func (*ErrUnsupportedType) Error

func (e *ErrUnsupportedType) Error() string

Error 实现 error。

type Filter

type Filter struct {
	// Kind 非空时只返回该分类的附件。
	Kind Kind
	// UploaderID 非零时只返回该用户上传的附件。
	UploaderID int64
	// Q 非空时按原始文件名模糊筛选。
	Q string
}

Filter 是附件列表的筛选条件。

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler 提供附件的 Console 接口。

func NewHandler

func NewHandler(service *Service, store *Store) *Handler

NewHandler 构造 Handler。

func (*Handler) Register

func (h *Handler) Register(console huma.API)

Register 挂载接口。

读操作对任何已认证用户开放:作者写文章时要能从附件库挑图,而挑图不该需要上传权限。 写操作要求 media:write,改动或删除他人的附件要求 media:delete_any。

type ImageInfo

type ImageInfo struct {
	Width  int
	Height int
}

ImageInfo 是原图的基本信息。

type ImageProcessor

type ImageProcessor interface {
	// Name 返回实现名,写入启动日志便于排查。
	Name() string
	// Process 解码图片(含 EXIF 方向纠正)并按 specs 生成缩略图。
	//
	// 返回的尺寸是**纠正方向之后**的,与缩略图和前台布局一致。
	Process(data []byte, specs []ThumbnailSpec) (ImageInfo, []Variant, error)
}

ImageProcessor 从原始图片字节生成缩略图。

抽成接口是为了给 libvips 留位置:默认实现是纯 Go 的(CGO_ENABLED=0 可编译, 单二进制不依赖系统库),大站可以换成 vips 实现以换取速度与更好的编码质量。 接入方式:新增 image_vips.go 并加 //go:build vips,同时给 image_purego.go 加 //go:build !vips。

func NewImageProcessor

func NewImageProcessor() ImageProcessor

NewImageProcessor 返回默认的图片处理器。

type Kind

type Kind string

Kind 是附件的粗分类,由 MIME 推导,供 Console 侧筛选。

const (
	KindImage    Kind = "image"
	KindVideo    Kind = "video"
	KindAudio    Kind = "audio"
	KindDocument Kind = "document"
	KindOther    Kind = "other"
)

附件分类。

type LocalStorage

type LocalStorage struct {
	// contains filtered or unexported fields
}

LocalStorage 把对象写到工作目录下的 uploads 子目录,由核心路由以静态文件提供。

func NewLocalStorage

func NewLocalStorage(root, urlPrefix string) (*LocalStorage, error)

NewLocalStorage 构造本地驱动。root 为存放目录,urlPrefix 为对外访问前缀(如 /uploads)。

func (*LocalStorage) Delete

func (s *LocalStorage) Delete(_ context.Context, key string) error

Delete 实现 Storage。

func (*LocalStorage) Driver

func (s *LocalStorage) Driver() string

Driver 实现 Storage。

func (*LocalStorage) Put

func (s *LocalStorage) Put(_ context.Context, key string, r io.Reader, _ int64, _ string) error

Put 实现 Storage:原子写入,先写临时文件再改名,避免读到写了一半的文件。

func (*LocalStorage) URL

func (s *LocalStorage) URL(key string) string

URL 实现 Storage:本地驱动给出站内相对地址,换域名或加 CDN 时无需改库。

type Media

type Media struct {
	bun.BaseModel `bun:"table:media,alias:m"`

	ID           int64       `bun:"id,pk,autoincrement" json:"id"`
	Filename     string      `bun:"filename,notnull"    json:"filename" doc:"存储用的随机化文件名"`
	OriginalName string      `bun:"original_name"       json:"originalName" doc:"上传时的原始文件名,仅作展示"`
	MIME         string      `bun:"mime,notnull"        json:"mime"`
	Kind         Kind        `bun:"kind,notnull"        json:"kind" enum:"image,video,audio,document,other"`
	Size         int64       `bun:"size"                json:"size" doc:"字节数"`
	Width        int         `bun:"width"               json:"width" doc:"图片宽度;非图片为 0"`
	Height       int         `bun:"height"              json:"height" doc:"图片高度;非图片为 0"`
	Driver       string      `bun:"driver,notnull"      json:"driver" doc:"写入时使用的存储驱动"`
	StorageKey   string      `bun:"storage_key,notnull" json:"storageKey" doc:"存储内的对象键"`
	URL          string      `bun:"url"                 json:"url" doc:"公开访问地址"`
	Thumbnails   []Thumbnail `bun:"thumbnails,type:jsonb" json:"thumbnails" doc:"按最长边生成的多档 WebP 缩略图"`
	Checksum     string      `bun:"checksum"            json:"checksum" doc:"原文件的 SHA-256"`
	UploaderID   int64       `bun:"uploader_id,notnull" json:"uploaderId"`
	Alt          string      `bun:"alt"                 json:"alt" doc:"无障碍替代文本"`
	Title        string      `bun:"title"               json:"title" doc:"标题"`
	CreatedAt    time.Time   `bun:"created_at,nullzero" json:"createdAt"`
	UpdatedAt    time.Time   `bun:"updated_at,nullzero" json:"updatedAt"`

	// Uploader 由存储层按需填充,不是数据库列。
	Uploader *UploaderView `bun:"-" json:"uploader,omitempty"`
}

Media 是一条附件记录。

func (*Media) Keys

func (m *Media) Keys() []string

Keys 返回该附件占用的全部对象键:原文件加各档缩略图。

删除附件时按此清单逐个删除,避免缩略图变成孤儿文件。

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module 是附件模块。

func New

func New() *Module

New 构造模块。

func (*Module) Migrations

func (m *Module) Migrations() fs.FS

Migrations 实现 app.Migrator。

func (*Module) Name

func (m *Module) Name() string

Name 实现 app.Module。

func (*Module) Navigation

func (m *Module) Navigation() app.Navigation

Navigation 实现 app.NavigationProvider。

func (*Module) Permissions

func (m *Module) Permissions() []app.Permission

Permissions 实现 app.PermissionProvider。

func (*Module) Register

func (m *Module) Register(a *app.App) error

Register 实现 app.Module:只做装配,不访问数据库。

func (*Module) Routes

func (m *Module) Routes(r app.Router)

Routes 实现 app.RouteProvider。

func (*Module) Service

func (m *Module) Service() *Service

Service 返回上传服务,供其他模块(如主题打包、导入导出)复用。

func (*Module) Settings

func (m *Module) Settings() []app.SettingGroup

Settings 实现 app.SettingsProvider。

type S3Config

type S3Config struct {
	// Endpoint 是服务地址,可带协议(https://minio.example.com)也可只写 host:port。
	Endpoint string
	Bucket   string
	Region   string
	// PathStyle 为真时用 endpoint/bucket/key 形式寻址。自建 MinIO、Ceph 一般需要它;
	// 各家兼容程度不一,故不做自动探测,交由部署者显式指定。
	PathStyle bool
	UseSSL    bool
	// PublicURL 是对象的公开访问前缀(如 CDN 域名);留空则由 endpoint 与 bucket 推导。
	PublicURL string
}

S3Config 是 S3 兼容存储的连接参数,来自 storage 设置分组(密钥除外)。

type S3Storage

type S3Storage struct {
	// contains filtered or unexported fields
}

S3Storage 是基于 minio-go 的 S3 兼容驱动。

func NewS3Storage

func NewS3Storage(cfg S3Config, accessKey, secretKey string) (*S3Storage, error)

NewS3Storage 构造 S3 驱动。

func (*S3Storage) Delete

func (s *S3Storage) Delete(ctx context.Context, key string) error

Delete 实现 Storage。S3 的删除本就幂等,对象不存在也返回成功。

func (*S3Storage) Driver

func (s *S3Storage) Driver() string

Driver 实现 Storage。

func (*S3Storage) Put

func (s *S3Storage) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error

Put 实现 Storage。

func (*S3Storage) URL

func (s *S3Storage) URL(key string) string

URL 实现 Storage。

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service 编排上传:类型判定、写存储、生成缩略图、落库。

func From

func From(a *app.App) *Service

From 取回上传服务;media 模块未装配时返回 nil。

调用方须能在 nil 时退化(不提供上传入口即可),以便单独测试其他模块。

func NewService

func NewService(opts ServiceOptions) *Service

NewService 构造 Service。

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, m *Media) error

Delete 删除附件记录并清理其占用的全部对象。

先删库再删文件:反过来一旦删完文件后落库失败,就会留下指向空文件的记录, 那比留下几个孤儿文件更糟——前台会拿到 404 图片。

func (*Service) Storage

func (s *Service) Storage(ctx context.Context) (Storage, error)

Storage 返回当前设置对应的存储实例。

func (*Service) Upload

func (s *Service) Upload(ctx context.Context, params *UploadParams) (*Media, error)

Upload 保存一个上传的文件并返回落库后的记录。

type ServiceOptions

type ServiceOptions struct {
	Store *Store
	// Settings 可为 nil:此时一律使用本地存储的默认配置。
	Settings *settings.Service
	// Images 可为 nil:缺省用纯 Go 实现。
	Images ImageProcessor
	// DataDir 是工作目录,本地存储写在它的 uploads 子目录下。
	DataDir string
	Logger  *slog.Logger
	// MaxUpload 是单个文件的字节上限,0 表示不额外限制(仍受路由层请求体上限约束)。
	MaxUpload int64
}

ServiceOptions 是构造 Service 的依赖。

type Storage

type Storage interface {
	// Driver 返回驱动名,随附件记录入库。
	Driver() string
	// Put 写入对象;size 为负表示长度未知。同名对象存在时覆盖。
	Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error
	// Delete 删除对象;对象不存在不算错误。
	Delete(ctx context.Context, key string) error
	// URL 返回对象的公开访问地址。
	URL(key string) string
}

Storage 是附件的对象存储抽象。

实现必须把 key 当作**不可信输入**再校验一次:调用方虽然只传自己生成的键, 但删除走的是库里的历史记录,多一道校验才能保证路径穿越在任何路径上都不成立。

type StorageSettings

type StorageSettings struct {
	Driver      string `json:"driver"`
	S3Endpoint  string `json:"s3Endpoint"`
	S3Bucket    string `json:"s3Bucket"`
	S3AccessKey string `json:"s3AccessKey"`
	S3SecretKey string `json:"s3SecretKey"`
	S3Region    string `json:"s3Region"`
	S3PathStyle bool   `json:"s3PathStyle"`
	S3UseSSL    bool   `json:"s3UseSsl"`
	S3PublicURL string `json:"s3PublicUrl"`
}

StorageSettings 是 storage 分组的有效值。

两个密钥字段是解密后的明文,只在这条链路里流转:设置服务 → S3 客户端。 出接口时它们恒为空串,见 settings.Group.Mask。

func (*StorageSettings) Credentials

func (st *StorageSettings) Credentials() (accessKey, secretKey string, err error)

Credentials 返回本次连接该用的访问密钥:后台填过就用后台的,否则回退环境变量。

只有一边填了就用一边、另一边去环境变量里凑是不行的——那样拼出来的是一个 谁也没配过的密钥对,错误信息还会指向「密钥不对」,而真正的问题是少填了一格。

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store 提供附件的持久化操作。

func NewStore

func NewStore(db *bun.DB, users *auth.Store) *Store

NewStore 构造 Store。

func (*Store) Create

func (s *Store) Create(ctx context.Context, m *Media) error

Create 写入附件记录;成功后回填 ID 与时间戳。

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, id int64) error

Delete 删除附件记录。文件本身由服务层在记录删除成功后清理。

func (*Store) Get

func (s *Store) Get(ctx context.Context, id int64) (*Media, error)

Get 按 ID 查附件。

func (*Store) Page

func (s *Store) Page(ctx context.Context, filter Filter, params api.PageParams) ([]Media, int, error)

Page 分页返回附件,按上传时间倒序。

func (*Store) UpdateMeta

func (s *Store) UpdateMeta(ctx context.Context, m *Media) error

UpdateMeta 更新附件的展示信息(alt 与标题),不动文件本身。

type Thumbnail

type Thumbnail struct {
	Name   string `json:"name" doc:"档位名,如 thumb / medium / large"`
	Key    string `json:"key" doc:"存储内的对象键"`
	URL    string `json:"url" doc:"公开访问地址"`
	Width  int    `json:"width"`
	Height int    `json:"height"`
	Size   int64  `json:"size" doc:"字节数"`
}

Thumbnail 是一档缩略图。

type ThumbnailSpec

type ThumbnailSpec struct {
	// Name 是档位名,写入缩略图记录并出现在文件名里。
	Name string
	// MaxEdge 是缩放后最长边的像素上限。
	MaxEdge int
}

ThumbnailSpec 是一档缩略图的规格:按最长边等比缩放。

type UploadParams

type UploadParams struct {
	// OriginalName 是客户端给出的文件名,只用于取扩展名与展示,绝不参与路径拼接。
	OriginalName string
	// Size 是内容字节数的客户端声明值:用于超限预判与 S3 分片决策,落库以实际读到的字节数为准。
	Size    int64
	Content io.Reader
	Alt     string
	Title   string
	// UploaderID 是上传者的用户 ID。
	UploaderID int64
}

UploadParams 描述一次上传。

type UploaderView

type UploaderView struct {
	ID          int64  `json:"id"`
	Username    string `json:"username"`
	DisplayName string `json:"displayName"`
	AvatarURL   string `json:"avatarUrl"`
}

UploaderView 是嵌入附件响应的上传者摘要,只含可公开的字段。

type Variant

type Variant struct {
	Name   string
	Width  int
	Height int
	// Ext 是编码格式对应的扩展名,如 .webp。
	Ext string
	// MIME 是编码格式的媒体类型。
	MIME string
	Data []byte
}

Variant 是一档已编码的缩略图。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL