chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user