Documentation
¶
Overview ¶
Package transfer 为 ofd-server 定义输入与输出两侧的抽象:待转换文档从哪里来, 转换结果写到哪里去。
服务端接收两类输入——HTTP 上传的字节流,或由调用方给出的 URL 拉取。URL 拉取 必须经过 allowlist 校验,否则调用方能让服务去请求内网地址或云元数据端点。 输出侧同理,调用方指定的目录与文件名必须限制在配置允许的根目录内。
放在 internal 下是因为它只服务于 ofd-server:Source/Sink 的取舍都按服务端 场景定(例如输出默认只新建、不允许覆盖)。需要把转换库接到对象存储的调用方 可以直接用 pkg/converter 的 []byte 与路径入参,不必依赖本包。
Index ¶
- Constants
- Variables
- func CopyLimited(ctx context.Context, w io.Writer, r io.Reader, limit int64) (int64, error)
- func ReadLimited(ctx context.Context, r io.Reader, limit int64) ([]byte, error)
- func ResolveInRoot(root, name string) (string, error)
- func SanitizeName(name string) string
- type BufferSink
- type BytesSource
- type DirSink
- type FTPSink
- type FileSource
- type HTTPAudit
- type HTTPSource
- type LimitedSink
- type Location
- type MinioSink
- func (s *MinioSink) Close() error
- func (s *MinioSink) EndpointURL() string
- func (s *MinioSink) MaxBytesLimit() int64
- func (s *MinioSink) Put(ctx context.Context, name string, r io.Reader) (Location, error)
- func (s *MinioSink) SetLogger(log *slog.Logger)
- func (s *MinioSink) WithBasePrefix(prefix string) (*MinioSink, error)
- type SFTPAuth
- type SFTPSink
- type Sink
- type Source
- type WebDAVSink
Constants ¶
const DefaultFTPTimeout = 60 * time.Second
DefaultFTPTimeout 是 FTP 连接与操作的默认超时。
const DefaultMaxBytes int64 = 64 << 20
DefaultMaxBytes 是单个文件的默认上限。输入与输出共用同一个上限常量,但可以 分别覆盖。
const DefaultMinioTimeout = 2 * time.Minute
DefaultMinioTimeout 是对象存储单次请求的默认超时。
const DefaultSFTPTimeout = 60 * time.Second
DefaultSFTPTimeout 是 SFTP 连接与操作的默认超时。
const DefaultWebDAVTimeout = 2 * time.Minute
DefaultWebDAVTimeout 是 WebDAV 单次请求的默认超时。
Variables ¶
var ErrExists = errors.New("目标已存在")
ErrExists 表示目标已存在且该目标不允许覆盖。
单列一个哨兵错误而不是靠消息文本判断,是因为调用方需要据此决定要不要重试: 重试一个"文件已存在"永远还是已存在,白白耗掉 worker 与退避时间。而它恰恰是 output.dir 指向共享目录、多个任务写同名文件时必然出现的错误。
Functions ¶
func CopyLimited ¶
CopyLimited 把 r 拷贝到 w,累计超过 limit 字节时中止并报错。分块读取,内存占用 与内容大小无关。
func ReadLimited ¶
ReadLimited 读取 r 的全部内容,超过 limit 字节即报错。
func ResolveInRoot ¶
ResolveInRoot 把 name 解析到 root 之内,并拒绝任何路径逃逸。
只允许单一段文件名:含分隔符、绝对路径、"." 或 ".." 的名字一律拒绝。这样 name 就无法表达 "../../etc/passwd" 这类意图,也不需要在拼好路径后再做前缀 判断(前缀判断会被 root 为 "/" 之类的边界情况骗到)。
func SanitizeName ¶
SanitizeName 把外部来源的文件名收敛成一个安全的单段文件名:去掉路径成分与控制 字符。结果为空时返回空串,由调用方补默认名。
Types ¶
type BufferSink ¶
type BufferSink struct {
Limit int64
// contains filtered or unexported fields
}
BufferSink 把结果留在内存里,用于把转换结果直接回传响应体。
func NewBufferSink ¶
func NewBufferSink(limit int64) *BufferSink
NewBufferSink 构造内存目标,limit 为 0 时用 DefaultMaxBytes。
func (*BufferSink) MaxBytesLimit ¶
func (s *BufferSink) MaxBytesLimit() int64
type BytesSource ¶
BytesSource 是内存来源,用于测试与小文件。
func NewBytesSource ¶
func NewBytesSource(data []byte, file string) *BytesSource
NewBytesSource 以给定文件名构造内存来源。
func (*BytesSource) Close ¶
func (s *BytesSource) Close() error
func (*BytesSource) Name ¶
func (s *BytesSource) Name() string
func (*BytesSource) Open ¶
func (s *BytesSource) Open(context.Context) (io.ReadCloser, error)
type DirSink ¶
type DirSink struct {
// Root 是允许写入的根目录。Put 会拒绝任何逃出该目录的 name。
Root string
// MaxBytes 单文件上限,0 时用 DefaultMaxBytes。
MaxBytes int64
// Overwrite 为真时允许覆盖已存在的普通文件。默认只新建:既避免误覆盖,也
// 顺带杜绝了"目标是指向目录外的符号链接"这一类写入劫持。
Overwrite bool
}
DirSink 把结果写入本地目录。
func (*DirSink) MaxBytesLimit ¶
type FTPSink ¶
type FTPSink struct {
// Addr 是服务器地址,形如 "ftp.example.com:21"。
Addr string
// User 与 Password 是登录凭据。不会出现在任何日志里。
User string
Password string
// BaseDir 是允许写入的远端根目录。留空表示远端用户登录后的家目录。
BaseDir string
// TLSConfig 非 nil 时使用隐式 FTPS(连上直接握手)。默认使用显式 FTPS
// (先读 220,再用 AUTH TLS 升级),这是更常见地部署形态。
TLSConfig *tls.Config
// Implicit 为真时使用隐式 FTPS;为假时使用显式 FTPS。
Implicit bool
// Insecure 允许明文 FTP。留空 TLSConfig 且置真时使用。生产环境不应开启。
Insecure bool
// Timeout 是连接、登录与单个操作的超时,0 时取 DefaultFTPTimeout。
Timeout time.Duration
// MaxBytes 单文件上限,0 时用 DefaultMaxBytes。
MaxBytes int64
// Overwrite 为真时允许覆盖远端同名文件。默认拒绝,避免误覆盖别人的文件。
Overwrite bool
// DisableEPSV 强制只用 PASV。少数服务器没实现 EPSV,而客户端默认优先
// 试它,失败后能否回落到 PASV 取决于服务器实现。
DisableEPSV bool
// OwnsPool 区分注册目标与 WithBaseDir 派生的实例。只有前者关池。
//
// 外部(cmd/ofd-server)从配置构造注册目标时置 true;派生实例不带它。
OwnsPool bool
// MaxIdle 与 IdleTTL 覆盖池的默认参数,0 表示用默认值。
//
// 这个结构体不参与序列化(配置类型在 cmd/ofd-server),所以不带 json 标签。
MaxIdle int
IdleTTL time.Duration
// contains filtered or unexported fields
}
FTPSink 把转换结果上传到 FTP/FTPS 服务器。
三个安全取舍,都是有代价的选择:
**默认要求 TLS**。FTP 的控制通道和文件内容都是明文,账号密码会直接暴露在 链路上。确实要在内网用明文时把 TLS 置空并在配置里显式声明,服务启动时会 记一条 WARN——让这个决定是自觉做的,而不是默认忽略的。
**不信任 PASV 返回的地址**。被动模式下服务器会告诉客户端"把数据连到这个 地址"。如果照着连,一个被攻陷或恶意的 FTP 服务器就能把服务的数据流引到 内网任意地址。jlaffaye/ftp 默认不信任,这里也刻意不打开 DialWithTrustPasvIP,代价是少数配置了 PASV 外部地址的服务器用不了。
**限制单文件大小**。读多少传多少,不先把内容读进内存,也拒绝超限的上传, 避免一个失控的转换把远端磁盘写满。
func (*FTPSink) Close ¶
Close 关闭连接。
Sink 接口没有 Close,调用方用可选的 io.Closer 断言来收尾;不实现也可以, 只是每次 PUT 都要重新握手。
func (*FTPSink) MaxBytesLimit ¶
type FileSource ¶
FileSource 是磁盘文件来源。Close 会删除该文件,因此路径必须由服务端创建、 而不是调用方指定。
func NewFileSource ¶
func NewFileSource(path string, keep bool) *FileSource
NewFileSource 构造磁盘来源,keep 为真表示不删除。
func (*FileSource) Close ¶
func (s *FileSource) Close() error
func (*FileSource) Name ¶
func (s *FileSource) Name() string
func (*FileSource) Open ¶
func (s *FileSource) Open(context.Context) (io.ReadCloser, error)
type HTTPAudit ¶
type HTTPAudit struct {
// Requested 是调用方给出的原始地址。
Requested string
// Fetched 是最终请求的地址(跟随重定向后会变)。
Fetched string
// Resolved 是拨号时校验通过的 IP 列表。
Resolved []string
// StatusCode 是响应状态。
StatusCode int
// Bytes 是读取到的字节数。
Bytes int64
}
HTTPAudit 记录一次 URL 拉取的实际去向,出问题时用来追溯。
type HTTPSource ¶
type HTTPSource struct {
// URL 是要拉取的地址。
URL string
// Allowlist 限定允许访问的主机与网段。空白名单下 Open 恒返回错误。
Allowlist *allowlist.List
// MaxBytes 响应体上限,0 时用 DefaultMaxBytes。
MaxBytes int64
// Timeout 整体超时,0 时用 30s。
Timeout time.Duration
// AllowRedirect 为真时跟随重定向。每跳仍会经过拨号时的白名单校验,因此不会
// 绕过限制;但重定向会把原始地址泄露给第三方,默认关闭。
AllowRedirect bool
// FileName 覆盖推导出的文件名。为空时从 Content-Disposition 与 URL 路径取。
FileName string
// UserAgent 供部分站点识别客户端。
UserAgent string
// Log 记录实际拉取的地址与解析结果,便于审计。
Log *slog.Logger
// contains filtered or unexported fields
}
HTTPSource 按 URL 拉取待转换文档。拉取方向由调用方给出地址,因此必须经过 allowlist 校验,否则服务会被当作跳板去请求内网地址或云元数据端点。
func (*HTTPSource) Audit ¶
func (s *HTTPSource) Audit() *HTTPAudit
Audit 返回最近一次 Open 的审计信息,Open 之前调用返回 nil。
func (*HTTPSource) Close ¶
func (s *HTTPSource) Close() error
func (*HTTPSource) Name ¶
func (s *HTTPSource) Name() string
Name 返回推导出的文件名。URL 无法推导时返回空,调用方需要显式指定格式。
func (*HTTPSource) Open ¶
func (s *HTTPSource) Open(ctx context.Context) (io.ReadCloser, error)
Open 拉取内容。响应体被包装成受上限约束的 ReadCloser,由调用方关闭。
type LimitedSink ¶
LimitedSink 表示对单个输出文件有明确大小上限的 Sink。 上限由转换服务在写入阶段执行,避免完整结果先缓存到内存后才失败。
type Location ¶
type Location struct {
// Kind 为 "stream"、"dir" 或后续实现的 "s3"、"ftp"。
Kind string `json:"kind"`
// Path 是本地绝对路径(Kind 为 "dir" 时)。
Path string `json:"path,omitempty"`
// Bucket 与 Key 用于对象存储。
Bucket string `json:"bucket,omitempty"`
Key string `json:"key,omitempty"`
// URL 是可直接访问的地址,由具体后端可选地填充。
URL string `json:"url,omitempty"`
// Size 是写入的字节数。
Size int64 `json:"size"`
}
Location 描述转换结果的存放位置,出现在任务状态与通知回调里。
type MinioSink ¶
type MinioSink struct {
// Endpoint 是 S3 端点的主机名(不含协议),如 "minio.internal:9000"。
Endpoint string
// Secure 为真时用 https。为假时用 http——S3 的签名本身防篡改,但
// 流量与凭据仍是明文,仅适用于内网。
Secure bool
// Region 是区域标识。多数自建部署可留空。
Region string
// AccessKey 与 SecretKey 是访问凭据。
AccessKey string
SecretKey string
// SessionToken 是临时凭据的令牌,永久凭据留空。
SessionToken string
// Bucket 是目标桶。
Bucket string
// BasePrefix 是桶内的前缀,等价于 FTP 的 BaseDir。
BasePrefix string
// Timeout 是单次请求超时,0 时取 DefaultMinioTimeout。
Timeout time.Duration
// MaxBytes 单对象上限,0 时用 DefaultMaxBytes。
MaxBytes int64
// Overwrite 为真时允许覆盖同名对象。默认拒绝。
Overwrite bool
// contains filtered or unexported fields
}
MinioSink 把转换结果写入 S3 兼容的对象存储(MinIO、AWS S3、Ceph RGW 等)。
三点与 FTP sink 的共同考量:
**凭据不上线**。地址、bucket、access key 都只存在于服务端配置里,调用方 只能给一个目标名。
**先探测再写**。对象存储没有"目录"概念,父目录是靠前缀隐含的,写入不会 失败于"目录不存在"。但重复覆盖会静默成功,因此默认仍拒绝覆盖同名对象。
**限制单对象大小**,与 FTP sink 同理。
func (*MinioSink) EndpointURL ¶
EndpointURL 拼出对象的可访问地址,用于任务状态展示。
func (*MinioSink) MaxBytesLimit ¶
type SFTPAuth ¶
type SFTPAuth struct {
// PrivateKeyFile 是私钥文件路径(PEM 或 OpenSSH 新格式)。
PrivateKeyFile string
// PrivateKeyPassphrase 是私钥口令。留空表示私钥未加密。
// 加密私钥且这里留空时连接会失败——这是有意的:不想把口令写进配置文件,
// 就该走 AgentSocket。
PrivateKeyPassphrase string
// AgentSocket 是 ssh-agent 的 socket 路径。配置里只有这个路径,
// 私钥与已解密的口令都由 agent 持有,是最干净的一种。
AgentSocket string
// Password 是口令认证的密码。
Password string
}
SFTPAuth 描述 SFTP 的登录方式。
私钥文件与 agent 都不需要把密钥材料放进服务配置;口令是最后手段,配置文件 里的口令应当等同于明文存储。
type SFTPSink ¶
type SFTPSink struct {
// Addr 是服务器地址,形如 "sftp.example.com:22"。
Addr string
// User 是登录用户名。
User string
// Auth 是认证方式。三种可叠加,客户端会按顺序尝试。
Auth SFTPAuth
// BaseDir 是允许写入的远端目录,空表示登录用户的家目录。
BaseDir string
// HostKeySHA256 是预期的主机密钥指纹(SHA256:...),与 OpenSSH 一致。
HostKeySHA256 string
// HostKeyFile 是 known_hosts 文件路径。
HostKeyFile string
// InsecureIgnoreHostKey 跳过主机密钥校验。仅供一次性排查。
InsecureIgnoreHostKey bool
// Timeout 是连接、认证与单次操作的超时,0 时取 DefaultSFTPTimeout。
Timeout time.Duration
// MaxBytes 单文件上限,0 时用 DefaultMaxBytes。
MaxBytes int64
// Overwrite 为真时允许覆盖远端同名文件。默认拒绝。
Overwrite bool
// OwnsPool 区分注册目标与 WithBaseDir 派生的实例,只有前者关池。
OwnsPool bool
// MaxIdle 与 IdleTTL 覆盖池的默认参数,0 表示用默认值。
MaxIdle int
IdleTTL time.Duration
// contains filtered or unexported fields
}
SFTPSink 把转换结果通过 SFTP 写入远端。
SFTP 跑在 SSH 上,因此天然加密,这点和明文 FTP 有本质区别。但也带来一个 FTP/WebDAV/MinIO 都没有的开关:**主机密钥校验**。
SSH 靠 host key 识别对方。只连不管的话,攻击者可以在中间人位置冒充服务器, 拿到私钥或口令、读到全部文件内容——而连接照样"成功"。所以这里默认必须校验, 三种方式按严格程度排:
- HostKeySHA256:配置里写死目标主机的指纹(ssh-keygen 打印的那个)。
- HostKeyFile:读 OpenSSH 的 known_hosts 文件。
- InsecureIgnoreHostKey:跳过校验。必须显式声明,配置校验也会拦一次。
三种都不配时报错,而不是退回"跳过校验"——那正是最容易出事的情况。
func (*SFTPSink) Close ¶
Close 关闭连接。
Sink 接口没有 Close,调用方用可选的 io.Closer 断言来收尾;不实现也可以, 只是每次 Put 都要重新握手。
func (*SFTPSink) MaxBytesLimit ¶
type Sink ¶
type Sink interface {
// Put 把 r 的内容写入名为 name 的目标,返回可定位的结果。
Put(ctx context.Context, name string, r io.Reader) (Location, error)
}
Sink 是转换结果的写入目标。
type Source ¶
type Source interface {
// Name 返回带扩展名的文件名。转换库按扩展名识别输入格式(docx/xlsx/html 等
// 无法靠魔数区分),因此这里的扩展名必须是可信的、由服务端按已解析的格式
// 派生,而不是直接采信调用方的原始文件名。
Name() string
// Open 打开内容供读取。调用方负责关闭返回的 ReadCloser。
Open(ctx context.Context) (io.ReadCloser, error)
// Close 释放来源占用的资源,对内存来源是空操作。
Close() error
}
Source 是待转换文档的来源。每次任务独占一个 Source,用完应当调用 Close 释放 其占用的临时文件。
type WebDAVSink ¶
type WebDAVSink struct {
// Endpoint 是服务地址,如 "https://dav.example.com/remote.php/dav/files/user"。
Endpoint string
// User 与 Password 是凭据。不会出现在任何日志里。
User string
Password string
// BaseDir 是允许写入的远端目录(相对 endpoint 根)。
BaseDir string
// Timeout 是单次请求超时,0 时取 DefaultWebDAVTimeout。
Timeout time.Duration
// MaxBytes 单文件上限,0 时用 DefaultMaxBytes。
MaxBytes int64
// Overwrite 为真时允许覆盖同名文件。默认拒绝。
Overwrite bool
// contains filtered or unexported fields
}
WebDAVSink 把转换结果写入 WebDAV 共享。
与 FTP 的差别:WebDAV 跑在 HTTP 上,天然有 TLS、有状态码、有中间件生态, 因此不需要像 FTP 那样处理明文与 PASV 地址。但它同样需要凭据,同样只接受 已注册的目标名,同样限制单文件大小。
func (*WebDAVSink) MaxBytesLimit ¶
func (s *WebDAVSink) MaxBytesLimit() int64
func (*WebDAVSink) WithBaseDir ¶
func (s *WebDAVSink) WithBaseDir(dir string) (*WebDAVSink, error)
WithBaseDir 派生一份换掉远端目录的副本,并拒绝含 ".." 的目录。