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 ¶
- Variables
- func IsShareNotReady(fc *model.FileCode) bool
- func IsTextShare(fc *model.FileCode) bool
- func LocalImportAbsPath(rootIdx int, relPath string) (string, error)
- func LocalImportRoots() []string
- type ContentRejectedError
- type FederationNotifier
- type FilePayload
- type FlagEventEmitter
- type GlobalQuotaExceededError
- type ImportLocalOpts
- type LocalFileEntry
- type MultiShareReq
- type NotifyServiceInterface
- type PickupCodeMinter
- type QuotaChecker
- type Service
- func (s *Service) BatchDeleteUserShares(ctx context.Context, userID uint, codes []string) (int, error)
- func (s *Service) BatchExtendUserShares(ctx context.Context, userID uint, codes []string, newExpireAt *time.Time) (int, error)
- func (s *Service) ChargePickup(ctx context.Context, code, tokenFingerprint, channel, viewerIP string) (charged bool, exhausted bool, err error)
- func (s *Service) CheckUploadSizeCap(ctx context.Context, userID uint, totalSize int64) error
- func (s *Service) CreateMultiFileShare(ctx context.Context, req *MultiShareReq) (*ShareResp, error)
- func (s *Service) CreateShare(ctx context.Context, req *ShareFileReq) (*ShareResp, error)
- func (s *Service) DeleteFile(ctx context.Context, fileID uint, userID *uint) error
- func (s *Service) DeleteFileByCode(ctx context.Context, code string, userID uint) error
- func (s *Service) DeleteLocalFile(rootIdx int, relPath string) error
- func (s *Service) FindShareByUploadID(ctx context.Context, uploadID string) (*model.FileCode, error)
- func (s *Service) GenerateCode() string
- func (s *Service) GetFileByCode(ctx context.Context, code string) (*model.FileCode, error)
- func (s *Service) GetFileWithUsage(ctx context.Context, code, password, viewerIP string, authedByToken bool) (*model.FileCode, error)
- func (s *Service) GetFilesByUserID(ctx context.Context, userID uint, page, pageSize int) ([]*model.FileCode, int64, error)
- func (s *Service) GetShareChild(ctx context.Context, code string, fileID uint) (*model.FileCode, *model.FileCodeFile, error)
- func (s *Service) GetShareMetadata(ctx context.Context, code string) (*ShareMetadata, error)
- func (s *Service) HardDeleteUserShare(ctx context.Context, userID uint, code string) error
- func (s *Service) HasPickupCharge(ctx context.Context, code, sessionKey string) (bool, error)
- func (s *Service) ImportLocalFile(ctx context.Context, opts ImportLocalOpts) (*ShareResp, error)
- func (s *Service) ListLocalFiles(rootIdx int, relDir string) ([]LocalFileEntry, error)
- func (s *Service) ListPickups(ctx context.Context, code string, limit int) ([]model.PickupCharge, error)
- func (s *Service) ListShareFiles(ctx context.Context, code string) ([]*ShareFileItem, error)
- func (s *Service) ListUserShares(ctx context.Context, userID uint, status, search string, page, pageSize int) ([]*sharemodel.UserShareItemData, int64, error)
- func (s *Service) OpenChildDownload(ctx context.Context, code string, fileID uint) (*model.FileCode, *model.FileCodeFile, *FilePayload, error)
- func (s *Service) OpenShareDownload(ctx context.Context, filePath string) (*FilePayload, error)
- func (s *Service) OpenShareDownloadRange(ctx context.Context, filePath string, start, length int64) (io.ReadCloser, int64, int64, error)
- func (s *Service) PresignDownloadURL(ctx context.Context, filePath string, ttl time.Duration, fileName ...string) (string, error)
- func (s *Service) RecordViewer(ctx context.Context, code, viewerIP string) (*model.FileCode, error)
- func (s *Service) RecordViewerAndNotify(ctx context.Context, code, viewerIP, viewerDetail string) error
- func (s *Service) RestoreUserShare(ctx context.Context, userID uint, code string) error
- func (s *Service) SaveUploadFile(ctx context.Context, fh *multipart.FileHeader, originalFilename string) (string, int64, string, error)
- func (s *Service) SetBaseURLProvider(fn func(context.Context) string)
- func (s *Service) SetFederationNotifier(f FederationNotifier)
- func (s *Service) SetFlagEventEmitter(e FlagEventEmitter)
- func (s *Service) SetModerator(m moderation.Moderator)
- func (s *Service) SetNotifyService(svc NotifyServiceInterface)
- func (s *Service) SetPickupMinter(m PickupCodeMinter)
- func (s *Service) SetQuotaChecker(qc QuotaChecker)
- func (s *Service) SetShareStatus(ctx context.Context, ids []uint, status string) (int64, error)
- func (s *Service) SetUserService(userService UserServiceInterface)
- func (s *Service) ShareBytes(ctx context.Context, opts ShareBytesOpts) (*ShareResp, error)
- func (s *Service) ShareFile(ctx context.Context, req *ShareFileReq) (*ShareResp, error)
- func (s *Service) ShareText(ctx context.Context, req *ShareTextReq) (*ShareResp, error)
- func (s *Service) ShareTextWithAuth(ctx context.Context, text string, expireValue int, expireStyle string, ...) (*ShareResp, error)
- func (s *Service) StatShareFile(ctx context.Context, filePath string) (int64, error)
- func (s *Service) UpdateFileUsage(ctx context.Context, code string) (bool, error)
- func (s *Service) ZipStream(ctx context.Context, fc *model.FileCode) (io.ReadCloser, error)
- type ShareBlockedError
- type ShareBytesOpts
- type ShareFileItem
- type ShareFileReq
- type ShareMetadata
- type ShareResp
- type ShareTextReq
- type StoredFileEntry
- type UserServiceInterface
Constants ¶
This section is empty.
Variables ¶
var ErrNotInRecycleBin = errors.New("分享不在回收站中(不存在或未处于软删除状态)")
ErrNotInRecycleBin 分享不在回收站(未软删/已硬删/不存在),restore 与 hard-delete 共用。0 行变更曾双双表现为 200 静默 no-op——hard 侧 v0.11.1 修复、restore 侧 2026-10-08 修复:均改为调用方显式 400。
ErrShareCountExhausted 取件次数已用完(2026-10-24:次数耗尽从 IsExpired 里 拆出后,「耗尽」不再混作「过期/不存在」,各面给独立错误码 —— 20004 CodeShareReachLimit / 匿名通道 20007 CodePickupCodeExhausted)。
本哨兵供 app/share 的元数据面(GetShareMetadata)区分「次数用完」与 「不存在/已过期/被封禁」⇒ handler 据此回 20004 而不是笼统的 404。
ErrShareFileNotFound 分享或其子文件不存在(调用方统一 404 语义,防探测)。
ErrShareNotReady 分享未完成上传(占位记录:文件还没传上来)。
与匿名通道 app/anonymous 的 ErrNotReady 同一语义 ⇒ 两个面对同一份占位分享 给出同一个业务码(20001)与文案,不再是"分享域 200 + 一条指向不存在文件的 下载直链"、匿名通道 20001 的口径分叉(2026-10-23 审计)。
Functions ¶
func IsShareNotReady ¶ added in v0.31.3
IsShareNotReady 判定记录是否为"未完成上传"的占位分享。 判定式在 model(唯一真相源,preview/select/download/metadata 共用)。
func IsTextShare ¶
IsTextShare 判定是否纯文本分享(领域事实已下沉 model.FileCode.IsTextShare; 本包装保留给既有调用方,逐步迁移后可删)。
func LocalImportAbsPath ¶
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(code string, expiresAt *time.Time)
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 {
}
FlagEventEmitter share.flagged 事件推送接口(bootstrap 注入 notify service 实现)
type GlobalQuotaExceededError ¶
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 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 {
}
PickupCodeMinter 取件码铸造窄接口(app/anonymous.Service 结构性满足)。 share 域不 import anonymous 域(域间零耦合),由 bootstrap 注入具体实现。
type QuotaChecker ¶
QuotaChecker 存储配额检查接口(bootstrap 注入 user service 实现)
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
func NewService ¶
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
CheckUploadSizeCap 登录用户单次上传总大小预检(多文件绑定按申报大小 fail-fast, 与匿名配额预检对称,先于合并落盘;匿名用户返回 nil——其约束走匿名日配额)。
func (*Service) CreateMultiFileShare ¶
CreateMultiFileShare 创建多文件分享(1 个取件码 ↔ N 个文件)。
与单文件 CreateShare 的差异:
- 主表 Size = 各文件之和;legacy 字段(FilePath/UUIDFileName/FileHash)与首个文件同步, 保证旧读取方(管理端列表/秒传/统计)零改动可用
- 子表每文件一行,取件端经 ListShareFiles 拉取
- 配额按总大小一次扣检;审核逐文件判定(任一 reject 整单拒绝,pending 整单待审)
失败时不回滚物理文件——调用方(handler)负责在 err != nil 时清理已落盘文件。
func (*Service) CreateShare ¶
CreateShare 创建分享记录(ShareFile 的语义化别名,便于其他 service 调用) 行为:单用户上传上限检查 → 存储配额检查 → 生成 code → 写 file_codes 表 → 返回 share_code / url
func (*Service) DeleteFile ¶
DeleteFile 删除文件
func (*Service) DeleteFileByCode ¶
DeleteFileByCode 根据分享码删除文件。
删除顺序(保证一致性):
- 查所有权
- 删 DB 记录(事务原子)
- 事务提交后删物理文件(不可回滚,失败记日志不阻断)
- 扣减用户 storage 统计(best-effort,失败记日志)
关键:先删 DB 再删物理,避免"物理删了但 DB 没删"的孤儿反向场景。
func (*Service) DeleteLocalFile ¶
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 ¶
GenerateCode 生成分享代码(8 位字母数字,crypto/rand 经 kit/uidgen 统一收口)。
func (*Service) GetFileByCode ¶
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)。 legacy 虚拟行(id=0)返回主表合成的子项。
func (*Service) GetShareMetadata ¶
GetShareMetadata 查询分享元数据(复用 GetFileByCode 的过期/封禁检查与 typed error)。 不存在/已过期/被封禁统一由调用方转 404,避免存在性区分扩大探测面。
密码保护分享的最小化返回(2026-10-05 审计 P2):持码者在输对密码前只拿到 has_password/type/encrypted——文件名常含敏感信息(简历/合同名),此前免密码 即泄露,且该端点无鉴权。普通分享仍返回全量字段。
func (*Service) HardDeleteUserShare ¶
HardDeleteUserShare 永久删除软删除的分享
func (*Service) HasPickupCharge ¶ added in v0.32.2
HasPickupCharge 判「该取件会话是否已计次」(取件页在额度用完时的放行判定: 已经取过的人仍可再打开,不重复扣次)。
func (*Service) ImportLocalFile ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
SetShareStatus 管理员设置分享管控状态(禁用/恢复/待审),返回受影响行数。
func (*Service) SetUserService ¶
func (s *Service) SetUserService(userService UserServiceInterface)
func (*Service) ShareBytes ¶
ShareBytes 把内存中的文件内容登记为文件分享。 校验链:文件名消毒 → 扩展名白名单 → 大小上限 → SaveBytes 落存储 → CreateShare。
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 ¶
StatShareFile 获取分享文件总大小(Range 解析需先知 total)。 后端非统一存储实例时返回 storage.ErrRangeUnsupported(调用方回退全量)。
func (*Service) UpdateFileUsage ¶
UpdateFileUsage 原子扣减剩余次数(DB 为准,防并发超卖)。 返回 ok=true 表示扣减成功(可下载);ok=false 表示已耗尽。
⚠️ 2026-10-24 起**下载链路不再直接调它**:扣次改由 ChargePickup 按「取件会话 (下载令牌)」执行(一个令牌至多扣 1 次 ⇒ 多文件逐个下载 / 断点续传 / 重复点击 不再重复吃次数)。本方法保留给「无令牌会话概念」的其它调用面。
func (*Service) ZipStream ¶
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 ShareFileReq ¶
type ShareFileReq struct {
}
type ShareMetadata ¶
type ShareMetadata struct {
// Name 文件名(仅 file 分享;text 分享不返回内容)
// FileCount 多文件分享的子文件数(单文件/text 分享为 0)
}
ShareMetadata 取件元数据(公开展示字段)
type ShareTextReq ¶
type ShareTextReq struct {
}
type StoredFileEntry ¶
type StoredFileEntry = storage.StoredFileEntry
StoredFileEntry 已落存储的文件项(多文件分享创建入参)。 结构定义已下沉 storage.StoredFileEntry(中立载体,request 等域零 share 依赖); 本别名保留既有引用路径,新代码可直接使用 storage.StoredFileEntry。