Skip to content

feat: az-blob-storage#66

Merged
RambokDev merged 4 commits into
mainfrom
feat/az-blob-storage-2
Jun 21, 2026
Merged

feat: az-blob-storage#66
RambokDev merged 4 commits into
mainfrom
feat/az-blob-storage-2

Conversation

@RambokDev

@RambokDev RambokDev commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated Azure Blob Storage as a new backup storage provider with support for both connection string and direct endpoint URL configuration options
  • Bug Fixes

    • Backup upload failures are now automatically reported to the server, ensuring proper failure status tracking

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@RambokDev, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 41 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 41338864-2643-4e46-a42e-22c798612aa4

📥 Commits

Reviewing files that changed from the base of the PR and between 221ed4e and 66cad4e.

📒 Files selected for processing (1)
  • src/tests/storage/azure_blob.rs
📝 Walkthrough

Walkthrough

Adds an Azure Blob Storage upload provider (AzureBlobProvider) with HMAC-SHA256 SAS authentication, chunked streaming block uploads via upload_stream_to_azure, and config resolution from connection string or endpoint URL. Also fixes BackupService::upload to notify the server of failed uploads via a backup_upload_status call when a per-storage upload fails.

Changes

Azure Blob Storage Provider

Layer / File(s) Summary
Dependencies and Azure config model
Cargo.toml, src/services/storage/providers/azure_blob/models.rs
Adds azure_core and azure_storage_blob v1.0.0 dependencies; defines AzureBlobProviderConfig with account_name, account_key, container_name, optional connection_string/endpoint_url, and a resolve() method that produces a ResolvedAzure by parsing connection string fields or falling back to direct endpoint URL.
SAS authentication helpers
src/services/storage/providers/azure_blob/helpers.rs (lines 1–111)
Defines ResolvedAzure, SasResource, SAS_VERSION, an HMAC-SHA256 base64 signer, build_service_sas constructing canonical-resource SAS query pairs, and build_sas_url assembling a full blob or container SAS URL.
Chunked block streaming uploader
src/services/storage/providers/azure_blob/helpers.rs (lines 113–186)
Defines BLOCK_SIZE (100 MiB), a stage_block helper for sequentially indexed block staging, and upload_stream_to_azure that reads a ByteStream into fixed-size chunks, stages blocks without full buffering, ensures at least one empty block for empty input, and commits the block list.
AzureBlobProvider implementation and module wiring
src/services/storage/providers/azure_blob/mod.rs, src/services/storage/providers/mod.rs, src/services/storage/mod.rs, src/services/mod.rs
Implements StorageProvider::upload with stepwise validation (missing file, metadata, stream, config), delegates to upload_stream_to_azure, returns UploadResult; wires the provider into get_provider dispatch and makes services::storage public.
Azurite integration tests
src/tests/storage/azure_blob.rs, src/tests/storage/mod.rs, src/tests/mod.rs
Adds test-only Account SAS helpers, an Azurite testcontainers launcher, a single-block SAS roundtrip test, and a multi-block upload stream test that verifies blob content byte-for-byte via a SAS GET.

Backup Upload Failure Notification

Layer / File(s) Summary
Failure status notification and regression test
src/services/backup/uploader.rs, src/tests/services/backup_uploader_tests.rs, src/tests/services/mod.rs
Adds a backup_upload_status call with empty path and size 0u64 in the per-storage failure branch; a WireMock regression test verifies the PATCH with "status": "failed" is sent when backup_file is None.

Sequence Diagram(s)

sequenceDiagram
  participant BackupService
  participant AzureBlobProvider
  participant helpers as upload_stream_to_azure
  participant AzureBlob as Azure Blob Service
  participant Server as Portabase Server

  BackupService->>AzureBlobProvider: upload(result, storage, encrypt)
  AzureBlobProvider->>AzureBlobProvider: resolve AzureBlobProviderConfig → ResolvedAzure
  AzureBlobProvider->>helpers: upload_stream_to_azure(resolved, container, blob, stream)
  loop stage BLOCK_SIZE chunks
    helpers->>AzureBlob: PUT block (SAS-authenticated)
    AzureBlob-->>helpers: 201 Created
  end
  helpers->>AzureBlob: commit_block_list
  AzureBlob-->>helpers: 201 Created
  helpers-->>AzureBlobProvider: Ok(())
  AzureBlobProvider-->>BackupService: UploadResult { success: true }

  alt upload fails
    AzureBlobProvider-->>BackupService: UploadResult { success: false }
    BackupService->>Server: PATCH backup_upload_status (status: failed, path: "", size: 0)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A new cloud home for backups, hooray!
