Skip to content

Commit 809046d

Browse files
committed
Presigned URLs for asset uploads with SHA-256 dedup
Assets are no longer included in the zip — collected separately and uploaded directly to S3 via presigned URLs from the API. Each asset gets a SHA-256 hash (hex in manifest, base64 for S3). Before uploading, HEAD checks compare x-amz-checksum-sha256 to skip unchanged files.
1 parent 04dbb66 commit 809046d

7 files changed

Lines changed: 249 additions & 47 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "openworkers-cli"
3-
version = "0.2.15"
3+
version = "0.3.0"
44
edition = "2024"
55
license = "MIT"
66
description = "CLI for OpenWorkers - Self-hosted Cloudflare Workers runtime"
@@ -51,6 +51,7 @@ uuid = { version = "1", features = ["serde"] }
5151
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls", "multipart"] }
5252
sha2 = "0.10"
5353
hex = "0.4"
54+
base64 = "0.22"
5455
zip = { version = "7", default-features = false, features = ["deflate"] }
5556
hmac = "0.12"
5657
mime_guess = "2"

src/backend/api.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use super::{
2-
Backend, BackendError, CreateDatabaseInput, CreateEnvironmentInput, CreateKvInput,
3-
CreateStorageInput, CreateWorkerInput, Database, DeployInput, Deployment, Environment,
4-
KvNamespace, StorageConfig, UpdateEnvironmentInput, UpdateWorkerInput, UploadResult, Worker,
2+
AssetManifestEntry, Backend, BackendError, CreateDatabaseInput, CreateEnvironmentInput,
3+
CreateKvInput, CreateStorageInput, CreateWorkerInput, Database, DeployInput, Deployment,
4+
Environment, KvNamespace, StorageConfig, UpdateEnvironmentInput, UpdateWorkerInput,
5+
UploadResult, Worker,
56
};
67
use crate::config::DEFAULT_API_URL;
78
use reqwest::Client;
@@ -209,6 +210,7 @@ impl Backend for ApiBackend {
209210
&self,
210211
name: &str,
211212
zip_data: Vec<u8>,
213+
assets_manifest: &[AssetManifestEntry],
212214
) -> Result<UploadResult, BackendError> {
213215
use reqwest::multipart::{Form, Part};
214216

@@ -220,7 +222,13 @@ impl Backend for ApiBackend {
220222
.mime_str("application/zip")
221223
.map_err(|e| BackendError::Api(e.to_string()))?;
222224

223-
let form = Form::new().part("file", part);
225+
let mut form = Form::new().part("file", part);
226+
227+
if !assets_manifest.is_empty() {
228+
let manifest_json = serde_json::to_string(assets_manifest)
229+
.map_err(|e| BackendError::Api(e.to_string()))?;
230+
form = form.text("assets", manifest_json);
231+
}
224232

225233
let response = self
226234
.request(

src/backend/db.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use super::{
2-
Backend, BackendError, CreateDatabaseInput, CreateEnvironmentInput, CreateKvInput,
3-
CreateStorageInput, CreateWorkerInput, Database, DeployInput, Deployment, Environment,
4-
EnvironmentValue, KvNamespace, StorageConfig, UpdateEnvironmentInput, UpdateWorkerInput,
5-
UploadResult, UploadWorkerInfo, UploadedCounts, Worker,
2+
AssetManifestEntry, Backend, BackendError, CreateDatabaseInput, CreateEnvironmentInput,
3+
CreateKvInput, CreateStorageInput, CreateWorkerInput, Database, DeployInput, DeployedInfo,
4+
Deployment, Environment, EnvironmentValue, KvNamespace, StorageConfig, UpdateEnvironmentInput,
5+
UpdateWorkerInput, UploadResult, UploadWorkerInfo, Worker,
66
};
77
use crate::config::PlatformStorageConfig;
88
use crate::s3::{S3Client, S3Config, get_mime_type};
@@ -345,6 +345,7 @@ impl Backend for DbBackend {
345345
&self,
346346
name: &str,
347347
zip_data: Vec<u8>,
348+
_assets_manifest: &[AssetManifestEntry],
348349
) -> Result<UploadResult, BackendError> {
349350
// 1. Get worker by name
350351
let worker = self.get_worker(name).await?;
@@ -540,6 +541,7 @@ impl Backend for DbBackend {
540541
.fetch_one(&self.pool)
541542
.await?;
542543

544+
let next_version: i32 = row.get("out_next_version");
543545
let functions_created: i32 = row.get("functions_created");
544546

545547
if functions_created > 0 {
@@ -556,13 +558,11 @@ impl Backend for DbBackend {
556558
prefix,
557559
});
558560

559-
let mut uploaded_count = 0;
560-
561561
for (path, content) in assets {
562562
let content_type = get_mime_type(&path);
563563

564564
match s3_client.put(&path, content, content_type).await {
565-
Ok(true) => uploaded_count += 1,
565+
Ok(true) => {}
566566
Ok(false) => eprintln!("Failed to upload {}", path),
567567
Err(e) => eprintln!("Error uploading {}: {}", path, e),
568568
}
@@ -595,10 +595,11 @@ impl Backend for DbBackend {
595595
name: worker.name,
596596
url,
597597
},
598-
uploaded: UploadedCounts {
599-
script: true,
600-
assets: uploaded_count,
601-
},
598+
deployed: Some(DeployedInfo {
599+
version: next_version,
600+
functions: functions_created,
601+
}),
602+
assets: None,
602603
})
603604
}
604605

src/backend/mock.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use super::{
2-
Backend, BackendError, CreateDatabaseInput, CreateEnvironmentInput, CreateKvInput,
3-
CreateStorageInput, CreateWorkerInput, Database, DeployInput, Deployment, Environment,
4-
KvNamespace, StorageConfig, UpdateEnvironmentInput, UpdateWorkerInput, UploadResult,
5-
UploadWorkerInfo, UploadedCounts, Worker,
2+
AssetManifestEntry, Backend, BackendError, CreateDatabaseInput, CreateEnvironmentInput,
3+
CreateKvInput, CreateStorageInput, CreateWorkerInput, Database, DeployInput, Deployment,
4+
Environment, KvNamespace, StorageConfig, UpdateEnvironmentInput, UpdateWorkerInput,
5+
UploadResult, UploadWorkerInfo, Worker,
66
};
77
use chrono::Utc;
88
use sha2::{Digest, Sha256};
@@ -184,6 +184,7 @@ impl Backend for MockBackend {
184184
&self,
185185
name: &str,
186186
_zip_data: Vec<u8>,
187+
_assets_manifest: &[AssetManifestEntry],
187188
) -> Result<UploadResult, BackendError> {
188189
let state = self.state.lock().unwrap();
189190

@@ -199,10 +200,8 @@ impl Backend for MockBackend {
199200
name: worker.name.clone(),
200201
url: format!("https://{}.workers.rocks", worker.name),
201202
},
202-
uploaded: UploadedCounts {
203-
script: true,
204-
assets: 0,
205-
},
203+
deployed: None,
204+
assets: None,
206205
})
207206
}
208207

src/backend/mod.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ pub struct DeployInput {
8989
pub struct UploadResult {
9090
pub success: bool,
9191
pub worker: UploadWorkerInfo,
92-
pub uploaded: UploadedCounts,
92+
pub deployed: Option<DeployedInfo>,
93+
pub assets: Option<Vec<PresignedAsset>>,
9394
}
9495

9596
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -102,9 +103,26 @@ pub struct UploadWorkerInfo {
102103

103104
#[derive(Debug, Clone, Serialize, Deserialize)]
104105
#[serde(rename_all = "camelCase")]
105-
pub struct UploadedCounts {
106-
pub script: bool,
107-
pub assets: i32,
106+
pub struct DeployedInfo {
107+
pub version: i32,
108+
pub functions: i32,
109+
}
110+
111+
#[derive(Debug, Clone, Serialize, Deserialize)]
112+
#[serde(rename_all = "camelCase")]
113+
pub struct PresignedAsset {
114+
pub path: String,
115+
pub head_url: String,
116+
pub put_url: String,
117+
}
118+
119+
#[derive(Debug, Clone, Serialize, Deserialize)]
120+
#[serde(rename_all = "camelCase")]
121+
pub struct AssetManifestEntry {
122+
pub path: String,
123+
pub size: usize,
124+
pub content_type: String,
125+
pub hash: String,
108126
}
109127

110128
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -291,6 +309,7 @@ pub trait Backend: Send + Sync {
291309
&self,
292310
name: &str,
293311
zip_data: Vec<u8>,
312+
assets_manifest: &[AssetManifestEntry],
294313
) -> impl std::future::Future<Output = Result<UploadResult, BackendError>> + Send;
295314

296315
// Environment methods

0 commit comments

Comments
 (0)