types

package
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: May 11, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Index

Constants

View Source
const (
	CodeFileTooLarge     = 10001 // 文件超过大小限制
	CodeFileTypeNotAllow = 10002 // 文件类型不允许
	CodeChecksumMismatch = 10003 // 校验和不匹配
	CodeFileReadError    = 10004 // 文件读取错误
	CodeInvalidParam     = 10005 // 参数无效
	CodeMultipartError   = 10006 // multipart 解析错误
)

上传错误码 10001-10099.

View Source
const (
	CodeSessionNotFound    = 20001 // 上传会话不存在
	CodeSessionExpired     = 20002 // 上传会话已过期
	CodeChunkIndexOutRange = 20003 // 分片索引越界
	CodeChunkDuplicate     = 20004 // 分片重复上传
	CodeChunkHashMismatch  = 20005 // 分片校验和不匹配
	CodeMergeIncomplete    = 20006 // 分片未全部上传
	CodeMergeHashFailed    = 20007 // 合并后校验失败
)

分片错误码 20001-20099.

View Source
const (
	CodeStorageWriteFail      = 30001 // 存储写入失败
	CodeStorageReadFail       = 30002 // 存储读取失败
	CodeStorageDeleteFail     = 30003 // 存储删除失败
	CodeObjectStatUnsupported = 30004 // 当前存储不支持对象 Stat
	CodeObjectCopyUnsupported = 30005 // 当前存储不支持对象 Copy
)

存储错误码 30001-30099.

View Source
const (
	CodeSTSNotConfigured   = 40001 // STS 未配置
	CodeSTSIssueFailed     = 40002 // STS 凭证签发失败
	CodeCallbackVerifyFail = 40003 // 回调验签失败
	CodeDirectFileNotFound = 40004 // 直传文件不存在
)

STS 直传错误码 40001-40099.

View Source
const (
	CodeMultipartNotSupported = 50001 // 当前存储后端不支持 Multipart 直传
	CodeMultipartInitFailed   = 50002 // 云端 Multipart 初始化失败
	CodeMultipartPresignFail  = 50003 // 预签名 URL 生成失败
	CodeMultipartCompleteFail = 50004 // 云端 Multipart 合并失败
	CodeMultipartAbortFail    = 50005 // 云端 Multipart 中止失败
	CodePartVerifyFail        = 50006 // 服务端二次校验 parts 失败
	CodePartETagMismatch      = 50007 // 客户端上报 ETag 与云端不一致
	CodeUnauthorized          = 50008 // 无权访问该会话(会话所有权校验失败)
)

Multipart 直传错误码 50001-50099.

View Source
const (
	StageInit     = "init"     // 会话初始化失败:参数校验 / 云端 InitMultipart 等
	StagePart     = "part"     // 分片上传阶段失败:SaveChunkData / 哈希不匹配 / ReportPartETag
	StageMerge    = "merge"    // 代理路径合并失败
	StageComplete = "complete" // Multipart 直传 Complete 阶段失败
	StageAbort    = "abort"    // 中止阶段失败(本地或云端清理失败)
)

上传事件阶段标识。UploadErrorEvent.Stage 字段使用这些常量。 字符串值保证可直接作为 Prometheus label 使用。

View Source
const (
	LifecyclePartSign        = "part_sign"
	LifecyclePartComplete    = "part_complete"
	LifecycleCompleteSuccess = "complete_success"
)
View Source
const (
	AbortReasonClientRequest = "client_request"
)

中止原因常量。

Variables

