-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: API for uploading a sway project #19
base: master
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
} | ||
|
||
impl<'r, 'o: 'r> Responder<'r, 'o> for ApiError { | ||
fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'o> { | ||
println!("API error: {}", self); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we use println_error
from the forc-tracing
crate here so our console output is standardised?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work! just a couple of nits
fs::create_dir_all(forc_path.clone()).unwrap(); | ||
let forc_path = fs::canonicalize(forc_path.clone()).unwrap(); | ||
|
||
install_forc_at_path(forc_version, &forc_path)?; | ||
|
||
// 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(), | ||
pinata_client.inner(), | ||
) | ||
.await?; | ||
|
||
let _ = db.conn().insert_upload(&upload)?; | ||
|
||
// Clean up the temporary directory. | ||
fs::remove_dir_all(upload_dir).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we introduce an error type that we can use instead of unwrapping on the filesystem calls?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or change these to an expect?
#[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<String>, | ||
pub bytecode_identifier: Option<String>, | ||
pub created_at: SystemTime, | ||
} | ||
|
||
#[derive(Insertable, Debug)] | ||
#[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<String>, | ||
pub bytecode_identifier: Option<String>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's fine but my instinct here would be to try and only have 1 of these types considering they are duplicates apart from the final created_at
field. Disregard if it doesn't make sense in the design though.
} | ||
} | ||
|
||
pub struct MockPinataClient; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a comment here explaining the intended purpose of the Mock Client?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also this is only used for testing; although minimal - this code would still get compiled into the release binary.
Hence maybe worthwhile creating and putting this within the test macro/mod or co-located with the tests.
BytecodeId(String), | ||
} | ||
|
||
pub async fn handle_project_upload( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doc comment for this fn would be useful
let project_dir = upload_dir.join("project"); | ||
|
||
// Unpack the tarball. | ||
let tarball = File::open(orig_tarball_path).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few unwraps in the fn that could be turned into expects
@@ -0,0 +1,8 @@ | |||
CREATE TABLE uploads ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would help with replayability and iterating/testing:
CREATE TABLE uploads ( | |
CREATE TABLE IF NOT EXISTS uploads ( |
@@ -0,0 +1 @@ | |||
DROP TABLE uploads |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: could also do same here:
DROP TABLE uploads | |
DROP TABLE IF EXISTS uploads |
|
||
/// The response to an upload_project request. | ||
#[derive(Serialize, Debug)] | ||
pub struct UploadResponse { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit?
pub struct UploadResponse { | |
pub struct PublishResponse { |
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)?; | ||
Ok(PinataClientImpl { pinata_api: api }) | ||
} | ||
_ => Err(UploadError::Ipfs), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: reduced nesting
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)?; | |
Ok(PinataClientImpl { pinata_api: api }) | |
} | |
_ => Err(UploadError::Ipfs), | |
} | |
dotenv().ok(); | |
let (api_key, secret_api_key) = match (env::var("PINATA_API_KEY"), env::var("PINATA_API_SECRET")) { | |
(Ok(key), Ok(secret)) => (key, secret), | |
_ => return Err(UploadError::Ipfs), | |
}; | |
let api = PinataApi::new(api_key, secret_api_key) | |
.map_err(|_| UploadError::Authentication)?; | |
api.test_authentication() | |
.await | |
.map_err(|_| UploadError::Authentication)?; | |
Ok(PinataClientImpl { pinata_api: api }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like both approaches 😅
With Sophie's it's compact which I'm a big fan of.
With Zeeshan's it's return early which I also a big fan of, but I've recently started trying to not leave variables dangling for the entire scope that aren't used (most of the time it's hard to do, but it's just a style that I've picked up lately).
Combining both approaches:
let api = match (env::var("PINATA_API_KEY"), env::var("PINATA_API_SECRET")) {
(Ok(api_key), Ok(secret_api_key)) => {
PinataApi::new(api_key, secret_api_key)
.map_err(|_| UploadError::Authentication)?
}
_ => return Err(UploadError::Ipfs),
};
api.test_authentication()
.await
.map_err(|_| UploadError::Authentication)?;
Ok(PinataClientImpl { pinata_api: api })
// Install the forc version if it's not already installed. | ||
let forc_path_str = format!("forc-{forc_version}"); | ||
let forc_path = PathBuf::from(&forc_path_str); | ||
fs::create_dir_all(forc_path.clone()).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could also use the tempfile
crate which handles creating temporary files and directories.
Not a fan of introducing external deps if not needed; but the good thing about the crate - and tempdir
function is that if an error occurs anywhere in the function before the directory is removed, the directory would still be removed.
This is because the dir removal is based on the Drop
trait impl - hence dir is removed as soon as the var goes out of scope.
^ this would ensure clean future runs of the function (assuming a previous run failed)
pub forc_version: String, | ||
pub abi_ipfs_hash: Option<String>, | ||
pub bytecode_identifier: Option<String>, | ||
pub created_at: SystemTime, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SystemTime
does not support timezones in the DB and is generally less ergonomic than time::OffsetDateTime
; for persistent data I'd reccomend TZ aware datetime primitive.
let unpacked_dir = upload_dir.join("unpacked"); | ||
let release_dir = unpacked_dir.join("out/release"); | ||
let project_dir = upload_dir.join("project"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if you can foresee any re-usage of these strings; then worthwhile making these constants
archive.unpack(&unpacked_dir).unwrap(); | ||
|
||
// Remove `out` directory if it exists. | ||
let _ = fs::remove_dir_all(unpacked_dir.join("out")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: worthwhile using tempfile (based on previous comment)
.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")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May need to handle amd64
(x86) machine; you may need to match architectures beforehand
} | ||
|
||
/// Installs the given version of forc at the specific root path using cargo-binstall. | ||
pub fn install_forc_at_path(forc_version: &str, forc_path: &Path) -> Result<(), UploadError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using the CLI; i'd reccomend using library/SDK functions to perform uploading operations.
They would be much lighter and ephemeral as one would not need to have the binary installed.
^ could be a future PR; just something to keep in mind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would reccomend an .env.example
file being committed in the repo; with the .env
file ignored; and instructions in readme as quickstart:
cp .env.local .env
pinata_client: &State<PinataClientImpl>, | ||
forc_version: &str, | ||
mut tarball: TempFile<'_>, | ||
) -> ApiResult<UploadResponse> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if forc_version
will need to be sanitised e.g could a request escape the working directory?
/upload_project?v1../../../../../tmp
Nice! Happy to approve once updated and you're happy for another review. |
Closes #18
This PR adds a new API,
upload_project
, that accepts a .tgz file containing a sway project and does the following:Notes: