Files
AWatch-rus/adk-rust/crates/rebuild-install-kit/src/main.rs
T

323 lines
11 KiB
Rust

use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use clap::Parser;
use flate2::Compression;
use flate2::write::GzEncoder;
use sha2::{Digest, Sha256};
use zip::write::SimpleFileOptions;
const KIT_DIR: &str = "install-kit-awindows-20260427-211240";
const README: &str = r#"ActivityWatch DetMir Windows Install Kit
Includes:
- windows/* (deploy scripts, collectors, common module, configs/examples)
- ansible/* (Windows and AW server playbooks, examples, inventory, tasks)
- aw-server/* (server installer, health orchestrator, RU patch loader, host groups, default settings)
- scripts/* (install-kit rebuild/validation, quality gates, browser/web smoke checks)
Source:
- Local project snapshot at build time.
Customer-specific deployment configs, inventories, passwords, tokens, domains,
IP addresses and runtime snapshots are intentionally excluded from this public
install-kit.
"#;
const ANSIBLE_FILES: &[&str] = &[
"ansible/README.md",
"ansible/deploy_aw_pfsense_poller.yml",
"ansible/deploy_aw_server.yml",
"ansible/deploy_aw_windows.yml",
"ansible/group_vars/all.example.yml",
"ansible/group_vars/pfsense-poller.example.yml",
"ansible/group_vars/proxmox-matrix.example.yml",
"ansible/group_vars/proxmox.example.yml",
"ansible/group_vars/windows.example.yml",
"ansible/install_full_stack.yml",
"ansible/inventory.example.ini",
"ansible/provision_proxmox_ct_and_deploy_aw.yml",
"ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml",
"ansible/tasks/provision_ct_and_deploy_aw.yml",
];
const AW_SERVER_FILES: &[&str] = &[
"aw-server/activitywatch-server.service",
"aw-server/apply_webui_ru_patch.sh",
"aw-server/aw-host-groups.json",
"aw-server/aw-ru-patch.js",
"aw-server/aw-rus-healthd.service",
"aw-server/aw-rus-healthd.timer",
"aw-server/aw-browser-smoke.service",
"aw-server/aw-browser-smoke.timer",
"aw-server/aw-slo-monitor.service",
"aw-server/aw-slo-monitor.timer",
"aw-server/aw-server.env.example",
"aw-server/aw-sw-cleanup.js",
"aw-server/aw-worktime-api.service",
"aw-server/aw-worktime-prewarm.service",
"aw-server/aw-worktime-prewarm.timer",
"aw-server/aw-worktime-panel.js",
"aw-server/install_aw_server.sh",
"aw-server/settings/classes-worktime.json",
"aw-server/settings/views-default.json",
];
const WINDOWS_FILES: &[&str] = &[
"windows/ActivityWatch.Windows.Common.psd1",
"windows/ActivityWatch.Windows.Common.psm1",
"windows/browser-domains-native-collector.ps1",
"windows/deploy-domain-users.ps1",
"windows/deploy-ensemble.ps1",
"windows/deploy-single-user.ps1",
"windows/AWatchRusCollectorGuardService.cs",
"windows/aw-collector-guard.ps1",
"windows/install-collector-guard-service.ps1",
"windows/dlp-endpoint-signals-collector.ps1",
"windows/dlp-policy.example.json",
"windows/dlp-policy.native-cross-os.example.json",
"windows/email-outbound-collector.ps1",
"windows/hardening-recovery.ps1",
"windows/migrate-awatch-rus-paths.ps1",
"windows/validate-deployment.ps1",
"windows/web-category-rules.example.json",
"windows/worktime-session-collector.ps1",
];
const SCRIPTS_FILES: &[&str] = &[
"scripts/aw-webui-browser-smoke.mjs",
"scripts/aw-webui-browser-smoke.sh",
"scripts/check_install_kit_vs_repo.sh",
"scripts/quality-gate.sh",
"scripts/rebuild_install_kit.sh",
"scripts/validate_install_kit.sh",
"scripts/verify_innosetup_installer.sh",
];
#[derive(Debug, Parser)]
#[command(about = "Rebuild ActivityWatch-Russian Windows install-kit directory and archives")]
struct Cli {
#[arg(long, default_value = ".")]
root: PathBuf,
}
fn main() {
let code = match run() {
Ok(()) => 0,
Err(err) => {
eprintln!("{err:#}");
1
}
};
std::process::exit(code);
}
fn run() -> Result<()> {
let cli = Cli::parse();
let root = cli
.root
.canonicalize()
.with_context(|| format!("canonicalize root {}", cli.root.display()))?;
rebuild(&root)
}
fn rebuild(root: &Path) -> Result<()> {
let kit = root.join(KIT_DIR);
for name in ["ansible", "aw-server", "windows", "scripts"] {
remove_if_exists(&kit.join(name))?;
}
remove_server_config_dirs(&kit)?;
for rel in ANSIBLE_FILES
.iter()
.chain(AW_SERVER_FILES)
.chain(WINDOWS_FILES)
.chain(SCRIPTS_FILES)
{
copy_file(root, &kit, rel)?;
}
write_file_replace(&kit.join("README-INSTALL-KIT.txt"), README.as_bytes())
.with_context(|| format!("write {}", kit.join("README-INSTALL-KIT.txt").display()))?;
write_manifest(root, &kit)?;
write_zip(root, &kit)?;
write_tar(root, &kit)?;
Ok(())
}
fn remove_server_config_dirs(kit: &Path) -> Result<()> {
if !kit.is_dir() {
return Ok(());
}
for entry in fs::read_dir(kit).with_context(|| format!("read dir {}", kit.display()))? {
let entry = entry.with_context(|| format!("read dir entry {}", kit.display()))?;
let path = entry.path();
if path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("server-configs-"))
{
remove_if_exists(&path)?;
}
}
Ok(())
}
fn copy_file(root: &Path, kit: &Path, rel: &str) -> Result<()> {
let src = root.join(rel);
let dest = kit.join(rel);
fs::create_dir_all(dest.parent().context("destination parent")?)
.with_context(|| format!("create parent for {}", dest.display()))?;
let bytes = fs::read(&src).with_context(|| format!("read {}", src.display()))?;
write_file_replace(&dest, &bytes)
.with_context(|| format!("copy {} to {}", src.display(), dest.display()))?;
Ok(())
}
fn write_file_replace(path: &Path, bytes: &[u8]) -> Result<()> {
remove_if_exists(path)?;
fs::write(path, bytes).with_context(|| format!("write {}", path.display()))?;
Ok(())
}
fn remove_if_exists(path: &Path) -> Result<()> {
if !path.exists() {
return Ok(());
}
let meta = fs::symlink_metadata(path).with_context(|| format!("stat {}", path.display()))?;
if meta.is_dir() {
fs::remove_dir_all(path).with_context(|| format!("remove dir {}", path.display()))?;
} else {
fs::remove_file(path).with_context(|| format!("remove file {}", path.display()))?;
}
Ok(())
}
fn write_manifest(root: &Path, kit: &Path) -> Result<()> {
let mut files = Vec::new();
collect_files(kit, &mut files)?;
files.retain(|path| path.file_name().and_then(|name| name.to_str()) != Some("MANIFEST.txt"));
files.sort();
let manifest = kit.join("MANIFEST.txt");
let mut out = String::new();
for path in files {
let digest = sha256_file(&path)?;
let rel = path
.strip_prefix(root)
.with_context(|| format!("strip root prefix from {}", path.display()))?;
out.push_str(&format!("{digest} {}\n", slash_path(rel)));
}
write_file_replace(&manifest, out.as_bytes())?;
Ok(())
}
fn write_zip(root: &Path, kit: &Path) -> Result<()> {
let archive_path = root.join(format!("{KIT_DIR}.zip"));
remove_if_exists(&archive_path)?;
let file = File::create(&archive_path)
.with_context(|| format!("create {}", archive_path.display()))?;
let mut zip = zip::ZipWriter::new(file);
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let mut files = Vec::new();
collect_files(kit, &mut files)?;
files.sort();
for path in files {
let rel = path
.strip_prefix(root)
.with_context(|| format!("strip root prefix from {}", path.display()))?;
let name = slash_path(rel);
zip.start_file(name, options).context("start zip file")?;
let mut input = File::open(&path).with_context(|| format!("open {}", path.display()))?;
std::io::copy(&mut input, &mut zip)
.with_context(|| format!("write zip {}", path.display()))?;
}
zip.finish().context("finish zip")?;
Ok(())
}
fn write_tar(root: &Path, kit: &Path) -> Result<()> {
let archive_path = root.join(format!("{KIT_DIR}.tar.gz"));
remove_if_exists(&archive_path)?;
let file = File::create(&archive_path)
.with_context(|| format!("create {}", archive_path.display()))?;
let encoder = GzEncoder::new(file, Compression::default());
let mut builder = tar::Builder::new(encoder);
let mut files = Vec::new();
collect_files(kit, &mut files)?;
files.sort();
for path in files {
builder
.append_path_with_name(
&path,
slash_path(
path.strip_prefix(root)
.with_context(|| format!("strip root prefix from {}", path.display()))?,
),
)
.with_context(|| format!("append tar {}", path.display()))?;
}
builder.finish().context("finish tar")?;
Ok(())
}
fn collect_files(path: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
for entry in fs::read_dir(path).with_context(|| format!("read dir {}", path.display()))? {
let entry = entry.with_context(|| format!("read dir entry {}", path.display()))?;
let entry_path = entry.path();
let file_type = entry
.file_type()
.with_context(|| format!("read file type {}", entry_path.display()))?;
if file_type.is_dir() {
collect_files(&entry_path, out)?;
} else if (file_type.is_file() || file_type.is_symlink()) && entry_path.is_file() {
out.push(entry_path);
}
}
Ok(())
}
fn sha256_file(path: &Path) -> Result<String> {
let mut file = File::open(path).with_context(|| format!("open {}", path.display()))?;
let mut hasher = Sha256::new();
let mut buf = [0_u8; 64 * 1024];
loop {
let read = file
.read(&mut buf)
.with_context(|| format!("read {}", path.display()))?;
if read == 0 {
break;
}
hasher.update(&buf[..read]);
}
Ok(format!("{:x}", hasher.finalize()))
}
fn slash_path(path: &Path) -> String {
path.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::remove_server_config_dirs;
#[test]
fn removes_server_config_dirs_only() {
let tmp = tempdir().unwrap();
let kit = tmp.path();
fs::create_dir_all(kit.join("server-configs-x")).unwrap();
fs::create_dir_all(kit.join("windows")).unwrap();
remove_server_config_dirs(kit).unwrap();
assert!(!kit.join("server-configs-x").exists());
assert!(kit.join("windows").exists());
}
}