View Source
var (
	ErrFileTooLarge     = NewErr(CodeFileTooLarge, http.StatusRequestEntityTooLarge, "文件超过大小限制")
	ErrFileTypeNotAllow = NewErr(CodeFileTypeNotAllow, http.StatusBadRequest, "文件类型不允许")
	ErrChecksumMismatch = NewErr(CodeChecksumMismatch, http.StatusBadRequest, "校验和不匹配")
	ErrInvalidParam     = NewErr(CodeInvalidParam, http.StatusBadRequest, "参数无效")
	ErrMultipartError   = NewErr(CodeMultipartError, http.StatusBadRequest, "multipart 解析错误")

	ErrSessionNotFound    = NewErr(CodeSessionNotFound, http.StatusNotFound, "上传会话不存在")
	ErrSessionExpired     = NewErr(CodeSessionExpired, http.StatusGone, "上传会话已过期")
	ErrChunkIndexOutRange = NewErr(CodeChunkIndexOutRange, http.StatusBadRequest, "分片索引越界")
	ErrChunkHashMismatch  = NewErr(CodeChunkHashMismatch, http.StatusBadRequest, "分片校验和不匹配")
	ErrMergeIncomplete    = NewErr(CodeMergeIncomplete, http.StatusConflict, "分片未全部上传")
	ErrMergeHashFailed    = NewErr(CodeMergeHashFailed, http.StatusInternalServerError, "合并后文件校验失败")

	ErrStorageWrite          = NewErr(CodeStorageWriteFail, http.StatusInternalServerError, "存储写入失败")
	ErrStorageRead           = NewErr(CodeStorageReadFail, http.StatusInternalServerError, "存储读取失败")
	ErrStorageDelete         = NewErr(CodeStorageDeleteFail, http.StatusInternalServerError, "存储删除失败")
	ErrObjectStatUnsupported = NewErr(CodeObjectStatUnsupported, http.StatusNotImplemented, "当前存储不支持对象 Stat")
	ErrObjectCopyUnsupported = NewErr(CodeObjectCopyUnsupported, http.StatusNotImplemented, "当前存储不支持对象 Copy")

	ErrSTSNotConfigured   = NewErr(CodeSTSNotConfigured, http.StatusServiceUnavailable, "STS 直传未配置")
	ErrSTSIssueFailed     = NewErr(CodeSTSIssueFailed, http.StatusInternalServerError, "STS 凭证签发失败")
	ErrCallbackVerifyFail = NewErr(CodeCallbackVerifyFail, http.StatusForbidden, "回调验签失败")
	ErrDirectFileNotFound = NewErr(CodeDirectFileNotFound, http.StatusNotFound, "直传文件不存在")

	ErrMultipartNotSupported = NewErr(CodeMultipartNotSupported, http.StatusNotImplemented, "当前存储不支持 Multipart 直传")
	ErrMultipartInitFailed   = NewErr(CodeMultipartInitFailed, http.StatusInternalServerError, "Multipart 初始化失败")
	ErrMultipartPresignFail  = NewErr(CodeMultipartPresignFail, http.StatusInternalServerError, "预签名 URL 生成失败")
	ErrMultipartCompleteFail = NewErr(CodeMultipartCompleteFail, http.StatusInternalServerError, "Multipart 合并失败")
	ErrMultipartAbortFail    = NewErr(CodeMultipartAbortFail, http.StatusInternalServerError, "Multipart 中止失败")
	ErrPartVerifyFail        = NewErr(CodePartVerifyFail, http.StatusBadRequest, "分片校验失败")
	ErrPartETagMismatch      = NewErr(CodePartETagMismatch, http.StatusBadRequest, "分片 ETag 不匹配")
	ErrUnauthorized          = NewErr(CodeUnauthorized, http.StatusForbidden, "无权访问该会话")
)

预定义错误.

Functions

func SanitizeFileName

func SanitizeFileName(name string) string

SanitizeFileName 去除文件名中的路径组件和不安全字符,防止目录穿越攻击。

func SanitizeUploadID

func SanitizeUploadID(id string) string

SanitizeUploadID 确保上传 ID 只包含安全字符,防止通过构造的 ID 进行目录穿越。

func ValidateStorageKey

func ValidateStorageKey(key string) bool

ValidateStorageKey 校验存储 key 是否安全(无路径穿越)。

Types

type CallbackVerifier

type CallbackVerifier interface {
	// VerifyCallback 验证回调签名并解析上传结果。
	VerifyCallback(req *http.Request) (*DirectUploadResult, error)
}

