Skip to content

Commit 61a47fb

Browse files
committed
feat(package): handle replaced pkg_id
1 parent 3835126 commit 61a47fb

File tree

8 files changed

+125
-40
lines changed

8 files changed

+125
-40
lines changed

soar-cli/src/state.rs

+101-10
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@ use std::{
44
sync::{Arc, Mutex, RwLockReadGuard},
55
};
66

7+
use nu_ansi_term::Color::{Blue, Green, Magenta, Red};
78
use once_cell::sync::OnceCell;
8-
use rusqlite::Connection;
9+
use rusqlite::{params, Connection};
910
use soar_core::{
10-
config::{get_config, Config},
11+
config::{get_config, Config, Repository},
1112
constants::CORE_MIGRATIONS,
12-
database::{connection::Database, migration::MigrationManager},
13+
database::{
14+
connection::Database,
15+
migration::MigrationManager,
16+
models::FromRow,
17+
packages::{FilterCondition, PackageQueryBuilder},
18+
},
1319
error::{ErrorContext, SoarError},
1420
metadata::fetch_metadata,
1521
SoarResult,
1622
};
17-
use tracing::error;
23+
use tracing::{error, info};
24+
25+
use crate::utils::Colored;
1826

1927
#[derive(Clone)]
2028
pub struct AppState {
@@ -50,20 +58,103 @@ impl AppState {
5058
for repo in &self.inner.config.repositories {
5159
let repo_clone = repo.clone();
5260
let task = tokio::task::spawn(async move { fetch_metadata(repo_clone, force).await });
53-
tasks.push(task);
61+
tasks.push((task, repo));
5462
}
5563

56-
for task in tasks {
57-
if let Err(err) = task
64+
for (task, repo) in tasks {
65+
match task
5866
.await
5967
.map_err(|err| SoarError::Custom(format!("Join handle error: {}", err)))?
6068
{
61-
if !matches!(err, SoarError::FailedToFetchRemote(_)) {
62-
return Err(err);
69+
Ok(Some(etag)) => {
70+
self.validate_packages(&repo, &etag).await?;
71+
info!("[{}] Repository synced", Colored(Magenta, &repo.name));
72+
}
73+
Err(err) => {
74+
if !matches!(err, SoarError::FailedToFetchRemote(_)) {
75+
return Err(err);
76+
}
77+
error!("{err}");
6378
}
64-
error!("{err}");
79+
_ => {}
6580
};
6681
}
82+
83+
Ok(())
84+
}
85+
86+
async fn validate_packages(&self, repo: &Repository, etag: &str) -> SoarResult<()> {
87+
let core_db = self.core_db()?;
88+
let repo_name = repo.name.clone();
89+
90+
let repo_path = repo.get_path()?;
91+
let metadata_db = repo_path.join("metadata.db");
92+
93+
let repo_db = Arc::new(Mutex::new(Connection::open(&metadata_db)?));
94+
95+
let installed_packages = PackageQueryBuilder::new(core_db.clone())
96+
.where_and("repo_name", FilterCondition::Eq(repo_name.to_string()))
97+
.load_installed()?;
98+
99+
struct RepoPackage {
100+
pkg_id: String,
101+
}
102+
103+
impl FromRow for RepoPackage {
104+
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
105+
Ok(Self {
106+
pkg_id: row.get("pkg_id")?,
107+
})
108+
}
109+
}
110+
111+
for pkg in installed_packages.items {
112+
let repo_package: Vec<RepoPackage> = PackageQueryBuilder::new(repo_db.clone())
113+
.select(&["pkg_id"])
114+
.where_and("pkg_id", FilterCondition::Eq(pkg.pkg_id.clone()))
115+
.where_and("repo_name", FilterCondition::Eq(pkg.repo_name.clone()))
116+
.load()?
117+
.items;
118+
119+
if repo_package.is_empty() {
120+
let replaced_by: Vec<RepoPackage> = PackageQueryBuilder::new(repo_db.clone())
121+
.select(&["pkg_id"])
122+
.where_and("repo_name", FilterCondition::Eq(pkg.repo_name))
123+
// there's no easy way to do this, could create scalar SQL
124+
// function, but this is enough for now
125+
.where_and(
126+
&format!("EXISTS (SELECT 1 FROM json_each(p.replaces) WHERE json_each.value = '{}')", pkg.pkg_id),
127+
FilterCondition::None,
128+
)
129+
.limit(1)
130+
.load()?
131+
.items;
132+
133+
if !replaced_by.is_empty() {
134+
let new_pkg_id = &replaced_by.first().unwrap().pkg_id;
135+
info!(
136+
"[{}] {} is replaced by {} in {}",
137+
Colored(Blue, "Note"),
138+
Colored(Red, &pkg.pkg_id),
139+
Colored(Green, new_pkg_id),
140+
Colored(Magenta, &repo_name)
141+
);
142+
143+
let conn = core_db.lock()?;
144+
conn.execute(
145+
"UPDATE packages SET pkg_id = ? WHERE pkg_id = ? AND repo_name = ?",
146+
params![new_pkg_id, pkg.pkg_id, repo_name],
147+
)?;
148+
}
149+
}
150+
}
151+
152+
let conn = repo_db.lock()?;
153+
conn.execute(
154+
"UPDATE repository SET name = ?, etag = ?",
155+
params![repo.name, etag],
156+
)?;
157+
67158
Ok(())
68159
}
69160

soar-core/migrations/metadata/V1_initial.sql

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ CREATE TABLE packages (
5858
provides JSONB,
5959
snapshots JSONB,
6060
repology JSONB,
61+
replaces JSONB,
6162
download_count INTEGER,
6263
download_count_week INTEGER,
6364
download_count_month INTEGER,

soar-core/src/database/connection.rs

+2-7
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,15 @@ impl Database {
4242
Ok(Database { conn })
4343
}
4444

45-
pub fn from_remote_metadata(
46-
&self,
47-
metadata: &[RemotePackage],
48-
repo_name: &str,
49-
etag: &str,
50-
) -> Result<()> {
45+
pub fn from_remote_metadata(&self, metadata: &[RemotePackage], repo_name: &str) -> Result<()> {
5146
let mut guard = self.conn.lock().unwrap();
5247
let _: String = guard.query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))?;
5348

5449
let tx = guard.transaction()?;
5550
{
5651
let statements = DbStatements::new(&tx)?;
5752
let mut repo = PackageRepository::new(&tx, statements, repo_name);
58-
repo.import_packages(&metadata, etag)?;
53+
repo.import_packages(&metadata)?;
5954
}
6055
tx.commit()?;
6156
Ok(())

soar-core/src/database/models.rs

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ pub struct Package {
8181
pub download_count_month: Option<u64>,
8282
pub download_count_week: Option<u64>,
8383
pub maintainers: Option<Vec<Maintainer>>,
84+
pub replaces: Option<Vec<String>>,
8485
}
8586

8687
impl FromRow for Package {
@@ -111,6 +112,7 @@ impl FromRow for Package {
111112
let provides = parse_provides("provides")?;
112113
let snapshots = parse_json_vec("snapshots")?;
113114
let repology = parse_json_vec("repology")?;
115+
let replaces = parse_json_vec("replaces")?;
114116

115117
Ok(Package {
116118
id: row.get("id")?,
@@ -158,6 +160,7 @@ impl FromRow for Package {
158160
download_count_month: row.get("download_count_month")?,
159161
repo_name: row.get("repo_name")?,
160162
maintainers,
163+
replaces,
161164
})
162165
}
163166
}
@@ -384,6 +387,7 @@ pub struct RemotePackage {
384387

385388
pub repology: Option<Vec<String>>,
386389
pub snapshots: Option<Vec<String>>,
390+
pub replaces: Option<Vec<String>>,
387391
}
388392

389393
fn should_create_original_symlink_impl(provides: Option<&Vec<PackageProvide>>) -> bool {

soar-core/src/database/packages/query.rs

+1
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ impl PackageQueryBuilder {
240240
"json(provides) AS provides",
241241
"json(snapshots) AS snapshots",
242242
"json(repology) AS repology",
243+
"json(replaces) AS replaces",
243244
"download_count",
244245
"download_count_week",
245246
"download_count_month",

soar-core/src/database/repository.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ impl<'a> PackageRepository<'a> {
1818
}
1919
}
2020

21-
pub fn import_packages(&mut self, metadata: &[RemotePackage], etag: &str) -> Result<()> {
21+
pub fn import_packages(&mut self, metadata: &[RemotePackage]) -> Result<()> {
2222
self.statements
2323
.repo_insert
24-
.execute(params![self.repo_name, etag])?;
24+
// to prevent incomplete sync, etag should only be updated once
25+
// all checks are done
26+
.execute(params![self.repo_name, ""])?;
2527

2628
for package in metadata {
2729
self.insert_package(package)?;
@@ -64,6 +66,7 @@ impl<'a> PackageRepository<'a> {
6466
let categories = serde_json::to_string(&package.categories).unwrap();
6567
let snapshots = serde_json::to_string(&package.snapshots).unwrap();
6668
let repology = serde_json::to_string(&package.repology).unwrap();
69+
let replaces = serde_json::to_string(&package.replaces).unwrap();
6770

6871
let provides = package.provides.clone().map(|vec| {
6972
vec.iter()
@@ -121,6 +124,7 @@ impl<'a> PackageRepository<'a> {
121124
provides,
122125
snapshots,
123126
repology,
127+
replaces,
124128
package.download_count,
125129
package.download_count_week,
126130
package.download_count_month

soar-core/src/database/statements.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ impl<'a> DbStatements<'a> {
3434
bsum, shasum, icon, desktop, appstream, homepages, notes,
3535
source_urls, tags, categories, build_id, build_date,
3636
build_action, build_script, build_log, provides, snapshots,
37-
repology, download_count, download_count_week,
37+
repology, replaces, download_count, download_count_week,
3838
download_count_month
3939
)
4040
VALUES
4141
(
4242
?1, jsonb(?2), ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13,
4343
jsonb(?14), ?15, ?16, ?17, ?18, jsonb(?19), ?20, ?21, ?22, ?23, ?24, ?25,
4444
?26, jsonb(?27), jsonb(?28), jsonb(?29), jsonb(?30), jsonb(?31), ?32, ?33, ?34, ?35, ?36,
45-
jsonb(?37), jsonb(?38), jsonb(?39), ?40, ?41, ?42
45+
jsonb(?37), jsonb(?38), jsonb(?39), jsonb(?40), ?41, ?42, ?43
4646
)
4747
ON CONFLICT (pkg_id, pkg_name, version) DO NOTHING",
4848
)?,

soar-core/src/metadata.rs

+8-19
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66

77
use futures::TryStreamExt;
88
use reqwest::header::{self, HeaderMap};
9-
use rusqlite::{params, Connection};
9+
use rusqlite::Connection;
1010
use tracing::info;
1111

1212
use crate::{
@@ -22,7 +22,6 @@ fn handle_json_metadata<P: AsRef<Path>>(
2222
metadata: &[RemotePackage],
2323
metadata_db: P,
2424
repo: &Repository,
25-
etag: &str,
2625
) -> SoarResult<()> {
2726
let metadata_db = metadata_db.as_ref();
2827
if metadata_db.exists() {
@@ -35,7 +34,7 @@ fn handle_json_metadata<P: AsRef<Path>>(
3534
manager.migrate_from_dir(METADATA_MIGRATIONS)?;
3635

3736
let db = Database::new(metadata_db)?;
38-
db.from_remote_metadata(metadata.as_ref(), &repo.name, &etag)?;
37+
db.from_remote_metadata(metadata.as_ref(), &repo.name)?;
3938

4039
Ok(())
4140
}
@@ -69,7 +68,7 @@ pub async fn fetch_public_key<P: AsRef<Path>>(
6968
Ok(())
7069
}
7170

72-
pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<()> {
71+
pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<Option<String>> {
7372
let repo_path = repo.get_path()?;
7473
let metadata_db = repo_path.join("metadata.db");
7574

@@ -91,7 +90,7 @@ pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<()> {
9190
.with_context(|| format!("reading file metadata from {}", metadata_db.display()))?;
9291
if let Ok(created) = file_info.created() {
9392
if repo.sync_interval() >= created.elapsed()?.as_millis() as u128 {
94-
return Ok(());
93+
return Ok(None);
9594
}
9695
}
9796
}
@@ -118,7 +117,7 @@ pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<()> {
118117
Some(remote_etag) => {
119118
let remote_etag = remote_etag.to_str().unwrap();
120119
if !force && etag == remote_etag {
121-
return Ok(());
120+
return Ok(None);
122121
}
123122
remote_etag.to_string()
124123
}
@@ -153,11 +152,6 @@ pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<()> {
153152
if magic_bytes == SQLITE_MAGIC_BYTES {
154153
fs::rename(&tmp_path, &metadata_db)
155154
.with_context(|| format!("renaming {} to {}", tmp_path, metadata_db.display()))?;
156-
let conn = Connection::open(&metadata_db)?;
157-
conn.execute(
158-
"UPDATE repository SET name = ?, etag = ?",
159-
params![repo.name, etag],
160-
)?;
161155
} else {
162156
let tmp_file = File::open(&tmp_path)
163157
.with_context(|| format!("opening temporary file {}", tmp_path))?;
@@ -169,7 +163,7 @@ pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<()> {
169163
))
170164
})?;
171165

172-
handle_json_metadata(&metadata, metadata_db, &repo, &etag)?;
166+
handle_json_metadata(&metadata, metadata_db, &repo)?;
173167
fs::remove_file(tmp_path.clone())
174168
.with_context(|| format!("removing temporary file {}", tmp_path))?;
175169
}
@@ -181,11 +175,6 @@ pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<()> {
181175
writer
182176
.write_all(&content)
183177
.with_context(|| format!("writing to metadata file {}", metadata_db.display()))?;
184-
let conn = Connection::open(&metadata_db)?;
185-
conn.execute(
186-
"UPDATE repository SET name = ?, etag = ?",
187-
params![repo.name, etag],
188-
)?;
189178
} else {
190179
let remote_metadata: Vec<RemotePackage> =
191180
serde_json::from_slice(&content).map_err(|err| {
@@ -195,8 +184,8 @@ pub async fn fetch_metadata(repo: Repository, force: bool) -> SoarResult<()> {
195184
))
196185
})?;
197186

198-
handle_json_metadata(&remote_metadata, metadata_db, &repo, &etag)?;
187+
handle_json_metadata(&remote_metadata, metadata_db, &repo)?;
199188
}
200189

201-
Ok(())
190+
Ok(Some(etag))
202191
}

0 commit comments

Comments
 (0)