Export/Import: ZIP export with scope selection, import with public post merge

Export (export.rs): ZIP archive with auto-chunking at 4GB. Four scopes:
identity only, posts only, posts+identity, everything (posts+key+follows+
profiles+settings). Includes blobs. Manifest JSON tracks metadata.

Import (import.rs): Read ZIP summary without importing (preview).
Import public posts into current identity with new PostIds + original
timestamps. Import as new identity (creates identity subdir from key).
Uses spawn_blocking for ZIP I/O to avoid Send issues with ZipArchive.

Tauri IPC: export_data, import_summary, import_public_posts,
import_as_new_identity commands. IdentityManager.base_dir() getter.

Frontend: Export wizard lightbox with scope radio buttons + output dir.
Import wizard with ZIP path, preview summary, action selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-31 20:56:03 -04:00
parent fb1e92985c
commit 8ef32e6df6
7 changed files with 786 additions and 3 deletions

View file

@ -2146,6 +2146,76 @@ async fn get_active_identity(state: State<'_, AppIdentity>) -> Result<Option<Ide
}))
}
// --- Export/Import IPC ---
#[tauri::command]
async fn export_data(
state: State<'_, AppNode>,
scope: String,
output_dir: String,
) -> Result<String, String> {
let node = get_node(&state).await;
let export_scope = match scope.as_str() {
"identity_only" => itsgoin_core::export::ExportScope::IdentityOnly,
"posts_only" => itsgoin_core::export::ExportScope::PostsOnly,
"posts_with_identity" => itsgoin_core::export::ExportScope::PostsWithIdentity,
"everything" => itsgoin_core::export::ExportScope::Everything,
_ => return Err("Invalid scope".to_string()),
};
let result = itsgoin_core::export::export_data(
&node.data_dir,
&node.storage,
&node.blob_store,
&node.node_id,
export_scope,
std::path::Path::new(&output_dir),
).await.map_err(|e| e.to_string())?;
let paths: Vec<String> = result.paths.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
Ok(format!("Exported {} posts, {} blobs to {} file(s): {}",
result.post_count, result.blob_count, paths.len(),
paths.join(", ")))
}
#[tauri::command]
async fn import_summary(zip_path: String) -> Result<String, String> {
let summary = itsgoin_core::import::read_import_summary(std::path::Path::new(&zip_path))
.map_err(|e| e.to_string())?;
serde_json::to_string(&summary).map_err(|e| e.to_string())
}
#[tauri::command]
async fn import_public_posts(
state: State<'_, AppNode>,
zip_path: String,
) -> Result<String, String> {
let node = get_node(&state).await;
let result = itsgoin_core::import::import_public_posts(
std::path::Path::new(&zip_path),
&node.storage,
&node.blob_store,
&node.node_id,
).await.map_err(|e| e.to_string())?;
Ok(result.message)
}
#[tauri::command]
async fn import_as_new_identity(
state: State<'_, AppIdentity>,
zip_path: String,
) -> Result<String, String> {
let mgr = state.lock().await;
let base_dir = mgr.base_dir().to_path_buf();
drop(mgr);
let node_id = itsgoin_core::import::import_as_identity(
std::path::Path::new(&zip_path),
&base_dir,
).map_err(|e| e.to_string())?;
Ok(format!("Identity {} imported — switch to it in Settings", &node_id[..12]))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tracing_subscriber::fmt()
@ -2334,6 +2404,10 @@ pub fn run() {
delete_identity,
import_identity_key,
get_active_identity,
export_data,
import_summary,
import_public_posts,
import_as_new_identity,
])
.build(tauri::generate_context!())
.expect("error while building tauri application")