CallbackVerifier 验证云存储的上传回调请求(可选接口)。 并非所有 STSProvider 都需要实现此接口。

type ChunkCompleteRequest added in v1.2.0

type ChunkCompleteRequest struct {
	UploadID string     `json:"upload_id" binding:"required"`
	Parts    []PartETag `json:"parts"     binding:"required"`
}

ChunkCompleteRequest Multipart 直传路径下,前端通知合并的请求体。 代理路径下沿用 /chunk/merge/:upload_id(无请求体)。

type ChunkInitRequest

type ChunkInitRequest struct {
	FileName  string `json:"file_name"            binding:"required"`
	FileSize  int64  `json:"file_size"            binding:"required,gt=0"`
	ChunkSize int64  `json:"chunk_size,omitempty"`
	Checksum  string `json:"checksum,omitempty"`  // 兼容旧字段,等价于 FileHash
	FileHash  string `json:"file_hash,omitempty"` // 整文件哈希,用于秒传查找
	Algorithm string `json:"algorithm,omitempty"`
}

ChunkInitRequest 初始化分片上传的 JSON 请求体。

FileHash 为可选字段:若客户端在上传前计算了整文件哈希,可在 init 时携带, 命中 HashStore 即触发秒传(零流量上传)。算法由 Algorithm 字段指定。

type ChunkInitResponse

type ChunkInitResponse struct {
	UploadID    string `json:"upload_id"`
	ChunkSize   int64  `json:"chunk_size"`
	TotalChunks int    `json:"total_chunks"`

	// 秒传命中时填充(Instant=true 时前端直接用 StorageKey 完成流程)
	Instant    bool   `json:"instant,omitempty"`
	StorageKey string `json:"storage_key,omitempty"`
	FileID     string `json:"file_id,omitempty"`
	URL        string `json:"url,omitempty"` // 配置 WithPublicURLBuilder 时填充,秒传命中可直接用

	// UploadPath 指示前端走哪条路径:
	//   "proxy"     — 传统代理模式,前端 POST 到 /chunk
	//   "multipart" — 云原生 Multipart,前端 PUT 到 PartURLs
	//   "instant"   — 秒传,前端直接提示完成
	UploadPath string `json:"upload_path,omitempty"`

	// Multipart 直传路径专用字段
	// PartURLs: partNum(1-based) -> 预签名 PUT URL。为空时需走按需签名 /chunk/parts/sign。
	PartURLs      map[int]string `json:"part_urls,omitempty"`
	PartSignTTL   int            `json:"part_sign_ttl,omitempty"`         // 建议的单片 URL 有效期(秒)
	RecommendConc int            `json:"recommend_concurrency,omitempty"` // 建议并发数
}

ChunkInitResponse 分片上传会话创建后的响应。

响应字段按能力分层:

  • 基础字段(UploadID / ChunkSize / TotalChunks):所有路径都返回
  • Instant / StorageKey / FileID:仅秒传命中时返回;前端识别后直接完成
  • UploadPath / PartURLs / PartSignTTL:仅 Multipart 直传路径返回;前端走方案 A 流程

老客户端不识别新字段,行为等价于原代理模式。

type ChunkStatusResponse

type ChunkStatusResponse struct {
	UploadID       string `json:"upload_id"`
	TotalChunks    int    `json:"total_chunks"`
	UploadedChunks []int  `json:"uploaded_chunks"`
	MissingChunks  []int  `json:"missing_chunks"`
}

ChunkStatusResponse 查询分片上传状态的响应。

type ChunkUploadResponse

type ChunkUploadResponse struct {
	UploadID      string `json:"upload_id"`
	ChunkIndex    int    `json:"chunk_index"`
	UploadedCount int    `json:"uploaded_count"`
	TotalChunks   int    `json:"total_chunks"`
}

ChunkUploadResponse 单个分片上传后的响应。

type CompleteResult added in v1.2.0

type CompleteResult struct {
	Key      string
	ETag     string // 云端合并后的对象 ETag(各家格式不同)
	Size     int64
	Location string // 对象最终 URL(可选)
}

CompleteResult Multipart 合并完成后的结果。

type DirectCompleteRequest

type DirectCompleteRequest struct {
	Key          string `json:"key" binding:"required"`
	KeyPrefix    string `json:"key_prefix,omitempty"`     // v1.4.0+ 推荐传递;不传 fallback 到 v1.3.0 行为
	KeyPrefixSig string `json:"key_prefix_sig,omitempty"` // 启用签名 + 传 KeyPrefix 时必填
	ETag         string `json:"etag,omitempty"`
}

DirectCompleteRequest 前端确认上传完成的请求体。

字段语义(v1.4.0+):

  • Key:必填,云存储对象键
  • KeyPrefix:**可选**——v1.3.0 客户端不传时服务端 fallback 到 keyPrefixFn 重算 (保留 v1.3.0 行为兼容),但跨天问题仍存在;建议升级前端在 IssueToken 后 原样回传 IssueToken 响应中的 KeyPrefix 以获得跨天修复
  • KeyPrefixSig:仅当业务方配置 WithKeyPrefixSigner 且客户端传了 KeyPrefix 时必填 防止攻击者凭借公开可见的他人 KeyPrefix 构造请求越权读取他人文件
  • ETag:可选

type DirectTokenRequest

type DirectTokenRequest struct {
	FileName string `json:"file_name"           binding:"required"`
	FileSize int64  `json:"file_size,omitempty"`
}

DirectTokenRequest 请求 STS 凭证的请求体。

type DirectTokenResponse

type DirectTokenResponse struct {
	*STSCredential
	Key string `json:"key"` // 建议的对象键,前端可直接使用
}

DirectTokenResponse STS 凭证响应(包含凭证和建议的对象键)。

type DirectUploadResult

type DirectUploadResult struct {
	Bucket string `json:"bucket"`
	Key    string `json:"key"`
	Size   int64  `json:"size"`
	ETag   string `json:"etag,omitempty"`
	URL    string `json:"url,omitempty"`
}

DirectUploadResult 前端直传完成后的结果。

URL 字段在配置了 WithPublicURLBuilder 时填充为业务方期望的对外可见 URL; 未配置时为空字符串,JSON 序列化因 omitempty 不输出。

type Error

type Error struct {
	Code       int    `json:"code"`
	Message    string `json:"message"`
	Details    string `json:"details,omitempty"`
	HTTPStatus int    `json:"-"`
	Err        error  `json:"-"`
}

Error 统一错误类型。

func NewErr

func NewErr(code, httpStatus int, message string) *Error

NewErr 创建一个新的 Error。

func WrapErr

func WrapErr(code, httpStatus int, message string, err error) *Error

WrapErr 包装一个底层错误为 Error。

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is added in v1.4.0

func (e *Error) Is(target error) bool

Is 实现 errors.Is 的扩展接口,按 Code 字段比较以支持错误分类。

这让带 Details 的副本(通过 WithDetails 派生)仍能被 sentinel 命中:

err := types.ErrInvalidParam.WithDetails("xxx")
errors.Is(err, types.ErrInvalidParam)  // true(v1.3.0 之前为 false)

仅在 target 也是 *Error 类型时按 Code 比较;否则交给 errors.Is 沿 Unwrap 链处理。

func (*Error) Unwrap

func (e *Error) Unwrap() error

func (*Error) WithDetails

func (e *Error) WithDetails(details string) *Error

WithDetails 返回一个带详情的 Error 副本。

type HashStore added in v1.2.0

type HashStore interface {
	// LookupByHash 按文件哈希查找已存储的文件。
	// algorithm: "md5" 或 "sha256"
	// 找不到返回 (nil, nil),不要返回错误。
	LookupByHash(ctx context.Context, algorithm, hash string) (*HashedFile, error)

	// Register 记录一次成功上传的哈希→key 映射。
	// 同一 hash 可能被多次 Register(不同用户上传相同内容)——实现方负责幂等或去重。
	Register(ctx context.Context, entry *HashedFile) error
}

