363 lines
14 KiB
Rust
363 lines
14 KiB
Rust
use super::compat_support::apply_bt_select_file_option;
|
|
use super::{
|
|
CoreError, DownloadEngine, DownloadId, DownloadStatus, InProcessRpcDispatcher, JsonRpcRequest,
|
|
JsonRpcResponse, QueuePositionMode, RpcError, RpcValue, SaveSessionTarget,
|
|
first_forbidden_change_global_option_key, first_forbidden_change_option_key, i64_from_usize,
|
|
is_rpc_uri_candidate, parse_optional_position, parse_required_file_index,
|
|
parse_uri_array_allow_empty, state_transition_rpc_error,
|
|
};
|
|
|
|
impl InProcessRpcDispatcher {
|
|
/// Handles `aria2.changeGlobalOption` after rejecting unsupported dynamic keys.
|
|
pub(super) fn handle_change_global_option(
|
|
&mut self,
|
|
request: JsonRpcRequest,
|
|
) -> JsonRpcResponse {
|
|
let Some(RpcValue::Object(map)) = request.params.first() else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.changeGlobalOption needs option object"),
|
|
);
|
|
};
|
|
if let Some(option) = first_forbidden_change_global_option_key(map) {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params(&format!(
|
|
"aria2.changeGlobalOption does not allow dynamic updates for option: {option}"
|
|
)),
|
|
);
|
|
}
|
|
let patch = self.rpc_object_to_patch(map.clone());
|
|
self.engine.apply_options(patch);
|
|
JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned()))
|
|
}
|
|
|
|
/// Handles `aria2.changeOption` by applying validated per-download option patches.
|
|
pub(super) fn handle_change_option(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
let gid = match self.parse_gid_from_first_param(&request, "aria2.changeOption") {
|
|
Ok(gid) => gid,
|
|
Err(error) => return JsonRpcResponse::error(request.id, error),
|
|
};
|
|
let Some(RpcValue::Object(map)) = request.params.get(1) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.changeOption needs option object"),
|
|
);
|
|
};
|
|
if let Some(option) = first_forbidden_change_option_key(map) {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params(&format!(
|
|
"aria2.changeOption does not allow dynamic updates for option: {option}"
|
|
)),
|
|
);
|
|
}
|
|
let patch = self.rpc_object_to_patch(map.clone());
|
|
let select_file_option = map
|
|
.get("select-file")
|
|
.map(|value| self.option_value_text(&self.rpc_value_to_option_value(value.clone())));
|
|
match self.engine.handle_mut(gid) {
|
|
Some(group) => {
|
|
if let Some(select_file_value) = select_file_option
|
|
&& let Err(message) = apply_bt_select_file_option(group, &select_file_value)
|
|
{
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params(&format!("invalid select-file option: {message}")),
|
|
);
|
|
}
|
|
group.options_mut().merge(patch);
|
|
JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned()))
|
|
}
|
|
None => JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::unsupported(&format!("Cannot change option for GID#{gid}")),
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Handles `aria2.changePosition` by moving waiting downloads within the queue.
|
|
pub(super) fn handle_change_position(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
let gid = match self.parse_gid_from_first_param(&request, "aria2.changePosition") {
|
|
Ok(gid) => gid,
|
|
Err(error) => return JsonRpcResponse::error(request.id, error),
|
|
};
|
|
let Some(position) = request.params.get(1).and_then(|value| match value {
|
|
RpcValue::Number(value) => Some(*value),
|
|
_ => None,
|
|
}) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.changePosition needs position"),
|
|
);
|
|
};
|
|
let Some(mode_text) = request.params.get(2).and_then(|value| match value {
|
|
RpcValue::String(value) => Some(value.as_str()),
|
|
_ => None,
|
|
}) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.changePosition needs mode"),
|
|
);
|
|
};
|
|
let mode = match mode_text {
|
|
"POS_SET" => QueuePositionMode::Set,
|
|
"POS_CUR" => QueuePositionMode::Cur,
|
|
"POS_END" => QueuePositionMode::End,
|
|
_ => {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("Illegal argument."),
|
|
);
|
|
}
|
|
};
|
|
match self.engine.change_position(gid, position, mode) {
|
|
Ok(dest) => {
|
|
JsonRpcResponse::success(request.id, RpcValue::Number(i64_from_usize(dest)))
|
|
}
|
|
Err(_) => JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::unsupported(&format!("GID#{gid} not found in the waiting queue.")),
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Handles `aria2.changeUri` by removing and inserting source URIs for a download.
|
|
pub(super) fn handle_change_uri(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
let gid = match self.parse_gid_from_first_param(&request, "aria2.changeUri") {
|
|
Ok(gid) => gid,
|
|
Err(error) => return JsonRpcResponse::error(request.id, error),
|
|
};
|
|
let Some(file_index) = request.params.get(1).and_then(parse_required_file_index) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.changeUri needs fileIndex"),
|
|
);
|
|
};
|
|
let Some(del_uris_value) = request.params.get(2) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.changeUri needs delUris"),
|
|
);
|
|
};
|
|
let del_uris = match parse_uri_array_allow_empty(del_uris_value, "aria2.changeUri delUris")
|
|
{
|
|
Ok(uris) => uris,
|
|
Err(error) => {
|
|
return JsonRpcResponse::error(request.id, RpcError::invalid_params(&error));
|
|
}
|
|
};
|
|
let Some(add_uris_value) = request.params.get(3) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.changeUri needs addUris"),
|
|
);
|
|
};
|
|
let add_uris = match parse_uri_array_allow_empty(add_uris_value, "aria2.changeUri addUris")
|
|
{
|
|
Ok(uris) => uris,
|
|
Err(error) => {
|
|
return JsonRpcResponse::error(request.id, RpcError::invalid_params(&error));
|
|
}
|
|
};
|
|
let position = match parse_optional_position(request.params.get(4), "aria2.changeUri") {
|
|
Ok(position) => position,
|
|
Err(error) => {
|
|
return JsonRpcResponse::error(request.id, RpcError::invalid_params(&error));
|
|
}
|
|
};
|
|
if file_index != 1 {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::unsupported("fileIndex is out of range"),
|
|
);
|
|
}
|
|
let Some(group) = self.engine.handle_mut(gid) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::unsupported(&format!("Cannot remove URIs from GID#{gid}")),
|
|
);
|
|
};
|
|
let mut deleted = 0_i64;
|
|
for uri in del_uris {
|
|
if group.context_mut().remove_first_matching_uri(&uri) {
|
|
deleted += 1;
|
|
}
|
|
}
|
|
let mut inserted = 0_i64;
|
|
if let Some(mut position) = position {
|
|
for uri in add_uris {
|
|
if !is_rpc_uri_candidate(&uri) {
|
|
continue;
|
|
}
|
|
group.context_mut().insert_uri(position, uri);
|
|
position += 1;
|
|
inserted += 1;
|
|
}
|
|
} else {
|
|
for uri in add_uris {
|
|
if !is_rpc_uri_candidate(&uri) {
|
|
continue;
|
|
}
|
|
group.context_mut().append_uri(uri);
|
|
inserted += 1;
|
|
}
|
|
}
|
|
JsonRpcResponse::success(
|
|
request.id,
|
|
RpcValue::Array(vec![RpcValue::Number(deleted), RpcValue::Number(inserted)]),
|
|
)
|
|
}
|
|
|
|
/// Handles `aria2.purgeDownloadResult` by removing all stopped download results.
|
|
pub(super) fn handle_purge_download_result(
|
|
&mut self,
|
|
request: JsonRpcRequest,
|
|
) -> JsonRpcResponse {
|
|
self.engine.purge_download_results();
|
|
JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned()))
|
|
}
|
|
|
|
/// Handles `aria2.removeDownloadResult` for a single stopped download result.
|
|
pub(super) fn handle_remove_download_result(
|
|
&mut self,
|
|
request: JsonRpcRequest,
|
|
) -> JsonRpcResponse {
|
|
let Some(gid_text) = request.params.first().and_then(|value| match value {
|
|
RpcValue::String(gid) => Some(gid.clone()),
|
|
_ => None,
|
|
}) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::invalid_params("aria2.removeDownloadResult needs gid"),
|
|
);
|
|
};
|
|
let Some(gid) = DownloadId::parse_hex(&gid_text) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::unsupported(&format!("Invalid GID {gid_text}")),
|
|
);
|
|
};
|
|
match self.engine.remove_download_result(gid) {
|
|
Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())),
|
|
Err(_) => JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError {
|
|
code: crate::model::RpcErrorCode::ApplicationError,
|
|
kind: crate::model::RpcErrorKind::Internal,
|
|
message: format!("Could not remove download result of GID#{gid_text}"),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Handles `aria2.saveSession` by writing the runtime session target when configured.
|
|
pub(super) fn handle_save_session(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
let target = self
|
|
.engine
|
|
.session()
|
|
.session_file()
|
|
.cloned()
|
|
.map(SaveSessionTarget::Path)
|
|
.unwrap_or(SaveSessionTarget::Memory);
|
|
match self.engine.save_session(target) {
|
|
Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())),
|
|
Err(error) => {
|
|
JsonRpcResponse::error(request.id, RpcError::unsupported(&error.to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handles graceful shutdown requests with the aria2 success sentinel.
|
|
pub(super) fn handle_shutdown(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
match self.engine.shutdown() {
|
|
Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())),
|
|
Err(error) => {
|
|
JsonRpcResponse::error(request.id, RpcError::unsupported(&error.to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handles forced shutdown requests with the aria2 success sentinel.
|
|
pub(super) fn handle_force_shutdown(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
match self.engine.force_shutdown() {
|
|
Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned())),
|
|
Err(error) => {
|
|
JsonRpcResponse::error(request.id, RpcError::unsupported(&error.to_string()))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handles pause-all variants by pausing eligible active or waiting downloads.
|
|
pub(super) fn handle_pause_all(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
let gids: Vec<_> = self
|
|
.engine
|
|
.registry()
|
|
.handles()
|
|
.filter(|handle| {
|
|
self.engine
|
|
.registry()
|
|
.get(handle.gid())
|
|
.is_some_and(|group| {
|
|
matches!(
|
|
group.status(),
|
|
DownloadStatus::Active | DownloadStatus::Waiting
|
|
)
|
|
})
|
|
})
|
|
.map(|handle| handle.gid())
|
|
.collect();
|
|
for gid in gids {
|
|
let _ = self.engine.pause(gid);
|
|
}
|
|
JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned()))
|
|
}
|
|
|
|
/// Handles `aria2.unpauseAll` by resuming eligible paused downloads.
|
|
pub(super) fn handle_unpause_all(&mut self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
let gids: Vec<_> = self
|
|
.engine
|
|
.registry()
|
|
.handles()
|
|
.filter(|handle| {
|
|
self.engine
|
|
.registry()
|
|
.get(handle.gid())
|
|
.is_some_and(|group| group.status() == &DownloadStatus::Paused)
|
|
})
|
|
.map(|handle| handle.gid())
|
|
.collect();
|
|
for gid in gids {
|
|
let _ = self.engine.resume(gid);
|
|
}
|
|
JsonRpcResponse::success(request.id, RpcValue::String("OK".to_owned()))
|
|
}
|
|
|
|
/// Handles single-download pause, resume, and remove state transitions.
|
|
pub(super) fn handle_state_transition<F>(
|
|
&mut self,
|
|
request: JsonRpcRequest,
|
|
method: &'static str,
|
|
mut apply: F,
|
|
) -> JsonRpcResponse
|
|
where
|
|
F: FnMut(&mut DownloadEngine, DownloadId) -> Result<(), CoreError>,
|
|
{
|
|
let Some(RpcValue::String(gid)) = request.params.first() else {
|
|
return JsonRpcResponse::error(request.id, RpcError::invalid_params(method));
|
|
};
|
|
let Some(gid) = DownloadId::parse_hex(gid) else {
|
|
return JsonRpcResponse::error(
|
|
request.id,
|
|
RpcError::unsupported(&format!("Invalid GID {gid}")),
|
|
);
|
|
};
|
|
match apply(&mut self.engine, gid) {
|
|
Ok(()) => JsonRpcResponse::success(request.id, RpcValue::String(gid.to_string())),
|
|
Err(error) => {
|
|
JsonRpcResponse::error(request.id, state_transition_rpc_error(method, gid, &error))
|
|
}
|
|
}
|
|
}
|
|
}
|