1061 lines
32 KiB
Rust
1061 lines
32 KiB
Rust
//! Native Inventory API v3 capability client and cache reconciliation.
|
|
|
|
#![allow(clippy::missing_errors_doc)]
|
|
#![allow(clippy::needless_pass_by_value)]
|
|
#![allow(clippy::too_many_arguments)]
|
|
|
|
use crate::client_core::ClientWeakHandle;
|
|
use crate::{Error, GridClient, InventoryFolder, InventoryItem};
|
|
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
|
use libremetaverse_types::compat::{CancellationToken, HttpResponse, Uri};
|
|
use libremetaverse_types::{AssetType, UUID};
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
use std::sync::Arc;
|
|
|
|
const MAX_FOLDER_DEPTH_REQUEST: i32 = 50;
|
|
const MAX_AIS_OBJECTS: usize = 100_000;
|
|
const LLSD_XML: &str = "application/llsd+xml";
|
|
|
|
type InventoryResult = (
|
|
bool,
|
|
Vec<InventoryFolder>,
|
|
Vec<InventoryItem>,
|
|
Vec<InventoryItem>,
|
|
);
|
|
type ParsedInventory = (Vec<InventoryFolder>, Vec<InventoryItem>, Vec<InventoryItem>);
|
|
type CollectionSections<'a> = (Option<&'a HashMap<String, OSD>>, Option<&'a OSD>);
|
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
pub struct AISResponseMeta {
|
|
broken_links_removed: Vec<UUID>,
|
|
items_removed: Vec<UUID>,
|
|
categories_removed: Vec<UUID>,
|
|
category_version_updates: HashMap<UUID, i32>,
|
|
}
|
|
|
|
impl AISResponseMeta {
|
|
pub fn new(
|
|
broken_links: Vec<UUID>,
|
|
items: Vec<UUID>,
|
|
categories: Vec<UUID>,
|
|
versions: HashMap<UUID, i32>,
|
|
) -> Result<Self, Error> {
|
|
Ok(Self {
|
|
broken_links_removed: broken_links,
|
|
items_removed: items,
|
|
categories_removed: categories,
|
|
category_version_updates: versions,
|
|
})
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn empty() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn broken_links_removed(&self) -> Vec<UUID> {
|
|
self.broken_links_removed.clone()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn items_removed(&self) -> Vec<UUID> {
|
|
self.items_removed.clone()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn categories_removed(&self) -> Vec<UUID> {
|
|
self.categories_removed.clone()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn category_version_updates(&self) -> HashMap<UUID, i32> {
|
|
self.category_version_updates.clone()
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn has_any_data(&self) -> bool {
|
|
!self.broken_links_removed.is_empty()
|
|
|| !self.items_removed.is_empty()
|
|
|| !self.categories_removed.is_empty()
|
|
|| !self.category_version_updates.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct InventoryAISClient {
|
|
client: ClientWeakHandle,
|
|
}
|
|
|
|
impl InventoryAISClient {
|
|
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
|
|
let client = client.ok_or(Error::ArgumentNull)?;
|
|
Ok(Self {
|
|
client: client.native_weak_handle(),
|
|
})
|
|
}
|
|
|
|
fn client(&self) -> Result<GridClient, Error> {
|
|
self.client.upgrade().ok_or(Error::InvalidOperation)
|
|
}
|
|
|
|
fn capability(&self, name: &str) -> Result<Option<Uri>, Error> {
|
|
let client = self.client()?;
|
|
let Some(simulator) = client.native_network()?.current_sim() else {
|
|
return Ok(None);
|
|
};
|
|
let Some(caps) = simulator.native_caps() else {
|
|
return Ok(None);
|
|
};
|
|
caps.capability_uri(name.to_owned())
|
|
}
|
|
|
|
pub(crate) fn native_is_available(&self) -> bool {
|
|
self.capability(Self::INVENTORY_CAP_NAME)
|
|
.ok()
|
|
.flatten()
|
|
.is_some()
|
|
}
|
|
|
|
fn endpoint(capability: &Uri, path: &str) -> Uri {
|
|
Uri(format!("{}/{}", capability.0.trim_end_matches('/'), path))
|
|
}
|
|
|
|
fn valid_category(category: &str) -> bool {
|
|
!category.is_empty()
|
|
&& category
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
|
|
}
|
|
|
|
fn token(token: Option<CancellationToken>) -> CancellationToken {
|
|
token.unwrap_or_default()
|
|
}
|
|
|
|
fn status_ok(response: &HttpResponse) -> bool {
|
|
response.is_success_status_code() || response.status_code == 304
|
|
}
|
|
|
|
async fn request(
|
|
&self,
|
|
cap_name: &str,
|
|
method: &str,
|
|
path: String,
|
|
headers: BTreeMap<String, String>,
|
|
payload: Option<OSD>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<Option<(HttpResponse, Vec<u8>)>, Error> {
|
|
let token = Self::token(cancellation_token);
|
|
token.throw_if_cancellation_requested()?;
|
|
let Some(capability) = self.capability(cap_name)? else {
|
|
return Ok(None);
|
|
};
|
|
let bytes = match payload {
|
|
Some(payload) => OSDParser::serialize_llsd_xml_bytes(payload)?,
|
|
None => Vec::new(),
|
|
};
|
|
let content_type = (!bytes.is_empty()).then(|| LLSD_XML.to_owned());
|
|
self.client()?
|
|
.native_http_caps_client()
|
|
.send_custom(
|
|
method,
|
|
Self::endpoint(&capability, &path),
|
|
headers,
|
|
content_type,
|
|
bytes,
|
|
token,
|
|
)
|
|
.await
|
|
.map(Some)
|
|
}
|
|
|
|
fn decode_map(bytes: Vec<u8>) -> Result<Option<HashMap<String, OSD>>, Error> {
|
|
if bytes.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
match OSDParser::deserialize_with_bytes(bytes)? {
|
|
OSD::Map(map) => Ok(Some(map)),
|
|
_ => Err(Error::Argument),
|
|
}
|
|
}
|
|
|
|
fn parse_all_map(&self, map: &HashMap<String, OSD>) -> Result<ParsedInventory, Error> {
|
|
let response = OSD::Map(map.clone());
|
|
let folders = self.parse_folders_osd(&response)?;
|
|
let items = self.parse_items_osd(&response)?;
|
|
let links = self.parse_links_osd(&response)?;
|
|
if folders.len() + items.len() + links.len() > MAX_AIS_OBJECTS {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
Ok((folders, items, links))
|
|
}
|
|
|
|
fn reconcile(
|
|
&self,
|
|
map: &HashMap<String, OSD>,
|
|
folders: &[InventoryFolder],
|
|
items: &[InventoryItem],
|
|
links: &[InventoryItem],
|
|
) -> Result<(), Error> {
|
|
let meta = Self::parse_meta_map(map)?;
|
|
if folders.is_empty() && items.is_empty() && links.is_empty() && !meta.has_any_data() {
|
|
return Ok(());
|
|
}
|
|
self.client()?.native_inventory()?.native_reconcile_ais(
|
|
folders.to_vec(),
|
|
items.iter().chain(links).cloned().collect(),
|
|
&meta,
|
|
)
|
|
}
|
|
|
|
async fn mutation(
|
|
&self,
|
|
cap_name: &str,
|
|
method: &str,
|
|
path: String,
|
|
headers: BTreeMap<String, String>,
|
|
payload: Option<OSD>,
|
|
gone_is_success: bool,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<
|
|
(
|
|
bool,
|
|
Vec<InventoryFolder>,
|
|
Vec<InventoryItem>,
|
|
Vec<InventoryItem>,
|
|
),
|
|
Error,
|
|
> {
|
|
let Some((response, bytes)) = self
|
|
.request(cap_name, method, path, headers, payload, cancellation_token)
|
|
.await?
|
|
else {
|
|
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
|
|
};
|
|
let success =
|
|
Self::status_ok(&response) || (gone_is_success && response.status_code == 410);
|
|
if !success {
|
|
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
|
|
}
|
|
let Some(map) = Self::decode_map(bytes)? else {
|
|
return Ok((true, Vec::new(), Vec::new(), Vec::new()));
|
|
};
|
|
let (folders, items, links) = self.parse_all_map(&map)?;
|
|
self.reconcile(&map, &folders, &items, &links)?;
|
|
Ok((true, folders, items, links))
|
|
}
|
|
|
|
async fn mutation_bool(
|
|
&self,
|
|
cap_name: &str,
|
|
method: &str,
|
|
path: String,
|
|
headers: BTreeMap<String, String>,
|
|
payload: Option<OSD>,
|
|
gone_is_success: bool,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
match self
|
|
.mutation(
|
|
cap_name,
|
|
method,
|
|
path,
|
|
headers,
|
|
payload,
|
|
gone_is_success,
|
|
cancellation_token,
|
|
)
|
|
.await
|
|
{
|
|
Ok((success, _, _, _)) => Ok(success),
|
|
Err(Error::Cancelled) => Err(Error::Cancelled),
|
|
Err(_error) => {
|
|
#[cfg(test)]
|
|
eprintln!("AIS mutation failed: {_error:?}");
|
|
Ok(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn get_parsed(
|
|
&self,
|
|
cap_name: &str,
|
|
path: String,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<InventoryResult, Error> {
|
|
let result = async {
|
|
let Some((response, bytes)) = self
|
|
.request(
|
|
cap_name,
|
|
"GET",
|
|
path,
|
|
BTreeMap::new(),
|
|
None,
|
|
cancellation_token,
|
|
)
|
|
.await?
|
|
else {
|
|
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
|
|
};
|
|
if !Self::status_ok(&response) {
|
|
return Ok((false, Vec::new(), Vec::new(), Vec::new()));
|
|
}
|
|
let Some(map) = Self::decode_map(bytes)? else {
|
|
return Ok((true, Vec::new(), Vec::new(), Vec::new()));
|
|
};
|
|
let parsed = self.parse_all_map(&map)?;
|
|
self.reconcile(&map, &parsed.0, &parsed.1, &parsed.2)?;
|
|
Ok((true, parsed.0, parsed.1, parsed.2))
|
|
}
|
|
.await;
|
|
match result {
|
|
Err(Error::Cancelled) => Err(Error::Cancelled),
|
|
Err(_) => Ok((false, Vec::new(), Vec::new(), Vec::new())),
|
|
value => value,
|
|
}
|
|
}
|
|
|
|
fn tid() -> Result<UUID, Error> {
|
|
UUID::random()
|
|
}
|
|
|
|
fn destination(uuid: UUID) -> BTreeMap<String, String> {
|
|
BTreeMap::from([("Destination".to_owned(), uuid.to_string())])
|
|
}
|
|
|
|
pub(crate) async fn native_create_inventory(
|
|
&self,
|
|
parent_uuid: UUID,
|
|
new_inventory: OSD,
|
|
create_link: bool,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<(bool, Option<InventoryItem>), Error> {
|
|
let path = format!("category/{parent_uuid}?tid={}", Self::tid()?);
|
|
match self
|
|
.mutation(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"POST",
|
|
path,
|
|
BTreeMap::new(),
|
|
Some(new_inventory),
|
|
false,
|
|
cancellation_token,
|
|
)
|
|
.await
|
|
{
|
|
Ok((success, _, items, links)) => Ok((
|
|
success,
|
|
if create_link { links } else { items }.into_iter().next(),
|
|
)),
|
|
Err(Error::Cancelled) => Err(Error::Cancelled),
|
|
Err(_) => Ok((false, None)),
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)] // Internal mapped helper used by InventoryManager batch links.
|
|
pub(crate) async fn native_create_inventory_links(
|
|
&self,
|
|
parent_uuid: UUID,
|
|
new_inventory: OSD,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<Vec<InventoryItem>, Error> {
|
|
let path = format!("category/{parent_uuid}?tid={}", Self::tid()?);
|
|
let (_, _, _, links) = self
|
|
.mutation(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"POST",
|
|
path,
|
|
BTreeMap::new(),
|
|
Some(new_inventory),
|
|
false,
|
|
cancellation_token,
|
|
)
|
|
.await?;
|
|
Ok(links)
|
|
}
|
|
|
|
pub(crate) async fn native_slam_folder(
|
|
&self,
|
|
folder: UUID,
|
|
payload: OSD,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"PUT",
|
|
format!("category/{folder}/links?tid={}", Self::tid()?),
|
|
BTreeMap::new(),
|
|
Some(payload),
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_remove_category(
|
|
&self,
|
|
id: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"DELETE",
|
|
format!("category/{id}"),
|
|
BTreeMap::new(),
|
|
None,
|
|
true,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_remove_item(
|
|
&self,
|
|
id: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"DELETE",
|
|
format!("item/{id}"),
|
|
BTreeMap::new(),
|
|
None,
|
|
true,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_copy_library_category(
|
|
&self,
|
|
source: UUID,
|
|
destination: UUID,
|
|
subfolders: bool,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
let suffix = if subfolders {
|
|
String::new()
|
|
} else {
|
|
",depth=0".to_owned()
|
|
};
|
|
self.mutation_bool(
|
|
Self::LIBRARY_CAP_NAME,
|
|
"COPY",
|
|
format!("category/{source}?tid={}{}", Self::tid()?, suffix),
|
|
Self::destination(destination),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_purge_descendents(
|
|
&self,
|
|
id: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"DELETE",
|
|
format!("category/{id}/children"),
|
|
BTreeMap::new(),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_update_category(
|
|
&self,
|
|
id: UUID,
|
|
payload: OSD,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"PATCH",
|
|
format!("category/{id}"),
|
|
BTreeMap::new(),
|
|
Some(payload),
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_update_item(
|
|
&self,
|
|
id: UUID,
|
|
payload: OSD,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"PATCH",
|
|
format!("item/{id}"),
|
|
BTreeMap::new(),
|
|
Some(payload),
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_copy_category(
|
|
&self,
|
|
source: UUID,
|
|
destination: UUID,
|
|
simulate: Option<bool>,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
let simulate = if simulate.unwrap_or(false) {
|
|
"&simulate=1"
|
|
} else {
|
|
""
|
|
};
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"COPY",
|
|
format!("category/{source}?tid={}{}", Self::tid()?, simulate),
|
|
Self::destination(destination),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_move_category(
|
|
&self,
|
|
source: UUID,
|
|
destination: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.native_update_category(
|
|
source,
|
|
OSD::Map(HashMap::from([(
|
|
"parent_id".into(),
|
|
OSD::UUID(destination),
|
|
)])),
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_put_category_children(
|
|
&self,
|
|
category: String,
|
|
payload: OSD,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok(false);
|
|
}
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"PUT",
|
|
format!("category/{category}/children?tid={}", Self::tid()?),
|
|
BTreeMap::new(),
|
|
Some(payload),
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_copy_category_children(
|
|
&self,
|
|
category: String,
|
|
destination: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok(false);
|
|
}
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"COPY",
|
|
format!("category/{category}/children?tid={}", Self::tid()?),
|
|
Self::destination(destination),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
#[allow(clippy::unused_async)] // Fixed Task<bool> API must return an immediately ready future.
|
|
pub(crate) async fn native_move_category_children(
|
|
&self,
|
|
_category: String,
|
|
_destination: UUID,
|
|
_token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
Ok(false)
|
|
}
|
|
pub(crate) async fn native_delete_category_children(
|
|
&self,
|
|
category: String,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok(false);
|
|
}
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"DELETE",
|
|
format!("category/{category}/children"),
|
|
BTreeMap::new(),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_put_category_links(
|
|
&self,
|
|
category: String,
|
|
payload: OSD,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok(false);
|
|
}
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"PUT",
|
|
format!("category/{category}/links?tid={}", Self::tid()?),
|
|
BTreeMap::new(),
|
|
Some(payload),
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_copy_category_links(
|
|
&self,
|
|
category: String,
|
|
destination: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok(false);
|
|
}
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"COPY",
|
|
format!("category/{category}/links?tid={}", Self::tid()?),
|
|
Self::destination(destination),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
#[allow(clippy::unused_async)] // Fixed Task<bool> API must return an immediately ready future.
|
|
pub(crate) async fn native_move_category_links(
|
|
&self,
|
|
_category: String,
|
|
_destination: UUID,
|
|
_token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
Ok(false)
|
|
}
|
|
pub(crate) async fn native_delete_category_links(
|
|
&self,
|
|
category: String,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok(false);
|
|
}
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"DELETE",
|
|
format!("category/{category}/links"),
|
|
BTreeMap::new(),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_copy_item(
|
|
&self,
|
|
item: UUID,
|
|
destination: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.mutation_bool(
|
|
Self::INVENTORY_CAP_NAME,
|
|
"COPY",
|
|
format!("item/{item}?tid={}", Self::tid()?),
|
|
Self::destination(destination),
|
|
None,
|
|
false,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_move_item(
|
|
&self,
|
|
item: UUID,
|
|
destination: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.native_update_item(
|
|
item,
|
|
OSD::Map(HashMap::from([(
|
|
"parent_id".into(),
|
|
OSD::UUID(destination),
|
|
)])),
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub(crate) async fn native_get_category(
|
|
&self,
|
|
category: String,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<InventoryResult, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok((false, vec![], vec![], vec![]));
|
|
}
|
|
self.get_parsed(
|
|
Self::INVENTORY_CAP_NAME,
|
|
format!("category/{category}"),
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_get_category_children(
|
|
&self,
|
|
category: String,
|
|
depth: i32,
|
|
recursive: bool,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<InventoryResult, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok((false, vec![], vec![], vec![]));
|
|
}
|
|
let depth = if recursive {
|
|
MAX_FOLDER_DEPTH_REQUEST
|
|
} else {
|
|
depth.min(MAX_FOLDER_DEPTH_REQUEST)
|
|
};
|
|
self.get_parsed(
|
|
Self::INVENTORY_CAP_NAME,
|
|
format!("category/{category}/children?depth={depth}"),
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_fetch_category_categories(
|
|
&self,
|
|
id: UUID,
|
|
inventory: bool,
|
|
recursive: bool,
|
|
depth: Option<i32>,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<InventoryResult, Error> {
|
|
let depth = if recursive {
|
|
MAX_FOLDER_DEPTH_REQUEST
|
|
} else {
|
|
depth.unwrap_or(0).min(MAX_FOLDER_DEPTH_REQUEST)
|
|
};
|
|
let cap = if inventory {
|
|
Self::INVENTORY_CAP_NAME
|
|
} else {
|
|
Self::LIBRARY_CAP_NAME
|
|
};
|
|
self.get_parsed(
|
|
cap,
|
|
format!("category/{id}/categories?depth={depth}"),
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_fetch_category_subset(
|
|
&self,
|
|
id: UUID,
|
|
children: Box<dyn Iterator<Item = UUID>>,
|
|
inventory: bool,
|
|
recursive: bool,
|
|
depth: Option<i32>,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<InventoryResult, Error> {
|
|
let children: Vec<_> = children.take(MAX_AIS_OBJECTS).collect();
|
|
if children.is_empty() {
|
|
return Ok((false, vec![], vec![], vec![]));
|
|
}
|
|
let depth = if recursive {
|
|
MAX_FOLDER_DEPTH_REQUEST
|
|
} else {
|
|
depth.unwrap_or(0).min(MAX_FOLDER_DEPTH_REQUEST)
|
|
};
|
|
let list = children
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(",");
|
|
let cap = if inventory {
|
|
Self::INVENTORY_CAP_NAME
|
|
} else {
|
|
Self::LIBRARY_CAP_NAME
|
|
};
|
|
self.get_parsed(
|
|
cap,
|
|
format!("category/{id}/children?depth={depth}&children={list}"),
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_get_category_links(
|
|
&self,
|
|
category: String,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<InventoryResult, Error> {
|
|
if !Self::valid_category(&category) {
|
|
return Ok((false, vec![], vec![], vec![]));
|
|
}
|
|
self.get_parsed(
|
|
Self::INVENTORY_CAP_NAME,
|
|
format!("category/{category}/links"),
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
pub(crate) async fn native_fetch_item(
|
|
&self,
|
|
id: UUID,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<InventoryResult, Error> {
|
|
let result = self
|
|
.get_parsed(
|
|
Self::INVENTORY_CAP_NAME,
|
|
format!("item/{id}"),
|
|
token.clone(),
|
|
)
|
|
.await?;
|
|
if result.0 && result.1.is_empty() && result.2.is_empty() && result.3.is_empty() {
|
|
// `get_parsed` handles embedded shapes; retrying is intentionally avoided.
|
|
}
|
|
Ok(result)
|
|
}
|
|
pub(crate) async fn native_fetch_cof(
|
|
&self,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.native_get_category_links("current".into(), token)
|
|
.await
|
|
.map(|v| v.0)
|
|
}
|
|
pub(crate) async fn native_fetch_orphans(
|
|
&self,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.get_parsed(Self::INVENTORY_CAP_NAME, "orphans".into(), token)
|
|
.await
|
|
.map(|v| v.0)
|
|
}
|
|
pub(crate) async fn native_empty_trash(
|
|
&self,
|
|
token: Option<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
self.native_delete_category_children("trash".into(), token)
|
|
.await
|
|
}
|
|
|
|
pub(crate) fn native_parse_items_from_embedded(
|
|
&self,
|
|
response: Option<OSD>,
|
|
) -> Result<Vec<InventoryItem>, Error> {
|
|
response.map_or(Ok(Vec::new()), |response| self.parse_items_osd(&response))
|
|
}
|
|
pub(crate) fn native_parse_folders_from_embedded(
|
|
&self,
|
|
response: OSDMap,
|
|
) -> Result<Vec<InventoryFolder>, Error> {
|
|
self.parse_folders_osd(&OSD::Map(response.snapshot()))
|
|
}
|
|
pub(crate) fn native_parse_links_from_embedded(
|
|
&self,
|
|
response: Option<OSD>,
|
|
) -> Result<Vec<InventoryItem>, Error> {
|
|
response.map_or(Ok(Vec::new()), |response| self.parse_links_osd(&response))
|
|
}
|
|
pub(crate) fn native_parse_embedded(
|
|
&self,
|
|
response: OSD,
|
|
folders: &mut Option<Vec<InventoryFolder>>,
|
|
items: &mut Option<Vec<InventoryItem>>,
|
|
links: &mut Option<Vec<InventoryItem>>,
|
|
) -> Result<(), Error> {
|
|
let parsed = match response {
|
|
OSD::Map(map) => self.parse_all_map(&map)?,
|
|
_ => return Err(Error::Argument),
|
|
};
|
|
*folders = Some(parsed.0);
|
|
*items = Some(parsed.1);
|
|
*links = Some(parsed.2);
|
|
Ok(())
|
|
}
|
|
|
|
fn collection_map<'a>(
|
|
response: &'a OSD,
|
|
plural: &str,
|
|
singular: &str,
|
|
) -> Option<CollectionSections<'a>> {
|
|
let OSD::Map(root) = response else {
|
|
return None;
|
|
};
|
|
let container = match root.get("_embedded") {
|
|
Some(OSD::Map(map)) => map,
|
|
_ => root,
|
|
};
|
|
let collection = match container.get(plural) {
|
|
Some(OSD::Map(map)) => Some(map),
|
|
_ => None,
|
|
};
|
|
Some((collection, container.get(singular)))
|
|
}
|
|
|
|
#[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
|
|
fn parse_items_osd(&self, response: &OSD) -> Result<Vec<InventoryItem>, Error> {
|
|
let Some((collection, singular)) = Self::collection_map(response, "items", "item") else {
|
|
return Ok(Vec::new());
|
|
};
|
|
let mut result = Vec::new();
|
|
if let Some(collection) = collection {
|
|
for value in collection.values() {
|
|
if let OSD::Map(map) = value {
|
|
result.push(InventoryItem::from_osd(OSD::Map(map.clone()))?);
|
|
}
|
|
}
|
|
}
|
|
if let Some(OSD::Map(map)) = singular
|
|
&& map.contains_key("item_id")
|
|
{
|
|
result.push(InventoryItem::from_osd(OSD::Map(map.clone()))?);
|
|
}
|
|
// GET /item/{id} returns the item itself at the top level.
|
|
if let OSD::Map(map) = response
|
|
&& map.contains_key("item_id")
|
|
{
|
|
result.push(InventoryItem::from_osd(response.clone())?);
|
|
}
|
|
Self::dedup_items(result)
|
|
}
|
|
|
|
#[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
|
|
fn parse_folders_osd(&self, response: &OSD) -> Result<Vec<InventoryFolder>, Error> {
|
|
let Some((collection, singular)) = Self::collection_map(response, "categories", "category")
|
|
else {
|
|
return Ok(Vec::new());
|
|
};
|
|
let mut result = Vec::new();
|
|
if let Some(collection) = collection {
|
|
for value in collection.values() {
|
|
if let OSD::Map(map) = value {
|
|
result.push(InventoryFolder::from_osd(OSD::Map(map.clone()))?);
|
|
}
|
|
}
|
|
}
|
|
if let Some(OSD::Map(map)) = singular
|
|
&& map.contains_key("category_id")
|
|
{
|
|
result.push(InventoryFolder::from_osd(OSD::Map(map.clone()))?);
|
|
}
|
|
Self::dedup_folders(result)
|
|
}
|
|
|
|
#[allow(clippy::unused_self)] // Parser belongs to the fixed instance API.
|
|
fn parse_links_osd(&self, response: &OSD) -> Result<Vec<InventoryItem>, Error> {
|
|
let Some((collection, _)) = Self::collection_map(response, "links", "link") else {
|
|
return Ok(Vec::new());
|
|
};
|
|
let mut result = Vec::new();
|
|
if let Some(collection) = collection {
|
|
for value in collection.values() {
|
|
let OSD::Map(map) = value else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let mut item = InventoryItem::from_osd(OSD::Map(map.clone()))?;
|
|
item.set_asset_type(AssetType::Link);
|
|
if let Some(agent) = map.get("agent_id") {
|
|
let id = agent.as_uuid()?;
|
|
item.base.set_owner_id(id);
|
|
item.set_creator_id(id);
|
|
item.set_last_owner_id(id);
|
|
}
|
|
result.push(item);
|
|
}
|
|
}
|
|
Self::dedup_items(result)
|
|
}
|
|
|
|
fn dedup_items(items: Vec<InventoryItem>) -> Result<Vec<InventoryItem>, Error> {
|
|
let mut ids = HashSet::new();
|
|
if items
|
|
.iter()
|
|
.any(|item| item.base.uuid() == UUID::zero() || !ids.insert(item.base.uuid()))
|
|
{
|
|
return Err(Error::Argument);
|
|
}
|
|
Ok(items)
|
|
}
|
|
fn dedup_folders(folders: Vec<InventoryFolder>) -> Result<Vec<InventoryFolder>, Error> {
|
|
let mut ids = HashSet::new();
|
|
if folders
|
|
.iter()
|
|
.any(|folder| folder.base.uuid() == UUID::zero() || !ids.insert(folder.base.uuid()))
|
|
{
|
|
return Err(Error::Argument);
|
|
}
|
|
Ok(folders)
|
|
}
|
|
|
|
pub(crate) fn native_parse_ais_response_meta(
|
|
response: Option<OSDMap>,
|
|
) -> Result<AISResponseMeta, Error> {
|
|
response.map_or_else(
|
|
|| Ok(AISResponseMeta::empty()),
|
|
|map| Self::parse_meta_map(&map.snapshot()),
|
|
)
|
|
}
|
|
fn parse_meta_map(map: &HashMap<String, OSD>) -> Result<AISResponseMeta, Error> {
|
|
let broken = Self::uuid_list(map.get("_broken_links_removed"))?;
|
|
let mut removed = Self::uuid_list(map.get("_removed_items"))?;
|
|
removed.extend(Self::uuid_list(map.get("_category_items_removed"))?);
|
|
let categories = Self::uuid_list(map.get("_categories_removed"))?;
|
|
let mut versions = HashMap::new();
|
|
if let Some(OSD::Map(values)) = map.get("_updated_category_versions") {
|
|
for (key, value) in values {
|
|
if let Ok(uuid) = UUID::parse(key.clone())
|
|
&& uuid != UUID::zero()
|
|
{
|
|
versions.insert(uuid, value.as_integer()?);
|
|
}
|
|
}
|
|
}
|
|
AISResponseMeta::new(broken, removed, categories, versions)
|
|
}
|
|
fn uuid_list(value: Option<&OSD>) -> Result<Vec<UUID>, Error> {
|
|
let mut values = Vec::new();
|
|
match value {
|
|
None => {}
|
|
Some(OSD::Array(array)) => {
|
|
for value in array {
|
|
let uuid = value.as_uuid()?;
|
|
if uuid != UUID::zero() {
|
|
values.push(uuid);
|
|
}
|
|
}
|
|
}
|
|
Some(OSD::Map(map)) => {
|
|
for key in map.keys() {
|
|
if let Ok(uuid) = UUID::parse(key.clone())
|
|
&& uuid != UUID::zero()
|
|
{
|
|
values.push(uuid);
|
|
}
|
|
}
|
|
}
|
|
Some(_) => return Err(Error::Argument),
|
|
}
|
|
Ok(values)
|
|
}
|
|
}
|