Files
SpotiFLAC-Mobile/rust_backend/crates/mobile/src/extensions.rs
T

277 lines
8.3 KiB
Rust

use crate::cancellation::RequestLease;
use spotiflac_extensions::environment::{
EnvironmentError, ExtensionEnvironment as CoreEnvironment,
};
use spotiflac_extensions::{ExtensionError, ExtensionRuntime, RuntimeLimits};
use std::path::Path;
use std::sync::{Arc, Mutex};
/// Keeps operation directories accessible until native finalization finishes.
/// Explicit close and object destruction both release only these grants.
#[derive(uniffi::Object)]
pub struct DownloadDirectoryScope {
grants: Mutex<Option<Vec<spotiflac_extensions::files::TemporaryGrant>>>,
}
#[uniffi::export]
impl DownloadDirectoryScope {
pub fn release(&self) {
self.grants
.lock()
.expect("download directory scope lock")
.take();
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum JsExtensionError {
#[error("extension runtime closed")]
Closed,
#[error("extension runtime busy")]
Busy,
#[error("execution timeout exceeded")]
Timeout,
#[error("{0}")]
Cancelled(String),
#[error("extension did not call registerExtension()")]
NotRegistered,
#[error("extension function not found: {0}")]
MissingFunction(String),
#[error("invalid extension input: {0}")]
InvalidInput(String),
#[error("{0}")]
Script(String),
#[error("extension environment closed")]
EnvironmentClosed,
#[error("{0}")]
Storage(String),
#[error("{0}")]
Manifest(String),
#[error("{0}")]
Gate(String),
#[error("{0}")]
Network(String),
#[error("{0}")]
Auth(String),
#[error("{0}")]
Index(String),
}
impl From<EnvironmentError> for JsExtensionError {
fn from(error: EnvironmentError) -> Self {
match error {
EnvironmentError::Closed => Self::EnvironmentClosed,
EnvironmentError::Runtime(error) => error.into(),
EnvironmentError::Storage(error) => Self::Storage(error.to_string()),
EnvironmentError::Manifest(error) => Self::Manifest(error.to_string()),
EnvironmentError::Gate(message) => Self::Gate(message),
EnvironmentError::Network(error) => Self::Network(error.to_string()),
EnvironmentError::Auth(error) => Self::Auth(error),
EnvironmentError::Index(error) => Self::Index(error),
}
}
}
impl From<ExtensionError> for JsExtensionError {
fn from(error: ExtensionError) -> Self {
match error {
ExtensionError::Closed => Self::Closed,
ExtensionError::Busy => Self::Busy,
ExtensionError::Timeout => Self::Timeout,
ExtensionError::Cancelled(error) => Self::Cancelled(error.to_string()),
ExtensionError::NotRegistered => Self::NotRegistered,
ExtensionError::MissingFunction(method) => Self::MissingFunction(method),
ExtensionError::InvalidInput(message) => Self::InvalidInput(message),
ExtensionError::Script(message) => Self::Script(message),
}
}
}
/// Migration runtime. Calls must run off the UI thread. The complete extension
/// manager and network/file/auth hosts are not yet wired into the mobile app.
#[derive(uniffi::Object)]
pub struct JsExtension {
inner: Arc<ExtensionRuntime>,
}
#[uniffi::export]
impl JsExtension {
pub fn download_state(&self) -> Arc<crate::progress::DownloadState> {
Arc::new(crate::progress::DownloadState {
inner: self.inner.download_state(),
})
}
#[uniffi::constructor]
pub fn new(
source: String,
settings_json: String,
timeout_ms: u64,
) -> Result<Self, JsExtensionError> {
let limits = RuntimeLimits {
timeout_ms: if timeout_ms == 0 { 30_000 } else { timeout_ms },
..RuntimeLimits::default()
};
Ok(Self {
inner: Arc::new(ExtensionRuntime::load(&source, &settings_json, limits)?),
})
}
pub fn call(
&self,
method: String,
arguments_json: String,
lease: Option<Arc<RequestLease>>,
timeout_ms: u64,
) -> Result<String, JsExtensionError> {
let lease = lease.map(|lease| Arc::clone(&lease.inner));
self.inner
.call(&method, &arguments_json, lease, timeout_ms)
.map_err(Into::into)
}
pub fn shutdown(&self) {
self.inner.shutdown();
}
}
#[derive(uniffi::Object)]
pub struct ExtensionEnvironment {
pub(crate) inner: Arc<CoreEnvironment>,
}
#[uniffi::export]
impl ExtensionEnvironment {
pub fn log_buffer(&self) -> Arc<crate::logging::LogBuffer> {
Arc::new(crate::logging::LogBuffer {
inner: self.inner.log_buffer(),
})
}
pub fn ffmpeg_commands(&self) -> Arc<crate::ffmpeg::FfmpegCommands> {
Arc::new(crate::ffmpeg::FfmpegCommands {
inner: self.inner.ffmpeg_commands(),
})
}
pub fn download_state(&self) -> Arc<crate::progress::DownloadState> {
Arc::new(crate::progress::DownloadState {
inner: self.inner.download_state(),
})
}
#[uniffi::constructor]
pub fn new(
data_directory: String,
master_key: String,
app_version: String,
) -> Result<Self, JsExtensionError> {
let master_key = zeroize::Zeroizing::new(master_key);
Ok(Self {
inner: Arc::new(CoreEnvironment::new(
Path::new(&data_directory),
&master_key,
&app_version,
)?),
})
}
pub fn settings(&self, extension_id: String) -> Result<String, JsExtensionError> {
Ok(serde_json::Value::Object(self.inner.settings(&extension_id)?).to_string())
}
pub fn set_allow_private_network(&self, allow: bool) -> Result<(), JsExtensionError> {
self.inner
.set_allow_private_network(allow)
.map_err(Into::into)
}
pub fn set_network_compatibility_options(
&self,
allow_http: bool,
insecure_tls: bool,
) -> Result<(), JsExtensionError> {
self.inner
.set_network_compatibility_options(allow_http, insecure_tls)
.map_err(Into::into)
}
pub fn cleanup_connections(&self) -> Result<(), JsExtensionError> {
self.inner.cleanup_connections().map_err(Into::into)
}
pub fn set_allowed_download_directories(
&self,
directories: Vec<String>,
) -> Result<(), JsExtensionError> {
self.inner
.set_allowed_download_directories(
&directories
.into_iter()
.map(std::path::PathBuf::from)
.collect::<Vec<_>>(),
)
.map_err(Into::into)
}
pub fn grant_download_directories(
&self,
directories: Vec<String>,
) -> Result<Arc<DownloadDirectoryScope>, JsExtensionError> {
let grants = directories
.iter()
.map(|path| {
self.inner
.grant_temporary_download_directory(Path::new(path))
})
.collect::<Result<Vec<_>, _>>()
.map_err(JsExtensionError::Storage)?;
Ok(Arc::new(DownloadDirectoryScope {
grants: Mutex::new(Some(grants)),
}))
}
pub fn set_runtime_state(&self, raw: String) -> Result<(), JsExtensionError> {
self.inner.set_runtime_state(&raw).map_err(Into::into)
}
pub fn set_session_grant(
&self,
extension_id: String,
grant: String,
) -> Result<(), JsExtensionError> {
let grant = zeroize::Zeroizing::new(grant);
self.inner
.set_session_grant(&extension_id, &grant)
.map_err(Into::into)
}
pub fn resolve_callback_state(&self, state: String) -> Result<String, JsExtensionError> {
self.inner
.resolve_callback_state(&state, false)
.map_err(Into::into)
}
pub fn consume_callback_state(&self, state: String) -> Result<String, JsExtensionError> {
self.inner
.resolve_callback_state(&state, true)
.map_err(Into::into)
}
pub fn set_auth_code(
&self,
extension_id: String,
code: String,
) -> Result<(), JsExtensionError> {
let code = zeroize::Zeroizing::new(code);
self.inner
.set_auth_code(&extension_id, &code)
.map_err(Into::into)
}
pub fn shutdown(&self) {
self.inner.shutdown();
}
}