Help us shape the future of Go! Take the 2026 Go Developer Survey

share

package
v0.32.41 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

秒传/去重的物理对象保护(2026-10-12,D1=A 同用户内去重)。

CreateShare 的去重钩子(service.go)让多个分享共享同一物理文件;本文件提供 共享对象的删除保护——任何删除分享物理对象的路径都必须先确认无其他分享引用 (FileCodeRepository.CountFilePathReferences,含软删行:回收站行的物理文件 留存供恢复),查不清时跳过删除(宁泄漏勿误删,孤儿由 janitor 对账兜底)。

files.go — 直传落盘与下载取流的存储收口(share 域存储接缝)。

HTTP 适配层(gen handler)不得直连 storage:磁盘布局(UUID 名 + 日期目录)、 「本地后端可绝对路径直传」「s3 可预签名直下」的分支知识全部封在本文件, 适配层只拿到 FilePayload(读流或本地绝对路径)与最终路径/大小。

NAS 本地文件管理(对标上游 2.7.0 data/local 管理端):白名单目录的列表/删除/导入。 与本地导入共用 upload.local_import 配置(enabled + roots)。 路径一律以「root 索引 + 白名单内相对路径」表达:客户端永不传绝对路径, root 取自服务端配置,杜绝目录穿越与任意路径删除。

NAS 本地文件免上传导入(P3):把服务器本地白名单目录内的文件登记为分享。 服务端从本地路径拷贝入统一存储(不经过浏览器上传通道),配额/审核/过期 与普通上传同链路。典型场景:fnOS/NAS 上已有的大文件直接生成提取码。

取件元数据查询(对标上游 /share/metadata)。 语义:查询不扣次数、不要求密码;供取件页在输入密码前渲染文件名/大小/有效期。 密码只暴露「是否设置」,文本内容/文件路径/密码哈希永不外泄。

内存内容直传分享(MCP/AI 通道,2026-10-06):AI 客户端经 MCP 端点上传小文件 (base64 随 JSON-RPC 携带),服务端写入统一存储后走与普通上传完全相同的 CreateShare 链路(配额/审核/自定义码/联邦公告)。与 localimport 的差异仅在 内容来源:无白名单路径校验,改为文件名消毒 + 扩展名白名单 + 大小上限把关。

Index

Constants

This section is empty.

Variables

View Source
var ErrNotInRecycleBin = errors.New("分享不在回收站中(不存在或未处于软删除状态)")

ErrNotInRecycleBin 分享不在回收站(未软删/已硬删/不存在),restore 与 hard-delete 共用。0 行变更曾双双表现为 200 静默 no-op——hard 侧 v0.11.1 修复、restore 侧 2026-10-08 修复:均改为调用方显式 400。

View Source
var ErrShareCountExhausted = errors.New("取件次数已用完")

ErrShareCountExhausted 取件次数已用完(2026-10-24:次数耗尽从 IsExpired 里 拆出后,「耗尽」不再混作「过期/不存在」,各面给独立错误码 —— 20004 CodeShareReachLimit / 匿名通道 20007 CodePickupCodeExhausted)。

本哨兵供 app/share 的元数据面(GetShareMetadata)区分「次数用完」与 「不存在/已过期/被封禁」⇒ handler 据此回 20004 而不是笼统的 404。

View Source
var ErrShareFileNotFound = errors.New("share file not found")

ErrShareFileNotFound 分享或其子文件不存在(调用方统一 404 语义,防探测)。

View Source
var ErrShareNotReady = errors.New("分享未完成上传,暂时无法取件")

ErrShareNotReady 分享未完成上传(占位记录:文件还没传上来)。

与匿名通道 app/anonymous 的 ErrNotReady 同一语义 ⇒ 两个面对同一份占位分享 给出同一个业务码(20001)与文案,不再是"分享域 200 + 一条指向不存在文件的 下载直链"、匿名通道 20001 的口径分叉(2026-10-23 审计)。

Functions

func IsShareNotReady added in v0.31.3

func IsShareNotReady(fc *model.FileCode) bool

IsShareNotReady 判定记录是否为"未完成上传"的占位分享。 判定式在 model(唯一真相源,preview/select/download/metadata 共用)。

func IsTextShare

func IsTextShare(fc *model.FileCode) bool

