Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion crates/tower-cmd/src/util/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::fs::File;
use tokio_util::io::ReaderStream;
use tower_package::Package;
use tower_package::{Package, compute_sha256_file};
use tower_telemetry::debug;

use tower_api::apis::configuration::Configuration;
Expand All @@ -20,6 +20,14 @@ pub async fn upload_file_with_progress(
content_type: &str,
progress_cb: Box<dyn Fn(u64, u64) + Send + Sync>,
) -> Result<DeployAppResponse, Error<DeployAppError>> {
let package_hash = match compute_sha256_file(&file_path).await {
Ok(hash) => hash,
Err(e) => {
debug!("Failed to compute package hash: {}", e);
output::die("Tower CLI failed to properly prepare your package for deployment. Check that you have permissions to read/write to your temporary directory, and if it keeps happening contact Tower support at https://tower.dev");
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is generic and doesn't mention the specific failure (checksum computation). Consider making it more specific: 'Tower CLI failed to compute package checksum during deployment preparation.'

Suggested change
output::die("Tower CLI failed to properly prepare your package for deployment. Check that you have permissions to read/write to your temporary directory, and if it keeps happening contact Tower support at https://tower.dev");
output::die("Tower CLI failed to compute package checksum during deployment preparation. Check that you have permissions to read/write to your temporary directory, and if it keeps happening contact Tower support at https://tower.dev");

Copilot uses AI. Check for mistakes.
}
};

// Get the file and its metadata
let file = File::open(file_path).await?;
let metadata = file.metadata().await?;
Expand All @@ -34,6 +42,7 @@ pub async fn upload_file_with_progress(
let client = ReqwestClient::new();
let mut req = client
.request(Method::POST, endpoint_url)
.header("X-Tower-Checksum-SHA256", package_hash)
.header("Content-Type", content_type)
.header("Content-Encoding", "gzip")
.body(Body::wrap_stream(progress_stream));
Expand Down
2 changes: 2 additions & 0 deletions crates/tower-cmd/src/util/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ impl<R: Stream<Item = Result<bytes::Bytes, std::io::Error>> + Unpin> Stream for
Poll::Ready(Some(Ok(chunk))) => {
let chunk_size = chunk.len() as u64;
let mut progress = self.progress.lock().unwrap();

*progress += chunk_size;
(self.progress_cb)(*progress, self.total_size);

Poll::Ready(Some(Ok(chunk)))
}
other => other,
Expand Down
1 change: 1 addition & 0 deletions crates/tower-package/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ config = { workspace = true }
glob = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
snafu = { workspace = true }
tmpdir = { workspace = true }
tokio = { workspace = true }
Expand Down
70 changes: 68 additions & 2 deletions crates/tower-package/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use config::Towerfile;
use tokio_tar::{Archive, Builder};
use glob::glob;
use tmpdir::TmpDir;
use sha2::{Sha256, Digest};

use async_compression::tokio::write::GzipEncoder;
use async_compression::tokio::bufread::GzipDecoder;
Expand Down Expand Up @@ -54,6 +55,10 @@ pub struct Manifest {
// modules_dir_name is the name of the modules directory within the package.
#[serde(default)]
pub modules_dir_name: String,

// checksum contains a hash of all the content in the package.
#[serde(default)]
pub checksum: String,
}

impl Manifest {
Expand Down Expand Up @@ -165,6 +170,7 @@ impl Package {
import_paths: vec![],
app_dir_name: "app".to_string(),
modules_dir_name: "modules".to_string(),
checksum: "".to_string(),
},
}
}
Expand Down Expand Up @@ -202,6 +208,11 @@ impl Package {
let gzip = GzipEncoder::new(file);
let mut builder = Builder::new(gzip);

// These help us compute the integrity of the package contents overall. For each path, we'll
// store a hash of the contents written to the file. Then we'll hash the final content to
// create a fingerprint of the data.
let mut path_hashes = HashMap::new();

// If the user didn't specify anything here we'll package everything under this directory
// and ship it to Tower.
let mut file_globs = spec.file_globs.clone();
Expand All @@ -228,6 +239,10 @@ impl Package {
for (physical_path, logical_path) in file_paths {
// All of the app code goes into the "app" directory.
let logical_path = app_dir.join(logical_path);

let hash = compute_sha256_file(&physical_path).await?;
path_hashes.insert(logical_path.clone(), hash);

builder.append_path_with_name(physical_path, logical_path).await?;
}

Expand All @@ -253,6 +268,10 @@ impl Package {
// Now we write all of these paths to the modules directory.
for (physical_path, logical_path) in file_paths {
let logical_path = module_dir.join(logical_path);

let hash = compute_sha256_file(&physical_path).await?;
path_hashes.insert(logical_path.clone(), hash);

debug!("adding file {}", logical_path.display());
builder.append_path_with_name(physical_path, logical_path).await?;
}
Expand All @@ -266,6 +285,7 @@ impl Package {
schedule: spec.schedule,
app_dir_name: app_dir.to_string_lossy().to_string(),
modules_dir_name: module_dir.to_string_lossy().to_string(),
checksum: compute_sha256_package(&path_hashes)?,
};

// the whole manifest needs to be written to a file as a convenient way to avoid having to
Expand All @@ -282,11 +302,10 @@ impl Package {
"Towerfile",
).await?;

// We'll need to delete the lines above here.
let mut gzip = builder.into_inner().await?;
gzip.shutdown().await?;

//// probably not explicitly required; however, makes the test suite pass so...
// probably not explicitly required; however, makes the test suite pass so...
let mut file = gzip.into_inner();
file.shutdown().await?;

Expand Down Expand Up @@ -490,3 +509,50 @@ fn should_ignore_file(p: &PathBuf) -> bool {

return false;
}

fn compute_sha256_package(path_hashes: &HashMap<PathBuf, String>) -> Result<String, Error> {
let mut sorted_keys: Vec<&PathBuf> = path_hashes.keys().collect();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe dumb question, but it seems we're passing in a full (relative?) path, whereas in the go code we're hashing the filename — are these the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think their cooincidentally the same. I need to do more research. This is especially interesting in the case of e.g. Windows. Let's ship this, and I'll follow up with a bug fix accordingly.

sorted_keys.sort();

// hasher that we'll use for computing the overall SHA256 hash.
let mut hasher = Sha256::new();

for key in sorted_keys {
// We need to sort the keys so that we can compute a consistent hash.
let value = path_hashes.get(key).unwrap();
Copy link

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using unwrap() here is unsafe since we're iterating over keys from the same HashMap. Consider using expect() with a descriptive message or handle the error case explicitly.

Suggested change
let value = path_hashes.get(key).unwrap();
let value = path_hashes.get(key).expect("Key not found in path_hashes during SHA256 computation");

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using unwrap() here is unsafe since we're iterating over keys from the same HashMap

what?

Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using unwrap() is unsafe here. Since we're iterating over keys from the same HashMap, this should never fail, but it's better to handle the error case properly or use a more explicit approach like path_hashes[key].

Suggested change
let value = path_hashes.get(key).unwrap();
let value = path_hashes.get(key).expect("Key not found in path_hashes");

Copilot uses AI. Check for mistakes.

let combined = format!("{}:{}", key.display(), value);
hasher.update(combined.as_bytes());
}

// Finalize and get the hash result
let result = hasher.finalize();

// Convert to hex string
Ok(format!("{:x}", result))
}

pub async fn compute_sha256_file(file_path: &PathBuf) -> Result<String, Error> {
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function is marked as public but appears to be an internal implementation detail. Consider making it private or moving it to a separate module if it needs to be shared.

Suggested change
pub async fn compute_sha256_file(file_path: &PathBuf) -> Result<String, Error> {
async fn compute_sha256_file(file_path: &PathBuf) -> Result<String, Error> {

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This function is marked as pub but appears to be an internal utility. Consider making it private or moving it to a separate module if it needs to be shared between crates.

Suggested change
pub async fn compute_sha256_file(file_path: &PathBuf) -> Result<String, Error> {
async fn compute_sha256_file(file_path: &PathBuf) -> Result<String, Error> {

Copilot uses AI. Check for mistakes.
// Open the file
let file = File::open(file_path).await?;
let mut reader = BufReader::new(file);

// Create a SHA256 hasher
let mut hasher = Sha256::new();

// Read file in chunks to handle large files efficiently
let mut buffer = [0; 8192]; // 8KB buffer
loop {
let bytes_read = reader.read(&mut buffer).await?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}

// Finalize and get the hash result
let result = hasher.finalize();

// Convert to hex string
Ok(format!("{:x}", result))
}
3 changes: 3 additions & 0 deletions crates/tower-package/tests/package_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ async fn it_packages_import_paths() {
// NOTE: These paths are joined by the OS so we need to be more specific about the expected
// path.
assert!(manifest.import_paths.contains(make_path!("modules", "shared")), "Import paths {:?} did not contain expected path", manifest.import_paths);

// We should have some integrity check here too.
assert!(!manifest.checksum.is_empty(), "Manifest integrity check was not set");
}

#[tokio::test]
Expand Down