149 lines
5.4 KiB
Rust
149 lines
5.4 KiB
Rust
use serde::Serialize;
|
|
use serde_json::Value;
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
|
|
const NODE_ALIAS: &str = "xx-gz-001";
|
|
const CHANNEL_ROOT: &str = "/var/lib/guanghu/education-data/channels/GH-EDU-CHANNEL-XX-001";
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct ChannelSnapshot {
|
|
node: Value,
|
|
fifth_domain_link: Value,
|
|
module_registry: Value,
|
|
update_authority: Value,
|
|
channel: Value,
|
|
code_channel: Value,
|
|
}
|
|
|
|
fn ssh_output(remote_command: &str) -> Result<String, String> {
|
|
let output = Command::new("/usr/bin/ssh")
|
|
.args([
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"ConnectTimeout=10",
|
|
NODE_ALIAS,
|
|
remote_command,
|
|
])
|
|
.output()
|
|
.map_err(|error| format!("SSH_LAUNCH_FAILED:{error}"))?;
|
|
|
|
if !output.status.success() {
|
|
return Err(format!(
|
|
"SSH_REMOTE_FAILED:{}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
));
|
|
}
|
|
|
|
String::from_utf8(output.stdout).map_err(|error| format!("SSH_OUTPUT_INVALID_UTF8:{error}"))
|
|
}
|
|
|
|
fn read_remote_json(path: &str) -> Result<Value, String> {
|
|
let payload = ssh_output(&format!("cat {path}"))?;
|
|
serde_json::from_str(&payload).map_err(|error| format!("REMOTE_JSON_INVALID:{path}:{error}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn connect_channel() -> Result<ChannelSnapshot, String> {
|
|
let code_version = ssh_output("curl -fsS http://127.0.0.1:3901/api/v1/version")?;
|
|
let repository_search =
|
|
ssh_output("curl -fsS 'http://127.0.0.1:3901/api/v1/repos/search?limit=20'")?;
|
|
|
|
Ok(ChannelSnapshot {
|
|
node: read_remote_json("/var/lib/guanghu/protocol/node.json")?,
|
|
fifth_domain_link: read_remote_json(
|
|
"/var/lib/guanghu/receipts/XX-GZ-001/FIFTH-DOMAIN-LINK-CURRENT.json",
|
|
)?,
|
|
module_registry: read_remote_json("/opt/guanghu/protocol/current/module-registry.json")?,
|
|
update_authority: read_remote_json("/opt/guanghu/protocol/current/update-authority.json")?,
|
|
channel: read_remote_json("/opt/guanghu/protocol/current/xiaoxin-channel.json")?,
|
|
code_channel: serde_json::json!({
|
|
"version": serde_json::from_str::<Value>(&code_version)
|
|
.map_err(|error| format!("CODE_CHANNEL_VERSION_INVALID:{error}"))?,
|
|
"repositories": serde_json::from_str::<Value>(&repository_search)
|
|
.map_err(|error| format!("CODE_CHANNEL_REPOSITORIES_INVALID:{error}"))?
|
|
}),
|
|
})
|
|
}
|
|
|
|
fn channel_file(kind: &str) -> Result<(&'static str, &'static str), String> {
|
|
match kind {
|
|
"document" => Ok(("teacher-training-plan.md", "GH-EDU-OFFICE-DOC-001")),
|
|
"sheet" => Ok(("teacher-training-data.csv", "GH-EDU-OFFICE-SHEET-001")),
|
|
_ => Err(format!("UNREGISTERED_CHANNEL_FILE:{kind}")),
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn read_channel_file(kind: String) -> Result<String, String> {
|
|
let (filename, _) = channel_file(&kind)?;
|
|
ssh_output(&format!(
|
|
"path='{CHANNEL_ROOT}/documents/{filename}'; if [ -f \"$path\" ]; then cat \"$path\"; fi"
|
|
))
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn save_channel_file(kind: String, content: String) -> Result<Value, String> {
|
|
let (filename, module_id) = channel_file(&kind)?;
|
|
let path = format!("{CHANNEL_ROOT}/documents/{filename}");
|
|
let receipt = format!("{CHANNEL_ROOT}/receipts/{kind}-CURRENT.json");
|
|
let remote_command = format!(
|
|
"set -eu; \
|
|
install -d -m 700 '{CHANNEL_ROOT}/documents' '{CHANNEL_ROOT}/receipts'; \
|
|
path='{path}'; tmp=\"${{path}}.tmp.$$\"; umask 077; cat > \"$tmp\"; mv \"$tmp\" \"$path\"; \
|
|
sha=$(sha256sum \"$path\" | awk '{{print $1}}'); \
|
|
now=$(date -u +%Y-%m-%dT%H:%M:%SZ); \
|
|
receipt='{receipt}'; rtmp=\"${{receipt}}.tmp.$$\"; \
|
|
printf '{{\"schema\":\"guanghu.channel-write-receipt/v1\",\"channelId\":\"GH-EDU-CHANNEL-XX-001\",\"nodeId\":\"XX-GZ-001\",\"moduleId\":\"{module_id}\",\"kind\":\"{kind}\",\"sha256\":\"%s\",\"writtenAt\":\"%s\",\"state\":\"PASS_100\"}}\\n' \"$sha\" \"$now\" > \"$rtmp\"; \
|
|
mv \"$rtmp\" \"$receipt\"; cat \"$receipt\""
|
|
);
|
|
|
|
let mut child = Command::new("/usr/bin/ssh")
|
|
.args([
|
|
"-o",
|
|
"BatchMode=yes",
|
|
"-o",
|
|
"ConnectTimeout=10",
|
|
NODE_ALIAS,
|
|
&remote_command,
|
|
])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.map_err(|error| format!("SSH_SAVE_LAUNCH_FAILED:{error}"))?;
|
|
|
|
child
|
|
.stdin
|
|
.take()
|
|
.ok_or_else(|| "SSH_SAVE_STDIN_UNAVAILABLE".to_string())?
|
|
.write_all(content.as_bytes())
|
|
.map_err(|error| format!("SSH_SAVE_WRITE_FAILED:{error}"))?;
|
|
|
|
let output = child
|
|
.wait_with_output()
|
|
.map_err(|error| format!("SSH_SAVE_WAIT_FAILED:{error}"))?;
|
|
if !output.status.success() {
|
|
return Err(format!(
|
|
"SSH_SAVE_REMOTE_FAILED:{}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
));
|
|
}
|
|
|
|
serde_json::from_slice(&output.stdout)
|
|
.map_err(|error| format!("SSH_SAVE_RECEIPT_INVALID:{error}"))
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.invoke_handler(tauri::generate_handler![
|
|
connect_channel,
|
|
read_channel_file,
|
|
save_channel_file
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("failed to run Guanghu Education Subdomain OS");
|
|
}
|