IsTextShare 判定是否纯文本分享(领域事实已下沉 model.FileCode.IsTextShare; 本包装保留给既有调用方,逐步迁移后可删)。

func LocalImportAbsPath

func LocalImportAbsPath(rootIdx int, relPath string) (string, error)

LocalImportAbsPath 把 (rootIdx, relPath) 解析为导入用的绝对路径(供管理端导入复用 ImportLocalFile;ImportLocalFile 内部会再做一次全量校验)

func LocalImportRoots

func LocalImportRoots() []string

LocalImportRoots 返回已配置白名单根目录(已 EvalSymlinks;未启用时为空)

Types

type ContentRejectedError

type ContentRejectedError struct{}

ContentRejectedError 内容未通过审核(moderation 命中且策略为 reject)

func (*ContentRejectedError) ErrCode

func (e *ContentRejectedError) ErrCode() int

func (*ContentRejectedError) Error

func (e *ContentRejectedError) Error() string

type FederationNotifier

type FederationNotifier interface {
	// ShareCreated 新分享公告到联邦(实现方内部自滤低熵码,异步不阻塞)。
	ShareCreated(code string, expiresAt *time.Time)
	// ShareDeleted 分享删除时撤销联邦公告(best-effort)。
	ShareDeleted(code string)
}

FederationNotifier P2P 联邦口令路由钩子(bootstrap 注入;nil = 非联邦模式)。 单向可选依赖:本包只定义窄接口,实现在 app/federation(实现方 import 本包, 本包不反向依赖——与 NotifyServiceInterface 同风格)。

type FilePayload

type FilePayload struct {
	ReadCloser io.ReadCloser
	Size       int64
	LocalAbs   string
}

FilePayload 下载取流载荷。LocalAbs 非空表示本地后端,调用方可走绝对路径 直传(原生 Range/断点续传);否则经 ReadCloser 流式中转(读至 EOF 关闭)。

type FlagEventEmitter

type FlagEventEmitter interface {
	EmitShareFlagged(code, reason, ownerIP string)
}

FlagEventEmitter share.flagged 事件推送接口(bootstrap 注入 notify service 实现)

type GlobalQuotaExceededError

type GlobalQuotaExceededError struct {
	Used  int64
	Quota int64
}

GlobalQuotaExceededError 站点级存储配额超限(handler 侧按业务码透传 CodeStorageQuota)。

func (*GlobalQuotaExceededError) ErrCode

func (e *GlobalQuotaExceededError) ErrCode() int

func (*GlobalQuotaExceededError) Error

func (e *GlobalQuotaExceededError) Error() string

type ImportLocalOpts

type ImportLocalOpts struct {
	AbsPath      string
	ExpireValue  int
	ExpireStyle  string
	RequireAuth  bool
	PasswordHash string
	CustomCode   string
	UserID       *uint
	OwnerIP      string
}

ImportLocalOpts 本地导入参数

type LocalFileEntry

type LocalFileEntry struct {
	Name    string    `json:"name"`
	Path    string    `json:"path"`
	Size    int64     `json:"size"`
	ModTime time.Time `json:"mod_time"`
	IsDir   bool      `json:"is_dir"`
}

LocalFileEntry 本地文件列表项(Path 为相对 root 的路径,正斜杠)

type MultiShareReq

type MultiShareReq struct {
	Entries      []StoredFileEntry
	ExpiredAt    *time.Time
	ExpiredCount int
	RequireAuth  bool
	PasswordHash string
	UserID       *uint
	UploadType   string
	OwnerIP      string
	Channel      string // direct/chunk/presign(metrics/审核 meta 用)
	Encrypted    bool   // E2E:所有子文件均为客户端密文(P1)
	CustomCode   string // 自定义取件码(P3;登录用户专属,handler 把关)
	Note         string // 文件备注(可空,取件页展示;2026-10-12 #489)
}

MultiShareReq 多文件分享创建请求。

type NotifyServiceInterface

type NotifyServiceInterface interface {
	CreateForUserSimple(ctx context.Context, userID uint, title, content, notifyType, level string) error
}

NotifyServiceInterface 取件通知接口(避免 share → notify 直接依赖)

type PickupCodeMinter added in v0.14.5

