196 lines
9.0 KiB
Rust
196 lines
9.0 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Mutex;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use dbx_plugin_sdk::{PluginEmitter, PluginError, PluginHandler, PluginMetadata, PluginServer, RequestContext};
|
|
use serde_json::{json, Value};
|
|
|
|
#[derive(Default)]
|
|
struct HelloPlugin {
|
|
connections: Mutex<HashMap<String, String>>,
|
|
}
|
|
|
|
impl PluginHandler for HelloPlugin {
|
|
fn handle(
|
|
&self,
|
|
_context: RequestContext,
|
|
method: &str,
|
|
params: Value,
|
|
emitter: &PluginEmitter,
|
|
) -> Result<Value, PluginError> {
|
|
match method {
|
|
"connection/test" => {
|
|
let connection = params.get("connection").cloned().unwrap_or_default();
|
|
Ok(json!({
|
|
"success": true,
|
|
"message": format!(
|
|
"Provider is ready for {}:{}.",
|
|
connection.get("host").and_then(Value::as_str).unwrap_or("localhost"),
|
|
connection.get("port").and_then(Value::as_u64).unwrap_or(0)
|
|
)
|
|
}))
|
|
}
|
|
"connection/connect" => {
|
|
let connection =
|
|
params.get("connection").ok_or_else(|| PluginError::new(-32602, "Missing connection"))?;
|
|
let connection_id = connection
|
|
.get("id")
|
|
.and_then(Value::as_str)
|
|
.ok_or_else(|| PluginError::new(-32602, "Missing connection id"))?;
|
|
let connection_name = connection.get("name").and_then(Value::as_str).unwrap_or("Hello connection");
|
|
self.connections
|
|
.lock()
|
|
.map_err(|_| PluginError::new(-32000, "Connection registry is poisoned"))?
|
|
.insert(connection_id.to_string(), connection_name.to_string());
|
|
emitter
|
|
.event("hello/connectionChanged", json!({ "connectionId": connection_id, "state": "connected" }))?;
|
|
Ok(json!({ "success": true }))
|
|
}
|
|
"connection/disconnect" => {
|
|
let connection_id = params
|
|
.get("connection")
|
|
.and_then(|connection| connection.get("id"))
|
|
.and_then(Value::as_str)
|
|
.ok_or_else(|| PluginError::new(-32602, "Missing connection id"))?;
|
|
self.connections
|
|
.lock()
|
|
.map_err(|_| PluginError::new(-32000, "Connection registry is poisoned"))?
|
|
.remove(connection_id);
|
|
emitter.event(
|
|
"hello/connectionChanged",
|
|
json!({ "connectionId": connection_id, "state": "disconnected" }),
|
|
)?;
|
|
Ok(json!({ "success": true }))
|
|
}
|
|
"connection/action" => {
|
|
match params.get("action").and_then(|action| action.get("id")).and_then(Value::as_str) {
|
|
Some("suggest-greeting") => Ok(json!({
|
|
"success": true,
|
|
"message": "Greeting updated by the plugin action.",
|
|
"fieldValues": {
|
|
"greeting": "Hello from plugin action"
|
|
}
|
|
})),
|
|
Some(action_id) => Err(PluginError::new(-32602, format!("Unknown connection action: {action_id}"))),
|
|
None => Err(PluginError::new(-32602, "Missing connection action id")),
|
|
}
|
|
}
|
|
"hello/greet" => {
|
|
let connection_id = params.get("connectionId").and_then(Value::as_str).unwrap_or_default();
|
|
let connection_name = self
|
|
.connections
|
|
.lock()
|
|
.map_err(|_| PluginError::new(-32000, "Connection registry is poisoned"))?
|
|
.get(connection_id)
|
|
.cloned()
|
|
.ok_or_else(|| PluginError::new(-32010, "Open a saved Hello connection first"))?;
|
|
let name = params.get("name").and_then(Value::as_str).unwrap_or("DBX");
|
|
emitter.event(
|
|
"hello/progress",
|
|
json!({ "stage": "started", "name": name, "connectionId": connection_id }),
|
|
)?;
|
|
thread::sleep(Duration::from_millis(120));
|
|
emitter.event(
|
|
"hello/progress",
|
|
json!({ "stage": "finished", "name": name, "connectionId": connection_id }),
|
|
)?;
|
|
Ok(json!({
|
|
"message": format!("Hello, {name}, from {connection_name}!"),
|
|
"connectionId": connection_id,
|
|
"pluginId": std::env::var("DBX_PLUGIN_ID").unwrap_or_default(),
|
|
"dbxVersion": std::env::var("DBX_APP_VERSION").unwrap_or_default()
|
|
}))
|
|
}
|
|
"filesystem/list" => {
|
|
require_filesystem_provider(¶ms)?;
|
|
let uri = params.get("uri").and_then(Value::as_str).unwrap_or("hello:/");
|
|
let entries = match uri {
|
|
"hello:/" => json!([
|
|
{
|
|
"name": "examples",
|
|
"uri": "hello:/examples/",
|
|
"kind": "directory",
|
|
"modifiedAt": "2026-07-28T00:00:00Z"
|
|
},
|
|
{
|
|
"name": "README.txt",
|
|
"uri": "hello:/README.txt",
|
|
"kind": "file",
|
|
"size": 78,
|
|
"modifiedAt": "2026-07-28T00:00:00Z",
|
|
"contentType": "text/plain"
|
|
}
|
|
]),
|
|
"hello:/examples/" => json!([
|
|
{
|
|
"name": "hello.json",
|
|
"uri": "hello:/examples/hello.json",
|
|
"kind": "file",
|
|
"size": 55,
|
|
"modifiedAt": "2026-07-28T00:00:00Z",
|
|
"contentType": "application/json"
|
|
}
|
|
]),
|
|
_ => return Err(PluginError::new(-32044, format!("Directory not found: {uri}"))),
|
|
};
|
|
Ok(json!({ "entries": entries }))
|
|
}
|
|
"filesystem/read" => {
|
|
require_filesystem_provider(¶ms)?;
|
|
let uri = params.get("uri").and_then(Value::as_str).unwrap_or_default();
|
|
let content = match uri {
|
|
"hello:/README.txt" => "This virtual file is rendered by DBX's host-owned plugin file manager.\n",
|
|
"hello:/examples/hello.json" => "{\n \"hello\": \"DBX\",\n \"provider\": \"filesystem\"\n}\n",
|
|
_ => return Err(PluginError::new(-32044, format!("File not found: {uri}"))),
|
|
};
|
|
let max_bytes = params.get("maxBytes").and_then(Value::as_u64).unwrap_or(content.len() as u64);
|
|
let preview_length = usize::try_from(max_bytes).unwrap_or(usize::MAX).min(content.len());
|
|
let preview = &content.as_bytes()[..preview_length];
|
|
Ok(json!({
|
|
"dataBase64": encode_base64(preview),
|
|
"contentType": if uri.ends_with(".json") { "application/json" } else { "text/plain" },
|
|
"truncated": preview_length < content.len(),
|
|
"etag": format!("hello-{preview_length}")
|
|
}))
|
|
}
|
|
_ => Err(PluginError::method_not_found(method)),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn require_filesystem_provider(params: &Value) -> Result<(), PluginError> {
|
|
if params.get("providerId").and_then(Value::as_str) == Some("dbx.example.hello.files") {
|
|
Ok(())
|
|
} else {
|
|
Err(PluginError::new(-32602, "Unknown filesystem provider"))
|
|
}
|
|
}
|
|
|
|
fn encode_base64(bytes: &[u8]) -> String {
|
|
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
let mut encoded = String::with_capacity(bytes.len().div_ceil(3) * 4);
|
|
for chunk in bytes.chunks(3) {
|
|
let first = chunk[0];
|
|
let second = chunk.get(1).copied().unwrap_or(0);
|
|
let third = chunk.get(2).copied().unwrap_or(0);
|
|
encoded.push(ALPHABET[(first >> 2) as usize] as char);
|
|
encoded.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char);
|
|
encoded.push(if chunk.len() > 1 {
|
|
ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char
|
|
} else {
|
|
'='
|
|
});
|
|
encoded.push(if chunk.len() > 2 { ALPHABET[(third & 0x3f) as usize] as char } else { '=' });
|
|
}
|
|
encoded
|
|
}
|
|
|
|
fn main() -> std::io::Result<()> {
|
|
let metadata = PluginMetadata::new("dbx.example.hello", env!("CARGO_PKG_VERSION"))
|
|
.with_capability("connections")
|
|
.with_capability("events")
|
|
.with_capability("filesystem");
|
|
PluginServer::new(metadata, HelloPlugin::default()).serve()
|
|
}
|