With SAS tokens signed the Azure way,
Block by block the data streams along,
And failures now report — nothing goes wrong.
The rabbit hops through containers with glee,
Committed blobs for all eternity! ☁️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: az-blob-storage' accurately describes the main feature addition of Azure Blob Storage support with a new provider implementation, new dependencies, tests, and configuration models.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/az-blob-storage-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/tests/services/backup_uploader_tests.rs (1)

49-53: ⚡ Quick win

Assert the full failed-status payload contract

Line 51 only validates status. Please also assert path and size so this regression test protects the exact failure payload behavior.

Proposed test matcher hardening
 Mock::given(method("PATCH"))
     .and(path("/agent/agent-1/backup/upload/status"))
-    .and(body_partial_json(json!({ "status": "failed" })))
+    .and(body_partial_json(json!({
+        "status": "failed",
+        "path": "",
+        "size": 0
+    })))
     .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tests/services/backup_uploader_tests.rs` around lines 49 - 53, The mock
assertion using body_partial_json in the test only validates the status field in
the request payload. Expand the json matcher to also assert the path and size
fields are present and correct in the failed-status payload. This ensures the
regression test fully protects the contract of the failure payload by validating
all required fields that are sent when a backup upload fails, not just the
status field.
src/services/storage/providers/azure_blob/mod.rs (1)

70-71: ⚡ Quick win

Unnecessary clone of entire storage struct.

storage.clone().config.try_into() clones the entire DatabaseStorage just to access config. Consider cloning only the config field.

♻️ Proposed fix
-        let config: AzureBlobProviderConfig = match storage.clone().config.try_into() {
+        let config: AzureBlobProviderConfig = match storage.config.clone().try_into() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/storage/providers/azure_blob/mod.rs` around lines 70 - 71, In
the AzureBlobProviderConfig initialization, the code is cloning the entire
`storage` struct and then accessing its `config` field unnecessarily. Instead of
calling `storage.clone().config.try_into()`, change it to clone only the config
field by calling `storage.config.clone().try_into()`. This avoids the expensive
clone of the entire DatabaseStorage struct and only clones the specific config
field that is needed for the try_into() conversion.
src/services/storage/providers/azure_blob/helpers.rs (2)

127-127: 💤 Low value

Potential unnecessary allocation with block.to_vec().

block is already Bytes, and .to_vec() creates a copy. If RequestContent::from() accepts Bytes or Vec<u8> by move, you could avoid the copy by using block.into() or checking if there's a more direct conversion path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/storage/providers/azure_blob/helpers.rs` at line 127, The
`stage_block` method call is creating an unnecessary copy of data by calling
`block.to_vec()` on a `Bytes` value. Since `block` is already `Bytes`, replace
the `.to_vec()` call with a direct conversion like `.into()` or check the
signature of `RequestContent::from()` to see if it accepts `Bytes` directly
without requiring a vector conversion. This will avoid the allocation overhead
and let the `Bytes` value be moved into `RequestContent` more efficiently.

59-62: 💤 Low value

Consider making SAS expiry and protocol configurable.

The SAS token expires in 1 hour (line 60-61), which could be insufficient for very large uploads over slow connections. Additionally, signed_protocol = "https,http" (line 62) allows HTTP for Azurite compatibility, but this weakens security in production.

Consider making these configurable or environment-dependent, or at minimum document the 1-hour upload window constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/storage/providers/azure_blob/helpers.rs` around lines 59 - 62,
The hardcoded SAS token expiry duration (1 hour in the signed_expiry
calculation) and the signed_protocol value ("https,http") should be made
configurable or environment-dependent. Extract these values from the function
body into configuration parameters that can be set based on the deployment
environment (production should only allow "https" while local/Azurite
environments can allow both "https,http"), and ensure the expiry duration is
adjustable to accommodate large uploads over slow connections. At minimum,
document the 1-hour constraint if these remain hardcoded.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/storage/providers/azure_blob/models.rs`:
- Around line 5-14: The AzureBlobProviderConfig struct currently derives
Serialize which poses a security risk as the account_key field containing
sensitive credentials could be accidentally exposed during serialization in logs
or error messages. Remove the Serialize derive from the struct definition, or
alternatively keep Serialize but add the #[serde(skip_serializing)] attribute
specifically to the account_key field to prevent that sensitive field from being
serialized while keeping the rest of the struct serializable.

---

