diff --git a/.env b/.env new file mode 100644 index 0000000..690a08e --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +# create sqlite if it doesn't already exist +DATABASE_URL=sqlite:///./test.db \ No newline at end of file diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..0f5c519 --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,46 @@ +name: Run tests + +on: + push: + branches: + - "*" + pull_request: + branches: + - "*" + +jobs: + cargo_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Run tests + run: cargo test --all + + cargo_clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Run clippy + run: cargo clippy --all -- -D warnings + + cargo_fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Run fmt + run: cargo fmt --all -- --check \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0e4973b..2e82581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,6 +237,21 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -326,6 +341,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -364,6 +385,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "config" version = "0.15.6" @@ -429,6 +464,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.16" @@ -463,6 +504,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -485,6 +561,43 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf1bedf64cdb9643204a36dd15b19a6ce8e7aa7f7b105868e9f1fad5ffa7d12" +dependencies = [ + "bitflags", + "byteorder", + "diesel_derives", + "itoa", + "libsqlite3-sys", + "pq-sys", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -515,6 +628,32 @@ dependencies = [ "const-random", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -530,6 +669,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + [[package]] name = "flate2" version = "1.0.35" @@ -551,9 +699,16 @@ name = "forgejo-pages" version = "0.1.0" dependencies = [ "actix-web", + "chrono", "config", + "diesel", + "dotenvy", + "fern", + "git2", + "lazy_static", "log", "serde", + "toml", ] [[package]] @@ -622,6 +777,21 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "git2" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "h2" version = "0.3.26" @@ -665,6 +835,12 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "0.2.12" @@ -688,6 +864,29 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -806,6 +1005,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -858,6 +1063,16 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "json5" version = "0.4.1" @@ -875,12 +1090,68 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libgit2-sys" +version = "0.18.0+1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "litemap" version = "0.7.4" @@ -959,6 +1230,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -974,6 +1254,24 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -1103,6 +1401,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pq-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc05d7ea95200187117196eee9edd0644424911821aeb28a18ce60ea0b8793" +dependencies = [ + "vcpkg", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -1233,6 +1540,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -1372,6 +1685,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.96" @@ -1601,6 +1920,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -1613,6 +1938,73 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 46f1a2f..501b5ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,11 @@ license = "GPLv2" actix-web = "4" config = "0.15" log = "0.4" -serde = "1" \ No newline at end of file +serde = "1" +lazy_static = "1.5.0" +git2 = "*" +chrono = "*" +diesel = { version = "2.2.0", features = ["postgres", "sqlite"] } +dotenvy = "0.15" +toml = "*" +fern = "0.7" \ No newline at end of file diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..315f58b --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "/home/aces/forgejo-pages/migrations" diff --git a/src/backends/git.rs b/src/backends/git.rs new file mode 100644 index 0000000..14c66d8 --- /dev/null +++ b/src/backends/git.rs @@ -0,0 +1,239 @@ +use std::borrow::BorrowMut; +use std::cell::Ref; +use std::cell::RefCell; +use std::ops::DerefMut; +use std::sync::Arc; + +use diesel::Connection; +use diesel::QueryResult; +use diesel::prelude::*; +use crate::conf; +use crate::conf::CONFIG; +use crate::data::PageError; +use crate::data::{self, Page, PageManager}; +use crate::schema::pages; + +#[derive(diesel::MultiConnection)] +enum AnyConnection { + Postgresql(PgConnection), + Sqlite(SqliteConnection), +} + +pub struct GitPage { + conn: Arc, + id: i32, +} + +impl GitPage { + fn get_connection(&self) -> &mut AnyConnection { + } +} + +impl Page for GitPage { + type Id = i32; + + fn id(&self) -> Self::Id { + self.id.clone() + } + + fn author(&self) -> String { + use crate::schema::pages::dsl::*; + + let connection = self.get_connection(); + + let page: Vec = pages + .filter(id.eq(self.id)) + .limit(1) + .select(author) + .load(connection) + .expect("Failed to load page"); + + // Return the author, assuming at least one result + page.get(0).cloned().unwrap_or_else(|| "Unknown author".to_string()) + } + + fn routing(&self) -> crate::data::PageRouting { + todo!() + } + + fn version(&self) -> String { + use crate::schema::git_page_source::dsl::*; + + let connection = self.get_connection(); + + let git_page_source: Vec = crate::schema::git_page_source::table + .filter(page_id.eq(self.id)) + .limit(1) + .select(repo_url) + .load(connection) + .expect("Failed to load git page source"); + + git_page_source.get(0).cloned().unwrap_or_else(|| "Unknown version".to_string()) + } + + fn update(&mut self) -> Result<(), crate::data::PageError> { + todo!() + } +} + +pub struct GitPageManager<'a> { + pub connection: RefCell, + pages: Vec>, +} + +impl GitPageManager<'_> { + pub fn new() -> Self { + let connection = AnyConnection::establish("sqlite://git.db").unwrap(); + GitPageManager { + connection: RefCell::new(connection), + pages: Vec::new(), + } + } +} + + +// Filesystem-based storage +// - D [PAGES_TEMP] +// - D [git_url] +// - D [PAGES_ROOT] +// - D [UUID] +// - F? CNAME +// - F? All other files +// - F? .git +impl<'a> GitPageManager<'a> { + fn create(&mut self, url: String, owner: String) -> Result<&GitPage<'a>, PageError> { + // - Prepare data + // - Insert page into database (STATUS: FETCHING) + // - Clone repo to local storage (temp) + // - Gather CNAME and other data from repo + // - Validate data (Perform checks) + // - Update page in database (STATUS: APPLYING) + // - Move repo to final storage location + // - Validate + // - Update page in database (STATUS: READY) + + // Insert the page into the database + let connection = &mut *self.connection.borrow_mut(); + let page_id: i32 = 0; + { + use crate::schema::pages::dsl::*; + let new_page = crate::models::NewPage { + name: url.clone(), + author: owner.clone(), + status: 1, + }; + + let res_insert = diesel::insert_into(pages) + .values(&new_page) + .execute(connection); + + if res_insert.is_err() { + return Err(PageError::InternalServerError(res_insert.err().unwrap().to_string())); + } + + // Get the ID of the page, given name and author + let res_page_id: QueryResult = pages + .filter(name.eq(url.clone())) + .filter(author.eq(owner.clone())) + .select(id) + .first(connection); + } + { + // Git tracking attachment + use crate::schema::git_page_source::dsl::*; + let res_insert_git = diesel::insert_into(git_page_source) + .values(&crate::models::NewGitPageSource { + page_id: 0, + repo_url: url.clone(), + branch: "master".to_string(), + }) + .execute(connection); + } + + + // Clone to filesystem (temp) + let temp_path = format!("{}/{}", CONFIG.temp_dir, url); + let _ = std::fs::create_dir_all(&temp_path); + let res_git_repo = git2::Repository::clone(&url, &temp_path); + + if res_git_repo.is_err() { + return Err(PageError::FetchError(res_git_repo.err().unwrap().to_string())); + } + + // Open CNAME file + let cname_path = format!("{}/CNAME", temp_path); + let cname = std::fs::read_to_string(&cname_path); + + // CNAME follows GitHub's rules + // Each line is a domain + let domains: Vec = cname.unwrap_or("".to_string()) + .split("\n") + .map(|s| s.to_string()) + .collect(); + + // TODO: Validation step here - Put this off for now + + // Move to final storage location + let final_path = format!("{}/{}", CONFIG.pages_root, page_id); + let _ = std::fs::create_dir_all(&final_path); + let _ = std::fs::rename(&temp_path, &final_path); + + // Update the page in the database (we are now ready) + { + use crate::schema::pages::dsl::*; + let res_update = diesel::update(pages) + .filter(id.eq(page_id)) + .set(status.eq(3)) + .execute(connection); + + if res_update.is_err() { + return Err(PageError::InternalServerError(res_update.err().unwrap().to_string())); + } + } + + // Create the page object + let page = GitPage { + conn: + id: page_id, + }; + + // Add the page to the list of pages + self.pages.push(page); + + // Return the page + let res_page = self.page(page_id); + + match res_page { + Ok(page) => Ok(page), + Err(err) => Err(PageError::InternalServerError("Failed to create page".to_string())), + } + + } +} + +impl<'a> PageManager<'a, GitPage<'a>> for GitPageManager<'a> { + + fn query(&self, author: String, path: String, host: Option) -> Result { + todo!() + } + + fn page(&self, id: as data::Page>::Id) -> Result<&GitPage<'a>, crate::data::PageError> { + let page = self.pages.iter().find(|page| page.id() == id); + match page { + Some(page) => Ok(page), + None => Err(crate::data::PageError::NotFound(format!("Page not found: {}", id))), + } + } + + fn page_mut(&mut self, id: as data::Page>::Id) -> Result<&mut GitPage<'a>, crate::data::PageError> { + let page = self.pages.iter_mut().find(|page| page.id() == id); + match page { + Some(page) => Ok(page), + None => Err(crate::data::PageError::NotFound(format!("Page not found: {}", id))), + } + } + + fn pages_iter(&'a self) -> impl Iterator> { + self.pages.iter() + } +} \ No newline at end of file diff --git a/src/backends/mod.rs b/src/backends/mod.rs new file mode 100644 index 0000000..2f6faf3 --- /dev/null +++ b/src/backends/mod.rs @@ -0,0 +1 @@ +pub mod git; \ No newline at end of file diff --git a/src/conf.rs b/src/conf.rs index 56f091f..bdc4510 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,11 +1,44 @@ +use lazy_static::lazy_static; use serde::Deserialize; -#[derive(Deserialize)] -pub struct Domain { +/* Variables */ +const CONFIG_PREFIX: &str = "FJ_PAGES_"; + +const CONFIG_DEFAULT: &str = r#" +[domain] +# The domain of the website +domain = "*" + +# The temporary directory for storing files +temp_dir = "/tmp" +"#; + +lazy_static! { + #[derive(Debug)] + pub static ref CONFIG: Config = { + // Identify path of the configuration file + let path = std::env::var(format!("{}CONFIG_PATH", CONFIG_PREFIX)).unwrap_or("config.toml".to_string()); + // Read the configuration file + let result_config = std::fs::read_to_string(path) + .unwrap_or(CONFIG_DEFAULT.to_string()); + // Parse the configuration file + toml::from_str(&result_config) + .expect("Failed to parse configuration file") + }; } -#[derive(Deserialize)] +/* Structures */ + +#[derive(Deserialize, Debug, Clone)] +pub struct Domain { + // Regex is allowed + domains: Vec +} + +#[derive(Deserialize, Debug, Clone)] pub struct Config { - pub domain: Domain + pub domain: Domain, + pub temp_dir: String, + pub pages_root: String, } \ No newline at end of file diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..1caa0cc --- /dev/null +++ b/src/data.rs @@ -0,0 +1,98 @@ +/// Data storage and retrieval + +/* -------------------------------------------------------------------------- */ +/* Data */ +/* -------------------------------------------------------------------------- */ + +use serde::{Deserialize, Serialize}; + +pub enum PageError { + NotFound(String), + Unauthorized(String), + InternalServerError(String), + Corrupted(String), + FetchError(String), +} + +pub trait Page: Sized { + type Id; + + /// Gets the identifier of the page + /// This can be used to differentiate between pages + /// + /// # Returns + /// + fn id(&self) -> Self::Id; + fn author(&self) -> String; + fn routing(&self) -> PageRouting; + /// Get the revision of the page + /// + /// # Returns + /// + /// * `String` - The revision of the page - This is implementation defined + fn version(&self) -> String; + fn update(&mut self) -> Result<(), PageError>; +} + +#[derive(Serialize, Deserialize)] +pub struct PageRouting { + pub hosts: Option>, +} + +pub enum PageStatus { + // 200 + Ok = 200, + Created = 201, + Accepted = 202, + NoContent = 204, + ResetContent = 205, + + // 300 + + // 400 + BadRequest = 400, + Unauthorized = 401, + // No one cares about 402... + Forbidden = 403, + NotFound = 404 +} + +pub struct PageQueryResult { + pub content: Option, + pub status: PageStatus +} + +pub trait PageManager<'a, P: Page + 'a> { + /// Resolve a page + /// + /// # Arguments + /// + /// * `author` - The author of the repository - This is required + /// * `host` - The host of the repository - This allows CNAMEs to be used to point to the repository + /// * `path` - The query path - This is treated either as a file path (in the case of a user page), + /// or as a repository path (in the case of a project page) + /// + /// # Returns + /// + /// * `PageQueryResult` - The result of the query - This can be page contents, etc + /// * `PageError` - Any error that occurred processing the query + /// + /// Note that page errors, such as Error 404, are not an error that occurs processing the query + /// Thus, they are returned via the `PageQueryResult` instead of the `PageError` + fn query(&self, author: String, path: String, host: Option) -> Result; + + fn page(&self, id: P::Id) -> Result<&P, PageError>; + fn page_mut(&mut self, id: P::Id) -> Result<&mut P, PageError>; + fn pages_iter(&'a self) -> impl Iterator; + /// Delete the page + /// This will remove the page from being hosted or stored + /// + /// # Returns + /// + /// * `Result<(), (PageError, Self)>` - If the page was deleted successfully, this will return `Ok(())` + /// Otherwise, it will return `Err((PageError, Self))` with the error and the page + /// + /// Consequently, since the page is not returned if it is deleted successfully, the page is consumed + /// And since it returns it if it fails, it is recoverable for failure handling + fn delete(&mut self, id: P::Id) -> Result<(), PageError>; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 134b499..d4f7240 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,36 @@ +use conf::CONFIG; + +pub mod backends; pub mod conf; -pub mod sitedata; +pub mod data; +pub mod schema; +pub mod models; + +fn prepare_logger() { + // Fern + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{}[{}][{}] {}", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + record.target(), + record.level(), + message + )) + }) + .level(log::LevelFilter::Debug) + .chain(std::io::stdout()) + .apply() + .expect("Failed to initialize logger"); +} fn main() { - + // Prepare logger + prepare_logger(); + + // Load configuration + let config = CONFIG.clone(); + + // Print configuration + println!("Configuration: {:#?}", config); } \ No newline at end of file diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..8e31a78 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,50 @@ +use diesel::prelude::{Insertable, Queryable}; + +use crate::schema::*; + +#[derive(Insertable)] +#[table_name = "pages"] +pub struct NewPage { + pub author: String, + pub name: String, + pub status: i32, +} + +#[derive(Queryable)] +pub struct Page { + pub id: i32, + pub author: String, + pub name: String, +} + +#[derive(Insertable)] +#[table_name = "routing_record"] +pub struct NewRoutingRecord { + pub page_id: i32, + pub record_type: String, + pub record_value: String, +} + +#[derive(Queryable)] +pub struct RoutingRecord { + pub id: i32, + pub page_id: i32, + pub record_type: String, + pub record_value: String, +} + +#[derive(Insertable)] +#[table_name = "git_page_source"] +pub struct NewGitPageSource { + pub page_id: i32, + pub repo_url: String, + pub branch: String, +} + +#[derive(Queryable)] +pub struct GitPageSource { + pub id: i32, + pub page_id: i32, + pub repo_url: String, + pub branch: String, +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..a7f11d8 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,46 @@ +// Diesel schema +diesel::table! { + pages (id) { + id -> Int4, + author -> Text, + name -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + // - 0: Invalid + // - 1: Fetching + // - 2: Validated + // - 3: Ready + status -> Integer + } +} + +diesel::table! { + routing_record (id) { + id -> Int4, + page_id -> Int4, + record_value -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::table! { + git_page_source (id) { + id -> Int4, + page_id -> Int4, + repo_url -> Text, + branch -> Text, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +diesel::joinable!(routing_record -> pages (page_id)); + +diesel::joinable!(git_page_source -> pages (page_id)); + +diesel::allow_tables_to_appear_in_same_query!( + pages, + routing_record, + git_page_source, +); \ No newline at end of file diff --git a/src/sitedata/control.rs b/src/sitedata/control.rs deleted file mode 100644 index 3ca80fd..0000000 --- a/src/sitedata/control.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub enum SiteError { - PageNotFound(String), - DataError(String), - NetworkError(String) -} - -pub trait Site { - fn get_page(&self, url: &str) -> Result, ()>; -} - -pub trait SiteControl { - fn is_ok() -> bool; - fn site(&self) -> S; - fn can_update(&self) -> bool; - fn repair(&mut self) -> Result<(), ()>; - fn update(&mut self) -> Result; -} \ No newline at end of file diff --git a/src/sitedata/git.rs b/src/sitedata/git.rs deleted file mode 100644 index 34be53a..0000000 --- a/src/sitedata/git.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub struct GitSiteProvider { - pub current_commit: String -} - -pub impl GitSiteProvider { - fn from_remote(url: &str) -> Self { - - - Self { - - } - } -} - -pub impl Site for GitSiteProvider { - -} \ No newline at end of file diff --git a/src/sitedata/mod.rs b/src/sitedata/mod.rs deleted file mode 100644 index 95d7fa2..0000000 --- a/src/sitedata/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod control; -pub mod git; \ No newline at end of file