type PickupCodeMinter interface {
	MintForShare(ctx context.Context, shareCode, fileName string, fileSize int64, requireAuth bool, expireAt *time.Time) (string, error)
}

PickupCodeMinter 取件码铸造窄接口(app/anonymous.Service 结构性满足)。 share 域不 import anonymous 域(域间零耦合),由 bootstrap 注入具体实现。

type QuotaChecker

type QuotaChecker interface {
	CheckQuota(ctx context.Context, userID uint, addBytes int64) error
}

QuotaChecker 存储配额检查接口(bootstrap 注入 user service 实现)

type Service

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

func NewService

func NewService(gormDB *gorm.DB, baseURL string, storageService storage.StorageInterface) *Service

func (*Service) BatchDeleteUserShares

func (s *Service) BatchDeleteUserShares(ctx context.Context, userID uint, codes []string) (int, error)

BatchDeleteUserShares 批量软删除

func (*Service) BatchExtendUserShares

func (s *Service) BatchExtendUserShares(ctx context.Context, userID uint, codes []string, newExpireAt *time.Time) (int, error)

BatchExtendUserShares 批量延期

func (*Service) ChargePickup added in v0.32.0

func (s *Service) ChargePickup(ctx context.Context, code, tokenFingerprint, channel, viewerIP string) (charged bool, exhausted bool, err error)

ChargePickup 按「取件会话」计次(2026-10-24)。

tokenFingerprint = 下载令牌的计次指纹(pkg/security 校验时给出;令牌未启用 或未携带时为空 ⇒ 无会话概念,按「每次交付都算一次取件」处理)。 viewerIP = 取件人 IP(可信代理解析后)⇒ 随计次记录落库,供 owner 侧逐条展示。

返回:

  • charged=true:本次交付**真正扣了**取件次数(该令牌的首次使用)⇒ 调用方 应在此记取件人 + 通知 owner;
  • exhausted=true:取件次数已用完且本令牌尚未计次 ⇒ 拒绝交付(20004);
  • charged=false, exhausted=false:同一令牌已计次(同一取件会话内的后续 交付:多文件逐个下载 / 断续传 / 重复点击)⇒ 不再扣次、也不再记取件人。

func (*Service) CheckUploadSizeCap added in v0.26.0

func (s *Service) CheckUploadSizeCap(ctx context.Context, userID uint, totalSize int64) error

CheckUploadSizeCap 登录用户单次上传总大小预检(多文件绑定按申报大小 fail-fast, 与匿名配额预检对称,先于合并落盘;匿名用户返回 nil——其约束走匿名日配额)。

func (*Service) CreateMultiFileShare

func (s *Service) CreateMultiFileShare(ctx context.Context, req *MultiShareReq) (*ShareResp, error)

CreateMultiFileShare 创建多文件分享(1 个取件码 ↔ N 个文件)。

与单文件 CreateShare 的差异:

  • 主表 Size = 各文件之和;主表 FilePath/UUIDFileName/FileHash 与首个文件同步 (管理端列表/秒传/统计读主表,零改动可用)
  • 子表每文件一行,取件端经 ListShareFiles 拉取
  • 配额按总大小一次扣检;审核逐文件判定(任一 reject 整单拒绝,pending 整单待审)

失败时不回滚物理文件——调用方(handler)负责在 err != nil 时清理已落盘文件。

func (*Service) CreateShare

func (s *Service) CreateShare(ctx context.Context, req *ShareFileReq) (*ShareResp, error)

CreateShare 创建分享记录(ShareFile 的语义化别名,便于其他 service 调用) 行为:单用户上传上限检查 → 存储配额检查 → 生成 code → 写 file_codes 表 → 返回 share_code / url

func (*Service) DeleteFile

func (s *Service) DeleteFile(ctx context.Context, fileID uint, userID *uint) error

DeleteFile 删除文件

func (*Service) DeleteFileByCode

func (s *Service) DeleteFileByCode(ctx context.Context, code string, userID uint) error

DeleteFileByCode 根据分享码删除文件。

删除顺序(保证一致性):

  1. 查所有权
  2. 删 DB 记录(事务原子)
  3. 事务提交后删物理文件(不可回滚,失败记日志不阻断)
  4. 扣减用户 storage 统计(best-effort,失败记日志)

关键:先删 DB 再删物理,避免"物理删了但 DB 没删"的孤儿反向场景。

func (*Service) DeleteLocalFile

func (s *Service) DeleteLocalFile(rootIdx int, relPath string) error

DeleteLocalFile 删除白名单内的单个普通文件(拒绝目录与非普通文件)

func (*Service) FindShareByUploadID added in v0.32.6

func (s *Service) FindShareByUploadID(ctx context.Context, uploadID string) (*model.FileCode, error)

FindShareByUploadID 按上传会话 ID 回读已建的分享(**上传完成面的幂等键**)。

背景(2026-10-24 审计,core v0.32.6):chunk/presign 的 Complete 此前不检查 是否已建过分享 ⇒ 同一 upload_id 二次 Complete 会**再建一条分享**(各有独立 取件额度 ⇒ 「最多取 N 次」实际可取 2N 次;真机已出现 1 组重复数据)。

⇒ 完成面先回读:命中即幂等返回(同一个分享码),不新建行、不重复扣次、 不重复计配额。未命中返回 (nil, nil)(调用方按「首次」继续建)。

func (*Service) GenerateCode

func (s *Service) GenerateCode() string

GenerateCode 生成分享代码(8 位字母数字,crypto/rand 经 kit/uidgen 统一收口)。

func (*Service) GetFileByCode

func (s *Service) GetFileByCode(ctx context.Context, code string) (*model.FileCode, error)

GetFileByCode 通过代码获取文件(**分享码或 6 位取件码**)。

解析规则见 dao.ResolveByCode(按码解析的唯一真相源):6 位取件码优先(真相源 file_codes.pickup_code 列),否则分享码/自定义码(精确 → 大小写折叠)。 本域与预览域/匿名域/MCP 共用该解析器 ⇒ 同一串码处处解析到同一个分享。

取件码能被解析 ⇒ 落地后 `{base}/#/share/{取件码}` 可直接打开取件页 (此前取件码只能走匿名通道,没有页面链接)。

为什么安全:① 取件码的枚举面本来就有(/anonymous/search 早能按取件码查,且与 本面同属 download 限流桶:per-IP 50 QPS / burst 20)⇒ 本改动不新增枚举面; 取件码空间 32^6 ≈ 10.7 亿(字符集 ABCDEFGHJKMNPQRSTUVWXYZ23456789)。 ⚠️ 精确点:分享域的**失败锁定**(per IP+code)只记密码错,**不存在的码不记 失败** ⇒ 枚举的防线是**限流**,不是锁定。 ② 取件码本就是「口令语义」的取件凭证(拿到码即可取件,API 指南 §6 明写) ⇒ 多一个入口不扩大信息面; ③ 取件码列是 DB 真相源,不依赖匿名域的 Redis 映射 ⇒ 映射过期/重启不失联; ④ 铸造取件码与创建自定义分享码两侧都做了**跨列避让** (app/anonymous.GenerateCode / 本文件 createWithCode)⇒ 两种码不会互相串。

func (*Service) GetFileWithUsage

func (s *Service) GetFileWithUsage(ctx context.Context, code, password, viewerIP string, authedByToken bool) (*model.FileCode, error)

GetFileWithUsage 获取文件并校验密码(**不扣次数、不记取件人**)。

扣次与记取件人由调用方在**内容交付前**统一执行(ChargePickup + 记 viewer): ① 扣次必须与「交付」同点(交付失败不该白吃次数); ② 记取件人必须与扣次同点(同一次取件只记一个取件人,重复下载不刷列表)。

viewerIP 由 handler 从可信解析 middleware.ClientIP(c) 注入。 authedByToken:调用方已校验有效下载令牌(令牌由取件查询在密码/取件校验通过后 签发,等价于已认证——否则密码保护分享"凭令牌下载"还要再输一次密码,令牌失效)。

func (*Service) GetFilesByUserID

func (s *Service) GetFilesByUserID(ctx context.Context, userID uint, page, pageSize int) ([]*model.FileCode, int64, error)

GetFilesByUserID 获取用户的文件列表

func (*Service) GetShareChild

func (s *Service) GetShareChild(ctx context.Context, code string, fileID uint) (*model.FileCode, *model.FileCodeFile, error)

GetShareChild 取分享内的单个子文件(校验归属,防跨分享枚举文件 ID)。

func (*Service) GetShareMetadata

func (s *Service) GetShareMetadata(ctx context.Context, code string) (*ShareMetadata, error)

GetShareMetadata 查询分享元数据(复用 GetFileByCode 的过期/封禁检查与 typed error)。 不存在/已过期/被封禁统一由调用方转 404,避免存在性区分扩大探测面。

密码保护分享的最小化返回(2026-10-05 审计 P2):持码者在输对密码前只拿到 has_password/type/encrypted——文件名常含敏感信息(简历/合同名),此前免密码 即泄露,且该端点无鉴权。普通分享仍返回全量字段。

func (*Service) HardDeleteUserShare

func (s *Service) HardDeleteUserShare(ctx context.Context, userID uint, code string) error

HardDeleteUserShare 永久删除软删除的分享

func (*Service) HasPickupCharge added in v0.32.2

func (s *Service) HasPickupCharge(ctx context.Context, code, sessionKey string) (bool, error)

HasPickupCharge 判「该取件会话是否已计次」(取件页在额度用完时的放行判定: 已经取过的人仍可再打开,不重复扣次)。

func (*Service) ImportLocalFile

func (s *Service) ImportLocalFile(ctx context.Context, opts ImportLocalOpts) (*ShareResp, error)

ImportLocalFile 把本地文件导入为分享。 校验链:功能开关 → 路径落在白名单目录内(EvalSymlinks 防穿越)→ 普通文件 → 大小上限 → 服务端拷贝入存储 → 复用 CreateShare(配额/审核/自定义码)。

func (*Service) ListLocalFiles

func (s *Service) ListLocalFiles(rootIdx int, relDir string) ([]LocalFileEntry, error)

ListLocalFiles 列出 root 下(可选子目录 relDir)一层的条目(目录优先、按名排序)

func (*Service) ListPickups added in v0.32.1

func (s *Service) ListPickups(ctx context.Context, code string, limit int) ([]model.PickupCharge, error)

ListPickups 取某分享的逐条取件明细(owner 侧「取件历史」用)。

数据源 = 计次记录表(一次取件 = 一行)⇒ 多文件逐个下载/断点续传不会被 显示成「取件 N 次」(那正是此前只有「最后 IP + 计数」时看不出来的失真)。

func (*Service) ListShareFiles

func (s *Service) ListShareFiles(ctx context.Context, code string) ([]*ShareFileItem, error)

ListShareFiles 取分享的文件列表(取件端展示/下载入口)。 子表为唯一真相源:每个文件分享都有对应子表行(启动期数据迁移已补齐存量)。

func (*Service) ListUserShares

func (s *Service) ListUserShares(ctx context.Context, userID uint, status, search string, page, pageSize int) ([]*sharemodel.UserShareItemData, int64, error)

ListUserShares 获取用户的分享列表(带筛选)。 status 取值:all/active/expired/text/file/deleted/viewed;search 模糊匹配 code/文件名/取件码。 筛选条件收散参由本域转换,transport 无需感知 dao.UserShareFilter。 返回契约生成模型(wire 形态由 idl/share.thrift 定义,handler 只组信封)。

func (*Service) OpenChildDownload

func (s *Service) OpenChildDownload(ctx context.Context, code string, fileID uint) (*model.FileCode, *model.FileCodeFile, *FilePayload, error)

OpenChildDownload 多文件分享的子文件取流。不存在/不属于该分享返回 ErrShareFileNotFound;读流失败原样返回(调用方区分 404 与 500)。

func (*Service) OpenShareDownload

func (s *Service) OpenShareDownload(ctx context.Context, filePath string) (*FilePayload, error)

OpenShareDownload 单文件下载取流(鉴权与扣次由调用方先行完成)。

func (*Service) OpenShareDownloadRange

func (s *Service) OpenShareDownloadRange(ctx context.Context, filePath string, start, length int64) (io.ReadCloser, int64, int64, error)

OpenShareDownloadRange 区间打开单文件下载流(Range 下载/断点续传, 鉴权与扣次由调用方先行完成)。返回 (区间读器, 待传字节数, 对象总大小)。 后端不支持区间读时返回 storage/opendal.ErrRangeUnsupported,调用方回退 OpenShareDownload 全量流(200);start 越界返回 os.ErrNotExist 语义(416)。

func (*Service) PresignDownloadURL

func (s *Service) PresignDownloadURL(ctx context.Context, filePath string, ttl time.Duration, fileName ...string) (string, error)

PresignDownloadURL s3 直下预签名 GET(download.s3_direct_download 开关的 执行段)。后端不支持预签名或签发失败返回 error,调用方回退服务端中转。 fileName 非空时签入 response-content-disposition,直下 302 客户端据此命名。

func (*Service) RecordViewer

func (s *Service) RecordViewer(ctx context.Context, code, viewerIP string) (*model.FileCode, error)

RecordViewer 记录取件人 IP / 时间(用于 owner 查看取件历史) 取件流程中调用。返回 fileCode 供后续通知使用

func (*Service) RecordViewerAndNotify

func (s *Service) RecordViewerAndNotify(ctx context.Context, code, viewerIP, viewerDetail string) error

RecordViewerAndNotify 记录取件人 + 给 owner 发通知。

  • viewerIP: 取件人 IP(由 handler 从可信解析 middleware.ClientIP(c) 注入)
  • viewerDetail: 额外通知内容

通知去重:同 code 5 分钟内只通知一次(用 LastNotifiedAt 字段)。 若 file_codes 找不到,静默跳过。

func (*Service) RestoreUserShare

func (s *Service) RestoreUserShare(ctx context.Context, userID uint, code string) error

RestoreUserShare 恢复软删除的分享。0 行恢复(硬删/不存在/本就活跃) 显式报错——此前 0 行也返回 nil,硬删后 restore 表现为 200 静默 no-op (2026-10-08 修复;与 v0.11.1 hard-delete 同款收口)。

func (*Service) SaveUploadFile

func (s *Service) SaveUploadFile(ctx context.Context, fh *multipart.FileHeader, originalFilename string) (string, int64, string, error)

SaveUploadFile 直传文件落盘。originalFilename 仅用于扩展名推导, 磁盘名由 UploadObjectRelPath 统一生成;返回落盘路径、实际大小与存储层 计算的文件哈希(供秒传/去重,可能为空),调用方据此创建分享记录。

func (*Service) SetBaseURLProvider added in v0.27.0

func (s *Service) SetBaseURLProvider(fn func(context.Context) string)

SetBaseURLProvider 注入对外 base 地址的动态来源(2026-10-22 站点公网地址)。 返回空串 = 未配置 → 回退构造期的 baseURL(env/yaml 的 server.base_url), 仍为空则由 baseurl.Resolve 按请求来源动态推断(publicBaseMiddleware)。 线程安全:装配期注入一次,运行期只读。

func (*Service) SetFederationNotifier

func (s *Service) SetFederationNotifier(f FederationNotifier)

SetFederationNotifier 注入 P2P 联邦口令路由钩子(bootstrap 调用;nil = 非联邦。 P1 后台热切换后为运行期写,与请求路径并发——须加锁)

func (*Service) SetFlagEventEmitter

func (s *Service) SetFlagEventEmitter(e FlagEventEmitter)

SetFlagEventEmitter 注入 share.flagged webhook 推送(bootstrap 调用)

func (*Service) SetModerator

func (s *Service) SetModerator(m moderation.Moderator)

SetModerator 注入内容审核钩子(bootstrap 调用;nil = 不审核)

func (*Service) SetNotifyService

func (s *Service) SetNotifyService(svc NotifyServiceInterface)

SetNotifyService 注入 notify service(取件时给 owner 发通知)

func (*Service) SetPickupMinter added in v0.14.5

func (s *Service) SetPickupMinter(m PickupCodeMinter)

SetPickupMinter 注入取件码铸造(anonymous 域实现;nil = 文件分享不铸 6 位取件码)

func (*Service) SetQuotaChecker

func (s *Service) SetQuotaChecker(qc QuotaChecker)

SetQuotaChecker 注入存储配额检查器(bootstrap 注入 user service 实现; 未注入时上传不检查配额——保持向后兼容)

func (*Service) SetShareStatus

func (s *Service) SetShareStatus(ctx context.Context, ids []uint, status string) (int64, error)

SetShareStatus 管理员设置分享管控状态(禁用/恢复/待审),返回受影响行数。

func (*Service) SetUserService

func (s *Service) SetUserService(userService UserServiceInterface)

func (*Service) ShareBytes

func (s *Service) ShareBytes(ctx context.Context, opts ShareBytesOpts) (*ShareResp, error)

ShareBytes 把内存中的文件内容登记为文件分享。 校验链:文件名消毒 → 扩展名白名单 → 大小上限 → SaveBytes 落存储 → CreateShare。

func (*Service) ShareFile

func (s *Service) ShareFile(ctx context.Context, req *ShareFileReq) (*ShareResp, error)

ShareFile 分享文件

func (*Service) ShareText

func (s *Service) ShareText(ctx context.Context, req *ShareTextReq) (*ShareResp, error)

ShareText 分享文本

func (*Service) ShareTextWithAuth

func (s *Service) ShareTextWithAuth(ctx context.Context, text string, expireValue int, expireStyle string, requireAuth bool, passwordHash string, userID *uint, ownerIP string, encrypted bool, customCode string) (*ShareResp, error)

ShareTextWithAuth 带认证的文本分享(用于 Handler)。 requireAuth=true 时必须提供非空 passwordHash(handler 侧负责哈希), 传入空哈希会被拒绝,防止创建出"密码保护形同虚设"的分享。 encrypted=true 时 text 为客户端密文(P1 E2E),跳过服务端转义与敏感词审核。

func (*Service) StatShareFile

func (s *Service) StatShareFile(ctx context.Context, filePath string) (int64, error)

StatShareFile 获取分享文件总大小(Range 解析需先知 total)。 后端非统一存储实例时返回 storage.ErrRangeUnsupported(调用方回退全量)。

func (*Service) UpdateFileUsage

func (s *Service) UpdateFileUsage(ctx context.Context, code string) (bool, error)

UpdateFileUsage 原子扣减剩余次数(DB 为准,防并发超卖)。 返回 ok=true 表示扣减成功(可下载);ok=false 表示已耗尽。

⚠️ 2026-10-24 起**下载链路不再直接调它**:扣次改由 ChargePickup 按「取件会话 (下载令牌)」执行(一个令牌至多扣 1 次 ⇒ 多文件逐个下载 / 断点续传 / 重复点击 不再重复吃次数)。本方法保留给「无令牌会话概念」的其它调用面。

func (*Service) ZipStream

func (s *Service) ZipStream(ctx context.Context, fc *model.FileCode) (io.ReadCloser, error)

ZipStream 把分享的全部文件打包为 zip,返回读端流(未知总长,逐文件从存储拉取)。

实现要点:

  • io.Pipe + goroutine 组装:handler 以 c.SetBodyStream(r, -1) 流式回写, 不落盘、不整包进内存(local/S3 统一经 StorageInterface.GetFileReader)
  • 读端关闭即停写(管道背压);组装出错时 CloseWithError 终止下载而非挂死
  • zip 内文件名用展示名去路径(防 Zip Slip),重名自动追加序号
  • context 用 WithoutCancel:handler 返回后请求 ctx 被取消,但流仍在被读取

分享无任何文件时返回错误(调用方转 404/500)。

type ShareBlockedError

type ShareBlockedError = pkgerrors.ShareBlocked

ShareBlockedError 分享处于管控拒绝态(管理员禁用 / 待审核)。 handler 侧按 ErrCode 透传(20012 blocked / 20013 pending_review)。 实现收口 pkg/errors.ShareBlocked——历史上 anonymous.BlockedError 持有一份 逐字相同的拷贝(状态常量还各自漂移),2026-10-10 合并;别名保持域内 构造点与 handler errors.As 用法不变。

type ShareBytesOpts

type ShareBytesOpts struct {
	FileName     string
	Content      []byte
	ExpireValue  int
	ExpireStyle  string
	PasswordHash string
	CustomCode   string
	UserID       *uint
	OwnerIP      string
}

ShareBytesOpts 内存内容建分享参数

type ShareFileItem

type ShareFileItem struct {
	ID       uint   `json:"id"`
	Name     string `json:"name"`
	Size     int64  `json:"size"`
	FileHash string `json:"file_hash"`
}

ShareFileItem 分享内单文件视图(取件端/管理端通用)。

type ShareFileReq