HashStore 是秒传(Instant Upload)的查找/注册接口。

由业务方实现(常见存储:MySQL / PostgreSQL / Redis / Mongo), 通过 WithHashStore 注入 gouploader。

语义:

  • LookupByHash: 文件级哈希到已存储对象的映射。命中即秒传成功。
  • Register: 上传完成后注册哈希→key 映射,供后续请求秒传命中。

未注入 HashStore 时,秒传功能自动关闭,所有 /chunk/init 请求走正常上传流程。

⚠️ 多租户安全警告:本接口不含 userID 维度。在多租户私有 bucket 场景下, 用户 B 凭知道的 hash 可命中用户 A 的私有文件 storage_key,构成越权访问漏洞。 多租户场景**必须**改用 UserScopedHashStore 接口(v1.4.0+)。

type HashedFile added in v1.2.0

type HashedFile struct {
	Algorithm  string `json:"algorithm"`   // "md5" / "sha256"
	Hash       string `json:"hash"`        // 文件完整哈希
	StorageKey string `json:"storage_key"` // 存储 key
	FileSize   int64  `json:"file_size"`
	MIMEType   string `json:"mime_type,omitempty"`
	CreatedAt  int64  `json:"created_at"` // Unix 秒
}

HashedFile 秒传索引条目。

type InstantHitEvent added in v1.2.0

type InstantHitEvent struct {
	FileHash   string
	Algorithm  string
	StorageKey string
	FileSize   int64
	UserID     string
	FileName   string
}

InstantHitEvent 秒传命中时触发的事件 payload。

type MultipartMeta added in v1.2.0

type MultipartMeta struct {
	ContentType        string
	ContentDisposition string
	CacheControl       string
	UserMetadata       map[string]string
}

MultipartMeta 创建 Multipart 时可设置的对象元数据。

type MultipartStorage added in v1.2.0

type MultipartStorage interface {
	Storage

	// InitMultipart 在云端创建一个新的 Multipart 上传会话,返回云厂商的 UploadID。
	// key 是最终对象键,meta 可包含 Content-Type、Content-Disposition 等元数据。
	InitMultipart(ctx context.Context, key string, meta MultipartMeta) (cloudUploadID string, err error)

	// PresignPart 为单个分片生成预签名 PUT URL。
	// 签名必须锁死 partNum 和 Content-Length,防止客户端越权/超额。
	PresignPart(ctx context.Context, req PresignPartRequest) (url string, err error)

	// ListParts 列出指定 UploadID 当前在云端已上传的分片。
	// 用于 Complete 前的服务端二次校验,防止客户端伪造 ETag。
	ListParts(ctx context.Context, key, cloudUploadID string) ([]PartInfo, error)

	// CompleteMultipart 通知云端合并所有分片为最终对象。
	// parts 必须按 PartNum 升序排列。
	CompleteMultipart(ctx context.Context, key, cloudUploadID string, parts []PartETag) (*CompleteResult, error)

	// AbortMultipart 中止云端 Multipart,释放悬挂的分片数据。
	// 必须幂等——对已完成或不存在的 uploadID 返回 nil。
	AbortMultipart(ctx context.Context, key, cloudUploadID string) error

	// ListOrphans 列出超过 olderThan 时长未完成的悬挂 Multipart。
	// 供后台 Orphan Cleanup Worker 扫描 + 批量 Abort。
	ListOrphans(ctx context.Context, olderThan time.Duration) ([]OrphanMultipart, error)
}

MultipartStorage 是 Storage 的可选扩展接口,提供云原生 Multipart 直传能力。

实现此接口的 Storage 允许客户端绕过服务端,直接把分片 PUT 到云厂商对象存储。 服务端只负责签发预签名 URL、记录 ETag、调用 Init/Complete/Abort。

ChunkUploadService 通过类型断言探测此接口:

