Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Arius: a Lightweight Tiered Archival Solution for Azure Blob Storage

CIcodecov

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier. It's content-addressable, deduplicated, client-side encrypted and versioned.

The name derives from the Greek for 'immortal'.

Arius7 is a deliberate Agentic Engineering (human-on-the-loop) rewrite of Arius.

Principles:

  • Code is not human-written
  • Important business logic may get glanced at
  • Tests is where the human attention goes to - so coverage is inherently high
  • Like Peter Steinberger, the ClawdBot creator, I never revert, only fix forward

Demo

Archive and restore at a glance:

Installation

Download the binary for your platform from the latest release.

Windows

Download arius-win-x64.exe and add its directory to your PATH

Linux (& Synology NAS)

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-linux-x64
chmod +x arius
sudo mv arius /usr/local/bin/

macOS

curl -Lo arius https://github.com/woutervanranst/Arius7/releases/latest/download/arius-osx-arm64
chmod +x arius
sudo mv arius /usr/local/bin/

Note: macOS may block the binary. Run xattr -c /usr/local/bin/arius to clear the quarantine flag.

Usage

arius archive <path> -a <name> -k <key> -c <container> [options]
arius restore <path> -a <name> -k <key> -c <container> [options]
arius ls -a <name> -k <key> -c <container> [options]
arius snapshot list -a <name> -k <key> -c <container>
arius snapshot diff <from> <to> -a <name> -k <key> -c <container>
arius repair-index -a <name> -k <key> -c <container>
arius update

Archive

arius archive ./photos \
-a mystorageaccount \
-c photos-backup \
-t Archive \
--write-pointers \
--remove-local

For large archives that change rarely, add --fast-hash to skip re-reading files whose content the local cache confirms as unchanged. The first run (or any run without --fast-hash) warms the cache; subsequent runs with --fast-hash only re-read changed files.

arius archive ./photos -a mystorageaccount -c photos-backup --fast-hash

Pointer sidecar files (.pointer.arius) are off by default. Pass --write-pointers to create them alongside the originals. --remove-local requires --write-pointers (you cannot remove the binary without leaving a local pointer record).

Restore

arius restore ./photos \
-a mystorageaccount \
-c photos-backup

Filter paths with --target-path.

List files in a snapshot

arius ls \
-a mystorageaccount \
-c photos-backup

Filter with --prefix <path> and --filter <substring>, and pick an older snapshot with -v <version>.

Snapshots

Every archive run records a snapshot — a point-in-time view of your files. List them oldest-first, then see exactly what changed between any two:

arius snapshot list -a mystorageaccount -c photos-backup
arius snapshot diff 5 6 -a mystorageaccount -c photos-backup

diff takes the index numbers shown by snapshot list (or a date prefix like 2024-04-02) and reports what was added, removed, modified, or had only its timestamps change.

Repair chunk index

Run this when archive, restore, or list reports that the chunk index is corrupt, incomplete, or missing entries:

arius repair-index \
-a mystorageaccount \
-c photos-backup

The repair command rebuilds the chunk index from committed chunks and can be rerun safely if it is interrupted.

Updating

Run:

arius update

This checks GitHub Releases for a newer version, downloads it, and replaces the binary in-place.

Account key

Pass -k on the command line, set ARIUS_KEY environment variable, authenticate with the Azure CLI or store it in a dotnet user-secrets set "arius:<account>:key" "<key>".

Development

Building, running each host locally, and the full test-suite architecture (unit · integration · E2E · mutation · benchmarks) are covered in the Development guide.

Blob Storage Structure

A single Azure Blob container holds the entire repository. Blobs are organized into virtual directories (prefixes):

<container>
├── chunks/ Content-addressable chunks (configurable tier)
├── chunks-rehydrated/ Temporary hot-tier copies during restore (auto-cleaned)
├── filetrees/ Merkle tree nodes — one text blob per directory (Cool tier)
├── snapshots/ Point-in-time snapshot manifests (Cool tier)
├── chunk-index/ Deduplication index shards (Cool tier)
|
| --- (v3/v5 legacy archives-only)
├── chunks-v5legacy-metadata/ Metadata for legacy chunks in Archive-tier (see ADR-0018)
└── states/ v5/v3 state databases; deprecated once migrated

How it fits together

The runtime coordinates four shared services: SnapshotService for snapshot manifests, FileTreeService for cached filetree blobs, ChunkIndexService for deduplication shard lookups and shard-cache ownership, and ChunkStorageService for chunk blob upload, download, hydration, rehydration, and cleanup planning. Feature handlers are expected to go through those shared services instead of depending directly on low-level blob abstractions such as IBlobContainerService, IBlobService, or IBlobServiceFactory, with only narrow exceptions where the feature itself is the blob-level boundary. Repository-local cache and log directories are derived consistently through the shared RepositoryPaths helper. Chunk hydration state is shared through Shared/ChunkStorage/ChunkHydrationStatus.

