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
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) |
|
} |
|
|
|
|
|
|