///|
/// 六个域子模块共享的 HTTP 客户端与认证状态。
///
/// 由根包的 `create_open_list_client` 创建,域子模块各持有它的一份拷贝。
/// token 存在共享的 `@ref.Ref` 里,因此各拷贝看到的是同一份登录状态:
/// 认证模块登录成功后,文件系统模块立刻就能用上新 token。
///
/// OpenList 的认证头形如 `Authorization: `,**没有** `Bearer` 前缀。
pub struct Client {
priv settings : Settings
priv credentials : Credentials
priv token : @ref.Ref[String?]
priv http : @moonhttp.Client
}
///|
/// `POST /api/auth/login` 的请求体。`otp_code` 为 `None` 时整个字段不会出现在
/// JSON 里(OpenList 只在字段非空时校验两步验证码)。
priv struct LoginRequest {
username : String
password : String
otp_code : String?
} derive(ToJson)
///|
/// 登录接口 `data` 的形状:`{"token": "..."}`。
priv struct TokenResponse {
token : String
} derive(@json.FromJson)
///|
/// 创建共享客户端。`transport` 只在测试里注入 Mock 传输层,生产不传。
pub fn Client::new(
settings : Settings,
credentials : Credentials,
transport? : &@moonhttp.Transport,
) -> Client {
let base_url = settings.base_url
.trim_end()
.to_owned()
.trim_end(chars="/")
.to_owned()
// 关掉 moonhttp 的「非 2xx 即抛错」校验:OpenList 把业务结果放在信封的
// `code` 里,HTTP 状态码只是粗粒度映射,必须让响应体走到我们手上再判断。
let defaults = @moonhttp.Config::default()
.with_base_url(base_url)
.with_validate_status(_ => true)
let defaults = match settings.timeout {
Some(milliseconds) => defaults.with_timeout(milliseconds)
None => defaults
}
let http = match transport {
Some(transport) => @moonhttp.Client::new(config=defaults, transport~)
None => @moonhttp.Client::new(config=defaults)
}
let token = match credentials {
// 空 token 表示匿名:公开端点用它去掉 `Authorization` 头。
Token(value) => if value == "" { None } else { Some(value) }
Password(_) => None
}
{ settings, credentials, token: @ref.new(token), http, }
}
///|
/// 连接设置。
pub fn Client::settings(self : Client) -> Settings {
self.settings
}
///|
/// 当前使用的认证凭证。
pub fn Client::credentials(self : Client) -> Credentials {
self.credentials
}
///|
/// 当前 token;尚未登录(账号密码凭证且还没登录过)时是 `None`。
pub fn Client::token(self : Client) -> String? {
self.token.val
}
///|
/// 直接覆盖 token,用于从外部持久化状态恢复会话,或主动让 token 失效。
pub fn Client::set_token(self : Client, token : String?) -> Unit {
self.token.val = token
}
///|
/// 是否已有可用 token(永久 token,或账号密码换来的限时 token)。
pub fn Client::is_logged_in(self : Client) -> Bool {
self.token.val is Some(_)
}
///|
/// 账号密码凭证才可能在 401 后重登;永久 token 过期只能换 token。
fn Client::can_relogin(self : Client) -> Bool {
match self.credentials {
Password(_) => true
Token(_) => false
}
}
///|
/// 拿到一个可用 token:永久 token 直接返回;账号密码则在首次使用时登录换取。
///
/// 已知限制:多个请求同时首次触发登录时,可能各发一次登录请求(服务端会
/// 各自签发 token,最后一次写入生效),不会造成状态错乱。
pub async fn Client::ensure_token(self : Client) -> String raise OpenListError {
match self.token.val {
Some(value) => value
None =>
match self.credentials {
Token(value) =>
if value == "" {
// 匿名凭证:不带凭证直接请求。
value
} else {
self.token.val = Some(value)
value
}
Password(credentials) => {
let value = self.password_login(credentials)
self.token.val = Some(value)
value
}
}
}
}
///|
/// 用账号密码换限时 token(`POST /api/auth/login`)。
///
/// 这是**唯一**会主动登录的地方,它不经过 `ensure_token`,所以不会递归。
/// 常见的失败是 401(用户名或密码错误)与 402(两步验证码错误)。
pub async fn Client::password_login(
self : Client,
credentials : PasswordCredentials,
) -> String raise OpenListError {
let request = LoginRequest::{
username: credentials.username,
password: credentials.password,
otp_code: credentials.otp_code,
}
let (status, envelope) = self.send_envelope(
@moonhttp.Method::Post,
"/api/auth/login",
body=@json.to_json(request),
)
envelope.check(status)
let response : TokenResponse = decode_data(envelope.data_or_null())
response.token
}
///|
/// 给配置挂上认证头与调用方自定义头,发出去,再把响应文本解析成信封。
///
/// 调用方自定义头后写,因此可以覆盖 `Authorization`。
async fn Client::execute(
self : Client,
config : @moonhttp.Config,
headers? : Array[(String, String)],
) -> (Int, Envelope) raise OpenListError {
let mut config = config
// 空 token 等于「匿名」:不带认证头。这样 `Credentials::Token("")` 就能
// 拿来访问 `/api/public/*` 与可选认证的 `/api/fs/*`(服务端按游客处理)。
match self.token.val {
Some(token) =>
if token != "" {
config = config.with_header("Authorization", token)
}
None => ()
}
match headers {
Some(pairs) =>
for pair in pairs {
let (key, value) = pair
config = config.with_header(key, value)
}
None => ()
}
let response = self.http.request(config) catch {
error => raise OpenListError::Http(error)
}
(response.status, Envelope::parse(response.status, response.text()))
}
///|
/// 普通 JSON 请求:拼配置后交给 `execute`。它不触发登录、也不检查 `code`,
/// 由调用方决定怎么处理信封(`request` 与各域子模块都走它)。
async fn Client::send_envelope(
self : Client,
http_method : @moonhttp.Method,
path : String,
query? : Json,
body? : Json,
headers? : Array[(String, String)],
) -> (Int, Envelope) raise OpenListError {
let mut config = @moonhttp.Config::new(path)
.with_method(http_method)
.with_validate_status(_ => true)
match query {
Some(params) => config = config.with_params(params)
None => ()
}
match body {
Some(payload) => config = config.with_data_from_json(payload)
None => ()
}
self.execute(config, headers?)
}
///|
/// 发送一个 JSON 请求,返回信封里的 `data`(原样,不做 `null` 剔除)。
///
/// 这是六个域子模块的统一出口,也是留给使用者的逃生通道:官方客户端没有
/// 封装的端点可以直接用它打到 `/api/...`(见 `docs/08-unsupported-endpoints.md`)。
///
/// 认证自动完成:收到 401 且凭证是账号密码时,会清掉旧 token 重登一次再
/// 重试一次;用永久 token 时直接报错(重试也没有意义)。
pub async fn Client::request(
self : Client,
http_method : @moonhttp.Method,
path : String,
query? : Json,
body? : Json,
headers? : Array[(String, String)],
) -> Json raise OpenListError {
let _ = self.ensure_token()
let (status, envelope) = self.send_envelope(
http_method,
path,
query?,
body?,
headers?,
)
if envelope.code == 200 {
return envelope.data_or_null()
}
if envelope.code == 401 && self.can_relogin() {
self.token.val = None
let _ = self.ensure_token()
let (retry_status, retry_envelope) = self.send_envelope(
http_method,
path,
query?,
body?,
headers?,
)
retry_envelope.check(retry_status)
return retry_envelope.data_or_null()
}
envelope.check(status)
envelope.data_or_null()
}
///|
/// 发送请求体来自流的请求(二进制上传)。
///
/// 用于 `/api/fs/put`、`/api/fs/multipart/chunk` 这类「body 就是原始字节」的
/// 端点——moonhttp 的请求体构造器没有裸字节那一档,二进制只能走流。
/// 与 `request` 一样检查信封,但不做 401 重试。
pub async fn Client::send_stream(
self : Client,
http_method : @moonhttp.Method,
path : String,
reader : &@io.Reader,
content_length? : Int,
headers? : Array[(String, String)],
) -> Json raise OpenListError {
let _ = self.ensure_token()
let mut config = @moonhttp.Config::new(path)
.with_method(http_method)
.with_validate_status(_ => true)
config = match content_length {
Some(length) => config.with_data_from_stream(reader, content_length=length)
None => config.with_data_from_stream(reader)
}
let (status, envelope) = self.execute(config, headers?)
envelope.check(status)
envelope.data_or_null()
}
///|
/// 发送 multipart/form-data 请求(`PUT /api/fs/form`)。
pub async fn Client::send_form(
self : Client,
http_method : @moonhttp.Method,
path : String,
form : @moonhttp.FormData,
headers? : Array[(String, String)],
) -> Json raise OpenListError {
let _ = self.ensure_token()
let config = @moonhttp.Config::new(path)
.with_method(http_method)
.with_validate_status(_ => true)
.with_data_from_form(form)
let (status, envelope) = self.execute(config, headers?)
envelope.check(status)
envelope.data_or_null()
}