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
804 changes: 427 additions & 377 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[workspace.package]
edition = "2021"
version = "0.3.21"
version = "0.3.22"


description = "Tower is the best way to host Python data apps in production"
Expand Down
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 25, 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 very generic and doesn't provide specific information about the checksum computation failure. Consider including the actual error details to help users diagnose the issue.

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(&format!("Tower CLI failed to properly prepare your package for deployment. Error: {}. Check that you have permissions to read/write to your temporary directory, and if it keeps happening contact Tower support at https://tower.dev", e));

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();
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 25, 2025

Choose a reason for hiding this comment

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

Using unwrap() here could cause a panic if the key doesn't exist in the HashMap. Since we're iterating over keys from the same HashMap, this should be safe, but consider using expect() with a descriptive message for better error handling.

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 Author

Choose a reason for hiding this comment

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

Ignoring for now, this needs a bit of work overall.


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> {
// 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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "tower"
version = "0.3.21"
version = "0.3.22"


description = "Tower CLI and runtime environment for Tower."
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.81"
channel = "1.88"
2 changes: 1 addition & 1 deletion uv.lock

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

Loading