A command-line tool for crate registry backup/export https://shipyard.rs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

224 lines
7.0 KiB

use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
use std::borrow::Cow;
use serde::Deserialize;
use clap::Parser;
use tracing::{debug, error, info, warn};
use tracing_subscriber::filter::EnvFilter;
use url::Url;
use anyhow::{anyhow, bail, Error};
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DestinationRegistryConfig {
pub api_url: Url,
pub token: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct SourceRegistryConfig {
pub index_dir: PathBuf,
pub crate_files_dir: PathBuf,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
#[serde(alias = "source")]
pub src: SourceRegistryConfig,
#[serde(alias = "destination")]
pub dst: DestinationRegistryConfig,
}
/// fields we need from Cargo.toml [package] section to combine with IndexMeta
/// to form a PublishMeta.
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct PackageStub {
pub name: String,
pub version: Version,
#[serde(default)]
pub authors: Vec<String>,
pub description: Option<String>,
pub license: Option<String>,
pub license_file: Option<PathBuf>,
#[serde(default)]
pub categories: Vec<String>,
#[serde(default)]
pub keywords: Vec<String>,
pub readme: Option<PathBuf>,
pub repository: Option<String>,
pub homepage: Option<String>,
pub documentation: Option<String>,
}
/// for parsing Cargo.toml to extract missing PublishMeta fields that do not appear
/// in IndexMeta
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct ManifestStub {
pub package: PackageStub,
}
/// full definition of cargo publish json
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct PublishMeta {
#[serde(borrow)]
pub name: String,
#[serde(alias = "version")]
pub vers: semver::Version,
#[serde(alias = "dependencies")]
#[serde(default)]
pub deps: Vec<PublishDependency>,
#[serde(default, borrow)]
pub features: BTreeMap<String, Vec<String>>,
#[serde(default, borrow)]
pub authors: Vec<String>,
#[serde(borrow)]
pub description: Option<String>,
#[serde(borrow)]
pub documentation: Option<String>,
#[serde(borrow)]
pub homepage: Option<String>,
#[serde(borrow)]
pub readme: Option<String>,
#[serde(borrow)]
pub readme_file: Option<String>,
#[serde(default, borrow)]
pub keywords: Vec<String>,
#[serde(default, borrow)]
pub categories: Vec<String>,
#[serde(borrow)]
pub license: Option<String>,
#[serde(borrow)]
pub license_file: Option<String>,
#[serde(borrow)]
pub repository: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub links: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub badges: Option<BTreeMap<String, String>>,
/// from ancient cargo versions
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub features2: Option<BTreeMap<String, Vec<String>>>,
/// from ancient cargo versions
#[serde(skip_serializing_if = "Option::is_none")]
pub v: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct PublishDependency {
pub optional: bool,
pub default_features: bool,
#[serde(borrow)]
pub name: String,
#[serde(borrow)]
pub features: Vec<String>,
// cargo and crates-io have this as string
#[serde(alias = "req")]
pub version_req: semver::VersionReq,
#[serde(borrow)]
pub target: Option<String>,
// crates-io has this as option
pub kind: PublishDependencyKind,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub registry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub explicit_name_in_toml: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct IndexMeta {
// same everything as publish metadata
#[serde(borrow)]
pub name: String,
#[serde(alias = "version")]
pub vers: semver::Version,
#[serde(alias = "dependencies", borrow)]
pub features: BTreeMap<String, Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub links: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub badges: Option<BTreeMap<String, String>>,
// modified format/field names
pub deps: Vec<IndexDependency>,
// fields that don't appear in publish metadata
pub cksum: String,
pub yanked: bool,
// ancient fields, these were actually written
// on sanskrit on stone tablets
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub features2: Option<BTreeMap<String, Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub v: Option<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct IndexDependency {
/// corresponds to `explicit_name_in_toml` field in `publish::Dependency`
/// when a dep is renamed in Cargo.toml, otherwise same as `package`.
#[serde(borrow)]
pub name: String,
/// corresponds to `name` in `publish::Dependency`
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub package: Option<String>,
/// in publish meta, this field is called `version_req`, and the index
/// format requires it to be renamed to `req`
#[serde(alias = "version_req")]
pub req: semver::VersionReq,
#[serde(borrow)]
pub features: Vec<String>,
pub optional: bool,
pub default_features: bool,
#[serde(borrow)]
pub target: Option<String>,
pub kind: DependencyKind,
#[serde(skip_serializing_if = "Option::is_none", borrow)]
pub registry: Option<String>,
}
/// Section in which this dependency was defined
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum DependencyKind {
/// Used at run time
Normal,
/// Used at build time, not available at run time
Build,
/// Not fetched and not used, except for when used direclty in a workspace
Dev,
}
fn extract_manifest_from_tar<R: Read>(rdr: R) -> Result<Option<String>, Error> {
let mut archive = tar::Archive::new(rdr);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
if path.ends_with("Cargo.toml.orig") {
let mut manifest_toml = String::new();
entry.read_to_string(&mut manifest_toml)?;
return Ok(Some(manifest_toml))
}
}
Ok(None)
}
fn extract_readme_from_tar<R: Read>(rdr: R, readme_path: &Path) -> Result<Option<String>, Error> {
let mut archive = tar::Archive::new(rdr);
for entry in archive.entries()? {
let mut entry = entry?;
let path = entry.path()?;
if path == readme_path || path.ends_with(readme_path) {
let mut out = String::new();
entry.read_to_string(&mut out)?;
return Ok(Some(out))
}
}
Ok(None)
}