diff --git a/deployment/Dockerfile b/deployment/Dockerfile index 03c136d..50c6043 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build -FROM lukemathwalker/cargo-chef:latest-rust-1.76 as chef +FROM lukemathwalker/cargo-chef:latest-rust-1.79 as chef WORKDIR /build/ # hadolint ignore=DL3008 @@ -19,6 +19,15 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef as builder +# Install the latest 20 versions of forc with cargo-binstall +RUN curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash +RUN tags=$(curl -s "https://api.github.com/repos/FuelLabs/sway/tags?per_page=20" | grep '"name"' | sed -E 's/.*"name": "v?([^"]+)".*/\1/') && \ + echo "Tags fetched from the repository:" && \ + for tag in $tags; do \ + echo "Tag: $tag" && \ + cargo binstall --no-confirm --root "/usr/local/cargo/bin/forc-$tag" --pkg-url="https://github.com/FuelLabs/sway/releases/download/v$tag/forc-binaries-linux_arm64.tar.gz" --bin-dir="forc-binaries/forc" --pkg-fmt="tgz" forc; \ + done + ENV CARGO_NET_GIT_FETCH_WITH_CLI=true COPY --from=planner /build/recipe.json recipe.json # Build our project dependecies, not our application! @@ -40,6 +49,7 @@ RUN apt-get update -y \ WORKDIR /root/ +COPY --from=builder /usr/local/cargo/bin/forc-* . COPY --from=builder /build/target/debug/forc_pub . COPY --from=builder /build/target/debug/forc_pub.d . COPY --from=builder /build/Rocket.toml . diff --git a/src/lib.rs b/src/lib.rs index 75d8e0e..80c3938 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod db; pub mod github; pub mod middleware; pub mod models; +pub mod pinata; pub mod schema; pub mod upload; pub mod util; diff --git a/src/main.rs b/src/main.rs index 7cc65c9..6b915c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,11 +10,12 @@ 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::pinata::{PinataClient, PinataClientImpl}; use forc_pub::upload::{handle_project_upload, UploadError}; use rocket::fs::TempFile; use rocket::http::{Cookie, CookieJar}; @@ -22,6 +23,7 @@ use rocket::{serde::json::Json, State}; use std::fs::{self}; use std::path::{Path, PathBuf}; use std::process::Command; +use std::time::Instant; use uuid::Uuid; #[derive(Default)] @@ -109,27 +111,29 @@ fn publish(request: Json, auth: TokenAuth) -> ApiResult, + pinata_client: &State, forc_version: &str, mut tarball: TempFile<'_>, ) -> ApiResult { - // Install the forc version. - eprintln!("Forc version: {:?}", forc_version); - + // Install the forc version if it's not already installed. let forc_path_str = format!("forc-{forc_version}"); - let forc_path = fs::canonicalize(PathBuf::from(&forc_path_str)).unwrap(); + let forc_path = PathBuf::from(&forc_path_str); + fs::create_dir_all(forc_path.clone()).unwrap(); + let forc_path = fs::canonicalize(forc_path.clone()).unwrap(); + let output = Command::new("cargo") - .arg("install") - .arg("forc") - .arg("--version") - .arg(forc_version) + .arg("binstall") + .arg("--no-confirm") .arg("--root") .arg(&forc_path) + .arg(format!("--pkg-url=https://github.com/FuelLabs/sway/releases/download/{forc_version}/forc-binaries-linux_arm64.tar.gz")) + .arg("--bin-dir=forc-binaries/forc") + .arg("--pkg-fmt=tgz") + .arg("forc") .output() .expect("Failed to execute cargo install"); - if output.status.success() { - println!("Successfully installed forc with tag {}", forc_version); - } else { + if !output.status.success() { return Err(ApiError::Upload(UploadError::InvalidForcVersion( forc_version.to_string(), ))); @@ -156,6 +160,7 @@ async fn upload_project( &orig_tarball_path, &forc_path, forc_version.to_string(), + pinata_client.inner(), ) .await?; let _ = db.conn().insert_upload(&upload)?; @@ -186,9 +191,12 @@ fn health() -> String { // Launch the rocket server. #[launch] -fn rocket() -> _ { +async fn rocket() -> _ { + let pinata_client = PinataClientImpl::new().await.expect("pinata client"); + rocket::build() .manage(Database::default()) + .manage(pinata_client) .attach(Cors) .mount( "/", diff --git a/src/pinata/mod.rs b/src/pinata/mod.rs new file mode 100644 index 0000000..cece464 --- /dev/null +++ b/src/pinata/mod.rs @@ -0,0 +1,64 @@ +use std::{env, path::PathBuf}; + +use dotenvy::dotenv; +use pinata_sdk::{PinByFile, PinataApi}; + +use crate::upload::UploadError; + +pub trait PinataClient: Sized { + fn new() -> impl std::future::Future> + Send; + fn upload_file_to_ipfs( + &self, + path: &PathBuf, + ) -> impl std::future::Future> + Send; +} + +pub struct PinataClientImpl { + pinata_api: PinataApi, +} + +impl PinataClient for PinataClientImpl { + async fn new() -> Result { + dotenv().ok(); + match (env::var("PINATA_API_KEY"), env::var("PINATA_API_SECRET")) { + (Ok(api_key), Ok(secret_api_key)) => { + let api = PinataApi::new(api_key, secret_api_key) + .map_err(|_| UploadError::Authentication)?; + api.test_authentication() + .await + .map_err(|_| UploadError::Authentication)?; + return Ok(PinataClientImpl { pinata_api: api }); + } + _ => { + return Err(UploadError::Ipfs); + } + } + } + + /// Uploads a file at the given path to a Pinata IPFS gateway. + async fn upload_file_to_ipfs(&self, path: &PathBuf) -> Result { + match self + .pinata_api + .pin_file(PinByFile::new(path.to_string_lossy())) + .await + { + Ok(pinned_object) => Ok(pinned_object.ipfs_hash), + Err(_) => Err(UploadError::Ipfs), + } + } +} + +pub struct MockPinataClient; + +impl PinataClient for MockPinataClient { + fn new() -> impl std::future::Future> + Send { + async { Ok(MockPinataClient) } + } + + fn upload_file_to_ipfs( + &self, + _path: &PathBuf, + ) -> impl std::future::Future> + Send { + async { Ok("ABC123".to_string()) } + } +} diff --git a/src/upload.rs b/src/upload.rs index 6ef4451..67f4882 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -1,14 +1,15 @@ use crate::models::NewUpload; +use crate::pinata::{self, PinataClient, PinataClientImpl}; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; use pinata_sdk::{PinByFile, PinataApi}; +use std::env; 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; @@ -37,9 +38,8 @@ pub async fn handle_project_upload( orig_tarball_path: &PathBuf, forc_path: &PathBuf, forc_version: String, + pinata_client: &impl PinataClient, ) -> 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"); @@ -53,8 +53,6 @@ pub async fn handle_project_upload( // 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") @@ -62,9 +60,7 @@ pub async fn handle_project_upload( .output() .expect("Failed to execute forc build"); - if output.status.success() { - println!("Successfully built project with forc"); - } else { + if !output.status.success() { return Err(UploadError::InvalidProject); } @@ -85,9 +81,7 @@ pub async fn handle_project_upload( .output() .expect("Failed to copy project files"); - if output.status.success() { - println!("Successfully copied project files"); - } else { + if !output.status.success() { return Err(UploadError::CopyFiles); } @@ -108,7 +102,9 @@ pub async fn handle_project_upload( enc.finish().unwrap(); // Store the tarball in IPFS. - let tarball_ipfs_hash = upload_file_to_ipfs(&final_tarball_path).await?; + let tarball_ipfs_hash = pinata_client + .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) { @@ -136,7 +132,7 @@ pub async fn handle_project_upload( // 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?; + let hash = pinata_client.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; @@ -157,25 +153,39 @@ pub async fn handle_project_upload( 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) - } +#[cfg(test)] +mod tests { + use crate::pinata::MockPinataClient; + + use super::*; + + #[tokio::test] + async fn handle_project_upload_success() { + let upload_id = Uuid::new_v4(); + let upload_dir = PathBuf::from("tmp/uploads/").join(upload_id.to_string()); + let orig_tarball_path = PathBuf::from("tests/fixtures/sway-project.tgz"); + let forc_version = "0.63.4"; + let forc_path = PathBuf::from("tests/fixtures/forc-0.63.4") + .canonicalize() + .unwrap(); + let mock_client = MockPinataClient::new().await.expect("mock pinata client"); + + let result = handle_project_upload( + &upload_dir, + &upload_id, + &orig_tarball_path, + &forc_path, + forc_version.to_string(), + &mock_client, + ) + .await + .expect("result ok"); + + assert!(result.id == upload_id); + assert_eq!(result.source_code_ipfs_hash, "ABC123".to_string()); + assert_eq!(result.abi_ipfs_hash, Some("ABC123".to_string())); + assert!(result.forc_version == forc_version); + // TODO: https://github.com/FuelLabs/forc.pub/issues/16 + // assert!(result.bytecode_identifier.is_some()); } } diff --git a/tests/fixtures/sway-project.tgz b/tests/fixtures/sway-project.tgz new file mode 100644 index 0000000..5532f49 Binary files /dev/null and b/tests/fixtures/sway-project.tgz differ