summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKornel <kornel@geekhood.net>2020-03-02 19:00:49 +0000
committerKornel <kornel@geekhood.net>2020-03-10 00:43:09 +0000
commit9e72f373a1042bfe3a8510d004b83a2af394772a (patch)
treeb76568ce7f98ba6d0dbfed3d8c340c205e03f397
parentf28189567a12c9f289eb4c678c485047f73a5be5 (diff)
author v0
-rw-r--r--crate_db/src/lib_crate_db.rs4
-rw-r--r--datadump/src/main.rs21
-rw-r--r--front_end/src/author_page.rs147
-rw-r--r--front_end/src/front_end.rs15
-rw-r--r--front_end/templates/author.rs.html39
-rw-r--r--kitchen_sink/src/lib_kitchen_sink.rs27
-rw-r--r--server/src/main.rs27
7 files changed, 267 insertions, 13 deletions
diff --git a/crate_db/src/lib_crate_db.rs b/crate_db/src/lib_crate_db.rs
index 6d7e4d3..4ddad18 100644
--- a/crate_db/src/lib_crate_db.rs
+++ b/crate_db/src/lib_crate_db.rs
@@ -643,8 +643,8 @@ impl CrateDb {
}).await
}
- pub async fn crates_by_author(&self, github_id: u32) -> FResult<Vec<CrateOwnerRow>> {
- self.with_read("crates_by_author", |conn| {
+ pub async fn crates_of_author(&self, github_id: u32) -> FResult<Vec<CrateOwnerRow>> {
+ self.with_read("crates_of_author", |conn| {
let mut query = conn.prepare_cached(r#"SELECT ac.crate_id, ac.invited_by_github_id, ac.invited_at, max(cv.created)
FROM author_crates ac JOIN crate_versions cv USING(crate_id)
WHERE ac.github_id = ?1
diff --git a/datadump/src/main.rs b/datadump/src/main.rs
index 5f21e59..c1da0e3 100644
--- a/datadump/src/main.rs
+++ b/datadump/src/main.rs
@@ -3,6 +3,7 @@
use chrono::prelude::*;
use kitchen_sink::CrateOwner;
use kitchen_sink::KitchenSink;
+use kitchen_sink::Origin;
use kitchen_sink::OwnerKind;
use libflate::gzip::Decoder;
use serde_derive::Deserialize;
@@ -18,8 +19,8 @@ type BoxErr = Box<dyn std::error::Error + Sync + Send>;
#[tokio::main]
async fn main() -> Result<(), BoxErr> {
- tokio::runtime::Handle::current()
- .spawn(async move {
+ tokio::runtime::Handle::current().spawn(async move {
+ let handle = tokio::runtime::Handle::current();
let mut a = Archive::new(Decoder::new(BufReader::new(File::open("db-dump.tar.gz")?))?);
let ksink = KitchenSink::new_default().await?;
@@ -76,12 +77,15 @@ async fn main() -> Result<(), BoxErr> {
index_downloads(crates, versions, &downloads, &ksink)?;
}
}
- if let (Some(crates), Some(teams), Some(users)) = (&crates, &teams, &users) {
+ }
+ }
+
+ if let (Some(crates), Some(teams), Some(users)) = (crates, teams, users) {
if let Some(crate_owners) = crate_owners.take() {
eprintln!("Indexing {} owners", crate_owners.len());
- index_owners(crates, crate_owners, teams, users, &ksink)?;
- }
- }
+ handle.spawn(async move {
+ index_owners(&crates, crate_owners, &teams, &users, &ksink).await.unwrap();
+ });
}
}
Ok(())
@@ -113,7 +117,7 @@ fn index_downloads(crates: &CratesMap, versions: &VersionsMap, downloads: &Versi
}
#[inline(never)]
-fn index_owners(crates: &CratesMap, owners: CrateOwners, teams: &Teams, users: &Users, ksink: &KitchenSink) -> Result<(), BoxErr> {
+async fn index_owners(crates: &CratesMap, owners: CrateOwners, teams: &Teams, users: &Users, ksink: &KitchenSink) -> Result<(), BoxErr> {
for (crate_id, owners) in owners {
if let Some(k) = crates.get(&crate_id) {
let owners: Vec<_> = owners
@@ -157,7 +161,8 @@ fn index_owners(crates: &CratesMap, owners: CrateOwners, teams: &Teams, users: &
})
})
.collect();
- ksink.set_crates_io_crate_owners(&k.to_ascii_lowercase(), owners).map_err(|_| "ugh")?;
+ let origin = Origin::from_crates_io_name(k);
+ ksink.index_crates_io_crate_owners(&origin, owners).await?;
}
}
Ok(())
diff --git a/front_end/src/author_page.rs b/front_end/src/author_page.rs
new file mode 100644
index 0000000..bf3a548
--- /dev/null
+++ b/front_end/src/author_page.rs
@@ -0,0 +1,147 @@
+use crate::templates;
+use crate::Page;
+use kitchen_sink::CResult;
+use kitchen_sink::KitchenSink;
+use kitchen_sink::RichAuthor;
+use kitchen_sink::UserType;
+use render_readme::Renderer;
+
+// pub struct User {
+// pub id: u32,
+// pub login: String,
+// pub name: Option<String>,
+// pub avatar_url: Option<String>, // "https://avatars0.githubusercontent.com/u/1111?v=4",
+// pub gravatar_id: Option<String>, // "",
+// pub html_url: String, // "https://github.com/zzzz",
+// pub blog: Option<String>, // "https://example.com
+// #[serde(rename = "type")]
+// pub user_type: UserType,
+// }
+
+/// Data sources used in `author.rs.html`
+pub struct AuthorPage<'a> {
+ pub aut: &'a RichAuthor,
+ pub kitchen_sink: &'a KitchenSink,
+ pub markup: &'a Renderer,
+}
+
+impl<'a> AuthorPage<'a> {
+ pub async fn new(aut: &'a RichAuthor, kitchen_sink: &'a KitchenSink, markup: &'a Renderer) -> CResult<AuthorPage<'a>> {
+ dbg!(&aut);
+ let crates = kitchen_sink.crates_of_author(aut).await?;
+
+ Ok(Self {
+ aut,
+ kitchen_sink,
+ markup,
+ })
+ }
+
+ pub fn is_org(&self) -> bool {
+ self.aut.github.user_type == UserType::Org
+ }
+
+ pub fn login(&self) -> &str {
+ &self.aut.github.login
+ }
+
+ pub fn github_url(&self) -> String {
+ format!("https://github.com/{}", self.aut.github.login)
+ }
+
+ pub fn page(&self) -> Page {
+ Page {
+ title: format!("Rust crates by @{}", self.login()),
+ ..Default::default()
+ }
+ }
+
+ pub fn render_markdown_str(&self, s: &str) -> templates::Html<String> {
+ templates::Html(self.markup.markdown_str(s, true, None))
+ }
+
+ // fn block<O>(&self, f: impl Future<Output = O>) -> O {
+ // self.handle.enter(|| futures::executor::block_on(f))
+ // }
+
+ // pub fn format_number(&self, num: impl Display) -> String {
+ // Numeric::english().format_int(num)
+ // }
+
+ // pub fn format_knumber(&self, num: usize) -> (String, &'static str) {
+ // let (num, unit) = match num {
+ // 0..=899 => (num, ""),
+ // 0..=8000 => return (format!("{}", ((num + 250) / 500) as f64 * 0.5), "K"), // 3.5K
+ // 0..=899_999 => ((num + 500) / 1000, "K"),
+ // 0..=9_999_999 => return (format!("{}", ((num + 250_000) / 500_000) as f64 * 0.5), "M"), // 3.5M
+ // _ => ((num + 500_000) / 1_000_000, "M"), // 10M
+ // };
+ // (Numeric::english().format_int(num), unit)
+ // }
+
+ // pub fn format_kbytes(&self, bytes: usize) -> String {
+ // let (num, unit) = match bytes {
+ // 0..=100_000 => ((bytes + 999) / 1000, "KB"),
+ // 0..=800_000 => ((bytes + 3999) / 5000 * 5, "KB"),
+ // 0..=9_999_999 => return format!("{}MB", ((bytes + 250_000) / 500_000) as f64 * 0.5),
+ // _ => ((bytes + 500_000) / 1_000_000, "MB"),
+ // };
+ // format!("{}{}", Numeric::english().format_int(num), unit)
+ // }
+
+ // fn format_number_frac(num: f64) -> String {
+ // if num > 0.05 && num < 10. && num.fract() > 0.09 && num.fract() < 0.9 {
+ // if num < 3. {
+ // format!("{:.1}", num)
+ // } else {
+ // format!("{}", (num * 2.).round() / 2.)
+ // }
+ // } else {
+ // Numeric::english().format_int(if num > 500. {
+ // (num / 10.).round() * 10.
+ // } else if num > 100. {
+ // (num / 5.).round() * 5.
+ // } else {
+ // num.round()
+ // })
+ // }
+ // }
+
+ // pub fn format_kbytes_range(&self, a: usize, b: usize) -> String {
+ // let min_bytes = a.min(b);
+ // let max_bytes = a.max(b);
+
+ // // if the range is small, just display the upper number
+ // if min_bytes * 4 > max_bytes * 3 || max_bytes < 250_000 {
+ // return self.format_kbytes(max_bytes);
+ // }
+
+ // let (denom, unit) = match max_bytes {
+ // 0..=800_000 => (1000., "KB"),
+ // _ => (1_000_000., "MB"),
+ // };
+ // let mut low_val = min_bytes as f64 / denom;
+ // let high_val = max_bytes as f64 / denom;
+ // if low_val > 1. && high_val > 10. {
+ // low_val = low_val.round(); // spread is so high that precision of low end isn't relevant
+ // }
+ // format!("{}–{}{}", Self::format_number_frac(low_val), Self::format_number_frac(high_val), unit)
+ // }
+
+ // /// Display number 0..1 as percent
+ // pub fn format_fraction(&self, num: f64) -> String {
+ // if num < 1.9 {
+ // format!("{:0.1}%", num)
+ // } else {
+ // format!("{}%", Numeric::english().format_int(num.round() as usize))
+ // }
+ // }
+
+ // pub fn format(date: &DateTime<FixedOffset>) -> String {
+ // date.format("%b %e, %Y").to_string()
+ // }
+
+ // pub fn format_month(date: &DateTime<FixedOffset>) -> String {
+ // date.format("%b %Y").to_string()
+ // }
+}
diff --git a/front_end/src/front_end.rs b/front_end/src/front_end.rs
index 31c32c8..0cea8f3 100644
--- a/front_end/src/front_end.rs
+++ b/front_end/src/front_end.rs
@@ -5,6 +5,7 @@
//! because the template engine Ructe doesn't support
//! complex expressions in the templates.
+mod author_page;
mod cat_page;
mod crate_page;
mod download_graph;
@@ -19,6 +20,7 @@ pub use crate::not_found_page::*;
pub use crate::search_page::*;
use futures::future::try_join_all;
+use crate::author_page::*;
use crate::crate_page::*;
use crate::urler::Urler;
use categories::Category;
@@ -26,6 +28,7 @@ use chrono::prelude::*;
use failure;
use failure::ResultExt;
use kitchen_sink::Compat;
+use kitchen_sink::RichAuthor;
use kitchen_sink::KitchenSink;
use kitchen_sink::{stopped, KitchenSinkErr};
use render_readme::Links;
@@ -134,6 +137,18 @@ pub async fn render_sitemap(sitemap: &mut impl Write, crates: &KitchenSink) -> R
Ok(())
}
+/// See `author.rs.html`
+pub async fn render_author_page<W: Write>(out: &mut W, aut: &RichAuthor, kitchen_sink: &KitchenSink, renderer: &Renderer) -> Result<(), failure::Error> {
+ if stopped() {
+ Err(KitchenSinkErr::Stopped)?;
+ }
+
+ let urler = Urler::new(None);
+ let c = AuthorPage::new(aut, kitchen_sink, renderer).await.context("New crate page")?;
+ templates::author(out, &urler, &c).context("author page io")?;
+ Ok(())
+}
+
/// See `crate_page.rs.html`
pub async fn render_crate_page<W: Write>(out: &mut W, all: &RichCrate, ver: &RichCrateVersion, kitchen_sink: &KitchenSink, renderer: &Renderer) -> Result<Option<DateTime<FixedOffset>>, failure::Error> {
if stopped() {
diff --git a/front_end/templates/author.rs.html b/front_end/templates/author.rs.html
new file mode 100644
index 0000000..244ddf2
--- /dev/null
+++ b/front_end/templates/author.rs.html
@@ -0,0 +1,39 @@
+@use crate::templates::base;
+@use crate::Urler;
+@use crate::AuthorPage;
+
+@(url: &Urler, c: &AuthorPage)
+
+@:base(&c.page(), {
+ <header id="author">
+ <div class="inner-col">
+ <div class="breadcrumbs">
+ <h1><a href="/">Lib.rs</a></h1> › @if c.is_org() {
+ Orgs
+ } else {
+ Users
+ }
+ </div>
+
+ <h2>
+ @"@"@c.login()
+ </h2>
+
+ <nav><ul>
+ <li><a href="@c.github_url()">GitHub</a></li>
+ </ul></nav>
+ </div>
+ </header>
+ <main>
+ <div class="inner-col">
+ Hi
+ </div>
+ </main>
+ </div>
+
+ <footer>
+ <div class="inner-col">
+ Bye
+ </div>
+ </footer>
+})
diff --git a/kitchen_sink/src/lib_kitchen_sink.rs b/kitchen_sink/src/lib_kitchen_sink.rs
index 78cf4dd..0498091 100644
--- a/kitchen_sink/src/lib_kitchen_sink.rs
+++ b/kitchen_sink/src/lib_kitchen_sink.rs
@@ -13,6 +13,7 @@ mod ctrlcbreak;
mod tarball;
pub use crate::ctrlcbreak::*;
+pub use crate_db::CrateOwnerRow;
pub use crate_db::builddb::Compat;
pub use crate_db::builddb::CompatibilityInfo;
pub use crates_io_client::CrateDepKind;
@@ -107,6 +108,8 @@ pub enum KitchenSinkErr {
CategoryQueryFailed,
#[fail(display = "crate not found: {:?}", _0)]
CrateNotFound(Origin),
+ #[fail(display = "author not found: {}", _0)]
+ AuthorNotFound(String),
#[fail(display = "crate {} not found in repo {}", _0, _1)]
CrateNotFoundInRepo(String, String),
#[fail(display = "crate is not a package: {:?}", _0)]
@@ -1865,6 +1868,10 @@ impl KitchenSink {
Err(KitchenSinkErr::OwnerWithoutLogin)?
}
+ pub async fn crates_of_author(&self, aut: &RichAuthor) -> CResult<Vec<CrateOwnerRow>> {
+ self.crate_db.crates_of_author(aut.github.id).await
+ }
+
async fn crate_owners(&self, origin: &Origin) -> CResult<Vec<CrateOwner>> {
match origin {
Origin::CratesIo(name) => {
@@ -1893,8 +1900,12 @@ impl KitchenSink {
}
}
- pub fn set_crates_io_crate_owners(&self, crate_name: &str, owners: Vec<CrateOwner>) -> Result<(), ()> {
- self.crates_io_owners_cache.set(crate_name, owners).map_err(drop)
+ pub async fn index_crates_io_crate_owners(&self, origin: &Origin, owners: Vec<CrateOwner>) -> CResult<()> {
+ self.crate_db.index_crate_owners(origin, &owners).await?;
+ if let Origin::CratesIo(name) = origin {
+ self.crates_io_owners_cache.set(&**name, owners)?;
+ }
+ Ok(())
}
// Sorted from the top, returns origins
@@ -2069,6 +2080,18 @@ impl KitchenSink {
let _ = self.url_check_cache.save();
self.crates_io.cleanup();
}
+
+ pub async fn author_by_login(&self, login: &str) -> CResult<RichAuthor> {
+ let github = self.gh.user_by_login(login).await?.ok_or_else(|| KitchenSinkErr::AuthorNotFound(login.to_owned()))?;
+ Ok(RichAuthor {
+ github
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct RichAuthor {
+ pub github: User,
}
/// This is used to uniquely identify authors based on as little information as is available
diff --git a/server/src/main.rs b/server/src/main.rs
index cfe6c44..cd38a34 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -146,9 +146,9 @@ async fn run_server() -> Result<(), failure::Error> {
eprintln!("Refresh failed: {}", e);
std::process::exit(1);
},
+ }
}
}
- }
}});
// watchdog
@@ -179,6 +179,7 @@ async fn run_server() -> Result<(), failure::Error> {
.route("/keywords/{keyword}", web::get().to(handle_keyword))
.route("/crates/{crate}", web::get().to(handle_crate))
.route("/crates/{crate}/rev", web::get().to(handle_crate_reverse_dependencies))
+ .route("/~{author}", web::get().to(handle_author))
.route("/install/{crate:.*}", web::get().to(handle_install))
.route("/debug/{crate:.*}", web::get().to(handle_debug))
.route("/gh/{owner}/{repo}/{crate}", web::get().to(handle_github_crate))
@@ -440,6 +441,30 @@ async fn handle_install(req: HttpRequest) -> Result<HttpResponse, ServerError> {
Ok(serve_cached((page, 7200, false, last_mod)))
}
+async fn handle_author(req: HttpRequest) -> Result<HttpResponse, ServerError> {
+ let login = req.match_info().query("author");
+ println!("author page for {:?}", login);
+ let state: &AServerState = req.app_data().expect("appdata");
+ if !is_alnum(login) {
+ return render_404_page(state, login);
+ }
+ let cache_file = state.page_cache_dir.join(format!("@{}.html", login));
+ Ok(serve_cached(
+ with_file_cache(state, cache_file, 3600, {
+ let login = login.to_owned();
+ let state = state.clone();
+ run_timeout(60, async move {
+ let crates = state.crates.load();
+ let mut page: Vec<u8> = Vec::with_capacity(32000);
+ let aut = crates.author_by_login(&login).await?;
+ front_end::render_author_page(&mut page, &aut, &crates, &state.markup).await?;
+ Ok::<_, failure::Error>((page, None))
+ })
+ })
+ .await?,
+ ))
+}
+
async fn handle_crate(req: HttpRequest) -> Result<HttpResponse, ServerError> {
let crate_name = req.match_info().query("crate");
println!("crate page for {:?}", crate_name);