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.
225 lines
7.0 KiB
225 lines
7.0 KiB
|
2 years ago
|
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)
|
||
|
|
}
|
||
|
|
|
||
|
|
|