affine_importer 0.1.2

AFFiNE import planning and batching core.
Documentation
mod bear;
mod markdown;
mod notion;
mod obsidian;

use bear::BearZipProvider;
use markdown::MarkdownZipProvider;
use notion::{NotionHtmlZipProvider, NotionMarkdownZipProvider, NotionZipProvider};
use obsidian::ObsidianProvider;

pub(in crate::planner::providers) use super::folder_hierarchy_deltas;
use super::{
  ImportAssetRef, ImportDocRef, IndexedArchive, add_asset_batch, doc_snapshot, empty_batch, entry_from_indexed,
  extract_html_icon, extract_html_title, file_name, folders_for_doc_path, frontmatter_meta, has_emitted_assets,
  hash_bytes, html_to_markdown, mime_from_path, normalize_import_path, parse_date_millis, parse_link_span,
  push_skipped_doc_warning, read_archive, register_csv_path, register_page_path, resolve_path, strip_extension,
  strip_frontmatter,
};
use crate::ImportRegistry;

pub(crate) fn register_builtin_providers(registry: &mut ImportRegistry) {
  registry.register(MarkdownZipProvider);
  registry.register(NotionZipProvider);
  registry.register(NotionMarkdownZipProvider);
  registry.register(NotionHtmlZipProvider);
  registry.register(ObsidianProvider);
  registry.register(BearZipProvider);
}

#[cfg(test)]
pub(in crate::planner::providers) mod tests {
  pub(in crate::planner::providers) fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
    use std::io::{Cursor, Write};

    use zip::{ZipWriter, write::SimpleFileOptions};

    let mut cursor = Cursor::new(Vec::new());
    {
      let mut writer = ZipWriter::new(&mut cursor);
      for (path, bytes) in entries {
        writer.start_file(path, SimpleFileOptions::default()).unwrap();
        writer.write_all(bytes).unwrap();
      }
      writer.finish().unwrap();
    }
    cursor.into_inner()
  }

  pub(in crate::planner::providers) fn temp_dir() -> std::path::PathBuf {
    let path = std::env::temp_dir().join(format!("affine-importer-dir-{}", nanoid::nanoid!()));
    std::fs::create_dir_all(&path).unwrap();
    path
  }

  fn zip_file(bytes: impl AsRef<[u8]>) -> std::path::PathBuf {
    use std::io::Write;

    let path = std::env::temp_dir().join(format!("affine-importer-{}.zip", nanoid::nanoid!()));
    let mut file = std::fs::OpenOptions::new()
      .write(true)
      .create_new(true)
      .open(&path)
      .unwrap();
    file.write_all(bytes.as_ref()).unwrap();
    path
  }

  pub(in crate::planner::providers) fn write_file(root: &std::path::Path, path: &str, bytes: &[u8]) {
    let path = root.join(path);
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(path, bytes).unwrap();
  }

  pub(in crate::planner::providers) fn unlimited_limits() -> crate::ImportBatchLimits {
    crate::ImportBatchLimits {
      max_docs: usize::MAX,
      max_blobs: usize::MAX,
      max_blob_bytes: u64::MAX,
    }
  }

  pub(in crate::planner::providers) fn merge_batch(target: &mut crate::ImportBatch, batch: crate::ImportBatch) {
    target.docs.extend(batch.docs);
    target.blobs.extend(batch.blobs);
    target.folders.extend(batch.folders);
    target.tags.extend(batch.tags);
    target.icons.extend(batch.icons);
    target.warnings.extend(batch.warnings);
    target.progress = batch.progress;
    target.entry_id = target.entry_id.take().or(batch.entry_id);
    target.is_workspace_file |= batch.is_workspace_file;
  }

  pub(in crate::planner::providers) fn merge_cursor(
    mut cursor: Box<dyn crate::ImportCursor>,
  ) -> crate::ImportResult<crate::ImportBatch> {
    let mut merged = super::empty_batch(0);
    while let Some(batch) = cursor.next_batch()? {
      merge_batch(&mut merged, batch);
    }
    merged.done = true;
    Ok(merged)
  }

  pub(in crate::planner::providers) fn import_cursor(
    format: &str,
    source: crate::ImportSource,
    options: crate::ImportOptions,
    limits: crate::ImportBatchLimits,
  ) -> crate::ImportResult<Box<dyn crate::ImportCursor>> {
    crate::ImportRegistry::with_builtin().create_cursor(format, source, options, limits)
  }

  pub(in crate::planner::providers) fn zip_cursor(
    format: &str,
    bytes: impl AsRef<[u8]>,
    options: crate::ImportOptions,
    limits: crate::ImportBatchLimits,
  ) -> crate::ImportResult<Box<dyn crate::ImportCursor>> {
    import_cursor(format, crate::ImportSource::FilePath(zip_file(bytes)), options, limits)
  }

  pub(in crate::planner::providers) fn plan_zip(
    format: &str,
    bytes: impl AsRef<[u8]>,
  ) -> crate::ImportResult<crate::ImportBatch> {
    zip_cursor(format, bytes, crate::ImportOptions::default(), unlimited_limits()).and_then(merge_cursor)
  }
}