flowchart TD
subgraph snapshots/
S["snapshots/2026-03-22T150000.000Z<br/><i>gzip + optional encrypt</i>"]
end
subgraph filetrees/
RT["filetrees/&lt;root-hash&gt;<br/><i>text tree blob</i>"]
CT["filetrees/&lt;child-hash&gt;<br/><i>text tree blob</i>"]
end
subgraph chunks/
L["chunks/&lt;content-hash&gt;<br/><b>large</b> — gzip + optional encrypt"]
TAR["chunks/&lt;tar-hash&gt;<br/><b>tar</b> — tar + gzip + optional encrypt"]
TH["chunks/&lt;content-hash&gt;<br/><b>thin</b> — metadata pointer to tar-hash"]
end
subgraph chunk-index/
CI["chunk-index/&lt;prefix&gt;<br/><i>gzip + optional encrypt</i>"]
end
S -- "rootHash" --> RT
RT -- "child dir hash" --> CT
RT -. "file content-hash" .-> L
CT -. "file content-hash" .-> TH
TH -- "tar-hash reference" --> TAR
CI -. "content-hash → chunk-hash" .-> L
CI -. "content-hash → chunk-hash" .-> TH
Loading

snapshots/

Each blob is a small JSON manifest (gzip-compressed, optionally AES-256-CBC encrypted) that captures a point-in-time state of the repository:

FieldDescription
timestampUTC time of snapshot creation
rootHashSHA-256 hash of the root Merkle tree node
fileCountTotal number of files
originalSizeLogical size: sum of original (uncompressed) file sizes in bytes
ariusVersionTool version that created the snapshot

Snapshots are immutable and never deleted. To browse the repository at a given point in time, resolve the snapshot, then walk the tree from rootHash.

filetrees/

Merkle tree nodes. Each blob is a UTF-8 text file named by its tree-hash (SHA-256 of the canonical text, optionally passphrase-seeded). A tree blob lists the entries in one directory — one line per entry, sorted by name:

See docs/design/core/shared/filetree.md for the archive-time staging, build, upload, and cache pipeline behind these nodes.

abc123... F 2026-03-25T10:00:00.0000000+00:00 2026-03-25T12:30:00.0000000+00:00 photo.jpg
def456... D subdir/
  • File entries (F): <content-hash> F <created> <modified> <name>
  • Directory entries (D): <tree-hash> D <name>
  • Names are always the last field and may contain spaces (no quoting needed).
  • File entries point to a content-hash in chunks/.
  • Directory entries point to another tree blob in filetrees/.
  • Walking from the root hash recursively reconstructs the full directory tree.

chunks/

Content-addressable storage for file data. Three blob types coexist under this prefix, distinguishable by their HTTP Content-Type header and arius_type metadata:

TypeBlob nameContent-TypeBodyTier
largechunks/<content-hash>application/aes256cbc+gzip or application/gzipSingle file: gzip + optional encryptConfigurable (-t)
tarchunks/<tar-hash>application/aes256cbc+tar+gzip or application/tar+gzipBundle of small files: tar + gzip + optional encryptConfigurable (-t)
thinchunks/<content-hash>text/plain; charset=utf-8Empty body; parent tar-hash in metadataAlways Cool

Routing rule: files >= 1 MB are uploaded individually as large chunks. Files < 1 MB are accumulated into tar bundles (target size 64 MB, can become larger depending on TAR overhead). For each file in a tar bundle, a thin pointer blob is created so that every content-hash has a corresponding blob in chunks/.

Thin chunks are kept on Cool tier and include their parent tar-hash in metadata so repair can rebuild mappings without downloading each thin chunk.

chunks-rehydrated/

Temporary prefix used only during restore. When chunks are stored on Archive tier, Arius initiates a server-side copy from chunks/<hash> to chunks-rehydrated/<hash> at Hot tier. Once rehydration completes and files are restored, these blobs are cleaned up.

chunk-index/

Deduplication index split into prefix-keyed shards. Each shard is a text file (gzip-compressed, optionally encrypted) where each line maps a content-hash to its chunk-hash, original size, stored chunk size, and the chunk's storage tier:

<content-hash> <chunk-hash> <original-size> <chunk-size> <tier>

For large files, content-hash equals chunk-hash and the chunk-hash field is omitted. For tar-bundled files, chunk-size is the full parent tar chunk size, so restore and rehydration estimates reflect the bytes Arius must actually download or rehydrate. The tier field records the chunk's storage tier at archive time (1=hot, 2=cool, 3=cold, 4=archive); it is a hint that lets ls report whether a file is readily downloadable or archived without contacting each blob. For tar-bundled files, chunk-hash is the tar-hash and the tier is the tar blob's tier. Arius keeps local chunk-index state in a SQLite cache under the repository state directory, validates touched prefixes lazily against the latest snapshot, and can rebuild the local cache or the remote shards from committed chunks when repair is needed.

Disaster recovery

Normal recovery is arius restore. But your data does not depend on the Arius binary. Every chunk is self-describing: an encryption envelope (detected from its leading magic bytes) wrapping a standard compression frame.

LayerFormats (auto-detected from magic bytes)
EncryptionAES-256-GCM (ArGCM1, current) · AES-256-CBC (Salted__, legacy)
Compressionzstd — standard RFC 8878 frame (28 B5 2F FD) or gzip (RFC 1952, legacy)

Emergency single-chunk recovery (without Arius)

recover-chunk.py decrypts and decompresses one chunk file given only the passphrase. It auto-detects both the encryption and the compression format, validates the XXH64 checksum, and refuses a truncated frame rather than emitting a partial prefix.

# Needs: pip install cryptography zstandard
python3 recover-chunk.py <encrypted-chunk-file><passphrase> [output-file]

Documentation

License

MIT

About

Arius is a lightweight archival solution, specifically built to leverage the Azure Blob Archive tier.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages