Documentation
¶
Overview ¶
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
- Variables
- func IsTextShare(fc *model.FileCode) bool
- func LocalImportAbsPath(rootIdx int, relPath string) (string, error)
- func LocalImportRoots() []string
- func ResolveBase(ctx context.Context, configured string) 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) 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) 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) ImportLocalFile(ctx context.Context, opts ImportLocalOpts) (*ShareResp, error)
- func (s *Service) ListLocalFiles(rootIdx int, relDir string) ([]LocalFileEntry, 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) 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 ¶
const PublicBaseCtxKey = "pb.public_base"
PublicBaseCtxKey hertz ctx 中请求级公开 base 的键。bootstrap 在 server.base_url 未配置时挂中间件,把每条请求的来源(scheme://host)写入 ctx(hertz RequestContext.Value 读取 Set 的 kv)。
Variables ¶
var ErrNotInRecycleBin = errors.New("分享不在回收站中(不存在或未处于软删除状态)")
ErrNotInRecycleBin 分享不在回收站(未软删/已硬删/不存在),restore 与 hard-delete 共用。0 行变更曾双双表现为 200 静默 no-op——hard 侧 v0.11.1 修复、restore 侧 2026-10-08 修复:均改为调用方显式 400。
ErrShareFileNotFound 分享或其子文件不存在(调用方统一 404 语义,防探测)。
Functions ¶
func IsTextShare ¶
IsTextShare 判定是否纯文本分享(领域事实已下沉 model.FileCode.IsTextShare; 本包装保留给既有调用方,逐步迁移后可删)。
func LocalImportAbsPath ¶
LocalImportAbsPath 把 (rootIdx, relPath) 解析为导入用的绝对路径(供管理端导入复用 ImportLocalFile;ImportLocalFile 内部会再做一次全量校验)
func LocalImportRoots ¶
func LocalImportRoots() []string
LocalImportRoots 返回已配置白名单根目录(已 EvalSymlinks;未启用时为空)
func ResolveBase ¶
ResolveBase 公开链接 base 解析:显式配置 > 请求来源(中间件注入)> 空串。 禁止回退到 server.host——那是监听地址(0.0.0.0),拼进分享链接对外不可达 (2026-10-07 iStoreOS 真机事故:分享成功弹窗给出 http://0.0.0.0:12345/#/s/x)。 双缺省时退相对路径(/share/CODE),浏览器侧可由 location.origin 补全。
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 NewService(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) 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) GenerateCode ¶
GenerateCode 生成分享代码(8 位字母数字,crypto/rand 经 kit/uidgen 统一收口)。
func (*Service) GetFileByCode ¶
GetFileByCode 通过代码获取文件
func (*Service) GetFileWithUsage ¶
func (s *Service) GetFileWithUsage(ctx context.Context, code, password, viewerIP string, authedByToken bool) (*model.FileCode, error)
GetFileWithUsage 获取文件并校验密码(不扣次数,扣次数由下载链路调 UpdateFileUsage)。 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) ImportLocalFile ¶
ImportLocalFile 把本地文件导入为分享。 校验链:功能开关 → 路径落在白名单目录内(EvalSymlinks 防穿越)→ 普通文件 → 大小上限 → 服务端拷贝入存储 → 复用 CreateShare(配额/审核/自定义码)。
func (*Service) ListLocalFiles ¶
func (s *Service) ListLocalFiles(rootIdx int, relDir string) ([]LocalFileEntry, error)
ListLocalFiles 列出 root 下(可选子目录 relDir)一层的条目(目录优先、按名排序)
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) SetFederationNotifier ¶
func (s *Service) SetFederationNotifier(f FederationNotifier)
SetFederationNotifier 注入 P2P 联邦口令路由钩子(bootstrap 调用;nil = 非联邦)
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 表示已耗尽。
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 ShareResp ¶
type ShareResp struct {
// KV 映射仅加速解析,8 位分享码永远可用,故铸造失败不影响主流程)
PickupCode string `json:"pickup_code,omitempty"`
}
type ShareTextReq ¶
type ShareTextReq struct {
}
type StoredFileEntry ¶
type StoredFileEntry = storage.StoredFileEntry
StoredFileEntry 已落存储的文件项(多文件分享创建入参)。 结构定义已下沉 storage.StoredFileEntry(中立载体,request 等域零 share 依赖); 本别名保留既有引用路径,新代码可直接使用 storage.StoredFileEntry。