if mp, ok := storage.(types.MultipartStorage); ok {
    // 走原生 Multipart 直传路径
} else {
    // 走本地分片合并路径(LocalStorage 兜底)
}

此接口不内置在主 module,由各厂商 adapter 实现:

github.com/gtkit/gouploader/aliyun
github.com/gtkit/gouploader/tencent
github.com/gtkit/gouploader/huawei

type OrphanMultipart added in v1.2.0

type OrphanMultipart struct {
	Key           string
	CloudUploadID string
	Initiated     time.Time
}

OrphanMultipart 悬挂 Multipart 的元信息。

type PartETag added in v1.2.0

type PartETag struct {
	PartNum int    `json:"part_num"`
	ETag    string `json:"etag"`
}

PartETag 客户端上报或 CompleteMultipart 需要传入的 part 信息。

type PartInfo added in v1.2.0

type PartInfo struct {
	PartNum      int
	Size         int64
	ETag         string
	LastModified time.Time
}

PartInfo 云端已上传分片的元信息(由 ListParts 返回)。

type PartSignRequest added in v1.2.0

type PartSignRequest struct {
	UploadID    string `json:"upload_id"    binding:"required"`
	PartNumbers []int  `json:"part_numbers" binding:"required"`
}

PartSignRequest 按需签名预签名 URL 的请求体。

type PartSignResponse added in v1.2.0

type PartSignResponse struct {
	URLs map[int]string `json:"urls"` // partNum -> presigned PUT URL
	TTL  int            `json:"ttl"`  // URL 有效期(秒)
}

PartSignResponse 按需签名响应。

type PresignPartRequest added in v1.2.0

type PresignPartRequest struct {
	Key           string
	CloudUploadID string
	PartNum       int
	ContentLength int64
	TTL           time.Duration
}

PresignPartRequest 预签名单个分片的请求参数。

签名时务必锁死以下参数,防止客户端越权:

  • Key:最终对象键
  • PartNum:分片号(1-based)
  • ContentLength:该分片精确字节数
  • TTL:URL 有效期(建议 15 分钟)

type ResumeRequest added in v1.2.0

type ResumeRequest struct {
	FileHash  string `json:"file_hash"           binding:"required"`
	Algorithm string `json:"algorithm,omitempty"` // 默认 sha256
	FileName  string `json:"file_name,omitempty"` // 可选,用于未命中秒传时的 init 提示
	FileSize  int64  `json:"file_size,omitempty"`
}

ResumeRequest 秒传 + 断点续传查询请求体。

type ResumeResponse added in v1.2.0

type ResumeResponse struct {
	Instant    bool   `json:"instant,omitempty"`
	StorageKey string `json:"storage_key,omitempty"`
	FileID     string `json:"file_id,omitempty"`
	URL        string `json:"url,omitempty"` // 秒传命中时,配置 WithPublicURLBuilder 后填充

	UploadID      string `json:"upload_id,omitempty"`
	UploadPath    string `json:"upload_path,omitempty"` // "proxy" / "multipart"
	TotalChunks   int    `json:"total_chunks,omitempty"`
	UploadedParts []int  `json:"uploaded_parts,omitempty"`
	MissingParts  []int  `json:"missing_parts,omitempty"`
}

ResumeResponse 秒传 + 断点续传查询响应。

返回值分三种情形:

  1. Instant=true:秒传命中,前端直接使用 StorageKey 完成
  2. UploadID != "":找到未完成的上传会话,UploadedParts 列出已传分片,MissingParts 列出待传分片
  3. 以上都没有:未命中任何已知状态,前端应走正常 /chunk/init 流程

type STSCredential

type STSCredential struct {
	AccessKeyID     string `json:"access_key_id"`
	AccessKeySecret string `json:"access_key_secret"`
	SecurityToken   string `json:"security_token"`
	Expiration      string `json:"expiration"` // ISO 8601 格式
	Region          string `json:"region"`
	Bucket          string `json:"bucket"`
	Endpoint        string `json:"endpoint"`                 // 前端用的公网 endpoint
	KeyPrefix       string `json:"key_prefix,omitempty"`     // 限定的上传路径前缀
	KeyPrefixSig    string `json:"key_prefix_sig,omitempty"` // KeyPrefix 的 HMAC 签名(v1.4.0+,配置 WithKeyPrefixSigner 时填充)
}

