fix & chore: handle hostname/URL cleaning within trait implementation

SUMMARY
    Renamed function parameters from "hostname" to "url" wherever a
    url::Url is received

    db_core::get_hostname is renamed to db_core::clean_url, better
    describing its new implementation

    forge_core::get_hostname is renamed to forge_core::get_url to better
    describe its new implementation

URL PROCESSING METHODS

    federate/federate-core
	Parses URL and returns only the hostname

    db/db-core
	Parses URL, cleans it by removing path and query parameters and
	returns the end result
This commit is contained in:
Aravinth Manivannan 2022-07-14 23:47:37 +05:30
parent 29f5556586
commit fe8bd2fb26
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
17 changed files with 273 additions and 232 deletions

1
Cargo.lock generated
View file

@ -1018,6 +1018,7 @@ version = "0.1.0"
dependencies = [
"async-trait",
"db-core",
"url",
]
[[package]]

View file

@ -60,25 +60,29 @@ pub mod dev {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// create a new forge on the database
pub struct CreateForge<'a> {
/// hostname of the forge instance: with scheme but remove trailing slash
pub hostname: &'a str,
pub struct CreateForge {
/// url of the forge instance: with scheme but remove trailing slash
pub url: Url,
/// forge type: which software is the instance running?
pub forge_type: ForgeImplementation,
}
/// Get hostname from URL
/// Utility function for uniform hostname format
pub fn get_hostname(url: &Url) -> String {
url.host().as_ref().unwrap().to_string()
/// Get url from URL
/// Utility function for uniform url format
pub fn clean_url(url: &Url) -> String {
let mut url = url.clone();
url.set_path("");
url.set_query(None);
url.set_fragment(None);
url.as_str().to_string()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// user data
pub struct User {
/// hostname of the forge instance: with scheme but remove trailing slash
/// hostname can be derived from html_link also, but used to link to user's forge instance
pub hostname: String,
/// url of the forge instance: with scheme but remove trailing slash
/// url can be derived from html_link also, but used to link to user's forge instance
pub url: String,
/// username of the user
pub username: String,
/// html link to the user profile
@ -90,9 +94,9 @@ pub struct User {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// add new user to database
pub struct AddUser<'a> {
/// hostname of the forge instance: with scheme but remove trailing slash
/// hostname can be derived from html_link also, but used to link to user's forge instance
pub hostname: &'a str,
/// url of the forge instance: with scheme but remove trailing slash
/// url can be derived from html_link also, but used to link to user's forge instance
pub url: Url,
/// username of the user
pub username: &'a str,
/// html link to the user profile
@ -108,9 +112,9 @@ pub struct AddRepository<'a> {
pub html_link: &'a str,
/// repository topic tags
pub tags: Option<Vec<&'a str>>,
/// hostname of the forge instance: with scheme but remove trailing slash
/// hostname can be derived from html_link also, but used to link to user's forge instance
pub hostname: &'a str,
/// url of the forge instance: with scheme but remove trailing slash
/// url can be derived from html_link also, but used to link to user's forge instance
pub url: Url,
/// repository name
pub name: &'a str,
/// repository owner
@ -124,8 +128,8 @@ pub struct AddRepository<'a> {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// data representing a forge instance
pub struct Forge {
/// hostname of the forge
pub hostname: String,
/// url of the forge
pub url: String,
/// type of the forge
pub forge_type: ForgeImplementation,
/// last crawl
@ -139,9 +143,9 @@ pub struct Repository {
pub html_url: String,
/// repository topic tags
pub tags: Option<Vec<String>>,
/// hostname of the forge instance: with scheme but remove trailing slash
/// hostname can be derived from html_link also, but used to link to user's forge instance
pub hostname: String,
/// url of the forge instance: with scheme but remove trailing slash
/// url can be derived from html_link also, but used to link to user's forge instance
pub url: String,
/// repository name
pub name: String,
/// repository owner
@ -155,8 +159,8 @@ pub struct Repository {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// represents a DNS challenge
pub struct Challenge {
/// hostname of the forge instance
pub hostname: String,
/// url of the forge instance
pub url: String,
/// key of TXT record
pub key: String,
/// value of TXT record
@ -186,13 +190,13 @@ pub trait SCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
async fn create_forge_instance(&self, f: &CreateForge) -> DBResult<()>;
/// get forge instance data
async fn get_forge(&self, hostname: &str) -> DBResult<Forge>;
async fn get_forge(&self, url: &Url) -> DBResult<Forge>;
/// delete forge instance
async fn delete_forge_instance(&self, hostname: &str) -> DBResult<()>;
async fn delete_forge_instance(&self, url: &Url) -> DBResult<()>;
/// check if a forge instance exists
async fn forge_exists(&self, hostname: &str) -> DBResult<bool>;
async fn forge_exists(&self, url: &Url) -> DBResult<bool>;
/// check if forge type exists
async fn forge_type_exists(&self, forge_type: &ForgeImplementation) -> DBResult<bool>;
@ -204,20 +208,20 @@ pub trait SCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
async fn add_user(&self, u: &AddUser) -> DBResult<()>;
/// get user data
async fn get_user(&self, username: &str, hostname: &str) -> DBResult<User>;
async fn get_user(&self, username: &str, url: &Url) -> DBResult<User>;
/// check if an user exists. When hostname of a forge instace is provided, username search is
/// check if an user exists. When url of a forge instace is provided, username search is
/// done only on that forge
async fn user_exists(&self, username: &str, hostname: Option<&str>) -> DBResult<bool>;
async fn user_exists(&self, username: &str, url: Option<&Url>) -> DBResult<bool>;
/// delete user
async fn delete_user(&self, username: &str, hostname: &str) -> DBResult<()>;
async fn delete_user(&self, username: &str, url: &Url) -> DBResult<()>;
/// delete repository
async fn delete_repository(&self, owner: &str, name: &str, hostname: &str) -> DBResult<()>;
async fn delete_repository(&self, owner: &str, name: &str, url: &Url) -> DBResult<()>;
/// check if a repository exists.
async fn repository_exists(&self, name: &str, owner: &str, hostname: &str) -> DBResult<bool>;
async fn repository_exists(&self, name: &str, owner: &str, url: &Url) -> DBResult<bool>;
/// Get all repositories
async fn get_all_repositories(&self, offset: u32, limit: u32) -> DBResult<Vec<Repository>>;

View file

@ -21,20 +21,20 @@ use crate::prelude::*;
/// adding forge works
pub async fn adding_forge_works<'a, T: SCDatabase>(
db: &T,
create_forge_msg: CreateForge<'a>,
create_forge_msg: CreateForge,
add_user_msg: AddUser<'a>,
add_user_msg2: AddUser<'a>,
add_repo_msg: AddRepository<'a>,
) {
let _ = db.delete_forge_instance(create_forge_msg.hostname).await;
let _ = db.delete_forge_instance(&create_forge_msg.url).await;
db.create_forge_instance(&create_forge_msg).await.unwrap();
assert!(
db.forge_exists(create_forge_msg.hostname).await.unwrap(),
"forge creation failed, forge exinstance check failure"
db.forge_exists(&create_forge_msg.url).await.unwrap(),
"forge creation failed, forge existence check failure"
);
{
let forge = db.get_forge(create_forge_msg.hostname).await.unwrap();
let forge = db.get_forge(&create_forge_msg.url).await.unwrap();
let forges = db.get_all_forges(0, 10).await.unwrap();
assert_eq!(forges.len(), 1);
@ -43,11 +43,11 @@ pub async fn adding_forge_works<'a, T: SCDatabase>(
create_forge_msg.forge_type
);
assert_eq!(
forges.get(0).as_ref().unwrap().hostname,
create_forge_msg.hostname
forges.get(0).as_ref().unwrap().url,
crate::clean_url(&create_forge_msg.url)
);
assert_eq!(forge.hostname, create_forge_msg.hostname);
assert_eq!(forge.url, crate::clean_url(&create_forge_msg.url));
assert_eq!(forge.forge_type, create_forge_msg.forge_type);
}
@ -56,10 +56,10 @@ pub async fn adding_forge_works<'a, T: SCDatabase>(
db.add_user(&add_user_msg2).await.unwrap();
{
let db_user = db
.get_user(add_user_msg.username, add_user_msg.hostname)
.get_user(add_user_msg.username, &add_user_msg.url)
.await
.unwrap();
assert_eq!(db_user.hostname, add_user_msg.hostname);
assert_eq!(db_user.url, crate::clean_url(&add_user_msg.url));
assert_eq!(db_user.username, add_user_msg.username);
assert_eq!(db_user.html_link, add_user_msg.html_link);
assert_eq!(
@ -70,7 +70,7 @@ pub async fn adding_forge_works<'a, T: SCDatabase>(
// verify user exists
assert!(db.user_exists(add_user_msg.username, None).await.unwrap());
assert!(db
.user_exists(add_user_msg.username, Some(add_user_msg.hostname))
.user_exists(add_user_msg.username, Some(&add_user_msg.url))
.await
.unwrap());
@ -78,24 +78,24 @@ pub async fn adding_forge_works<'a, T: SCDatabase>(
db.create_repository(&add_repo_msg).await.unwrap();
// verify repo exists
assert!(db
.repository_exists(add_repo_msg.name, add_repo_msg.owner, add_repo_msg.hostname)
.repository_exists(add_repo_msg.name, add_repo_msg.owner, &add_repo_msg.url)
.await
.unwrap());
// delete repository
db.delete_repository(add_repo_msg.owner, add_repo_msg.name, add_repo_msg.hostname)
db.delete_repository(add_repo_msg.owner, add_repo_msg.name, &add_repo_msg.url)
.await
.unwrap();
assert!(!db
.repository_exists(add_repo_msg.name, add_repo_msg.owner, add_repo_msg.hostname)
.repository_exists(add_repo_msg.name, add_repo_msg.owner, &add_repo_msg.url)
.await
.unwrap());
// delete user
db.delete_user(add_user_msg.username, add_user_msg.hostname)
db.delete_user(add_user_msg.username, &add_user_msg.url)
.await
.unwrap();
assert!(!db
.user_exists(add_user_msg.username, Some(add_user_msg.hostname))
.user_exists(add_user_msg.username, Some(&add_user_msg.url))
.await
.unwrap());
}

View file

@ -20,6 +20,7 @@ path = "src/lib.rs"
sqlx = { version = "0.5.11", features = [ "sqlite", "time", "offline", "runtime-actix-rustls" ] }
db-core = {path = "../db-core"}
async-trait = "0.1.51"
url = { version = "2.2.2", features = ["serde"] }
[dev-dependencies]
actix-rt = "2"

View file

@ -22,6 +22,7 @@ use db_core::dev::*;
use sqlx::sqlite::SqlitePool;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::time::OffsetDateTime;
use url::Url;
pub mod errors;
#[cfg(test)]
@ -112,26 +113,25 @@ impl SCDatabase for Database {
}
/// delete forge instance
async fn delete_forge_instance(&self, hostname: &str) -> DBResult<()> {
sqlx::query!(
"DELETE FROM starchart_forges WHERE hostname = ($1)",
hostname,
)
.execute(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
async fn delete_forge_instance(&self, url: &Url) -> DBResult<()> {
let url = db_core::clean_url(url);
sqlx::query!("DELETE FROM starchart_forges WHERE hostname = ($1)", url,)
.execute(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
Ok(())
}
/// create forge instance DB
async fn create_forge_instance(&self, f: &CreateForge) -> DBResult<()> {
let now = now_unix_time_stamp();
let url = db_core::clean_url(&f.url);
let forge_type = f.forge_type.to_str();
sqlx::query!(
"INSERT INTO
starchart_forges (hostname, verified_on, forge_type )
VALUES ($1, $2, (SELECT ID FROM starchart_forge_type WHERE name = $3))",
f.hostname,
url,
now,
forge_type,
)
@ -143,7 +143,8 @@ impl SCDatabase for Database {
}
/// get forge instance data
async fn get_forge(&self, hostname: &str) -> DBResult<Forge> {
async fn get_forge(&self, url: &Url) -> DBResult<Forge> {
let url = db_core::clean_url(url);
let f = sqlx::query_as!(
InnerForge,
"SELECT
@ -159,7 +160,7 @@ impl SCDatabase for Database {
WHERE
hostname = $1;
",
hostname,
url,
)
.fetch_one(&self.pool)
.await
@ -200,13 +201,11 @@ impl SCDatabase for Database {
}
/// check if a forge instance exists
async fn forge_exists(&self, hostname: &str) -> DBResult<bool> {
match sqlx::query!(
"SELECT ID FROM starchart_forges WHERE hostname = $1",
hostname
)
.fetch_one(&self.pool)
.await
async fn forge_exists(&self, url: &Url) -> DBResult<bool> {
let url = db_core::clean_url(url);
match sqlx::query!("SELECT ID FROM starchart_forges WHERE hostname = $1", url)
.fetch_one(&self.pool)
.await
{
Ok(_) => Ok(true),
Err(Error::RowNotFound) => Ok(false),
@ -232,6 +231,7 @@ impl SCDatabase for Database {
/// add new user to database
async fn add_user(&self, u: &AddUser) -> DBResult<()> {
let now = now_unix_time_stamp();
let url = db_core::clean_url(&u.url);
sqlx::query!(
"INSERT INTO
starchart_users (
@ -240,7 +240,7 @@ impl SCDatabase for Database {
)
VALUES (
(SELECT ID FROM starchart_forges WHERE hostname = $1), $2, $3, $4, $5, $6)",
u.hostname,
url,
u.username,
u.html_link,
u.profile_photo,
@ -255,46 +255,51 @@ impl SCDatabase for Database {
}
/// get user data
async fn get_user(&self, username: &str, hostname: &str) -> DBResult<User> {
async fn get_user(&self, username: &str, url: &Url) -> DBResult<User> {
struct InnerUser {
profile_photo_html_url: Option<String>,
html_url: String,
}
let url = db_core::clean_url(url);
let res = sqlx::query_as!(
InnerUser,
"SELECT html_url, profile_photo_html_url FROM starchart_users WHERE username = $1 AND
hostname_id = (SELECT ID FROM starchart_forges WHERE hostname = $2)",
username,
hostname,
url,
)
.fetch_one(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
Ok(User {
username: username.into(),
hostname: hostname.into(),
url: url.into(),
profile_photo: res.profile_photo_html_url,
html_link: res.html_url,
})
}
/// check if an user exists. When hostname of a forge instace is provided, username search is
/// check if an user exists. When url of a forge instace is provided, username search is
/// done only on that forge
async fn user_exists(&self, username: &str, hostname: Option<&str>) -> DBResult<bool> {
match hostname {
Some(hostname) => match sqlx::query!(
"SELECT ID FROM starchart_users WHERE username = $1 AND
async fn user_exists(&self, username: &str, url: Option<&Url>) -> DBResult<bool> {
match url {
Some(url) => {
let url = db_core::clean_url(url);
match sqlx::query!(
"SELECT ID FROM starchart_users WHERE username = $1 AND
hostname_id = (SELECT ID FROM starchart_forges WHERE hostname = $2)",
username,
hostname,
)
.fetch_one(&self.pool)
.await
{
Ok(_) => Ok(true),
Err(Error::RowNotFound) => Ok(false),
Err(e) => Err(DBError::DBError(Box::new(e).into())),
},
username,
url,
)
.fetch_one(&self.pool)
.await
{
Ok(_) => Ok(true),
Err(Error::RowNotFound) => Ok(false),
Err(e) => Err(DBError::DBError(Box::new(e).into())),
}
}
None => match sqlx::query!(
"SELECT ID FROM starchart_users WHERE username = $1",
username
@ -310,7 +315,8 @@ impl SCDatabase for Database {
}
/// check if a repo exists.
async fn repository_exists(&self, name: &str, owner: &str, hostname: &str) -> DBResult<bool> {
async fn repository_exists(&self, name: &str, owner: &str, url: &Url) -> DBResult<bool> {
let url = db_core::clean_url(url);
match sqlx::query!(
"SELECT ID FROM starchart_repositories
WHERE
@ -321,7 +327,7 @@ impl SCDatabase for Database {
hostname_id = (SELECT ID FROM starchart_forges WHERE hostname = $3)",
name,
owner,
hostname,
url,
)
.fetch_one(&self.pool)
.await
@ -336,6 +342,7 @@ impl SCDatabase for Database {
async fn create_repository(&self, r: &AddRepository) -> DBResult<()> {
// unimplemented!()
let now = now_unix_time_stamp();
let url = db_core::clean_url(&r.url);
sqlx::query!(
"INSERT INTO
starchart_repositories (
@ -346,7 +353,7 @@ impl SCDatabase for Database {
(SELECT ID FROM starchart_users WHERE username = $2),
$3, $4, $5, $6, $7, $8
);",
r.hostname,
url,
r.owner,
r.name,
r.description,
@ -389,12 +396,13 @@ impl SCDatabase for Database {
}
/// delete user
async fn delete_user(&self, username: &str, hostname: &str) -> DBResult<()> {
async fn delete_user(&self, username: &str, url: &Url) -> DBResult<()> {
let url = db_core::clean_url(url);
sqlx::query!(
" DELETE FROM starchart_users WHERE username = $1 AND
hostname_id = (SELECT ID FROM starchart_forges WHERE hostname = $2)",
username,
hostname
url
)
.execute(&self.pool)
.await
@ -403,7 +411,8 @@ impl SCDatabase for Database {
}
/// delete repository
async fn delete_repository(&self, owner: &str, name: &str, hostname: &str) -> DBResult<()> {
async fn delete_repository(&self, owner: &str, name: &str, url: &Url) -> DBResult<()> {
let url = db_core::clean_url(url);
sqlx::query!(
" DELETE FROM starchart_repositories
WHERE
@ -414,7 +423,7 @@ impl SCDatabase for Database {
hostname_id = (SELECT ID FROM starchart_forges WHERE hostname = $3)",
name,
owner,
hostname
url
)
.execute(&self.pool)
.await
@ -437,14 +446,24 @@ impl SCDatabase for Database {
}
async fn get_dns_challenge(&self, key: &str) -> DBResult<Challenge> {
struct InnerChallenge {
hostname: String,
key: String,
value: String,
}
let res = sqlx::query_as!(
Challenge,
InnerChallenge,
"SELECT key, value, hostname FROM starchart_dns_challenges WHERE key = $1",
key
)
.fetch_one(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
let res = Challenge {
url: res.hostname,
key: res.key,
value: res.value,
};
Ok(res)
}
@ -463,7 +482,7 @@ impl SCDatabase for Database {
"INSERT INTO
starchart_dns_challenges (hostname, value, key, created )
VALUES ($1, $2, $3, $4);",
challenge.hostname,
challenge.url,
challenge.value,
challenge.key,
now,
@ -480,8 +499,8 @@ impl SCDatabase for Database {
struct InnerRepository {
/// html link to the repository
pub html_url: String,
/// hostname of the forge instance: with scheme but remove trailing slash
/// hostname can be derived from html_link also, but used to link to user's forge instance
/// url of the forge instance: with scheme but remove trailing slash
/// url can be derived from html_link also, but used to link to user's forge instance
pub hostname: String,
/// repository name
pub name: String,
@ -554,7 +573,7 @@ LIMIT $1 OFFSET $2
};
res.push(Repository {
html_url: repo.html_url,
hostname: repo.hostname,
url: repo.hostname,
name: repo.name,
username: repo.username,
description: repo.description,
@ -580,7 +599,7 @@ struct InnerForge {
impl From<InnerForge> for Forge {
fn from(f: InnerForge) -> Self {
Self {
hostname: f.hostname,
url: f.hostname,
last_crawl_on: f.last_crawl_on,
forge_type: ForgeImplementation::from_str(&f.name).unwrap(),
}

View file

@ -25,7 +25,7 @@ use db_core::tests::*;
#[actix_rt::test]
async fn everything_works() {
const HOSTNAME: &str = "https://test-gitea.example.com";
const URL: &str = "https://test-gitea.example.com";
const HTML_PROFILE_URL: &str = "https://test-gitea.example.com/user1";
const HTML_PROFILE_PHOTO_URL_2: &str = "https://test-gitea.example.com/profile-photo/user2";
const USERNAME: &str = "user1";
@ -35,33 +35,35 @@ async fn everything_works() {
const HTML_REPO_URL: &str = "https://test-gitea.example.com/user1/starchart";
const TAGS: [&str; 3] = ["test", "starchart", "spider"];
let hostname = Url::parse(HOSTNAME).unwrap();
let hostname = get_hostname(&hostname);
let url = Url::parse(URL).unwrap();
let create_forge_msg = CreateForge {
hostname: &hostname,
url: url.clone(),
forge_type: ForgeImplementation::Gitea,
};
let add_user_msg = AddUser {
hostname: &hostname,
url: url.clone(),
html_link: HTML_PROFILE_URL,
profile_photo: None,
username: USERNAME,
};
let add_user_msg_2 = AddUser {
hostname: &hostname,
url: url.clone(),
html_link: HTML_PROFILE_PHOTO_URL_2,
profile_photo: Some(HTML_PROFILE_PHOTO_URL_2),
username: USERNAME2,
};
let url = env::var("SQLITE_DATABASE_URL").expect("Set SQLITE_DATABASE_URL env var");
let pool_options = SqlitePoolOptions::new().max_connections(2);
let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url });
let db = connection_options.connect().await.unwrap();
db.migrate().await.unwrap();
let db = {
let url = env::var("SQLITE_DATABASE_URL").expect("Set SQLITE_DATABASE_URL env var");
let pool_options = SqlitePoolOptions::new().max_connections(2);
let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url });
let db = connection_options.connect().await.unwrap();
db.migrate().await.unwrap();
db
};
let add_repo_msg = AddRepository {
html_link: HTML_REPO_URL,
@ -70,7 +72,7 @@ async fn everything_works() {
owner: USERNAME,
website: None,
description: None,
hostname: &hostname,
url,
};
adding_forge_works(

View file

@ -20,6 +20,7 @@ use std::path::PathBuf;
use std::result::Result;
use async_trait::async_trait;
use url::Url;
use db_core::prelude::*;
@ -37,16 +38,16 @@ pub trait Federate: Sync + Send {
async fn rm_util(&self, path: &Path) -> Result<(), Self::Error>;
/// create forge instance
async fn create_forge_instance(&self, f: &CreateForge<'_>) -> Result<(), Self::Error>;
async fn create_forge_instance(&self, f: &CreateForge) -> Result<(), Self::Error>;
/// delete forge instance
async fn delete_forge_instance(&self, hostname: &str) -> Result<(), Self::Error>;
async fn delete_forge_instance(&self, url: &Url) -> Result<(), Self::Error>;
/// check if a forge instance exists
async fn forge_exists(&self, hostname: &str) -> Result<bool, Self::Error>;
async fn forge_exists(&self, url: &Url) -> Result<bool, Self::Error>;
/// check if an user exists.
async fn user_exists(&self, username: &str, hostname: &str) -> Result<bool, Self::Error>;
async fn user_exists(&self, username: &str, url: &Url) -> Result<bool, Self::Error>;
/// create user instance
async fn create_user(&self, f: &AddUser<'_>) -> Result<(), Self::Error>;
@ -59,20 +60,24 @@ pub trait Federate: Sync + Send {
&self,
name: &str,
owner: &str,
hostname: &str,
url: &Url,
) -> Result<bool, Self::Error>;
/// delete user
async fn delete_user(&self, username: &str, hostname: &str) -> Result<(), Self::Error>;
async fn delete_user(&self, username: &str, url: &Url) -> Result<(), Self::Error>;
/// delete repository
async fn delete_repository(
&self,
owner: &str,
name: &str,
hostname: &str,
url: &Url,
) -> Result<(), Self::Error>;
/// publish results in tar ball
async fn tar(&self) -> Result<PathBuf, Self::Error>;
}
pub fn get_hostname(url: &Url) -> &str {
url.host_str().unwrap()
}

View file

@ -21,34 +21,34 @@ use crate::*;
/// adding forge works
pub async fn adding_forge_works<'a, T: Federate>(
ff: &T,
create_forge_msg: CreateForge<'a>,
create_forge_msg: CreateForge,
create_user_msg: AddUser<'a>,
add_repo_msg: AddRepository<'a>,
) {
let _ = ff.delete_forge_instance(create_forge_msg.hostname).await;
assert!(!ff.forge_exists(&create_forge_msg.hostname).await.unwrap());
let _ = ff.delete_forge_instance(&create_forge_msg.url).await;
assert!(!ff.forge_exists(&create_forge_msg.url).await.unwrap());
ff.create_forge_instance(&create_forge_msg).await.unwrap();
assert!(ff.forge_exists(&create_forge_msg.hostname).await.unwrap());
assert!(ff.forge_exists(&create_forge_msg.url).await.unwrap());
// add user
assert!(!ff
.user_exists(&create_user_msg.username, &create_user_msg.hostname)
.user_exists(&create_user_msg.username, &create_user_msg.url)
.await
.unwrap());
ff.create_user(&create_user_msg).await.unwrap();
assert!(ff
.user_exists(&create_user_msg.username, &create_user_msg.hostname)
.user_exists(&create_user_msg.username, &create_user_msg.url)
.await
.unwrap());
// add repository
assert!(!ff
.repository_exists(add_repo_msg.name, add_repo_msg.owner, add_repo_msg.hostname)
.repository_exists(add_repo_msg.name, add_repo_msg.owner, &add_repo_msg.url)
.await
.unwrap());
ff.create_repository(&add_repo_msg).await.unwrap();
assert!(ff
.repository_exists(add_repo_msg.name, add_repo_msg.owner, add_repo_msg.hostname)
.repository_exists(add_repo_msg.name, add_repo_msg.owner, &add_repo_msg.url)
.await
.unwrap());
@ -56,17 +56,17 @@ pub async fn adding_forge_works<'a, T: Federate>(
ff.tar().await.unwrap();
// delete repository
ff.delete_repository(add_repo_msg.owner, add_repo_msg.name, add_repo_msg.hostname)
ff.delete_repository(add_repo_msg.owner, add_repo_msg.name, &add_repo_msg.url)
.await
.unwrap();
// delete user
ff.delete_user(create_user_msg.username, create_user_msg.hostname)
ff.delete_user(create_user_msg.username, &create_user_msg.url)
.await
.unwrap();
// delete user
ff.delete_forge_instance(create_forge_msg.hostname)
ff.delete_forge_instance(&create_forge_msg.url)
.await
.unwrap();
}

View file

@ -20,6 +20,7 @@ use std::path::{Path, PathBuf};
use async_trait::async_trait;
use serde::Serialize;
use tokio::fs;
use url::Url;
use db_core::prelude::*;
@ -58,7 +59,8 @@ impl PccFederate {
Ok(path)
}
pub async fn get_instance_path(&self, hostname: &str, create_dirs: bool) -> FResult<PathBuf> {
pub async fn get_instance_path(&self, url: &Url, create_dirs: bool) -> FResult<PathBuf> {
let hostname = federate_core::get_hostname(url);
let path = self.get_content_path(false).await?.join(hostname);
if create_dirs {
self.create_dir_if_not_exists(&path).await?;
@ -69,13 +71,10 @@ impl PccFederate {
pub async fn get_user_path(
&self,
username: &str,
hostname: &str,
url: &Url,
create_dirs: bool,
) -> FResult<PathBuf> {
let path = self
.get_instance_path(hostname, false)
.await?
.join(username);
let path = self.get_instance_path(url, false).await?.join(username);
if create_dirs {
self.create_dir_if_not_exists(&path).await?;
}
@ -86,10 +85,10 @@ impl PccFederate {
&self,
name: &str,
owner: &str,
hostname: &str,
url: &Url,
create_dirs: bool,
) -> FResult<PathBuf> {
let path = self.get_user_path(owner, hostname, false).await?.join(name);
let path = self.get_user_path(owner, url, false).await?.join(name);
if create_dirs {
self.create_dir_if_not_exists(&path).await?;
}
@ -129,21 +128,21 @@ impl Federate for PccFederate {
}
/// create forge instance
async fn create_forge_instance(&self, f: &CreateForge<'_>) -> FResult<()> {
let path = self.get_instance_path(f.hostname, true).await?;
async fn create_forge_instance(&self, f: &CreateForge) -> FResult<()> {
let path = self.get_instance_path(&f.url, true).await?;
self.write_util(f, &path.join(INSTANCE_INFO_FILE)).await?;
Ok(())
}
/// delete forge instance
async fn delete_forge_instance(&self, hostname: &str) -> FResult<()> {
let path = self.get_instance_path(hostname, false).await?;
async fn delete_forge_instance(&self, url: &Url) -> FResult<()> {
let path = self.get_instance_path(&url, false).await?;
self.rm_util(&path).await
}
/// check if a forge instance exists
async fn forge_exists(&self, hostname: &str) -> Result<bool, Self::Error> {
let path = self.get_instance_path(hostname, false).await?;
async fn forge_exists(&self, url: &Url) -> Result<bool, Self::Error> {
let path = self.get_instance_path(url, false).await?;
if path.exists() && path.is_dir() {
let instance = path.join(INSTANCE_INFO_FILE);
Ok(instance.exists() && instance.is_file())
@ -153,8 +152,8 @@ impl Federate for PccFederate {
}
/// check if an user exists.
async fn user_exists(&self, username: &str, hostname: &str) -> Result<bool, Self::Error> {
let path = self.get_user_path(username, hostname, false).await?;
async fn user_exists(&self, username: &str, url: &Url) -> Result<bool, Self::Error> {
let path = self.get_user_path(username, url, false).await?;
if path.exists() && path.is_dir() {
let user = path.join(USER_INFO_FILE);
Ok(user.exists() && user.is_file())
@ -165,14 +164,14 @@ impl Federate for PccFederate {
/// create user instance
async fn create_user(&self, f: &AddUser<'_>) -> Result<(), Self::Error> {
let path = self.get_user_path(f.username, f.hostname, true).await?;
let path = self.get_user_path(f.username, &f.url, true).await?;
self.write_util(f, &path.join(USER_INFO_FILE)).await
}
/// add repository instance
async fn create_repository(&self, f: &AddRepository<'_>) -> Result<(), Self::Error> {
let path = self
.get_repo_path(f.name, f.owner, f.hostname, true)
.get_repo_path(f.name, f.owner, &f.url, true)
.await?
.join(REPO_INFO_FILE);
let publiccode: schema::Repository = f.into();
@ -184,9 +183,9 @@ impl Federate for PccFederate {
&self,
name: &str,
owner: &str,
hostname: &str,
url: &Url,
) -> Result<bool, Self::Error> {
let path = self.get_repo_path(name, owner, hostname, false).await?;
let path = self.get_repo_path(name, owner, url, false).await?;
if path.exists() && path.is_dir() {
let repo = path.join(REPO_INFO_FILE);
Ok(repo.exists() && repo.is_file())
@ -196,8 +195,8 @@ impl Federate for PccFederate {
}
/// delete user
async fn delete_user(&self, username: &str, hostname: &str) -> Result<(), Self::Error> {
let path = self.get_user_path(username, hostname, false).await?;
async fn delete_user(&self, username: &str, url: &Url) -> Result<(), Self::Error> {
let path = self.get_user_path(username, url, false).await?;
self.rm_util(&path).await?;
Ok(())
}
@ -207,9 +206,9 @@ impl Federate for PccFederate {
&self,
owner: &str,
name: &str,
hostname: &str,
url: &Url,
) -> Result<(), Self::Error> {
let path = self.get_repo_path(name, owner, hostname, false).await?;
let path = self.get_repo_path(name, owner, url, false).await?;
self.rm_util(&path).await
}

View file

@ -22,7 +22,7 @@ use federate_core::tests;
#[actix_rt::test]
async fn everything_works() {
const HOSTNAME: &str = "https://test-gitea.example.com";
const URL: &str = "https://test-gitea.example.com";
const HTML_PROFILE_URL: &str = "https://test-gitea.example.com/user1";
const USERNAME: &str = "user1";
@ -32,16 +32,15 @@ async fn everything_works() {
let tmp_dir = Temp::new_dir().unwrap();
let hostname = Url::parse(HOSTNAME).unwrap();
let hostname = get_hostname(&hostname);
let url = Url::parse(URL).unwrap();
let create_forge_msg = CreateForge {
hostname: &hostname,
url: url.clone(),
forge_type: ForgeImplementation::Gitea,
};
let add_user_msg = AddUser {
hostname: &hostname,
url: url.clone(),
html_link: HTML_PROFILE_URL,
profile_photo: None,
username: USERNAME,
@ -54,7 +53,7 @@ async fn everything_works() {
owner: USERNAME,
website: None,
description: None,
hostname: &hostname,
url: url.clone(),
};
let pcc = PccFederate::new(tmp_dir.to_str().unwrap().to_string())

View file

@ -17,6 +17,7 @@ path = "src/lib.rs"
[dependencies]
async-trait = "0.1.51"
url = { version = "2.2.2", features = ["serde"] }
[dependencies.db-core]
path = "../../db/db-core"

View file

@ -20,6 +20,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use db_core::prelude::*;
use url::Url;
pub mod prelude {
pub use super::*;
@ -33,10 +34,10 @@ pub mod dev {
}
#[derive(Clone, Debug)]
pub struct User<'a> {
/// hostname of the forge instance: with scheme but remove trailing slash
/// hostname can be derived from html_link also, but used to link to user's forge instance
pub hostname: &'a str,
pub struct User {
/// url of the forge instance: with scheme but remove trailing slash
/// url can be derived from html_link also, but used to link to user's forge instance
pub url: Url,
/// username of the user
pub username: Arc<String>,
/// html link to the user profile
@ -45,10 +46,10 @@ pub struct User<'a> {
pub profile_photo: Option<String>,
}
impl<'a> From<&'a User<'a>> for AddUser<'a> {
impl<'a> From<&'a User> for AddUser<'a> {
fn from(u: &'a User) -> Self {
Self {
hostname: u.hostname,
url: u.url.clone(),
username: u.username.as_str(),
html_link: &u.html_link,
profile_photo: u.profile_photo.as_deref(),
@ -58,25 +59,25 @@ impl<'a> From<&'a User<'a>> for AddUser<'a> {
#[derive(Clone, Debug)]
/// add new repository to database
pub struct Repository<'a> {
pub struct Repository {
/// html link to the repository
pub html_link: String,
/// repository topic tags
pub tags: Option<Vec<Arc<String>>>,
/// hostname of the forge instance: with scheme but remove trailing slash
/// hostname can be deras_ref().map(|p| p.as_str()),ived from html_link also, but used to link to user's forge instance
pub hostname: &'a str,
/// url of the forge instance: with scheme but remove trailing slash
/// url can be deras_ref().map(|p| p.as_str()),ived from html_link also, but used to link to user's forge instance
pub url: Url,
/// repository name
pub name: String,
/// repository owner
pub owner: Arc<User<'a>>,
pub owner: Arc<User>,
/// repository description, if any
pub description: Option<String>,
/// repository website, if any
pub website: Option<String>,
}
impl<'a> From<&'a Repository<'a>> for AddRepository<'a> {
impl<'a> From<&'a Repository> for AddRepository<'a> {
fn from(r: &'a Repository) -> Self {
let tags = if let Some(rtags) = &r.tags {
let mut tags = Vec::with_capacity(rtags.len());
@ -88,7 +89,7 @@ impl<'a> From<&'a Repository<'a>> for AddRepository<'a> {
None
};
Self {
hostname: r.hostname,
url: r.url.clone(),
name: &r.name,
description: r.description.as_deref(),
owner: r.owner.username.as_str(),
@ -99,21 +100,21 @@ impl<'a> From<&'a Repository<'a>> for AddRepository<'a> {
}
}
pub type UserMap<'a> = HashMap<Arc<String>, Arc<User<'a>>>;
pub type UserMap = HashMap<Arc<String>, Arc<User>>;
pub type Tags = HashSet<Arc<String>>;
pub type Repositories<'a> = Vec<Repository<'a>>;
pub type Repositories = Vec<Repository>;
pub struct CrawlResp<'a> {
pub repos: Repositories<'a>,
pub struct CrawlResp {
pub repos: Repositories,
pub tags: Tags,
pub users: UserMap<'a>,
pub users: UserMap,
}
#[async_trait]
pub trait SCForge: std::marker::Send + std::marker::Sync + CloneSPForge {
async fn is_forge(&self) -> bool;
async fn crawl(&self, limit: u64, page: u64, rate_limit: u64) -> CrawlResp;
fn get_hostname(&self) -> &str;
fn get_url(&self) -> &Url;
fn forge_type(&self) -> ForgeImplementation;
}

View file

@ -23,7 +23,6 @@ use tokio::task::JoinHandle;
use url::Url;
use db_core::ForgeImplementation;
use db_core::SCDatabase;
use forge_core::dev::*;
use forge_core::Repository;
@ -37,24 +36,24 @@ const GITEA_IDENTIFIER: &str = "gitea";
pub struct Gitea {
pub instance_url: Url,
pub client: Client,
hostname: String,
url: Url,
}
impl Gitea {
pub fn new(instance_url: Url, client: Client) -> Self {
let hostname = db_core::get_hostname(&instance_url);
let url = Url::parse(&db_core::clean_url(&instance_url)).unwrap();
Self {
instance_url,
client,
hostname,
url,
}
}
}
impl PartialEq for Gitea {
fn eq(&self, other: &Self) -> bool {
self.hostname == other.hostname && self.instance_url == other.instance_url
self.url == other.url && self.instance_url == other.instance_url
}
}
@ -80,8 +79,8 @@ impl SCForge for Gitea {
}
}
fn get_hostname(&self) -> &str {
&self.hostname
fn get_url(&self) -> &Url {
&self.url
}
fn forge_type(&self) -> ForgeImplementation {
@ -125,7 +124,7 @@ impl SCForge for Gitea {
username,
html_link: profile_url.to_string(),
profile_photo: Some(u.avatar_url),
hostname: &g.hostname,
url: g.url.clone(),
})
}
@ -174,7 +173,7 @@ impl SCForge for Gitea {
}
let frepo = Repository {
hostname: &self.hostname,
url: self.url.clone(),
website: empty_is_none(&repo.website),
name: repo.name,
owner: user,

View file

@ -97,18 +97,17 @@ pub async fn add_submit(
db: &BoxDB,
) -> ServiceResult<TXTChallenge> {
let url_hostname = Url::parse(&payload.hostname).unwrap();
let hostname = get_hostname(&url_hostname);
let key = TXTChallenge::get_challenge_txt_key(&ctx, &hostname);
let key = TXTChallenge::get_challenge_txt_key(&ctx, &url_hostname);
if db.dns_challenge_exists(&key).await? {
let value = db.get_dns_challenge(&key).await?.value;
Ok(TXTChallenge { key, value })
} else {
let challenge = TXTChallenge::new(ctx, &hostname);
let challenge = TXTChallenge::new(ctx, &url_hostname);
let c = Challenge {
key: challenge.key,
value: challenge.value,
hostname: url_hostname.to_string(),
url: url_hostname.to_string(),
};
db.create_dns_challenge(&c).await?;
@ -180,8 +179,7 @@ mod tests {
};
println!("{}", payload.hostname);
let hostname = get_hostname(&Url::parse(&payload.hostname).unwrap());
let hostname = Url::parse(&payload.hostname).unwrap();
let key = TXTChallenge::get_challenge_txt_key(&ctx, &hostname);
db.delete_dns_challenge(&key).await.unwrap();

View file

@ -87,7 +87,7 @@ pub async fn get_verify(
let challenge = Challenge {
key: payload.hostname,
value: "".into(),
hostname: "".into(),
url: "".into(),
};
PageError::new(VerifyChallenge::new(&ctx.settings, &challenge), e)
@ -124,7 +124,7 @@ pub async fn submit_verify(
let challenge = Challenge {
key: payload.hostname.clone(),
value: "".into(),
hostname: "".into(),
url: "".into(),
};
PageError::new(VerifyChallenge::new(&ctx.settings, &challenge), e)
@ -143,7 +143,8 @@ pub async fn submit_verify(
let federate = federate.clone();
let db = db.clone();
let fut = async move {
ctx.crawl(&value.hostname, &db, &federate).await;
ctx.crawl(&Url::parse(&value.url).unwrap(), &db, &federate)
.await;
};
tokio::spawn(fut);

View file

@ -15,7 +15,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use std::future::Future;
use log::info;
use tokio::sync::oneshot::{error::TryRecvError, Receiver};
use url::Url;
use db_core::prelude::*;
@ -25,31 +28,32 @@ use gitea::Gitea;
use crate::ctx::Ctx;
use crate::db::BoxDB;
use crate::federate::ArcFederate;
use crate::ArcCtx;
impl Ctx {
pub async fn crawl(&self, instance_url: &str, db: &BoxDB, federate: &ArcFederate) {
let forge: Box<dyn SCForge> = Box::new(Gitea::new(
Url::parse(instance_url).unwrap(),
self.client.clone(),
));
pub async fn crawl(&self, instance_url: &Url, db: &BoxDB, federate: &ArcFederate) {
info!("[crawl][{instance_url}] Init crawling");
let forge: Box<dyn SCForge> =
Box::new(Gitea::new(instance_url.clone(), self.client.clone()));
if !forge.is_forge().await {
unimplemented!("Forge type unimplemented");
}
let mut page = 1;
let hostname = forge.get_hostname();
if !db.forge_exists(hostname).await.unwrap() {
info!("[crawl][{hostname}] Creating forge");
let msg = CreateForge {
hostname,
let url = forge.get_url();
if !db.forge_exists(url).await.unwrap() {
info!("[crawl][{url}] Creating forge");
let mut msg = CreateForge {
url: url.clone(),
forge_type: forge.forge_type(),
};
db.create_forge_instance(&msg).await.unwrap();
} else {
if !federate.forge_exists(hostname).await.unwrap() {
let forge = db.get_forge(hostname).await.unwrap();
if !federate.forge_exists(&url).await.unwrap() {
let forge = db.get_forge(&url).await.unwrap();
let msg = CreateForge {
hostname,
url: url.clone(),
forge_type: forge.forge_type,
};
federate.create_forge_instance(&msg).await.unwrap();
@ -57,7 +61,7 @@ impl Ctx {
}
loop {
info!("[crawl][{hostname}] Crawling. page: {page}");
info!("[crawl][{url}] Crawling. page: {page}");
let res = forge
.crawl(
self.settings.crawler.items_per_api_call,
@ -66,23 +70,23 @@ impl Ctx {
)
.await;
if res.repos.is_empty() {
info!("[crawl][{hostname}] Finished crawling. pages: {}", page - 1);
info!("[crawl][{url}] Finished crawling. pages: {}", page - 1);
break;
}
for (username, u) in res.users.iter() {
if !db
.user_exists(username, Some(forge.get_hostname()))
.user_exists(username, Some(forge.get_url()))
.await
.unwrap()
{
info!("[crawl][{hostname}] Creating user: {username}");
info!("[crawl][{url}] Creating user: {username}");
let msg = u.as_ref().into();
db.add_user(&msg).await.unwrap();
federate.create_user(&msg).await.unwrap();
} else {
if !federate
.user_exists(username, forge.get_hostname())
.user_exists(username, forge.get_url())
.await
.unwrap()
{
@ -94,17 +98,17 @@ impl Ctx {
for r in res.repos.iter() {
if !db
.repository_exists(&r.name, &r.owner.username, r.hostname)
.repository_exists(&r.name, &r.owner.username, &r.url)
.await
.unwrap()
{
info!("[crawl][{hostname}] Creating repository: {}", r.name);
info!("[crawl][{url}] Creating repository: {}", r.name);
let msg = r.into();
db.create_repository(&msg).await.unwrap();
federate.create_repository(&msg).await.unwrap();
} else {
if !federate
.repository_exists(&r.name, &r.owner.username, r.hostname)
.repository_exists(&r.name, &r.owner.username, &r.url)
.await
.unwrap()
{
@ -116,7 +120,6 @@ impl Ctx {
page += 1;
}
federate.tar().await.unwrap();
}
}
@ -133,21 +136,19 @@ mod tests {
#[actix_rt::test]
async fn crawl_gitea() {
let (db, ctx, federate, _tmp_dir) = sqlx_sqlite::get_ctx().await;
ctx.crawl(GITEA_HOST, &db, &federate).await;
let hostname = get_hostname(&Url::parse(GITEA_HOST).unwrap());
assert!(db.forge_exists(&hostname).await.unwrap());
assert!(db
.user_exists(GITEA_USERNAME, Some(&hostname))
.await
.unwrap());
let url = Url::parse(GITEA_HOST).unwrap();
ctx.crawl(&url, &db, &federate).await;
// let hostname = get_hostname(&Url::parse(GITEA_HOST).unwrap());
assert!(db.forge_exists(&url).await.unwrap());
assert!(db.user_exists(GITEA_USERNAME, Some(&url)).await.unwrap());
assert!(db.user_exists(GITEA_USERNAME, None).await.unwrap());
for i in 0..100 {
let repo = format!("repository_{i}");
assert!(db
.repository_exists(&repo, GITEA_USERNAME, hostname.as_str())
.repository_exists(&repo, GITEA_USERNAME, &url)
.await
.unwrap())
}
assert!(db.forge_exists(&hostname).await.unwrap());
assert!(db.forge_exists(&url).await.unwrap());
}
}

View file

@ -20,6 +20,7 @@ use trust_dns_resolver::{
config::{ResolverConfig, ResolverOpts},
AsyncResolver,
};
use url::Url;
use crate::utils::get_random;
use crate::ArcCtx;
@ -39,11 +40,15 @@ impl TXTChallenge {
format!("starchart-{}", &ctx.settings.server.domain)
}
pub fn get_challenge_txt_key(ctx: &ArcCtx, hostname: &str) -> String {
format!("{}.{}", Self::get_challenge_txt_key_prefix(ctx), hostname)
pub fn get_challenge_txt_key(ctx: &ArcCtx, hostname: &Url) -> String {
format!(
"{}.{}",
Self::get_challenge_txt_key_prefix(ctx),
hostname.host_str().unwrap()
)
}
pub fn new(ctx: &ArcCtx, hostname: &str) -> Self {
pub fn new(ctx: &ArcCtx, hostname: &Url) -> Self {
let key = Self::get_challenge_txt_key(ctx, hostname);
let value = get_random(VALUES_LEN);
Self { key, value }
@ -62,7 +67,7 @@ impl TXTChallenge {
pub mod tests {
use super::*;
use crate::tests::sqlx_sqlite;
pub const BASE_DOMAIN: &str = "forge.forgeflux.org";
pub const BASE_DOMAIN: &str = "https://forge.forgeflux.org";
pub const VALUE: &str = "ifthisvalueisretrievedbyforgefluxstarchartthenthetestshouldpass";
#[actix_rt::test]
@ -71,12 +76,17 @@ pub mod tests {
let (_db, ctx, _federate, _tmp_dir) = sqlx_sqlite::get_ctx().await;
let key = TXTChallenge::get_challenge_txt_key(&ctx, BASE_DOMAIN);
let base_hostname = Url::parse(BASE_DOMAIN).unwrap();
let key = TXTChallenge::get_challenge_txt_key(&ctx, &base_hostname);
let mut txt_challenge = TXTChallenge {
value: VALUE.to_string(),
key: key.clone(),
};
assert_eq!(TXTChallenge::get_challenge_txt_key(&ctx, BASE_DOMAIN), key,);
assert_eq!(
TXTChallenge::get_challenge_txt_key(&ctx, &base_hostname),
key,
);
assert!(
txt_challenge.verify_txt().await.unwrap(),