// 文件上传的三种形态,共用一条 PUT 管线(`send_put`):
// - `put_file`:入参是已在内存的完整 Bytes(BytesReader 拉取式供给);
// - `put_file_stream`:入参是任意 `@io.Reader`,边读边发;
// - `put_file_from_path`:入参是本地文件路径,内部打开 / 取长度 / 边读边发 / 关闭。
// 三者都经 moonhttp 0.5 的流式请求体发送:声明长度走定长 Content-Length,
// 长度未知走 chunked;传输层每 64 KiB 分块触发上传进度。
// 下载实现按方向拆到 download_ops.mbt(RL-04)。
///|
/// Bytes 的拉取式读取流:把已在内存的文件内容按块交给传输层,
/// 不引入协程与管道、也不拷贝整份数据(视图切片直接供给泵循环)。
/// 单次上传独占一个实例,光标走到尾部即 EOF。
priv struct BytesReader {
data : Bytes
mut pos : Int32
// read_some 复用这块内部缓冲做中转,须与实例同生命周期;
// 重放(423 自动重试)时整块换成新缓冲,避免上一轮残留字节
mut buf : @io.ReaderBuffer
}
///|
impl @io.Reader for BytesReader with fn _get_internal_buffer(self) {
self.buf
}
///|
impl @io.Reader for BytesReader with fn _direct_read(
self,
dst,
offset~,
max_len~,
) -> Int32 {
let remaining = self.data.length() - self.pos
if remaining <= 0 {
return 0
}
let n = if max_len < remaining { max_len } else { remaining }
dst.blit_from_bytesview(offset, self.data[self.pos:self.pos + n])
self.pos = self.pos + n
n
}
///|
/// 上传文件(PUT 整体写入),返回响应元数据(ETag / Last-Modified /
/// Location),可直接用于后续的条件请求。
///
/// 这是**全量字节**形态:`data` 契约是「完整内容已在内存」。上传过程
/// 本身是流式的(不额外整份拷贝),但内容要先在调用方内存里;内容在磁盘、
/// 或大到不想整块驻留时,用 `put_file_from_path` / `put_file_stream`。
///
/// - `data`:完整文件内容,包装成流式请求体发送;
/// - `content_type`:可选 MIME 类型;
/// - `conditions`:可选 `If-Match` / `If-None-Match`(乐观并发控制,
/// 如 `If-None-Match: "*"` 表示「仅当资源不存在时才创建」);
/// - `on_progress`:上传进度,按传输层 64 KiB 分块节奏触发,
/// `total` 为声明的请求体长度。
///
/// 内容整份在内存,因此这条路径**可重放**:写请求被 423 拒绝、且客户端
/// 开启 `retry_on_423` 并持有覆盖锁时,会自动补锁令牌重试一次
/// (`put_file_stream` / `put_file_from_path` 的一次性流不重试)。
pub async fn WebdavClient::put_file(
self : WebdavClient,
path~ : String,
data~ : Bytes,
content_type? : String,
conditions? : @types.Conditions,
on_progress? : (@types.Progress) -> Unit,
signal? : @moonhttp.AbortSignal,
) -> @types.PutResult raise @types.WebdavError {
let reader = BytesReader::{ data, pos: 0, buf: @io.ReaderBuffer::new(), }
let reset = fn() {
reader.pos = 0
reader.buf = @io.ReaderBuffer::new()
}
let rewind : (() -> Unit)? = Some(reset)
self.send_put(
path~,
reader~,
content_length=data.length(),
content_type?,
conditions?,
on_progress?,
rewind?,
signal?,
)
}
///|
/// 流式上传:数据源是任意读取流(`&@io.Reader`),传输层边读边写,
/// 请求体不再需要整块驻留内存。
///
/// - `reader`:拉取式读取流。`read_some` 返回 `None`(EOF)即请求体收尾。
/// `@fs.File`(本地文件)、`@io.MemoryReader`(边生成边推)、
/// `@io.pipe()` 的读端(生产者协程)都能直接接;
/// - `content_length`:请求体长度。给了走定长 `Content-Length`(进度
/// `total` 已知,兼容 HTTP/1.0 目标);不给走 `Transfer-Encoding: chunked`。
/// 写出字节数与声明不符时传输层报错(本地数据与声明对不上);
/// - 其余参数语义与 `put_file` 一致。
///
/// **流是一次性资源**:同一条流不要发两次请求;遇到 307/308 这类要求
/// 原样重放请求体的重定向时,moonhttp 会直接报错而不是静默发空体。
/// 同理,这条路径**不参与 423 自动重试**(拨不回起点),
/// 需要重试能力请在内存里用 `put_file`。
pub async fn WebdavClient::put_file_stream(
self : WebdavClient,
path~ : String,
reader~ : &@io.Reader,
content_length? : Int32,
content_type? : String,
conditions? : @types.Conditions,
on_progress? : (@types.Progress) -> Unit,
signal? : @moonhttp.AbortSignal,
) -> @types.PutResult raise @types.WebdavError {
self.send_put(
path~,
reader~,
content_length?,
content_type?,
conditions?,
on_progress?,
signal?,
)
}
///|
/// 从本地文件路径流式上传:内部打开文件、用文件长度声明
/// `Content-Length`、把文件句柄当读取流交给传输层,边读边发,
/// 成功或失败都关闭句柄——**内容不整份读进内存**。
///
/// - `path`:服务端目标路径;`local_path`:本地待上传文件的绝对 / 相对路径;
/// - 本地文件打不开、路径是目录时抛 `WebdavError::Io`,此时不会发出任何请求;
/// - 其余参数语义与 `put_file` 一致(`on_progress` 的 `total` 为文件长度)。
///
/// 注意:长度在打开时取快照,传输过程中文件被改写会导致
/// 「写出字节数与声明不符」,传输层报 `Http` 错误;大文件的整跳超时见
/// `WebdavConfig::timeout_ms`(覆盖「建连 → 写完整个请求体 → 响应头」)。
pub async fn WebdavClient::put_file_from_path(
self : WebdavClient,
path~ : String,
local_path~ : String,
content_type? : String,
conditions? : @types.Conditions,
on_progress? : (@types.Progress) -> Unit,
signal? : @moonhttp.AbortSignal,
) -> @types.PutResult raise @types.WebdavError {
let (file, size) = open_local_source(local_path)
let outcome : Result[@types.PutResult, @types.WebdavError] = Ok(
self.send_put(
path~,
reader=file,
content_length=size,
content_type?,
conditions?,
on_progress?,
signal?,
),
) catch {
error => Err(error)
}
file.close()
match outcome {
Ok(result) => result
Err(error) => raise error
}
}
///|
/// PUT 管线的公共实现:三种上传形态只差「数据从哪来」——请求头、
/// 条件请求、进度接线与状态码判定完全一致,不重复三份。
///
/// `rewind` 是「数据源可重放」的标志:给了才允许 423 自动重试
/// (重试前先调它把数据源拨回起点),一次性流不给。
async fn WebdavClient::send_put(
self : WebdavClient,
path~ : String,
reader~ : &@io.Reader,
content_length? : Int32,
content_type? : String,
conditions? : @types.Conditions,
on_progress? : (@types.Progress) -> Unit,
rewind? : () -> Unit,
signal? : @moonhttp.AbortSignal,
) -> @types.PutResult raise @types.WebdavError {
let url = self.url_for(path)
let cfg = build_put_config(
url, reader, content_length, content_type, conditions, on_progress,
)
let mut response = self.perform(cfg, signal?)
match (response.status == 423, self.retry_on_423, rewind) {
(true, true, Some(reset)) =>
match self.covering_lock_token(path) {
Some(token) => {
reset()
let retry_conditions = conditions
.unwrap_or(@types.Conditions::new())
.with_lock_token(token)
let retry_cfg = build_put_config(
url,
reader,
content_length,
content_type,
Some(retry_conditions),
on_progress,
)
response = self.perform(retry_cfg, signal?)
}
None => ()
}
_ => ()
}
let checked = check_status(response, operation="PUT", ok=[200, 201, 204])
{
status: checked.status,
created: checked.status == 201,
etag: checked.headers.get("ETag"),
last_modified: checked.headers.get("Last-Modified"),
location: checked.headers.get("Location"),
}
}
///|
/// 组装 PUT 请求配置(流式体 / Content-Type / 进度 / 条件头),
/// 供 423 自动重试时原样重建。
fn build_put_config(
url : String,
reader : &@io.Reader,
content_length : Int32?,
content_type : String?,
conditions : @types.Conditions?,
on_progress : ((@types.Progress) -> Unit)?,
) -> @config.Config raise @types.WebdavError {
let mut cfg = @config.Config::new(url)
.with_method(@config.Method::Put)
.with_data_from_stream(reader, content_length?)
match content_type {
Some(mime) => cfg = cfg.with_header("Content-Type", mime)
None => ()
}
match adapt_progress(on_progress) {
Some(report) => cfg = cfg.with_on_upload_progress(report)
None => ()
}
apply_conditions(cfg, conditions, operation="PUT", url~)
}
///|
/// 打开本地文件作为上传数据源:返回(文件句柄,字节长度)。
/// 打不开、或路径是目录,都抛 `Io`(调用方据此定位本地问题,
/// 而不是把它当成服务端返回的错误)。
async fn open_local_source(
local_path : String,
) -> (@fs.File, Int32) raise @types.WebdavError {
let file = @fs.open(local_path, mode=@fs.Mode::ReadOnly) catch {
error =>
raise @types.WebdavError::io(
message="打开本地文件失败(\{local_path}):\{error}",
)
}
if file.kind() == @fs.FileKind::Directory {
file.close()
raise @types.WebdavError::io(
message="本地路径是目录,不能作为上传数据源:\{local_path}",
)
}
let size = file.size() catch {
error => {
file.close()
raise @types.WebdavError::io(
message="读取本地文件长度失败(\{local_path}):\{error}",
)
}
}
(file, size.to_int())
}