STSCredential 各云厂商 STS 签发的临时凭证(统一输出格式)。 前端使用此凭证直接上传到云存储,无需经过后端中转。

type STSProvider

type STSProvider interface {
	// IssueCredential 签发一组临时凭证。
	// keyPrefix 限定上传的对象键前缀(如 "user-123/2026/04/"),
	// Provider 实现负责将其编入 STS Policy,确保前端只能上传到指定路径。
	IssueCredential(ctx context.Context, keyPrefix string) (*STSCredential, error)
}

STSProvider 签发前端直传所需的 STS 临时凭证。

各云厂商分别实现此接口:

  • 阿里云 OSS:AssumeRole
  • 腾讯云 COS:GetFederationToken
  • 华为云 OBS:CreateTemporaryAccessKeyByAgency
  • AWS S3:AssumeRole

type SignedURLStorage added in v1.4.0

type SignedURLStorage interface {
	Storage

	// GetSignedGetURL 生成对象的预签名 GET URL,TTL 内可被 HTTP GET 直接访问。
	//
	// 参数:
	//   - key:对象键
	//   - ttl:URL 有效期,必须 > 0;上限因厂商而异(普遍 7 天),超出时由 SDK 报错
	//
	// 返回的 URL 含敏感签名参数,业务方不应持久化存储——每次需要时重新签名。
	//
	// CDN 域名重写:若 Config.CDNHost 非空,签名 URL 的 host 会被替换为 CDN 域名
	// (行为与 PresignPart 一致)。
	//
	// ctx 取消能力:除阿里云 OSS Go SDK V1 限制外,其他实现支持 ctx 取消。
	GetSignedGetURL(ctx context.Context, key string, ttl time.Duration) (string, error)
}

SignedURLStorage 是 Storage 的可选扩展接口,提供"生成预签名 GET URL"能力。

业务场景:私有 bucket(对象未公开读)下,前端无法直接 GET 对象 URL, 需要服务端临时签发一个带签名参数的 URL 让前端 / 浏览器下载。

业务方使用 type assertion 检测底层 Storage 是否实现:

if su, ok := storage.(gouploader.SignedURLStorage); ok {
    url, err := su.GetSignedGetURL(ctx, "2026/04/30/x.png", 15*time.Minute)
}

内置实现:

  • aliyun.Storage / tencent.Storage / huawei.Storage(详见各子 module)
  • internal/store.S3Storage(基于 minio-go)
  • LocalStorage 不实现(本地文件无"签名"语义)

与 WithPublicURLBuilder 协作可在私有 bucket 场景下闭环:

gouploader.WithPublicURLBuilder(func(key string) string {
    if su, ok := storage.(gouploader.SignedURLStorage); ok {
        url, _ := su.GetSignedGetURL(context.Background(), key, 15*time.Minute)
        return url
    }
    return ""
})

type Storage

type Storage interface {
	// Save 从流式 Reader 中持久化文件。size 为预期总字节数,未知时传 -1。
	Save(ctx context.Context, key string, reader io.Reader, size int64) error

	// Get 返回已存储文件的 ReadCloser。调用方必须关闭。
	Get(ctx context.Context, key string) (io.ReadCloser, error)

	// Delete 按 key 删除文件。
	Delete(ctx context.Context, key string) error

	// Exists 检查 key 是否存在。
	Exists(ctx context.Context, key string) (bool, error)
}

Storage 抽象文件持久化层。 所有方法均使用 io.Reader 流式传输,不会将完整文件缓冲到内存。

type UploadAbortEvent added in v1.2.0

type UploadAbortEvent struct {
	UploadID   string
	Key        string // 可为空(代理路径合并前 abort)
	UserID     string
	UploadPath string // "proxy" | "multipart"
	Reason     string // 当前固定 "client_request",预留扩展
}

