Skip to main content

void_core/ops/
import.rs

1//! Shared fetch+decrypt pipeline for importing published commits from IPFS.
2//!
3//! Used by both `fork` (creates new repo) and `pull-request` (creates branch
4//! in existing repo). Consolidates the fetch commit → fetch metadata → fetch
5//! shards pipeline into a single reusable function.
6
7use camino::Utf8PathBuf;
8
9use crate::cid;
10use crate::crypto::{
11    CommitReader, ContentKey, EncryptedCommit, EncryptedManifest, EncryptedMetadata,
12    EncryptedRepoManifest, EncryptedShard, KeyVault,
13};
14use crate::metadata::{self, MetadataBundle};
15use crate::store::{FsStore, IpfsStore, ObjectStoreExt, RemoteStore};
16use std::sync::Arc;
17use crate::support::ToVoidCid;
18use crate::{Result, VoidError};
19
20/// Options for importing a published commit from IPFS.
21pub struct ImportOptions {
22    /// Local object store to write fetched objects into.
23    pub store: FsStore,
24    /// Remote store to fetch from (IpfsStore or daemon-backed).
25    pub remote: Arc<dyn RemoteStore>,
26    /// CID of the published commit (as string).
27    pub commit_cid: String,
28    /// Content key for decrypting the commit.
29    pub content_key: ContentKey,
30    /// Optional callback for shard fetch progress (fetched, total).
31    pub on_shard_progress: Option<Box<dyn Fn(usize, usize)>>,
32}
33
34/// Result of fetching and decrypting a published commit.
35pub struct ForeignCommit {
36    /// The decrypted commit object.
37    pub commit: metadata::Commit,
38    /// The encrypted commit blob (stored in local store).
39    pub commit_encrypted: EncryptedCommit,
40    /// Reader for decrypting commit-related objects (metadata, shards).
41    pub reader: CommitReader,
42    /// Decrypted metadata bundle.
43    pub metadata: MetadataBundle,
44    /// Repo name extracted from the embedded repo manifest (best-effort).
45    pub repo_name: Option<String>,
46}
47
48/// Fetch a published commit from IPFS, decrypt it with a content key,
49/// and store all objects (commit, metadata, manifest, shards) in the local store.
50///
51/// This is the shared pipeline used by both `fork` and `pull-request`.
52pub fn fetch_published_commit(
53    opts: ImportOptions,
54) -> Result<ForeignCommit> {
55    // Phase 1: Fetch + decrypt commit
56    let commit_cid = cid::parse(&opts.commit_cid)?;
57
58    let commit_encrypted = EncryptedCommit::from_bytes(opts.remote.fetch_raw(&commit_cid)?);
59    opts.store.put_blob(&commit_encrypted)?;
60
61    let vault = KeyVault::from_content_key(opts.content_key);
62    let (commit_plaintext, reader) = CommitReader::open_with_vault(&vault, &commit_encrypted)?;
63    let commit = commit_plaintext.parse()?;
64
65    // Phase 2: Fetch + store metadata
66    let metadata_cid = commit.metadata_bundle.to_void_cid()?;
67    let metadata_encrypted = EncryptedMetadata::from_bytes(opts.remote.fetch_raw(&metadata_cid)?);
68    opts.store.put_blob(&metadata_encrypted)?;
69
70    let metadata: MetadataBundle = reader.decrypt_metadata(&metadata_encrypted)?;
71
72    // Fetch + store manifest (required for file extraction)
73    if let Some(ref manifest_cid) = commit.manifest_cid {
74        let mcid = cid::from_bytes(manifest_cid.as_bytes())?;
75        let manifest_encrypted = EncryptedManifest::from_bytes(opts.remote.fetch_raw(&mcid)?);
76        opts.store.put_blob(&manifest_encrypted)?;
77    }
78
79    // Fetch + store all shards
80    let total_shards = metadata.shard_map.ranges.iter().filter(|r| r.cid.is_some()).count();
81    let mut fetched = 0usize;
82    for range in &metadata.shard_map.ranges {
83        let shard_cid_typed = match range.cid.as_ref() {
84            Some(c) => c,
85            None => continue,
86        };
87        let shard_cid = cid::from_bytes(shard_cid_typed.as_bytes())?;
88        let shard_encrypted = EncryptedShard::from_bytes(opts.remote.fetch_raw(&shard_cid)?);
89        opts.store.put_blob(&shard_encrypted)?;
90        fetched += 1;
91        if let Some(ref on_progress) = opts.on_shard_progress {
92            on_progress(fetched, total_shards);
93        }
94    }
95
96    // Best-effort: extract repo name from embedded repo manifest
97    let repo_name = extract_repo_name(&commit, &reader, opts.remote.as_ref());
98
99    Ok(ForeignCommit {
100        commit,
101        commit_encrypted,
102        reader,
103        metadata,
104        repo_name,
105    })
106}
107
108/// Create an FsStore from a void directory's objects path.
109pub fn objects_store(void_dir: &std::path::Path) -> Result<FsStore> {
110    let objects_dir = Utf8PathBuf::try_from(void_dir.join("objects"))
111        .map_err(|e| VoidError::Io(std::io::Error::new(
112            std::io::ErrorKind::InvalidInput,
113            format!("invalid objects path: {e}"),
114        )))?;
115    FsStore::new(objects_dir)
116}
117
118/// Extract repo name from the commit's embedded repo manifest (best-effort).
119fn extract_repo_name(
120    commit: &metadata::Commit,
121    reader: &CommitReader,
122    remote: &dyn RemoteStore,
123) -> Option<String> {
124    let rm_cid_bytes = commit.repo_manifest_cid.as_ref()?;
125    let rm_cid = rm_cid_bytes.to_void_cid().ok()?;
126    let rm_encrypted = EncryptedRepoManifest::from_bytes(remote.fetch_raw(&rm_cid).ok()?);
127    let manifest = reader.decrypt_repo_manifest(&rm_encrypted).ok()?;
128    let rm_json = manifest.to_json().ok()?;
129    let value: serde_json::Value = serde_json::from_slice(&rm_json).ok()?;
130    value
131        .get("repoName")
132        .and_then(|v| v.as_str())
133        .map(String::from)
134}