~saiko/modsite

e1551b9816594bd6fbd72d12301913da6c1ba3ef — 2xsaiko 2 months ago 1824453 master
Dependencies/Required by v1
A migrations/20200525070952-merge-mod-link-and-mod-link-external/_props => migrations/20200525070952-merge-mod-link-and-mod-link-external/_props +4 -0
@@ 0,0 1,4 @@
// Auto-generated migration metadata. Do not edit.
id   8072ec8412ba40b5861a39f7502ed5fd
name "Merge mod_link and mod_link_external"
date 1590390592

A migrations/20200525070952-merge-mod-link-and-mod-link-external/apply.sql => migrations/20200525070952-merge-mod-link-and-mod-link-external/apply.sql +13 -0
@@ 0,0 1,13 @@
DROP TABLE mod_link;

ALTER TABLE mod_link_external
    RENAME TO mod_link;

ALTER TABLE mod_link
    ADD COLUMN child_ref uuid REFERENCES mod_base (uuid);

ALTER TABLE mod_link
    RENAME CONSTRAINT mod_link_external_pkey TO mod_link_pkey;

ALTER TABLE mod_link
    RENAME CONSTRAINT mod_link_external_mod_base_fkey TO mod_link_mod_base_fkey;
\ No newline at end of file

A migrations/20200525070952-merge-mod-link-and-mod-link-external/unapply.sql => migrations/20200525070952-merge-mod-link-and-mod-link-external/unapply.sql +23 -0
@@ 0,0 1,23 @@
ALTER TABLE mod_link
    RENAME CONSTRAINT mod_link_mod_base_fkey TO mod_link_external_mod_base_fkey;

ALTER TABLE mod_link
    RENAME CONSTRAINT mod_link_pkey TO mod_link_external_pkey;

ALTER TABLE mod_link
    DROP COLUMN child_ref;

ALTER TABLE mod_link
    RENAME TO mod_link_external;

CREATE TABLE mod_link
(
    mod_base  uuid      NOT NULL,
    link_type link_type NOT NULL,
    child     uuid      NOT NULL,
    range     VARCHAR   NOT NULL,

    PRIMARY KEY (mod_base, link_type, child),
    FOREIGN KEY (mod_base) REFERENCES mod_base (uuid),
    FOREIGN KEY (child) REFERENCES mod_base (uuid)
);
\ No newline at end of file

A migrations/20200525073948-make-mod-link-per-mod-version/_props => migrations/20200525073948-make-mod-link-per-mod-version/_props +4 -0
@@ 0,0 1,4 @@
// Auto-generated migration metadata. Do not edit.
id   3a49deff6ec04b7eafd6d25f8a1fbffa
name "Make mod_link per mod version"
date 1590392388

A migrations/20200525073948-make-mod-link-per-mod-version/apply.sql => migrations/20200525073948-make-mod-link-per-mod-version/apply.sql +17 -0
@@ 0,0 1,17 @@
DELETE
FROM mod_link;

ALTER TABLE mod_link
    ADD COLUMN file_version varchar NOT NULL;

ALTER TABLE mod_link
    DROP CONSTRAINT mod_link_pkey;

ALTER TABLE mod_link
    DROP CONSTRAINT mod_link_mod_base_fkey;

ALTER TABLE mod_link
    ADD PRIMARY KEY (mod_base, file_version, link_type, child);

ALTER TABLE mod_link
    ADD CONSTRAINT mod_link_mod_base_file_version_fkey FOREIGN KEY (mod_base, file_version) REFERENCES mod_version (mod_base, file_version);
\ No newline at end of file

A migrations/20200525073948-make-mod-link-per-mod-version/unapply.sql => migrations/20200525073948-make-mod-link-per-mod-version/unapply.sql +17 -0
@@ 0,0 1,17 @@
DELETE
FROM mod_link;

ALTER TABLE mod_link
    DROP CONSTRAINT mod_link_mod_base_file_version_fkey;

ALTER TABLE mod_link
    DROP CONSTRAINT mod_link_pkey;

ALTER TABLE mod_link
    ADD CONSTRAINT mod_link_mod_base_fkey FOREIGN KEY (mod_base) REFERENCES mod_base (uuid);

ALTER TABLE mod_link
    ADD PRIMARY KEY (mod_base, link_type, child);

ALTER TABLE mod_link
    DROP COLUMN file_version;
\ No newline at end of file