UploadAbortEvent 会话被中止时触发的事件 payload。

type UploadErrorEvent added in v1.2.0

type UploadErrorEvent struct {
	Stage    string
	UploadID string
	Key      string
	FileHash string
	UserID   string
	Err      error
}

UploadErrorEvent 任一上传阶段失败时触发的事件 payload。

Stage 取自 StageInit / StagePart / StageMerge / StageComplete / StageAbort。 空字段含义:

  • UploadID 为空:失败发生在 session 创建之前
  • Key 为空:上传路径尚未分配存储 key(proxy 路径 merge 前)
  • FileHash 为空:请求未携带文件哈希

type UploadInitEvent added in v1.2.0

type UploadInitEvent struct {
	UploadID    string
	FileName    string
	FileSize    int64
	FileHash    string
	Algorithm   string
	UserID      string
	UploadPath  string // "proxy" | "multipart"
	StorageKey  string // 仅 multipart 路径非空
	TotalChunks int
	ChunkSize   int64
}

UploadInitEvent 会话初始化成功时触发的事件 payload。

type UploadLifecycleEvent added in v1.5.0

type UploadLifecycleEvent struct {
	Stage         string
	UploadID      string
	Key           string
	UserID        string
	UploadPath    string
	PartNum       int
	PartNumbers   []int
	FileID        string
	FileName      string
	FileSize      int64
	Checksum      string
	CloudUploadID string
}

UploadLifecycleEvent describes successful upload lifecycle milestones.

type UploadResult

type UploadResult struct {
	FileID     string `json:"file_id"`
	FileName   string `json:"file_name"`
	FileSize   int64  `json:"file_size"`
	StorageKey string `json:"storage_key"`
	Checksum   string `json:"checksum,omitempty"`
	URL        string `json:"url,omitempty"`
}

UploadResult 文件上传完成后返回的结果(普通上传和分片合并)。

URL 字段在配置了 WithPublicURLBuilder 时填充为业务方期望的对外可见 URL; 未配置时为空字符串,JSON 序列化因 omitempty 不输出。

type UserScopedHashStore added in v1.4.0

type UserScopedHashStore interface {
	HashStore

	// LookupByHashForUser 按 (userID, algorithm, hash) 三元组查找已存储文件。
	// 仅返回属于 userID 的文件——A 的文件不应被 B 的查询命中。
	// 找不到返回 (nil, nil),不要返回错误。
	LookupByHashForUser(ctx context.Context, userID, algorithm, hash string) (*HashedFile, error)

	// RegisterForUser 记录一次成功上传的 (userID, hash, key) 映射。
	// 同一 (userID, hash) 可能被多次注册——实现方负责幂等或去重。
	RegisterForUser(ctx context.Context, userID string, entry *HashedFile) error
}

UserScopedHashStore 是 HashStore 的可选扩展接口,提供按用户隔离的秒传索引。

多租户私有 bucket 场景下**必须**实现此接口,避免用户 B 凭知道的 hash 命中用户 A 的私有文件。service 层会通过 type assertion 自动选择: 实现了本接口就调用 LookupByHashForUser / RegisterForUser;否则回退旧 HashStore 接口(伴随 Warn 日志提醒)。

典型实现(数据库后端):

func (s *dbHashStore) LookupByHashForUser(ctx, userID, algo, hash string) (*types.HashedFile, error) {
    var row struct{...}
    err := s.db.WhereRaw("user_id = ? AND algorithm = ? AND hash = ?", userID, algo, hash).First(&row).Error
    if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil }
    return &types.HashedFile{...}, err
}

func (s *dbHashStore) RegisterForUser(ctx, userID string, entry *types.HashedFile) error {
    return s.db.Create(&model{UserID: userID, ...entry}).Error
}

注入:调用 WithHashStore 即可(type assertion 自动检测扩展接口)。

Jump to

Keyboard shortcuts

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