diff --git a/.gitignore b/.gitignore index c426b01..9114395 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ target build node_modules tmp -.vscode/* \ No newline at end of file +.vscode/* + +# Forc binaries installed by the server. +forc-* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fb82881..887d12f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.1" @@ -129,7 +135,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.2", "object", "rustc-demangle", ] @@ -262,6 +268,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -282,6 +297,72 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.107", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.107", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn 1.0.107", +] + [[package]] name = "devise" version = "0.3.1" @@ -400,6 +481,38 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", + "synstructure", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -423,6 +536,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -436,16 +571,19 @@ dependencies = [ "diesel", "diesel_migrations", "dotenvy", + "flate2", "hex", "nanoid", + "pinata-sdk", "rand", "regex", - "reqwest", + "reqwest 0.12.2", "rocket", "serde", "serde_json", "serial_test", "sha2", + "tar", "thiserror", "tokio", "uuid", @@ -803,6 +941,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.23", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -839,6 +990,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -923,9 +1080,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" @@ -1003,6 +1166,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1012,6 +1185,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -1179,7 +1361,7 @@ checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.42.0", ] @@ -1245,6 +1427,22 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinata-sdk" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc47c8f8fdb02310c4cec4a8d5b5521c1c7e5b0d107cf83278f3330fd7238760" +dependencies = [ + "derive_builder", + "failure", + "log", + "reqwest 0.11.27", + "serde", + "serde_json", + "tokio", + "walkdir", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1359,6 +1557,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "ref-cast" version = "1.0.14" @@ -1414,6 +1621,47 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.15", + "http 0.2.8", + "http-body 0.4.5", + "hyper 0.14.23", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "reqwest" version = "0.12.2" @@ -1430,7 +1678,7 @@ dependencies = [ "http-body 1.0.0", "http-body-util", "hyper 1.2.0", - "hyper-tls", + "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", @@ -1562,6 +1810,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.20.7" @@ -1595,6 +1856,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scc" version = "2.1.0" @@ -1844,6 +2114,12 @@ dependencies = [ "loom", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -1878,6 +2154,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.107", + "unicode-xid", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -1899,6 +2187,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -1908,7 +2207,7 @@ dependencies = [ "cfg-if", "fastrand", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "remove_dir_all", "winapi", ] @@ -2225,6 +2524,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2284,6 +2592,10 @@ name = "uuid" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", + "serde", +] [[package]] name = "valuable" @@ -2303,6 +2615,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -2421,6 +2743,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2678,6 +3009,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index d128183..77b9f80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,11 @@ reqwest = { version = "0.12.2", features = ["json"] } thiserror = "1.0.58" diesel = { version = "2.1.6", features = ["postgres", "uuid", "r2d2"] } dotenvy = "0.15" -uuid = "1.8.0" +uuid = { version = "1.8.0", features = ["v4", "serde"] } diesel_migrations = "2.1.0" rand = "0.8.5" sha2 = "0.10.8" serial_test = "3.1.1" +pinata-sdk = "1.1.0" +tar = "0.4.41" +flate2 = "1.0.33" diff --git a/migrations/2024-09-24-205353_uploads/down.sql b/migrations/2024-09-24-205353_uploads/down.sql new file mode 100644 index 0000000..b6d36e8 --- /dev/null +++ b/migrations/2024-09-24-205353_uploads/down.sql @@ -0,0 +1 @@ +DROP TABLE uploads diff --git a/migrations/2024-09-24-205353_uploads/up.sql b/migrations/2024-09-24-205353_uploads/up.sql new file mode 100644 index 0000000..8ca6412 --- /dev/null +++ b/migrations/2024-09-24-205353_uploads/up.sql @@ -0,0 +1,8 @@ +CREATE TABLE uploads ( + id uuid PRIMARY KEY NOT NULL, + source_code_ipfs_hash VARCHAR NOT NULL, + forc_version VARCHAR NOT NULL, + abi_ipfs_hash VARCHAR, + bytecode_identifier VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +) \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index cae4c6a..43aec28 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -23,6 +23,8 @@ pub enum ApiError { Database(#[from] crate::db::error::DatabaseError), #[error("GitHub error: {0}")] Github(#[from] crate::github::GithubError), + #[error("GitHub error: {0}")] + Upload(#[from] crate::upload::UploadError), } impl<'r, 'o: 'r> Responder<'r, 'o> for ApiError { @@ -30,6 +32,7 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for ApiError { match self { ApiError::Database(_) => Err(Status::InternalServerError), ApiError::Github(_) => Err(Status::Unauthorized), + ApiError::Upload(_) => Err(Status::BadRequest), } } } diff --git a/src/api/publish.rs b/src/api/publish.rs index 336b585..f839fee 100644 --- a/src/api/publish.rs +++ b/src/api/publish.rs @@ -1,8 +1,16 @@ -use rocket::serde::Deserialize; +use rocket::serde::{Deserialize, Serialize}; +use uuid::Uuid; /// The publish request. #[derive(Deserialize, Debug)] pub struct PublishRequest { pub name: String, pub version: String, + pub upload_id: String, +} + +/// The response to an upload_project request. +#[derive(Serialize, Debug)] +pub struct UploadResponse { + pub upload_id: Uuid, } diff --git a/src/db/error.rs b/src/db/error.rs index 7f2c3a5..77bf4ee 100644 --- a/src/db/error.rs +++ b/src/db/error.rs @@ -12,4 +12,6 @@ pub enum DatabaseError { InsertSessionFailed(String), #[error("Failed to save token for user: {0}")] InsertTokenFailed(String), + #[error("Failed to upload: {0}")] + InsertUploadFailed(String), } diff --git a/src/db/mod.rs b/src/db/mod.rs index 6824487..29621b6 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,6 @@ pub mod api_token; pub mod error; +pub mod upload; mod user_session; use self::error::DatabaseError; diff --git a/src/db/upload.rs b/src/db/upload.rs new file mode 100644 index 0000000..a65972f --- /dev/null +++ b/src/db/upload.rs @@ -0,0 +1,30 @@ +use super::error::DatabaseError; +use super::{models, schema, DbConn}; +use diesel::prelude::*; +use uuid::Uuid; + +impl DbConn { + /// Insert an upload record into the database and return the record. + pub fn insert_upload( + &mut self, + upload: &models::NewUpload, + ) -> Result { + // Insert new upload record + let saved_upload = diesel::insert_into(schema::uploads::table) + .values(upload) + .returning(models::Upload::as_returning()) + .get_result(self.inner()) + .map_err(|_| DatabaseError::InsertUploadFailed(upload.id.to_string()))?; + + Ok(saved_upload) + } + + /// Fetch an upload record given the upload ID. + pub fn get_upload(&mut self, upload_id: Uuid) -> Result { + schema::uploads::table + .filter(schema::uploads::id.eq(upload_id)) + .select(models::Upload::as_returning()) + .first::(self.inner()) + .map_err(|_| DatabaseError::NotFound(upload_id.to_string())) + } +} diff --git a/src/lib.rs b/src/lib.rs index c633861..75d8e0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,5 @@ pub mod github; pub mod middleware; pub mod models; pub mod schema; +pub mod upload; pub mod util; diff --git a/src/main.rs b/src/main.rs index 8603d02..7cc65c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,18 +4,25 @@ extern crate rocket; use forc_pub::api::api_token::{CreateTokenRequest, CreateTokenResponse, Token, TokensResponse}; -use forc_pub::api::publish::PublishRequest; +use forc_pub::api::publish::{PublishRequest, UploadResponse}; +use forc_pub::api::ApiError; use forc_pub::api::{ auth::{LoginRequest, LoginResponse, UserResponse}, ApiResult, EmptyResponse, }; -use forc_pub::db::Database; +use forc_pub::db::{Database}; use forc_pub::github::handle_login; use forc_pub::middleware::cors::Cors; use forc_pub::middleware::session_auth::{SessionAuth, SESSION_COOKIE_NAME}; use forc_pub::middleware::token_auth::TokenAuth; +use forc_pub::upload::{handle_project_upload, UploadError}; +use rocket::fs::TempFile; use rocket::http::{Cookie, CookieJar}; use rocket::{serde::json::Json, State}; +use std::fs::{self}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use uuid::Uuid; #[derive(Default)] pub struct ServerState { @@ -95,6 +102,70 @@ fn publish(request: Json, auth: TokenAuth) -> ApiResult", + format = "application/x-www-form-urlencoded", + data = "" +)] +async fn upload_project( + db: &State, + forc_version: &str, + mut tarball: TempFile<'_>, +) -> ApiResult { + // Install the forc version. + eprintln!("Forc version: {:?}", forc_version); + + let forc_path_str = format!("forc-{forc_version}"); + let forc_path = fs::canonicalize(PathBuf::from(&forc_path_str)).unwrap(); + let output = Command::new("cargo") + .arg("install") + .arg("forc") + .arg("--version") + .arg(forc_version) + .arg("--root") + .arg(&forc_path) + .output() + .expect("Failed to execute cargo install"); + + if output.status.success() { + println!("Successfully installed forc with tag {}", forc_version); + } else { + return Err(ApiError::Upload(UploadError::InvalidForcVersion( + forc_version.to_string(), + ))); + } + + // Create an upload ID and temporary directory. + let upload_id = Uuid::new_v4(); + let upload_dir_str = format!("tmp/uploads/{}", upload_id); + let upload_dir = Path::new(&upload_dir_str); + + fs::create_dir_all(upload_dir).unwrap(); + + // Persist the file to disk. + let orig_tarball_path = upload_dir.join("original.tgz"); + tarball + .persist_to(&orig_tarball_path) + .await + .map_err(|_| ApiError::Upload(UploadError::SaveFile))?; + + // Handle the project upload and store the metadata in the database. + let upload = handle_project_upload( + upload_dir, + &upload_id, + &orig_tarball_path, + &forc_path, + forc_version.to_string(), + ) + .await?; + let _ = db.conn().insert_upload(&upload)?; + + // Clean up the temp directory. + fs::remove_dir_all(upload_dir).unwrap(); + + Ok(Json(UploadResponse { upload_id })) +} + /// Catches all OPTION requests in order to get the CORS related Fairing triggered. #[options("/<_..>")] fn all_options() { @@ -128,6 +199,7 @@ fn rocket() -> _ { new_token, delete_token, publish, + upload_project, tokens, all_options, health diff --git a/src/models.rs b/src/models.rs index d6cbdd6..7ec54d2 100644 --- a/src/models.rs +++ b/src/models.rs @@ -63,3 +63,25 @@ pub struct NewApiToken { pub token: Vec, pub expires_at: Option, } + +#[derive(Queryable, Selectable, Debug, Clone)] +#[diesel(table_name = crate::schema::uploads)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Upload { + pub id: Uuid, + pub source_code_ipfs_hash: String, + pub forc_version: String, + pub abi_ipfs_hash: Option, + pub bytecode_identifier: Option, + pub created_at: SystemTime, +} + +#[derive(Insertable)] +#[diesel(table_name = crate::schema::uploads)] +pub struct NewUpload { + pub id: Uuid, + pub source_code_ipfs_hash: String, + pub forc_version: String, + pub abi_ipfs_hash: Option, + pub bytecode_identifier: Option, +} diff --git a/src/schema.rs b/src/schema.rs index 6152c56..ddfd09c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -20,6 +20,17 @@ diesel::table! { } } +diesel::table! { + uploads (id) { + id -> Uuid, + source_code_ipfs_hash -> Varchar, + forc_version -> Varchar, + abi_ipfs_hash -> Nullable, + bytecode_identifier -> Nullable, + created_at -> Timestamp, + } +} + diesel::table! { users (id) { id -> Uuid, @@ -36,4 +47,4 @@ diesel::table! { diesel::joinable!(api_tokens -> users (user_id)); diesel::joinable!(sessions -> users (user_id)); -diesel::allow_tables_to_appear_in_same_query!(api_tokens, sessions, users,); +diesel::allow_tables_to_appear_in_same_query!(api_tokens, sessions, uploads, users,); diff --git a/src/upload.rs b/src/upload.rs new file mode 100644 index 0000000..6ef4451 --- /dev/null +++ b/src/upload.rs @@ -0,0 +1,181 @@ +use crate::models::NewUpload; +use flate2::read::GzDecoder; +use flate2::write::GzEncoder; +use flate2::Compression; +use pinata_sdk::{PinByFile, PinataApi}; +use std::fs::{self, File}; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::env; +use tar::Archive; +use thiserror::Error; +use uuid::Uuid; + +#[derive(Error, Debug)] +pub enum UploadError { + #[error("The project is too large to be uploaded.")] + TooLarge, + #[error("Failed to save zip file.")] + SaveFile, + #[error("Failed to copy files.")] + CopyFiles, + #[error("Invalid Forc version: {0}")] + InvalidForcVersion(String), + #[error("Not a Sway project.")] + InvalidProject, + #[error("Failed to authenticate.")] + Authentication, + #[error("Failed to upload to IPFS.")] + Ipfs, +} + +pub async fn handle_project_upload( + upload_dir: &Path, + upload_id: &Uuid, + orig_tarball_path: &PathBuf, + forc_path: &PathBuf, + forc_version: String, +) -> Result { + eprintln!("upload_id: {:?}", upload_id); + + let unpacked_dir = upload_dir.join("unpacked"); + let release_dir = unpacked_dir.join("out/release"); + let project_dir = upload_dir.join("project"); + + // Unpack the tarball. + let tarball = File::open(orig_tarball_path).unwrap(); + let decompressed = GzDecoder::new(tarball); + let mut archive = Archive::new(decompressed); + archive.unpack(&unpacked_dir).unwrap(); + + // Remove `out` directory if it exists. + let _ = fs::remove_dir_all(unpacked_dir.join("out")); + + eprintln!("forc_path: {:?}", forc_path); + + let output = Command::new(format!("{}/bin/forc", forc_path.to_string_lossy())) + .arg("build") + .arg("--release") + .current_dir(&unpacked_dir) + .output() + .expect("Failed to execute forc build"); + + if output.status.success() { + println!("Successfully built project with forc"); + } else { + return Err(UploadError::InvalidProject); + } + + // Copy files that are part of the Sway project to a new directory. + let output = Command::new("rsync") + .args([ + "-av", + "--prune-empty-dirs", + "--include=*/", + "--include=Forc.toml", + "--include=Forc.lock", + "--include=*.sw", + "--exclude=*", + "unpacked/", + "project", + ]) + .current_dir(upload_dir) + .output() + .expect("Failed to copy project files"); + + if output.status.success() { + println!("Successfully copied project files"); + } else { + return Err(UploadError::CopyFiles); + } + + // Pack the new tarball. + let final_tarball_path = upload_dir.join("project.tgz"); + let tar_gz = File::create(&final_tarball_path).unwrap(); + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut tar = tar::Builder::new(enc); + + // Add files to the tar archive + tar.append_dir_all(".", &project_dir).unwrap(); + + // Finish writing the tar archive + tar.finish().unwrap(); + + // Make sure the GzEncoder finishes and flushes all data + let enc = tar.into_inner().unwrap(); + enc.finish().unwrap(); + + // Store the tarball in IPFS. + let tarball_ipfs_hash = upload_file_to_ipfs(&final_tarball_path).await?; + + fn find_abi_file_in_dir(dir: &Path) -> Option { + if let Ok(dir) = fs::read_dir(dir) { + // Iterate over the directory's contents + for entry in dir { + if let Ok(entry) = entry { + let path = entry.path(); + + // Check if the path is a file and ends with "-abi.json" + if path.is_file() { + if let Some(file_name) = path.file_name() { + if let Some(file_name_str) = file_name.to_str() { + if file_name_str.ends_with("-abi.json") { + return Some(path); // Return the first found file + } + } + } + } + } + } + } + None + } + + // Store the ABI in IPFS. + let (abi_ipfs_hash, bytecode_identifier) = + if let Some(abi_path) = find_abi_file_in_dir(&release_dir) { + let hash = upload_file_to_ipfs(&abi_path).await?; + + // TODO: https://github.com/FuelLabs/forc.pub/issues/16 Calculate the bytecode identifier and store in the database along with the ABI hash. + let bytecode_identifier = None; + + (Some(hash), bytecode_identifier) + } else { + (None, None) + }; + + let upload = NewUpload { + id: *upload_id, + source_code_ipfs_hash: tarball_ipfs_hash, + forc_version, + abi_ipfs_hash, + bytecode_identifier, + }; + + Ok(upload) +} + +async fn upload_file_to_ipfs(path: &PathBuf) -> Result { + match (env::var("PINATA_API_KEY"), env::var("PINATA_API_SECRET")) { + (Ok(api_key), Ok(secret_api_key)) => { + // TODO: move to server context + + let api = + PinataApi::new(api_key, secret_api_key).map_err(|_| UploadError::Authentication)?; + api.test_authentication() + .await + .map_err(|_| UploadError::Authentication)?; + + match api.pin_file(PinByFile::new(path.to_string_lossy())).await { + Ok(pinned_object) => Ok(pinned_object.ipfs_hash), + Err(_) => Err(UploadError::Ipfs), + } + } + _ => { + // TODO: fallback to a local IPFS node for tests + Err(UploadError::Ipfs) + } + } +}