Compare commits

..

11 Commits

Author SHA1 Message Date
charlesgauthereau e62de95827 chore(release): 1.1.5 2026-02-19 21:15:55 +01:00
Charles GTE fc3612fa3f Merge pull request #14 from Portabase/dev
fix: s3 provider port config.
2026-02-19 21:15:19 +01:00
charlesgauthereau 18fe211ad1 fix: s3 provider port config. 2026-02-19 21:14:35 +01:00
charlesgauthereau 74ada01232 chore: Cargo.lock 2026-02-19 16:10:37 +01:00
charlesgauthereau 4fef360c06 chore(release): 1.1.4 2026-02-19 16:10:17 +01:00
charlesgauthereau 2647941008 fix: tus.rs 2026-02-19 16:09:43 +01:00
Charles GTE b4bdc89888 Merge pull request #13 from Portabase/fix/error-handling
fix/error-handling
2026-02-18 19:28:33 +01:00
charlesgauthereau 829e5bade3 fix: tus.rs 2026-02-18 17:28:59 +01:00
charlesgauthereau fc9a4ef8fb chore(release): 1.1.4-rc.1 2026-02-18 17:28:27 +01:00
charlesgauthereau 3c5734ed4e fix: tus.rs 2026-02-18 17:27:24 +01:00
charlesgauthereau 4b77896ecb fix: Cargo.lock 2026-02-16 11:30:38 +01:00
7 changed files with 133 additions and 42 deletions
+2 -2
View File
@@ -22,5 +22,5 @@ keywords:
- self-hosted
- portabase
license: Apache-2.0
version: 1.1.3
date-released: "2026-02-16"
version: 1.1.5
date-released: "2026-02-19"
Generated
+1 -1
View File
@@ -2851,7 +2851,7 @@ dependencies = [
[[package]]
name = "portabase-agent"
version = "1.1.3-rc.1"
version = "1.1.4"
dependencies = [
"aes",
"aes-gcm",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "portabase-agent"
version = "1.1.3"
version = "1.1.5"
edition = "2024"
[dependencies]
+1 -1
View File
@@ -16,7 +16,7 @@ services:
APP_ENV: development
LOG: debug
TZ: "Europe/Paris"
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiOWZjYjhmNjUtNTcyMC00N2M3LThkNWItYWFlMmVjOTc2OWU1IiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiZjg4Y2E0MDMtNDgwOS00NGM4LTlkZjItY2VkNWYwYzhkNTM2IiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
#POOLING: 1
#DATABASES_CONFIG_FILE: "config.toml"
extra_hosts:
+11 -13
View File
@@ -32,7 +32,6 @@ impl StorageProvider for S3Provider {
storage: &DatabaseStorage,
encrypt: Option<bool>,
) -> UploadResult {
let Some(file_path) = result.backup_file else {
return UploadResult {
storage_id: storage.id.clone(),
@@ -59,13 +58,7 @@ impl StorageProvider for S3Provider {
let encrypt = encrypt.unwrap_or(false);
let upload = match build_stream(
&file_path,
encrypt,
&ctx.edge_key.master_key_b64,
)
.await
{
let upload = match build_stream(&file_path, encrypt, &ctx.edge_key.master_key_b64).await {
Ok(u) => u,
Err(e) => {
error!("Stream build failed: {}", e);
@@ -102,15 +95,20 @@ impl StorageProvider for S3Provider {
let region = Region::new(config.region.clone().unwrap_or("us-east-1".to_string()));
let scheme = if config.ssl { "https" } else { "http" };
let endpoint = match config.port {
Some(port) => format!("{scheme}://{}:{port}", config.end_point_url),
None => format!("{scheme}://{}", config.end_point_url),
};
info!("S3 endpoint to {}", &endpoint);
let sdk_config = s3::config::Builder::new()
.credentials_provider(credentials)
.region(region)
.force_path_style(true)
.endpoint_url(format!(
"{}://{}",
if config.ssl { "https" } else { "http" },
config.end_point_url
))
.endpoint_url(endpoint)
.behavior_version(BehaviorVersion::latest())
.build();
@@ -8,4 +8,5 @@ pub struct S3ProviderConfig {
pub end_point_url: String,
pub ssl: bool,
pub region: Option<String>,
pub port: Option<String>,
}
+116 -24
View File
@@ -1,8 +1,8 @@
use anyhow::Result;
use anyhow::{Context, Result};
use bytes::Bytes;
use futures::{Stream, StreamExt};
use log::info;
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use tracing::{error, info};
const PATCH_CHUNK_SIZE: usize = 1 * 1024 * 1024;
@@ -18,39 +18,62 @@ where
let client = reqwest::Client::new();
info!("File size: {}", total_size);
info!("Endpoint URL: {}", tus_endpoint);
let mut headers = HeaderMap::new();
headers.insert("Tus-Resumable", HeaderValue::from_static("1.0.0"));
headers.insert("Upload-Defer-Length", HeaderValue::from_static("1"));
let mut create_headers = HeaderMap::new();
create_headers.insert("Tus-Resumable", HeaderValue::from_static("1.0.0"));
create_headers.insert("Upload-Defer-Length", HeaderValue::from_static("1"));
let resp = client
.post(tus_endpoint)
.headers(headers.clone())
.headers(create_headers.clone())
.send()
.await?;
.await
.context("Failed to send POST to create TUS upload")?;
if !resp.status().is_success() {
anyhow::bail!("Failed to create upload: {}", resp.status());
let status = resp.status();
let headers = resp.headers().clone();
let body = resp
.text()
.await
.unwrap_or_else(|_| "<failed to read body>".into());
error!(
"TUS creation failed | status={} | headers={:?} | body={}",
status, headers, body
);
anyhow::bail!(
"Failed to create upload.\nStatus: {}\nHeaders: {:?}\nBody: {}",
status,
headers,
body
);
}
let upload_url = resp
.headers()
.get("Location")
.ok_or_else(|| anyhow::anyhow!("Missing Location header"))?
.to_str()?
.context("TUS creation response missing Location header")?
.to_str()
.context("Invalid Location header value")?
.to_string();
let mut stream = Box::pin(
encrypted_stream.map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
);
let mut stream = Box::pin(encrypted_stream);
let mut offset: u64 = 0;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
let chunk = chunk.context("Stream produced IO error")?;
for sub_chunk in chunk.chunks(PATCH_CHUNK_SIZE) {
let mut patch_headers = extra_headers.clone();
patch_headers.insert("Tus-Resumable", HeaderValue::from_static("1.0.0"));
patch_headers.insert("Upload-Offset", HeaderValue::from_str(&offset.to_string())?);
patch_headers.insert(
"Upload-Offset",
HeaderValue::from_str(&offset.to_string())
.context("Invalid offset header value")?,
);
patch_headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("application/offset+octet-stream"),
@@ -61,24 +84,69 @@ where
.headers(patch_headers)
.body(sub_chunk.to_vec())
.send()
.await?;
.await
.with_context(|| format!("PATCH request failed at offset {}", offset))?;
if !patch_resp.status().is_success() {
let status = patch_resp.status();
let headers = patch_resp.headers().clone();
let body = patch_resp
.text()
.await
.unwrap_or_else(|_| "<failed to read body>".into());
error!(
"TUS PATCH failure | offset={} | status={} | body={}",
offset, status, body
);
anyhow::bail!(
"Chunk upload failed at offset {}: {}",
"Chunk upload failed.\n\
URL: {}\n\
Offset: {}\n\
Status: {}\n\
Headers: {:?}\n\
Body: {}",
upload_url,
offset,
patch_resp.status()
status,
headers,
body
);
}
if let Some(server_offset) = patch_resp.headers().get("Upload-Offset") {
let server_offset = server_offset
.to_str()
.context("Invalid Upload-Offset header")?
.parse::<u64>()
.context("Failed to parse Upload-Offset header")?;
let expected = offset + sub_chunk.len() as u64;
if server_offset != expected {
anyhow::bail!(
"Offset mismatch detected.\nLocal expected: {}\nServer returned: {}",
expected,
server_offset
);
}
}
offset += sub_chunk.len() as u64;
// info!("Progress: {}/{}", offset, total_size);
}
}
let mut finalize_headers = extra_headers.clone();
finalize_headers.insert("Tus-Resumable", HeaderValue::from_static("1.0.0"));
finalize_headers.insert("Upload-Offset", HeaderValue::from_str(&offset.to_string())?);
finalize_headers.insert("Upload-Length", HeaderValue::from_str(&offset.to_string())?);
finalize_headers.insert(
"Upload-Offset",
HeaderValue::from_str(&offset.to_string()).context("Invalid finalize offset header")?,
);
finalize_headers.insert(
"Upload-Length",
HeaderValue::from_str(&offset.to_string()).context("Invalid finalize length header")?,
);
finalize_headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("application/offset+octet-stream"),
@@ -88,11 +156,35 @@ where
.patch(&upload_url)
.headers(finalize_headers)
.send()
.await?;
.await
.context("Finalize PATCH request failed")?;
if !finalize_resp.status().is_success() {
anyhow::bail!("Failed to finalize upload");
let status = finalize_resp.status();
let body = finalize_resp
.text()
.await
.unwrap_or_else(|_| "<failed to read body>".into());
error!(
"TUS finalize failure | offset={} | status={} | body={}",
offset, status, body
);
anyhow::bail!(
"Finalize upload failed.\n\
URL: {}\n\
Final offset: {}\n\
Status: {}\n\
Body: {}",
upload_url,
offset,
status,
body
);
}
info!("Upload completed successfully. Final size: {}", offset);
Ok(())
}