share

package
v0.33.2 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 37 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 ErrDownloadStalled = errors.New("下载停滞超时:连续无字节流动")

ErrDownloadStalled 下载停滞超时(download.download_timeout,秒):传输中连续 N 秒无任何字节流动(存储卡死 / 客户端消失),守卫主动中断下载回收连接。 注:响应已进入流式写出后无法再改状态码,客户端表现为传输中断/不完整下载。

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;未启用时为空)

func ValidateWrappedKey added in v0.33.1

func ValidateWrappedKey(wrappedKey string) error

ValidateWrappedKey 校验口令型 E2E 的信封密钥形状。

空串 = 「不带信封」⇒ 合法(明文分享 / 旧 URL-hash-key 形态)。 非空 ⇒ 必须是**标准 base64**(带 padding,与前端 utils/e2e.ts 的 toB64 同款) 且解码后字节数 ∈ [1, 512]。

返回值是业务错误(400 / 10001),handler 可直接透传。

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)
	WrappedKey   string // 口令型 E2E 信封密钥(与单文件同语义:非空⇒铸 6 位取件码)
	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, wrappedKey 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)
	// WrappedKey 口令型 E2E 信封密钥(base64 0x01||salt||nonce||ct||tag;契约 ShareFileReq.wrapped_key)。
	// 非空 ⇒ 口令型(铸 6 位取件码);空且 Encrypted ⇒ 旧 URL-hash-key 形态(不铸码)。
	WrappedKey string
	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"`
	// HasWrappedKey 是否携带口令型 E2E 信封密钥(true=解密需口令;只给标志不给本体——
	// wrapped_key 本体仅随 GetShare 详情下发)。
	HasWrappedKey bool `json:"has_wrapped_key"`
	// 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)
	WrappedKey   string     `json:"wrapped_key,omitempty"` // 口令型 E2E 信封密钥(仅创建响应/内部视图)
	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)
	// WrappedKey 口令型 E2E 信封密钥(base64 0x01||salt||nonce||ct||tag;契约 ShareTextReq.wrapped_key)。
	// 非空 ⇒ 口令型(铸 6 位取件码,两因子:码+口令);空且 Encrypted ⇒ 旧 URL-hash-key 形态(不铸码)。
	WrappedKey string
	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