Add Phase 5 enhancements: security, i18n, analysis, backup, notifications
- Database v8 migration: tags, pinned, avg_startup_ms columns - Security scanning with CVE matching and batch scan - Bundled library extraction and vulnerability reports - Desktop notification system for security alerts - Backup/restore system for AppImage configurations - i18n framework with gettext support - Runtime analysis and Wayland compatibility detection - AppStream metadata and Flatpak-style build support - File watcher module for live directory monitoring - Preferences panel with GSettings integration - CLI interface for headless operation - Detail view: tabbed layout with ViewSwitcher in title bar, health score, sandbox controls, changelog links - Library view: sort dropdown, context menu enhancements - Dashboard: system status, disk usage, launch history - Security report page with scan and export - Packaging: meson build, PKGBUILD, metainfo
This commit is contained in:
223
src/cli.rs
223
src/cli.rs
@@ -59,6 +59,17 @@ pub enum Commands {
|
||||
/// Path to the AppImage
|
||||
path: String,
|
||||
},
|
||||
/// Export app library to a JSON file
|
||||
Export {
|
||||
/// Output file path (default: stdout)
|
||||
#[arg(long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
/// Import app library from a JSON file
|
||||
Import {
|
||||
/// Path to the JSON file to import
|
||||
file: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn run_command(command: Commands) -> ExitCode {
|
||||
@@ -81,6 +92,8 @@ pub fn run_command(command: Commands) -> ExitCode {
|
||||
Commands::CheckUpdates => cmd_check_updates(&db),
|
||||
Commands::Duplicates => cmd_duplicates(&db),
|
||||
Commands::Launch { path } => cmd_launch(&db, &path),
|
||||
Commands::Export { output } => cmd_export(&db, output.as_deref()),
|
||||
Commands::Import { file } => cmd_import(&db, &file),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,3 +674,213 @@ fn do_inspect(path: &std::path::Path, appimage_type: &discovery::AppImageType) -
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Export/Import library ---
|
||||
|
||||
fn cmd_export(db: &Database, output: Option<&str>) -> ExitCode {
|
||||
let records = match db.get_all_appimages() {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let appimages: Vec<serde_json::Value> = records
|
||||
.iter()
|
||||
.map(|r| {
|
||||
serde_json::json!({
|
||||
"path": r.path,
|
||||
"app_name": r.app_name,
|
||||
"app_version": r.app_version,
|
||||
"integrated": r.integrated,
|
||||
"notes": r.notes,
|
||||
"categories": r.categories,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let export_data = serde_json::json!({
|
||||
"version": 1,
|
||||
"exported_at": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
|
||||
"appimages": appimages,
|
||||
});
|
||||
|
||||
let json_str = match serde_json::to_string_pretty(&export_data) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Error serializing export data: {}", e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(path) = output {
|
||||
if let Err(e) = std::fs::write(path, &json_str) {
|
||||
eprintln!("Error writing to {}: {}", path, e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
} else {
|
||||
println!("{}", json_str);
|
||||
}
|
||||
|
||||
eprintln!("Exported {} AppImages", records.len());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn cmd_import(db: &Database, file: &str) -> ExitCode {
|
||||
let content = match std::fs::read_to_string(file) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading {}: {}", file, e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let data: serde_json::Value = match serde_json::from_str(&content) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Error parsing JSON: {}", e);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let entries = match data.get("appimages").and_then(|a| a.as_array()) {
|
||||
Some(arr) => arr,
|
||||
None => {
|
||||
eprintln!("Error: JSON missing 'appimages' array");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let total = entries.len();
|
||||
let mut imported = 0u32;
|
||||
let mut skipped = 0u32;
|
||||
|
||||
for entry in entries {
|
||||
let path_str = match entry.get("path").and_then(|p| p.as_str()) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let file_path = std::path::Path::new(path_str);
|
||||
if !file_path.exists() {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate that the file is actually an AppImage
|
||||
let appimage_type = match discovery::detect_appimage(file_path) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
eprintln!(" Skipping {} - not a valid AppImage", path_str);
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let metadata = std::fs::metadata(file_path);
|
||||
let size_bytes = metadata.as_ref().map(|m| m.len() as i64).unwrap_or(0);
|
||||
let is_executable = metadata
|
||||
.as_ref()
|
||||
.map(|m| {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
m.permissions().mode() & 0o111 != 0
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let filename = file_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
let file_modified = metadata
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|m| m.modified().ok())
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.and_then(|dur| {
|
||||
chrono::DateTime::from_timestamp(dur.as_secs() as i64, 0)
|
||||
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
|
||||
});
|
||||
|
||||
let id = match db.upsert_appimage(
|
||||
path_str,
|
||||
&filename,
|
||||
Some(appimage_type.as_i32()),
|
||||
size_bytes,
|
||||
is_executable,
|
||||
file_modified.as_deref(),
|
||||
) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
eprintln!(" Error registering {}: {}", path_str, e);
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Restore metadata fields from the export
|
||||
let app_name = entry.get("app_name").and_then(|v| v.as_str());
|
||||
let app_version = entry.get("app_version").and_then(|v| v.as_str());
|
||||
let categories = entry.get("categories").and_then(|v| v.as_str());
|
||||
|
||||
if app_name.is_some() || app_version.is_some() {
|
||||
db.update_metadata(
|
||||
id,
|
||||
app_name,
|
||||
app_version,
|
||||
None,
|
||||
None,
|
||||
categories,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).ok();
|
||||
}
|
||||
|
||||
// Restore notes if present
|
||||
if let Some(notes_str) = entry.get("notes").and_then(|v| v.as_str()) {
|
||||
db.update_notes(id, Some(notes_str)).ok();
|
||||
}
|
||||
|
||||
// If it was integrated in the export, integrate it now
|
||||
let was_integrated = entry
|
||||
.get("integrated")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if was_integrated {
|
||||
// Need the full record to integrate
|
||||
if let Ok(Some(record)) = db.get_appimage_by_id(id) {
|
||||
if !record.integrated {
|
||||
match integrator::integrate(&record) {
|
||||
Ok(result) => {
|
||||
db.set_integrated(
|
||||
id,
|
||||
true,
|
||||
Some(&result.desktop_file_path.to_string_lossy()),
|
||||
).ok();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" Warning: could not integrate {}: {}", path_str, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imported += 1;
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"Imported {} of {} AppImages ({} skipped - file not found)",
|
||||
imported,
|
||||
total,
|
||||
skipped,
|
||||
);
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user