M site/src/fabric_mod/mod.rs => site/src/fabric_mod/mod.rs +6 -0
@@ 71,6 71,12 @@ pub struct NestedJarEntry {
#[derive(Debug, Clone, Deserialize)]
pub struct VersionRange(String);

impl VersionRange {
    pub fn into_inner(self) -> String { self.0 }

    pub fn as_str(&self) -> &str { &self.0 }
}

impl Default for Environment {
    fn default() -> Self { Environment::Any }
}

M site/src/indexer/db.rs => site/src/indexer/db.rs +15 -11
@@ 1,18 1,14 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use crate::db::types::{Environment, LinkType};
use crate::fabric_mod::Environment as FabricEnv;

use crate::db;
use crate::fabric_mod::Environment;

#[derive(Serialize, Deserialize)]
pub struct ModInfo {
    pub id: String,
    pub display_version: String,
    pub versions: HashMap<String, ModVersionInfo>,
}

#[derive(Serialize, Deserialize)]
pub struct ModVersionInfo {
    pub modid: String,
    pub version: String,


@@ 25,14 21,22 @@ pub struct ModVersionInfo {
    pub license: Option<String>,
    pub environment: Environment,
    pub file_url: String,

    pub links: Vec<ModLink>,
}

pub struct ModLink {
    pub typ: LinkType,
    pub child: String,
    pub range: String,
}

impl From<Environment> for db::types::Environment {
    fn from(other: Environment) -> Self {
impl From<FabricEnv> for Environment {
    fn from(other: FabricEnv) -> Self {
        match other {
            Environment::Any => db::types::Environment::Any,
            Environment::Client => db::types::Environment::Client,
            Environment::Server => db::types::Environment::Server,
            FabricEnv::Any => Environment::Any,
            FabricEnv::Client => Environment::Client,
            FabricEnv::Server => Environment::Server,
        }
    }
}
\ No newline at end of file

M site/src/indexer/mod.rs => site/src/indexer/mod.rs +26 -2
@@ 4,8 4,9 @@ use std::io::Cursor;
use zip::ZipArchive;

use crate::CRATE_VERSION;
use crate::db::types::LinkType;
use crate::fabric_mod::FabricMod;
use crate::indexer::db::{ModInfo, ModVersionInfo};
use crate::indexer::db::{ModInfo, ModLink, ModVersionInfo};
use crate::modconf::modsrc::{FileSource, Mod};

mod maven;


@@ 77,6 78,28 @@ async fn index_mod(id: String, file_source: &FileSource, client: &reqwest::Clien
            }
        };

        let mut links = Vec::new();

        for (child, v) in fm.breaks {
            links.push(ModLink { child, range: v.into_inner(), typ: LinkType::Breaks });
        }

        for (child, v) in fm.conflicts {
            links.push(ModLink { child, range: v.into_inner(), typ: LinkType::Conflicts });
        }

        for (child, v) in fm.depends {
            links.push(ModLink { child, range: v.into_inner(), typ: LinkType::Depends });
        }

        for (child, v) in fm.recommends {
            links.push(ModLink { child, range: v.into_inner(), typ: LinkType::Recommends });
        }

        for (child, v) in fm.suggests {
            links.push(ModLink { child, range: v.into_inner(), typ: LinkType::Suggests });
        }

        let version_info = ModVersionInfo {
            modid: fm.id,
            // this is the mod version as in fabric.mod.json


@@ 88,8 111,9 @@ async fn index_mod(id: String, file_source: &FileSource, client: &reqwest::Clien
            issues: fm.contact.issues,
            source: fm.contact.sources,
            license: fm.license,
            environment: fm.environment,
            environment: fm.environment.into(),
            file_url: v.file_url,
            links,
        };

        // key is the mod version as in the source (maven/github)!!!

M site/src/main.rs => site/src/main.rs +23 -7
@@ 1,4 1,4 @@
#![feature(decl_macro, proc_macro_hygiene, trait_alias)]
#![feature(decl_macro, proc_macro_hygiene)]

use std::collections::HashMap;
use std::fs::File;


@@ 10,10 10,9 @@ use std::time::Duration;
use log::info;
use log::LevelFilter;
use simplelog::{CombinedLogger, Config, ConfigBuilder, TerminalMode, TermLogger, WriteLogger};
use sqlx::{Executor, PgConnection, PgPool, Pool, Postgres};
use sqlx::{PgConnection, PgPool, Pool};
use uuid::Uuid;

use crate::db::types::Environment;
use crate::indexer::db::ModInfo;
use crate::modconf::UserFiles;



@@ 27,8 26,6 @@ mod db;
const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
const GIT_HASH: &str = env!("GIT_HASH");

trait PgExec = Executor<Database=Postgres>;

#[actix_rt::main]
async fn main() {
    let config = load_config();


@@ 121,6 118,7 @@ async fn put_into_db(modinfo: HashMap<String, ModInfo>, db: &Pool<PgConnection>)
    let mut ta = db.begin().await?;

    sqlx::query!("UPDATE mod_base SET display_version = NULL").execute(&mut ta).await?;
    sqlx::query!("DELETE FROM mod_link").execute(&mut ta).await?;
    sqlx::query!("DELETE FROM mod_version").execute(&mut ta).await?;
    sqlx::query!("DELETE FROM mod_base").execute(&mut ta).await?;



@@ 129,17 127,35 @@ async fn put_into_db(modinfo: HashMap<String, ModInfo>, db: &Pool<PgConnection>)
        sqlx::query!("INSERT INTO mod_base (uuid, id, display_version) VALUES ($1, $2, $3)", mod_base, mod_info.id, None::<&str>).execute(&mut ta).await?;

        for (file_version, vi) in mod_info.versions {
            let e: Environment = vi.environment.into();
            sqlx::query_unchecked!(
                "INSERT INTO mod_version (mod_base, version, id, name, description, homepage, issues, source, license, download_link, environment, file_version) \
                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
                mod_base, vi.version, vi.modid, vi.name, vi.description, vi.homepage, vi.issues, vi.source, vi.license, vi.file_url, e, file_version)
                mod_base, vi.version, vi.modid, vi.name, vi.description, vi.homepage, vi.issues, vi.source, vi.license, vi.file_url, vi.environment, file_version)
                .execute(&mut ta).await?;

            for link in vi.links {
                sqlx::query_unchecked!(
                    "INSERT INTO mod_link (mod_base, file_version, link_type, child, range) \
                     VALUES ($1, $2, $3, $4, $5)",
                    mod_base, file_version, link.typ, link.child, link.range)
                    .execute(&mut ta).await?;
            }
        }

        sqlx::query!("UPDATE mod_base SET display_version = $1 WHERE uuid = $2", mod_info.display_version, mod_base)
            .execute(&mut ta).await?;
    }

    sqlx::query!(
        "UPDATE mod_link l \
         SET child_ref = ( \
             SELECT b.uuid \
             FROM mod_version v \
             JOIN mod_base b ON b.uuid = v.mod_base \
             WHERE v.id = l.child \
             LIMIT 1)")
        .execute(&mut ta).await?;

    ta.commit().await?;
    Ok(())
}
\ No newline at end of file

M site/src/site/mod_page.rs => site/src/site/mod_page.rs +122 -4
@@ 1,3 1,5 @@
use std::collections::HashMap;

use actix_web::{HttpRequest, HttpResponse, Responder, web};
use futures_util::TryStreamExt;
use handlebars::Handlebars;


@@ 5,7 7,8 @@ use serde::Serialize;
use sqlx::{FromRow, PgConnection, Pool};
use url::Url;

use crate::db::types::Environment;
use crate::db::types::{Environment, LinkType};
use crate::fabric_mod::VersionRange;
use crate::site::db_access::{Db, map_db_err};
use crate::site::geninfo::GenInfo;



@@ 19,6 22,9 @@ pub async fn mod_page(req: HttpRequest, web::Query(PageState { id, v: version })
    };
    let all_versions = ModVersion::load_all(&id, &from_db.file_version, &db, &req).await.map_err(map_db_err)?;

    let dependencies = get_dependencies(&id, &from_db.file_version, &db, &req).await.map_err(map_db_err)?;
    let required_by = get_required_by(&id, &from_db.version, &db, &req).await.map_err(map_db_err)?;

    let body = hb.render("mod", &Data {
        id: &id,
        name: from_db.name.clone().unwrap_or_else(|| from_db.modid.clone()),


@@ 26,6 32,10 @@ pub async fn mod_page(req: HttpRequest, web::Query(PageState { id, v: version })
        mcversions: vec![],
        allmods: all_versions,
        geninfo: GenInfo::current(),
        dependencies_count: dependencies.len(),
        dependencies,
        required_by_count: required_by.len(),
        required_by,
    }).unwrap();

    Ok(HttpResponse::Ok().body(body))


@@ 39,6 49,10 @@ struct Data<'a> {
    mcversions: Vec<ByMcVersion>,
    allmods: Vec<ModVersion>,
    geninfo: GenInfo,
    dependencies_count: usize,
    dependencies: Vec<Dependency>,
    required_by_count: usize,
    required_by: Vec<RequiredBy>,
}

#[derive(Serialize)]


@@ 61,7 75,8 @@ impl ModVersion {
        let db: Vec<_> = sqlx::query!(
                "SELECT v.download_link, v.file_version FROM mod_version v \
                 JOIN mod_base b ON b.uuid = v.mod_base \
                 WHERE b.id = $1", id)
                 WHERE b.id = $1 \
                 ORDER BY v.file_version DESC", id)
            .fetch(db)
            .try_collect().await?;



@@ 81,6 96,7 @@ impl ModVersion {
struct FromDb {
    modid: String,
    name: Option<String>,
    version: String,
    file_version: String,
    description: Option<String>,
    homepage: Option<String>,


@@ 94,7 110,7 @@ struct FromDb {
impl FromDb {
    async fn load(id: &str, db: &Pool<PgConnection>) -> sqlx::Result<FromDb> {
        sqlx::query_as_unchecked!(FromDb,
            "SELECT v.id as modid, v.name, v.file_version, v.description, v.homepage, v.issues, v.source, v.license, v.download_link, v.environment \
            "SELECT v.id as modid, v.name, v.version, v.file_version, v.description, v.homepage, v.issues, v.source, v.license, v.download_link, v.environment \
             FROM mod_version v \
             INNER JOIN mod_base b ON b.uuid = v.mod_base \
             WHERE b.id = $1 AND v.file_version = b.display_version", id)


@@ 103,10 119,112 @@ impl FromDb {

    async fn load_ver(id: &str, v: &str, db: &Pool<PgConnection>) -> sqlx::Result<FromDb> {
        sqlx::query_as_unchecked!(FromDb,
            "SELECT v.id as modid, v.name, v.file_version, v.description, v.homepage, v.issues, v.source, v.license, v.download_link, v.environment \
            "SELECT v.id as modid, v.name, v.version, v.file_version, v.description, v.homepage, v.issues, v.source, v.license, v.download_link, v.environment \
             FROM mod_version v \
             INNER JOIN mod_base b ON b.uuid = v.mod_base \
             WHERE b.id = $1 AND v.file_version = $2", id, v)
            .fetch_one(db).await
    }
}

#[derive(Serialize)]
struct Dependency {
    link: Option<Url>,
    mod_name: Option<String>,
    id: String,
    range: String,
    optional: bool,
}

async fn get_dependencies(id: &str, v: &str, db: &Pool<PgConnection>, req: &HttpRequest) -> sqlx::Result<Vec<Dependency>> {
    #[derive(FromRow)]
    struct FromDb {
        child: String,
        link_type: LinkType,
        range: String,
        id: Option<String>,
        name: Option<String>,
    }

    let deps: Vec<FromDb> = sqlx::query_as_unchecked!(FromDb,
        "SELECT l.child, l.link_type, l.range, cb.id, cv.name \
         FROM mod_link l \
         JOIN mod_base b ON b.uuid = l.mod_base \
         LEFT JOIN mod_base cb ON cb.uuid = l.child_ref \
         LEFT JOIN mod_version cv ON cv.mod_base = cb.uuid AND cv.file_version = cb.display_version \
         WHERE b.id = $1 AND l.file_version = $2 AND l.link_type IN ('depends', 'recommends', 'suggests')", id, v)
        .fetch(db)
        .try_collect().await?;

    Ok(deps.into_iter()
        .map(|db| {
            Dependency {
                link: db.id.map(|id| PageState::from_id(id).link(req)),
                mod_name: db.name,
                id: db.child,
                range: db.range,
                optional: db.link_type == LinkType::Recommends || db.link_type == LinkType::Suggests,
            }
        }).collect())
}

#[derive(Serialize)]
struct RequiredBy {
    mod_name: String,
    id: String,
    versions: Vec<RequiredByV>,
    optional: bool,
}

#[derive(Serialize)]
struct RequiredByV {
    link: Url,
    version: String,
}

async fn get_required_by(id: &str, mod_v: &str, db: &Pool<PgConnection>, req: &HttpRequest) -> sqlx::Result<Vec<RequiredBy>> {
    #[derive(FromRow)]
    struct FromDb {
        range: String,
        link_type: LinkType,
        id: String,
        file_version: String,
        name: String,
    }

    let deps: Vec<FromDb> = sqlx::query_as_unchecked!(FromDb,
        "SELECT DISTINCT l.range, l.link_type, pb.id, pv.file_version, pv.name
         FROM mod_link l
         JOIN mod_version pv ON pv.mod_base = l.mod_base AND pv.file_version = l.file_version
         JOIN mod_base pb ON pb.uuid = pv.mod_base
         JOIN mod_version v ON v.id = l.child
         JOIN mod_base b ON b.uuid = v.mod_base
         WHERE b.id = $1
         ORDER BY pv.name ASC, pv.file_version DESC", id)
        .fetch(db)
        .try_collect().await?;

    let mut map = HashMap::new();

    for entry in deps {
        let optional = entry.link_type == LinkType::Recommends || entry.link_type == LinkType::Suggests;
        // if version is invalid just let it pass :^)
        if !semver_rs::satisfies(mod_v, &entry.range, None).unwrap_or(true) { continue; }

        let vec = map.entry((entry.id.clone(), optional)).or_insert_with(|| RequiredBy {
            mod_name: entry.name.clone(),
            id: entry.id.clone(),
            versions: Vec::new(),
            optional,
        });
        vec.versions.push(RequiredByV {
            link: PageState::from_id_ver(&entry.id, &entry.file_version).link(req),
            version: entry.file_version,
        })
    }

    let mut v = Vec::with_capacity(map.len());
    map.drain().map(|(_, v)| v).for_each(|e| v.push(e));

    Ok(v)
}
\ No newline at end of file

M site/src/site/pagestates.rs => site/src/site/pagestates.rs +5 -4
@@ 33,10 33,11 @@ pub struct ModPage<'a> {

impl<'a> ModPage<'a> {
    pub fn from_id(id: impl Into<Cow<'a, str>>) -> Self {
        ModPage {
            id: id.into(),
            v: None,
        }
        ModPage { id: id.into(), v: None }
    }

    pub fn from_id_ver(id: impl Into<Cow<'a, str>>, ver: impl Into<Cow<'a, str>>) -> Self {
        ModPage { id: id.into(), v: Some(ver.into()) }
    }

    pub fn link(&self, req: &HttpRequest) -> Url {

M static/mod.css => static/mod.css +7 -0
@@ 25,4 25,11 @@

#top-links a {
    margin: 0 5px;
}

.deplist .deprange {
    color: #777777;
    font-style: italic;
    margin-left: 4px;
    margin-right: 4px;
}
\ No newline at end of file

M templates/mod.html.hbs => templates/mod.html.hbs +18 -18
@@ 19,28 19,28 @@
    </div>
    <hr>
    <h2>Files</h2>
      {{#each mcversions}}
        <h3>{{mojang_name}}</h3>
        <ul>
          {{#each modversions}}
            {{>mod_in_vlist}}
          {{/each}}
        </ul>
      {{/each}}
      <h3>All Files</h3>
      {{#each allmods}}
        {{>mod_in_vlist}}
      {{/each}}
    <h2>Dependencies ({{dependencies_count}})</h2>
    <ul>
    {{#each dependencies}}
      <li><a href="{{link}}">{{mod_name}}</a>{{#if optional}} <i>(optional)</i>{{/if}}</li>
    {{#each mcversions}}
      <h3>{{mojang_name}}</h3>
      <ul>
        {{#each modversions}}
          {{>mod_in_vlist}}
        {{/each}}
      </ul>
    {{/each}}
    <h3>All Files</h3>
    {{#each allmods}}
      {{>mod_in_vlist}}
    {{/each}}
    <h2>Dependencies ({{dependencies_count}})</h2>
    <ul id="dependencies" class="deplist">
      {{#each dependencies}}
        <li>{{#if link}}<a href="{{link}}">{{mod_name}}</a>{{else}}{{id}}{{/if}}<span class="deprange">{{range}}</span>{{#if optional}} <i>(optional)</i>{{/if}}</li>
      {{/each}}
    </ul>
    <h2>Required by ({{required_by_count}})</h2>
    <ul>
    <ul id="required-by" class="deplist">
      {{#each required_by}}
        <li><a href="{{link}}">{{mod_name}}</a>{{#if optional}} <i>(optional)</i>{{/if}}</li>
        <li>{{mod_name}} ({{#each versions}}<a href="{{link}}">{{version}}</a>{{#if @last}}{{else}}, {{/if}}{{/each}}){{#if optional}} <i>(optional)</i>{{/if}}</li>
      {{/each}}
    </ul>
    <hr>