type ShareFileReq struct {
	Channel      string // 上传通道(direct/chunk/presign/anonymous,metrics 用;可空)
	FilePath     string
	Size         int64
	Text         string
	ExpiredAt    *time.Time
	ExpiredCount int
	RequireAuth  bool
	PasswordHash string
	UserID       *uint
	UploadType   string
	OwnerIP      string
	FileHash     string
	IsChunked    bool
	UploadID     string
	Encrypted    bool   // E2E:存储内容为客户端密文(P1)
	CustomCode   string // 自定义取件码(P3;登录用户专属,handler 把关)
	Note         string // 文件备注(可空,取件页展示;2026-10-12 #489)
}

type ShareMetadata

type ShareMetadata struct {
	Code string `json:"code"`
	Type string `json:"type"` // text | file
	// Name 文件名(仅 file 分享;text 分享不返回内容)
	Name         string     `json:"name,omitempty"`
	Size         int64      `json:"size"`
	HasPassword  bool       `json:"has_password"`
	ExpiredAt    *time.Time `json:"expired_at"`
	ExpiredCount int        `json:"expired_count"` // -1 无限 / 0 耗尽 / >0 剩余
	UsedCount    int        `json:"used_count"`
	Encrypted    bool       `json:"encrypted"`
	// FileCount 多文件分享的子文件数(单文件/text 分享为 0)
	FileCount int64     `json:"file_count"`
	CreatedAt time.Time `json:"created_at"`
}

ShareMetadata 取件元数据(公开展示字段)

type ShareResp

type ShareResp struct {
	ID           uint       `json:"id"`
	Code         string     `json:"code"`
	UUIDFileName string     `json:"uuid_file_name"`
	FilePath     string     `json:"file_path"`
	Size         int64      `json:"size"`
	Text         string     `json:"text"`
	ExpiredAt    *time.Time `json:"expired_at"`
	ExpiredCount int        `json:"expired_count"`
	UsedCount    int        `json:"used_count"`
	FileHash     string     `json:"file_hash"`
	IsChunked    bool       `json:"is_chunked"`
	UploadID     string     `json:"upload_id"`
	UserID       *uint      `json:"user_id"`
	UploadType   string     `json:"upload_type"`
	RequireAuth  bool       `json:"require_auth"`
	Encrypted    bool       `json:"encrypted"` // E2E 客户端加密标记(P1)
	OwnerIP      string     `json:"owner_ip"`
	Status       string     `json:"status"`         // 管控状态(normal/blocked/pending_review)
	ShareURL     string     `json:"share_url"`      // 相对分享链接
	FullShareURL string     `json:"full_share_url"` // 完整分享链接
	// PickupCode 6 位取件码(2026-10-07 起文件分享铸造;永久分享/未注入 minter 时为空。
	// KV 映射仅加速解析,8 位分享码永远可用,故铸造失败不影响主流程)
	PickupCode string `json:"pickup_code,omitempty"`
	// Deduped 秒传去重命中(2026-10-12):本分享未写入新存储对象,引用调用者
	// 自己历史同 hash+size 文件的既有对象(D1=A 同用户内去重)
	Deduped bool `json:"deduped,omitempty"`
}

type ShareTextReq

type ShareTextReq struct {
	Text         string
	ExpiredAt    *time.Time
	ExpiredCount int
	RequireAuth  bool
	PasswordHash string
	UserID       *uint
	UploadType   string
	OwnerIP      string
	Encrypted    bool   // E2E:Text 为客户端密文(P1)
	CustomCode   string // 自定义取件码(P3;登录用户专属,handler 把关)
}

type StoredFileEntry

type StoredFileEntry = storage.StoredFileEntry

StoredFileEntry 已落存储的文件项(多文件分享创建入参)。 结构定义已下沉 storage.StoredFileEntry(中立载体,request 等域零 share 依赖); 本别名保留既有引用路径,新代码可直接使用 storage.StoredFileEntry。

type UserServiceInterface

type UserServiceInterface interface {
	UpdateUserStats(userID uint, statsType string, value int64) error
	// GetUploadSizeCap 生效的单次上传大小上限(管理员可为单用户降限;0=不限)
	GetUploadSizeCap(ctx context.Context, userID uint) int64
}

UserServiceInterface 定义用户服务接口,避免循环依赖

Jump to

Keyboard shortcuts

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