///|
/// 上传:流式上传(`PUT /api/fs/put`)与表单上传(`PUT /api/fs/form`)。
///
/// 服务端把上传元信息全放在**请求头**里(`File-Path`、`X-File-Size`、
/// `Overwrite`、哈希……),请求体就是原始字节。`File-Path` 要求 URL 编码,
/// 这里统一用 RFC 3986 的百分号编码(`@core.url_path_encode`)。
///|
/// 组装上传用的请求头。
///
/// `Overwrite` 只在明确要求「不覆盖」时才出现:服务端把缺失与任意非
/// `"false"` 的值都当作「允许覆盖」。`size` 是文件字节数:服务端在
/// `Content-Length` 缺失(chunked)时才读 `X-File-Size`,但既然知道大小就
/// 一并带上——请求因此自描述,也不受代理改写长度的影响。
///
/// `send_content_type=false` 给表单上传用:那时请求的 `Content-Type` 必须是
/// `multipart/form-data; boundary=…`,业务 MIME 只能挂在 multipart 分片上
/// (服务端读 `file.Header.Get("Content-Type")`),不能被这里覆盖掉。
fn put_headers(
path : String,
size : Int?,
overwrite? : Bool,
content_type? : String,
last_modified? : Int64,
hash? : FileHash,
send_content_type? : Bool,
) -> Array[(String, String)] {
let headers : Array[(String, String)] = [
("File-Path", @core.url_path_encode(path)),
]
match size {
Some(value) => headers.push(("X-File-Size", value.to_string()))
None => ()
}
if overwrite == Some(false) {
headers.push(("Overwrite", "false"))
}
if send_content_type != Some(false) {
match content_type {
Some(value) => headers.push(("Content-Type", value))
None => ()
}
}
match last_modified {
Some(value) => headers.push(("Last-Modified", value.to_string()))
None => ()
}
match hash {
Some(value) => {
match value.md5 {
Some(text) => headers.push(("X-File-Md5", text))
None => ()
}
match value.sha1 {
Some(text) => headers.push(("X-File-Sha1", text))
None => ()
}
match value.sha256 {
Some(text) => headers.push(("X-File-Sha256", text))
None => ()
}
}
None => ()
}
headers
}
///|
/// 从上传响应里读任务:`{"task": {...}}`。
///
/// `data` 是 `null` 时表示服务端已经同步传完(没有任务),返回 `None`。
fn decode_task(data : Json) -> @core.TaskInfo? raise @core.OpenListError {
match data {
Null => None
Object(fields) =>
match fields.get("task") {
Some(task) => Some(@core.decode_data(task))
None => None
}
_ => None
}
}
///|
/// 从归档/离线下载响应里读任务数组(键名是 `task` 或 `tasks`)。
///
/// 归档解压用的键是 `task`(值是数组),离线下载用的是 `tasks`,所以两个键
/// 都试一遍;都没有时给空数组。
fn decode_tasks(data : Json) -> Array[@core.TaskInfo] raise @core.OpenListError {
match data {
Object(fields) =>
match fields.get("tasks") {
Some(tasks) =>
match tasks {
Null => []
_ => @core.decode_data(tasks)
}
None =>
match fields.get("task") {
Some(task) =>
match task {
Null => []
_ => @core.decode_data(task)
}
None => []
}
}
_ => []
}
}
///|
/// 流式上传一个文件(`PUT /api/fs/put`)。
///
/// `reader` 是文件内容的读取流:大文件不要先读进内存,直接用
/// `@fs.File::open(...)` 之类的读到 `Reader` 传进来。`content_length` 给了就按
/// 定长发送(同时带上 `X-File-Size`),不给就按 `Transfer-Encoding: chunked`
/// 发送——那种情况下服务端只认 `X-File-Size`,所以流式上传务必给出大小。
///
/// 返回值:`as_task` 为真(且服务端接受了异步任务)时是刚创建的任务,否则是
/// `None`——**同步上传完毕**。要进度回调可以用
/// `OpenListClient::request` 自己拼配置,或后续版本再加。
pub async fn FileSystem::put(
self : FileSystem,
path : String,
reader : &@io.Reader,
content_length? : Int,
overwrite? : Bool,
as_task? : Bool,
content_type? : String,
last_modified? : Int64,
hash? : FileHash,
) -> @core.TaskInfo? raise @core.OpenListError {
let headers = put_headers(
path,
content_length,
overwrite?,
content_type?,
last_modified?,
hash?,
)
if as_task == Some(true) {
headers.push(("As-Task", "true"))
}
let data = self.client.send_stream(
@moonhttp.Method::Put,
"/api/fs/put",
reader,
content_length?,
headers~,
)
decode_task(data)
}
///|
/// 上传一块已经在内存里的字节(`PUT /api/fs/put` 的便捷版)。
///
/// 适合小文件;大文件请用 `put` 传流,避免把整个文件读进内存。
pub async fn FileSystem::put_bytes(
self : FileSystem,
path : String,
data : Bytes,
overwrite? : Bool,
as_task? : Bool,
content_type? : String,
last_modified? : Int64,
hash? : FileHash,
) -> @core.TaskInfo? raise @core.OpenListError {
self.put(
path,
@core.bytes_reader(data),
content_length=data.length(),
overwrite?,
as_task?,
content_type?,
last_modified?,
hash?,
)
}
///|
/// 用 `multipart/form-data` 上传(`PUT /api/fs/form`)。
///
/// 表单字段名固定是 `file`;`filename` 只用于让服务端猜 MIME 类型。
/// 除了请求体形态不同,头与语义都和 `put` 一样。
pub async fn FileSystem::put_form(
self : FileSystem,
path : String,
filename : String,
data : Bytes,
overwrite? : Bool,
as_task? : Bool,
content_type? : String,
last_modified? : Int64,
hash? : FileHash,
) -> @core.TaskInfo? raise @core.OpenListError {
let headers = put_headers(
path,
Some(data.length()),
overwrite?,
content_type?,
last_modified?,
hash?,
send_content_type=false,
)
if as_task == Some(true) {
headers.push(("As-Task", "true"))
}
let form = @moonhttp.FormData::new().append_file(
"file",
filename,
data,
content_type?,
)
let payload = self.client.send_form(
@moonhttp.Method::Put,
"/api/fs/form",
form,
headers~,
)
decode_task(payload)
}