2024-12-11 09:42:06 +01:00
|
|
|
use crate::ides::IdeVersion;
|
|
|
|
|
use anyhow::anyhow;
|
|
|
|
|
use base64::prelude::BASE64_STANDARD;
|
|
|
|
|
use base64::Engine;
|
|
|
|
|
use futures::stream::iter;
|
|
|
|
|
use futures::{StreamExt, TryStreamExt};
|
|
|
|
|
use lazy_static::lazy_static;
|
|
|
|
|
use log::{debug, info, warn};
|
|
|
|
|
use reqwest::{Client, StatusCode};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::borrow::Cow;
|
|
|
|
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
|
|
|
use std::fs::exists;
|
|
|
|
|
use std::future;
|
2024-12-11 12:01:46 +01:00
|
|
|
use std::mem::take;
|
2024-12-11 09:42:06 +01:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::process::Stdio;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::time::Duration;
|
2024-12-11 12:01:46 +01:00
|
|
|
use tokio::fs::{read_dir, read_to_string, write};
|
2024-12-11 09:42:06 +01:00
|
|
|
use tokio::process::Command;
|
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
use tokio::time::timeout;
|
|
|
|
|
use tokio_retry2::strategy::ExponentialBackoff;
|
|
|
|
|
use tokio_retry2::{Retry, RetryError};
|
2024-12-11 12:01:46 +01:00
|
|
|
use tokio_stream::wrappers::ReadDirStream;
|
2024-12-11 09:42:06 +01:00
|
|
|
use version_compare::Version;
|
|
|
|
|
use which::which;
|
|
|
|
|
|
|
|
|
|
const ALL_PLUGINS_JSON: &str = "all_plugins.json";
|
|
|
|
|
|
|
|
|
|
lazy_static! {
|
|
|
|
|
static ref NIX_PREFETCH_URL: PathBuf =
|
|
|
|
|
which("nix-prefetch-url").expect("nix-prefetch-url not in PATH");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialOrd, PartialEq, Ord, Eq, Hash)]
|
|
|
|
|
pub struct PluginVersion(String);
|
|
|
|
|
|
|
|
|
|
impl PluginVersion {
|
|
|
|
|
const SEPARATOR: &'static str = "/--/";
|
|
|
|
|
pub fn new(name: &str, version: &str) -> Self {
|
|
|
|
|
Self(format!("{}{}{}", name, Self::SEPARATOR, version))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Plugins for which download requests have 404ed
|
|
|
|
|
type FourOFourCache = HashSet<PluginVersion>;
|
|
|
|
|
|
|
|
|
|
pub struct PluginDb {
|
|
|
|
|
// all_plugins caches all entries, ides contains references to them.
|
|
|
|
|
all_plugins: BTreeMap<PluginVersion, &'static PluginDbEntry>,
|
|
|
|
|
ides: HashMap<IdeVersion, BTreeMap<String, String>>,
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 12:01:46 +01:00
|
|
|
impl PluginDb {
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
all_plugins: Default::default(),
|
|
|
|
|
ides: Default::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init(init: impl IntoIterator<Item = (PluginVersion, PluginDbEntry)>) -> PluginDb {
|
|
|
|
|
Self {
|
|
|
|
|
// see insert on why we do this
|
|
|
|
|
all_plugins: init
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(k, v)| {
|
|
|
|
|
// see insert on why we do this
|
|
|
|
|
let v: &'static _ = Box::leak(Box::new(v));
|
|
|
|
|
(k, v)
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
ides: Default::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn insert(
|
|
|
|
|
&mut self,
|
|
|
|
|
ideversion: &IdeVersion,
|
|
|
|
|
name: &str,
|
|
|
|
|
version: &str,
|
|
|
|
|
entry: &PluginDbEntry,
|
|
|
|
|
) {
|
|
|
|
|
let version_entry = self.ides.entry(ideversion.clone()).or_default();
|
|
|
|
|
// We leak here since self-referential structs are otherwise a nightmare and it doesn't
|
|
|
|
|
// really matter in this CLI app.
|
|
|
|
|
self.all_plugins
|
|
|
|
|
.entry(PluginVersion::new(name, version))
|
|
|
|
|
.or_insert_with(|| Box::leak(Box::new(entry.clone())));
|
|
|
|
|
version_entry.insert(name.to_string(), version.to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 09:42:06 +01:00
|
|
|
#[derive(Debug, PartialEq, Deserialize)]
|
|
|
|
|
pub struct PluginDetails {
|
|
|
|
|
category: Option<PluginDetailsCategory>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Deserialize)]
|
|
|
|
|
pub struct PluginDetailsCategory {
|
|
|
|
|
#[serde(rename = "idea-plugin")]
|
|
|
|
|
idea_plugin: Vec<PluginDetailsIdeaPlugin>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Deserialize)]
|
|
|
|
|
pub struct PluginDetailsIdeaPlugin {
|
|
|
|
|
version: String,
|
|
|
|
|
#[serde(rename = "idea-version")]
|
|
|
|
|
idea_version: PluginDetailsIdeaVersion,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Deserialize)]
|
|
|
|
|
pub struct PluginDetailsIdeaVersion {
|
|
|
|
|
#[serde(rename = "since-build")]
|
|
|
|
|
since_build: Option<String>,
|
|
|
|
|
#[serde(rename = "until-build")]
|
|
|
|
|
until_build: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PluginDbEntry {
|
|
|
|
|
#[serde(rename = "p")]
|
|
|
|
|
pub path: String,
|
|
|
|
|
#[serde(rename = "h")]
|
|
|
|
|
pub hash: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn index(url: &str) -> anyhow::Result<Vec<String>> {
|
|
|
|
|
Ok(reqwest::get(url).await?.json().await?)
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 12:01:46 +01:00
|
|
|
/// Load the plugin database, all_plugins.json only!
|
|
|
|
|
pub async fn db_load(out_dir: &Path) -> anyhow::Result<PluginDb> {
|
2024-12-11 09:42:06 +01:00
|
|
|
let file = out_dir.join(ALL_PLUGINS_JSON);
|
|
|
|
|
if exists(&file)? {
|
2024-12-11 12:01:46 +01:00
|
|
|
Ok(PluginDb::init(serde_json::from_str::<'_, HashMap<_, _>>(
|
|
|
|
|
&read_to_string(file).await?,
|
|
|
|
|
)?))
|
2024-12-11 09:42:06 +01:00
|
|
|
} else {
|
2024-12-11 12:01:46 +01:00
|
|
|
Ok(PluginDb::new())
|
2024-12-11 09:42:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 12:01:46 +01:00
|
|
|
/// Load the plugin database, including the IDE mappings.
|
|
|
|
|
/// WARNING: Does not populate build numbers for IDEs!
|
|
|
|
|
pub async fn db_load_full(out_dir: &Path) -> anyhow::Result<PluginDb> {
|
|
|
|
|
let mut db = db_load(out_dir).await?;
|
|
|
|
|
let db_mut = Arc::new(RwLock::new(&mut db));
|
|
|
|
|
|
|
|
|
|
ReadDirStream::new(read_dir(out_dir.join("ides")).await?)
|
|
|
|
|
.and_then(|file| {
|
|
|
|
|
let db_mut = db_mut.clone();
|
|
|
|
|
async move {
|
|
|
|
|
let Some(ideversion) =
|
2024-12-11 12:04:29 +01:00
|
|
|
IdeVersion::from_json_filename(&file.file_name().to_string_lossy())
|
2024-12-11 12:01:46 +01:00
|
|
|
else {
|
|
|
|
|
warn!(
|
|
|
|
|
"Invalid JSON file in ide directory skipped: {}",
|
|
|
|
|
file.path().display()
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
let ide_mapping: BTreeMap<String, String> =
|
|
|
|
|
serde_json::from_str(&read_to_string(file.path()).await?)?;
|
|
|
|
|
let mut lck = db_mut.write().await;
|
|
|
|
|
let db_mut = &mut *lck;
|
|
|
|
|
db_mut.ides.insert(ideversion, ide_mapping);
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.try_collect::<()>()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(db)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn db_update(
|
|
|
|
|
db: &mut PluginDb,
|
2024-12-11 09:42:06 +01:00
|
|
|
ides: &[IdeVersion],
|
|
|
|
|
pluginkeys: &[String],
|
2024-12-11 12:01:46 +01:00
|
|
|
) -> anyhow::Result<()> {
|
2024-12-11 09:42:06 +01:00
|
|
|
let client = Arc::new(
|
|
|
|
|
Client::builder()
|
|
|
|
|
.timeout(Duration::from_secs(600))
|
|
|
|
|
.build()?,
|
|
|
|
|
);
|
|
|
|
|
let fof_cache = Arc::new(RwLock::new(FourOFourCache::new()));
|
2024-12-11 12:01:46 +01:00
|
|
|
let db = Arc::new(RwLock::new(db));
|
2024-12-11 09:42:06 +01:00
|
|
|
|
|
|
|
|
let mut futures = Vec::new();
|
|
|
|
|
|
|
|
|
|
for pluginkey in pluginkeys {
|
|
|
|
|
let fof_cache = fof_cache.clone();
|
|
|
|
|
let db = db.clone();
|
|
|
|
|
let client = client.clone();
|
|
|
|
|
|
|
|
|
|
// Create a future that will be retried 3 times, has a timeout of 1200 seconds per try
|
|
|
|
|
// and polls process_plugin to process this plugin for this IDE version. process_plugin
|
|
|
|
|
// will update the database.
|
|
|
|
|
futures.push(async move {
|
|
|
|
|
Retry::spawn(ExponentialBackoff::from_millis(250).take(3), move || {
|
|
|
|
|
let fof_cache = fof_cache.clone();
|
|
|
|
|
let db = db.clone();
|
|
|
|
|
let client = client.clone();
|
|
|
|
|
async move {
|
|
|
|
|
let res = timeout(
|
|
|
|
|
Duration::from_secs(1200),
|
|
|
|
|
process_plugin(
|
|
|
|
|
db.clone(),
|
|
|
|
|
client.clone(),
|
|
|
|
|
ides,
|
|
|
|
|
pluginkey,
|
|
|
|
|
fof_cache.clone(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
match res {
|
|
|
|
|
Ok(Ok(v)) => Ok(v),
|
|
|
|
|
Ok(Err(e)) => {
|
|
|
|
|
warn!("failed plugin processing {pluginkey}: {e}. Might retry.");
|
|
|
|
|
Err(RetryError::transient(e))
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
|
|
|
|
"failed plugin processing {pluginkey} due to timeout. Might retry."
|
|
|
|
|
);
|
|
|
|
|
Err(RetryError::transient(anyhow!("timeout").context(e)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iter(futures)
|
|
|
|
|
.buffered(16)
|
|
|
|
|
// TODO: try_collect does not exit early. try_all does. Is there any better way to do this?
|
|
|
|
|
.try_all(|()| future::ready(true))
|
|
|
|
|
.await?;
|
|
|
|
|
|
2024-12-11 12:01:46 +01:00
|
|
|
Ok(())
|
2024-12-11 09:42:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Various hacks to support (or skip) some very odd cases
|
|
|
|
|
fn hacks_for_details_key(pluginkey: &str) -> Option<&str> {
|
|
|
|
|
match pluginkey {
|
|
|
|
|
// The former is the real ID, but it trips up the plugin endpoint...
|
|
|
|
|
"23.bytecode-disassembler" => Some("bytecode-disassembler"),
|
|
|
|
|
// Has invalid version numbers
|
|
|
|
|
"com.valord577.mybatis-navigator" => None,
|
|
|
|
|
// ZIP contains invalid file names
|
|
|
|
|
"io.github.kings1990.FastRequest" => None,
|
|
|
|
|
v => Some(v),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn process_plugin(
|
2024-12-11 12:01:46 +01:00
|
|
|
db: Arc<RwLock<&mut PluginDb>>,
|
2024-12-11 09:42:06 +01:00
|
|
|
client: Arc<Client>,
|
|
|
|
|
ides: &[IdeVersion],
|
|
|
|
|
pluginkey: &str,
|
|
|
|
|
fof_cache: Arc<RwLock<FourOFourCache>>,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
debug!("Processing {pluginkey}...");
|
|
|
|
|
|
|
|
|
|
let Some(pluginkey_for_details) = hacks_for_details_key(pluginkey) else {
|
|
|
|
|
warn!("{pluginkey}: plugin is marked as broken, skipping...");
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let req = client
|
|
|
|
|
.get(format!(
|
|
|
|
|
"https://plugins.jetbrains.com/plugins/list?pluginId={}",
|
|
|
|
|
pluginkey_for_details
|
|
|
|
|
))
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
if !req.status().is_success() {
|
|
|
|
|
return Err(anyhow!(
|
|
|
|
|
"{} failed details request: {}",
|
|
|
|
|
pluginkey,
|
|
|
|
|
req.status()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
let details: PluginDetails = serde_xml_rs::from_str(&req.text().await?)?;
|
|
|
|
|
|
|
|
|
|
let Some(category) = details.category else {
|
|
|
|
|
warn!("{pluginkey}: No plugin details available. Skipping!");
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut versions = category.idea_plugin;
|
|
|
|
|
versions.sort_by(|a, b| b.version.cmp(&a.version));
|
|
|
|
|
|
|
|
|
|
for ide in ides {
|
|
|
|
|
match supported_version(ide, &versions) {
|
|
|
|
|
None => warn!("{pluginkey}: IDE {ide:?} not supported."),
|
|
|
|
|
Some(version) => {
|
|
|
|
|
let entry =
|
2024-12-11 12:01:46 +01:00
|
|
|
get_db_entry(&client, pluginkey, &version.version, &db, &fof_cache).await?;
|
2024-12-11 09:42:06 +01:00
|
|
|
if let Some(entry) = entry {
|
|
|
|
|
let mut lck = db.write().await;
|
2024-12-11 12:01:46 +01:00
|
|
|
let db_mut = &mut *lck;
|
|
|
|
|
db_mut.insert(ide, pluginkey, &version.version, &entry);
|
2024-12-11 09:42:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn supported_version<'a>(
|
|
|
|
|
ide: &IdeVersion,
|
|
|
|
|
versions: &'a Vec<PluginDetailsIdeaPlugin>,
|
|
|
|
|
) -> Option<&'a PluginDetailsIdeaPlugin> {
|
|
|
|
|
let build_version = Version::from(&ide.build_number).unwrap();
|
|
|
|
|
for version in versions {
|
|
|
|
|
if let Some(min) = version.idea_version.since_build.as_ref() {
|
|
|
|
|
if build_version < Version::from(&min.replace(".*", ".0")).unwrap() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(max) = version.idea_version.until_build.as_ref() {
|
|
|
|
|
if build_version > Version::from(&max.replace(".*", ".99999999")).unwrap() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Some(version);
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_db_entry<'a>(
|
|
|
|
|
client: &Client,
|
|
|
|
|
pluginkey: &str,
|
|
|
|
|
version: &str,
|
2024-12-11 12:01:46 +01:00
|
|
|
current_db: &RwLock<&mut PluginDb>,
|
2024-12-11 09:42:06 +01:00
|
|
|
fof_cache: &RwLock<FourOFourCache>,
|
|
|
|
|
) -> anyhow::Result<Option<Cow<'a, PluginDbEntry>>> {
|
|
|
|
|
let key = PluginVersion::new(pluginkey, version);
|
|
|
|
|
// Look in current_db
|
|
|
|
|
{
|
|
|
|
|
let db_lck = current_db.read().await;
|
|
|
|
|
let v = db_lck.all_plugins.get(&key);
|
|
|
|
|
if let Some(v) = v {
|
|
|
|
|
return Ok(Some(Cow::Borrowed(v)));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
if fof_cache.read().await.contains(&key) {
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info!(
|
|
|
|
|
"{}@{}: Plugin not yet cached, downloading for hash...",
|
|
|
|
|
pluginkey, version
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let req = client
|
|
|
|
|
.head(format!(
|
|
|
|
|
"https://plugins.jetbrains.com/plugin/download?pluginId={}&version={}",
|
|
|
|
|
pluginkey, version
|
|
|
|
|
))
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if req.status() == StatusCode::NOT_FOUND {
|
|
|
|
|
warn!("{}@{}: not available: skipping", pluginkey, version);
|
|
|
|
|
fof_cache.write().await.insert(key);
|
|
|
|
|
return Ok(None);
|
|
|
|
|
} else if !req.status().is_success() {
|
|
|
|
|
return Err(anyhow!(
|
|
|
|
|
"{}@{}: failed download HEAD request: {}",
|
|
|
|
|
pluginkey,
|
|
|
|
|
version,
|
|
|
|
|
req.status()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PREFIX_OF_ALL_URLS: &str = "https://downloads.marketplace.jetbrains.com/";
|
|
|
|
|
// Query parameters don't seem to result in different files, probably only for analytics.
|
|
|
|
|
// Remove them to save some space.
|
|
|
|
|
// Also remove the https://downloads.marketplace.jetbrains.com/ prefix.
|
|
|
|
|
let mut url = req.url().clone();
|
|
|
|
|
url.set_query(None);
|
|
|
|
|
let url = url.to_string();
|
|
|
|
|
|
|
|
|
|
let is_jar = url.ends_with(".jar");
|
|
|
|
|
let hash_nix32 = get_nix32_hash(
|
|
|
|
|
&format!("{pluginkey}-{version}-source").replace(|c: char| !c.is_alphanumeric(), "-"),
|
|
|
|
|
&url,
|
|
|
|
|
!is_jar,
|
|
|
|
|
is_jar,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
let hash = BASE64_STANDARD.encode(
|
|
|
|
|
nix_base32::from_nix_base32(&hash_nix32)
|
|
|
|
|
.ok_or_else(|| anyhow!("{}@{}: failed decoding nix hash", pluginkey, version,))?,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let path = url
|
|
|
|
|
.strip_prefix(PREFIX_OF_ALL_URLS)
|
|
|
|
|
.expect("expect all URLs to start with prefix.")
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
Ok(Some(Cow::Owned(PluginDbEntry { path, hash })))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_nix32_hash(
|
|
|
|
|
name: &str,
|
|
|
|
|
url: &str,
|
|
|
|
|
unpack: bool,
|
|
|
|
|
executable: bool,
|
|
|
|
|
) -> anyhow::Result<String> {
|
|
|
|
|
let mut parameters = Vec::with_capacity(8);
|
|
|
|
|
parameters.push("--type");
|
|
|
|
|
parameters.push("sha256");
|
|
|
|
|
parameters.push("--name");
|
|
|
|
|
parameters.push(name);
|
|
|
|
|
if unpack {
|
|
|
|
|
parameters.push("--unpack");
|
|
|
|
|
}
|
|
|
|
|
if executable {
|
|
|
|
|
parameters.push("--executable");
|
|
|
|
|
}
|
|
|
|
|
parameters.push(url);
|
|
|
|
|
|
|
|
|
|
let child = Command::new(&*NIX_PREFETCH_URL)
|
|
|
|
|
.args(parameters)
|
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
|
.kill_on_drop(true)
|
|
|
|
|
.spawn()?;
|
|
|
|
|
|
|
|
|
|
let result = child.wait_with_output().await?;
|
|
|
|
|
if !result.status.success() {
|
|
|
|
|
return Err(anyhow!("nix-prefetch-url failed for {url}"));
|
|
|
|
|
}
|
|
|
|
|
let out = String::from_utf8(result.stdout)?.trim().to_string();
|
|
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn db_save(output_folder: &Path, db: PluginDb) -> anyhow::Result<()> {
|
|
|
|
|
// all plugins
|
|
|
|
|
let out_path = output_folder.join(ALL_PLUGINS_JSON);
|
|
|
|
|
debug!("Generating {out_path:?}...");
|
|
|
|
|
write(out_path, serde_json::to_string_pretty(&db.all_plugins)?).await?;
|
|
|
|
|
|
|
|
|
|
// mappings
|
|
|
|
|
let output_folder = output_folder.join("ides");
|
|
|
|
|
for (ide, plugins) in db.ides {
|
2024-12-11 12:01:46 +01:00
|
|
|
let out_path = output_folder.join(ide.to_json_filename());
|
2024-12-11 09:42:06 +01:00
|
|
|
debug!("Generating {out_path:?}...");
|
|
|
|
|
write(out_path, serde_json::to_string_pretty(&plugins)?).await?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-12-11 12:01:46 +01:00
|
|
|
|
|
|
|
|
pub async fn db_cleanup(db: &mut PluginDb) -> anyhow::Result<()> {
|
2024-12-11 12:04:29 +01:00
|
|
|
let used_keys: HashSet<_> = db
|
2024-12-11 12:01:46 +01:00
|
|
|
.ides
|
|
|
|
|
.values()
|
|
|
|
|
.flat_map(|ides| {
|
|
|
|
|
ides.iter()
|
|
|
|
|
.map(|(name, version)| PluginVersion::new(name, version))
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
db.all_plugins = take(&mut db.all_plugins)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter(|(k, _)| used_keys.contains(k))
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|