///|
/// 分片上传:`/api/fs/multipart/*`。
///
/// 全部请求参数都在**请求头**里,请求体是原始分片字节。服务端会尽量复用未
/// 完成的会话:`multipart_init` 返回的 `resumed` 为真时,`snapshot.received`
/// 里已经有服务端收过的分片区间,重传会被拒绝(429/409),所以按区间跳过。
///
/// 注意两点服务端约束:
///
/// - 分片大小由服务端决定(默认 10MB,且有 1MB 下限),`chunk_size` 只是建议值;
/// 实际的每片大小以 `snapshot.chunk_size` 为准;
/// - `X-File-Size` 必须大于 0,空文件请走 `/fs/put`。
///|
/// 分片上传的请求头。
fn multipart_headers(
path : String,
size : Int64,
chunk_size? : Int,
overwrite? : Bool,
content_type? : String,
last_modified? : Int64,
hash? : FileHash,
) -> Array[(String, String)] {
let headers = put_headers(
path,
// `size` 是 Int64,这里自己按原值补头,避免先窄化成 Int。
None,
overwrite?,
content_type?,
last_modified?,
hash?,
)
headers.push(("X-File-Size", size.to_string()))
match chunk_size {
Some(value) => headers.push(("X-Chunk-Size", value.to_string()))
None => ()
}
headers
}
///|
/// 判断某个分片是否已经被服务端收下(`received` 里是闭区间)。
fn chunk_received(received : Array[Array[Int]], index : Int) -> Bool {
let mut hit = false
for range in received {
if range.length() >= 2 && index >= range[0] && index <= range[1] {
hit = true
}
}
hit
}
///|
/// 初始化(或续用)一个分片上传会话(`POST /api/fs/multipart/init`)。
///
/// `size` 是文件总字节数,必须大于 0。`chunk_size` 只是建议值,服务端会把它
/// 夹到 `[1MB, 配置上限]` 之间;实际值看返回的 `snapshot.chunk_size`。
pub async fn FileSystem::multipart_init(
self : FileSystem,
path : String,
size : Int64,
chunk_size? : Int,
overwrite? : Bool,
content_type? : String,
last_modified? : Int64,
hash? : FileHash,
) -> @core.MultipartInitResponse raise @core.OpenListError {
let headers = multipart_headers(
path,
size,
chunk_size?,
overwrite?,
content_type?,
last_modified?,
hash?,
)
let data = self.client.request(
@moonhttp.Method::Post,
"/api/fs/multipart/init",
headers~,
)
@core.decode_data(data)
}
///|
/// 上传一个分片(`PUT /api/fs/multipart/chunk`)。
///
/// `index` 从 0 开始。分片被拒时(分片乱序、重复、会话不见了)服务端会把当前
/// 会话快照放在错误响应的 `data` 里,所以抛出的 `OpenListError::Api` 的
/// `ApiError::data` 里仍然能拿到进度,可以据此重试或继续。
pub async fn FileSystem::multipart_chunk(
self : FileSystem,
upload_id : String,
index : Int,
data : Bytes,
) -> @core.SessionSnapshot raise @core.OpenListError {
let headers : Array[(String, String)] = [
("X-Upload-Id", upload_id),
("X-Chunk-Index", index.to_string()),
]
let payload = self.client.send_stream(
@moonhttp.Method::Put,
"/api/fs/multipart/chunk",
@core.bytes_reader(data),
content_length=data.length(),
headers~,
)
@core.decode_data(payload)
}
///|
/// 声明分片传完,等服务端合并(`POST /api/fs/multipart/complete`)。
///
/// 返回最终快照;驱动真正写盘失败时会抛 `OpenListError::Api`,`state` 与
/// `error` 在错误的 `data` 里。
pub async fn FileSystem::multipart_complete(
self : FileSystem,
upload_id : String,
) -> @core.SessionSnapshot raise @core.OpenListError {
let data = self.client.request(
@moonhttp.Method::Post,
"/api/fs/multipart/complete",
headers=[("X-Upload-Id", upload_id)],
)
@core.decode_data(data)
}
///|
/// 按会话 ID 查分片上传进度(`GET /api/fs/multipart/status?upload_id=`)。
pub async fn FileSystem::multipart_status(
self : FileSystem,
upload_id : String,
) -> @core.SessionSnapshot raise @core.OpenListError {
let query = @core.query_json([("upload_id", Some(Json::string(upload_id)))])
let data = self.client.request(
@moonhttp.Method::Get,
"/api/fs/multipart/status",
query~,
)
@core.decode_data(data)
}
///|
/// 按「目标路径 + 总大小」查分片上传进度
/// (`GET /api/fs/multipart/status?path=&size=`)。
///
/// 用于客户端重启后找回未完成的会话,不需要记住 `upload_id`。
pub async fn FileSystem::multipart_status_by_path(
self : FileSystem,
path : String,
size : Int64,
) -> @core.SessionSnapshot raise @core.OpenListError {
let query = @core.query_json([
("path", Some(Json::string(path))),
("size", Some(Json::number(size.to_double()))),
])
let data = self.client.request(
@moonhttp.Method::Get,
"/api/fs/multipart/status",
query~,
)
@core.decode_data(data)
}
///|
/// 放弃一个分片上传会话(`POST /api/fs/multipart/abort`)。
pub async fn FileSystem::multipart_abort(
self : FileSystem,
upload_id : String,
) -> Unit raise @core.OpenListError {
self.post_quiet("/api/fs/multipart/abort", headers=[
("X-Upload-Id", upload_id),
])
}
///|
/// 一步做完「初始化 → 逐片上传 → 合并」。
///
/// 分片大小以服务端返回的 `snapshot.chunk_size` 为准;已经在服务端手里的分片
/// (续传场景)会被跳过,所以这个函数可以安全地用同一个文件重跑。
///
/// 只适合「整块字节已在内存里」的场景(手机端小文件、测试);大文件请按需自己
/// 调 `multipart_init` / `multipart_chunk` / `multipart_complete` 传流。
pub async fn FileSystem::multipart_upload(
self : FileSystem,
path : String,
data : Bytes,
chunk_size? : Int,
overwrite? : Bool,
content_type? : String,
hash? : FileHash,
) -> @core.SessionSnapshot raise @core.OpenListError {
let total = data.length()
let init = self.multipart_init(
path,
total.to_int64(),
chunk_size?,
overwrite?,
content_type?,
hash?,
)
let mut snapshot = init.snapshot
let raw_chunk = snapshot.chunk_size.to_int()
// 服务端一定会给出正的分片大小;万一没有就退化成「一次传完」。
let chunk = if raw_chunk > 0 { raw_chunk } else { total }
let mut index = 0
while index * chunk < total {
let start = index * chunk
let end = if start + chunk < total { start + chunk } else { total }
if !chunk_received(snapshot.received, index) {
let piece = data.exact_view(start~, end~).to_owned()
snapshot = self.multipart_chunk(snapshot.upload_id, index, piece)
}
index = index + 1
}
self.multipart_complete(snapshot.upload_id)
}