Nitpick comments:
In `@src/services/storage/providers/azure_blob/helpers.rs`:
- Line 127: The `stage_block` method call is creating an unnecessary copy of
data by calling `block.to_vec()` on a `Bytes` value. Since `block` is already
`Bytes`, replace the `.to_vec()` call with a direct conversion like `.into()` or
check the signature of `RequestContent::from()` to see if it accepts `Bytes`
directly without requiring a vector conversion. This will avoid the allocation
overhead and let the `Bytes` value be moved into `RequestContent` more
efficiently.
- Around line 59-62: The hardcoded SAS token expiry duration (1 hour in the
signed_expiry calculation) and the signed_protocol value ("https,http") should
be made configurable or environment-dependent. Extract these values from the
function body into configuration parameters that can be set based on the
deployment environment (production should only allow "https" while local/Azurite
environments can allow both "https,http"), and ensure the expiry duration is
adjustable to accommodate large uploads over slow connections. At minimum,
document the 1-hour constraint if these remain hardcoded.

In `@src/services/storage/providers/azure_blob/mod.rs`:
- Around line 70-71: In the AzureBlobProviderConfig initialization, the code is
cloning the entire `storage` struct and then accessing its `config` field
unnecessarily. Instead of calling `storage.clone().config.try_into()`, change it
to clone only the config field by calling `storage.config.clone().try_into()`.
This avoids the expensive clone of the entire DatabaseStorage struct and only
clones the specific config field that is needed for the try_into() conversion.

In `@src/tests/services/backup_uploader_tests.rs`:
- Around line 49-53: The mock assertion using body_partial_json in the test only
validates the status field in the request payload. Expand the json matcher to
also assert the path and size fields are present and correct in the
failed-status payload. This ensures the regression test fully protects the
contract of the failure payload by validating all required fields that are sent
when a backup upload fails, not just the status field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4e0a39e8-0967-47bc-b193-e7ec36828605

📥 Commits

Reviewing files that changed from the base of the PR and between e504d09 and 221ed4e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • src/services/backup/uploader.rs
  • src/services/mod.rs
  • src/services/storage/mod.rs
  • src/services/storage/providers/azure_blob/helpers.rs
  • src/services/storage/providers/azure_blob/mod.rs
  • src/services/storage/providers/azure_blob/models.rs
  • src/services/storage/providers/mod.rs
  • src/tests/mod.rs
  • src/tests/services/backup_uploader_tests.rs
  • src/tests/services/mod.rs
  • src/tests/storage/azure_blob.rs
  • src/tests/storage/mod.rs

Comment on lines +5 to +14
#[derive(Debug, Deserialize, Serialize)]
pub struct AzureBlobProviderConfig {
pub account_name: String,
pub account_key: String,
pub container_name: String,
#[serde(default)]
pub connection_string: String,
#[serde(default)]
pub endpoint_url: Option<String>,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid deriving Serialize on config containing secrets.

AzureBlobProviderConfig contains account_key, and deriving Serialize could lead to accidental exposure in logs, debug output, or error messages if the struct is ever serialized. Consider removing Serialize or using #[serde(skip_serializing)] on account_key.

🛡️ Proposed fix
 #[derive(Debug, Deserialize, Serialize)]
 pub struct AzureBlobProviderConfig {
     pub account_name: String,
+    #[serde(skip_serializing)]
     pub account_key: String,
     pub container_name: String,
     #[serde(default)]
+    #[serde(skip_serializing)]
     pub connection_string: String,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Debug, Deserialize, Serialize)]
pub struct AzureBlobProviderConfig {
pub account_name: String,
pub account_key: String,
pub container_name: String,
#[serde(default)]
pub connection_string: String,
#[serde(default)]
pub endpoint_url: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AzureBlobProviderConfig {
pub account_name: String,
#[serde(skip_serializing)]
pub account_key: String,
pub container_name: String,
#[serde(default)]
#[serde(skip_serializing)]
pub connection_string: String,
#[serde(default)]
pub endpoint_url: Option<String>,
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/storage/providers/azure_blob/models.rs` around lines 5 - 14, The
AzureBlobProviderConfig struct currently derives Serialize which poses a
security risk as the account_key field containing sensitive credentials could be
accidentally exposed during serialization in logs or error messages. Remove the
Serialize derive from the struct definition, or alternatively keep Serialize but
add the #[serde(skip_serializing)] attribute specifically to the account_key
field to prevent that sensitive field from being serialized while keeping the
rest of the struct serializable.

@codecov

codecov Bot commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.17647% with 47 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rc/services/storage/providers/azure_blob/models.rs 0.00% 43 Missing ⚠️
...c/services/storage/providers/azure_blob/helpers.rs 96.19% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@RambokDev RambokDev merged commit 8897568 into main Jun 21, 2026
3 checks passed
@RambokDev RambokDev deleted the feat/az-blob-storage-2 branch June 21, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant