| .cursor | ||
| docs | ||
| src | ||
| utils | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| Directory.Build.props | ||
| LICENSE.md | ||
| README.md | ||
MaksIT.LTO.Backup
Cross-platform toolkit for LTO tape backup and restore. Console, Avalonia UI, and Worker share one service layer.
See LICENSE.md (GPLv2). Changes: CHANGELOG.md. Contributing: CONTRIBUTING.md.
| Host | Project | Role |
|---|---|---|
| Console | MaksIT.LTO.Backup |
Interactive menu / backup --name CLI |
| Avalonia | MaksIT.LTO.Backup.UI |
Code-behind operations UI (Windows + Linux; no MVVM). SimpleTheme Dark styles aligned with Cluster Console |
| Worker | MaksIT.LTO.Backup.Service |
Scheduled library backups (Windows service / systemd) |
Device access modes:
- Physical — real drive/library on Windows (
\\.\Tape0,\\.\Changer0; admin) or Linux (/dev/nst0,/dev/sg*;tape/diskgroup) - Emulated — file-backed drive + library (no hardware; primary CI / no-hardware path)
Current line is
0.1.0-alpha.1(prerelease, not production-ready). Emulator coverage is the primary validation path; physical tape/changer and Linux SMB are not hardware-proven yet. Use at your own risk.
If you find this project useful, please consider supporting its development:
Features
- Backup and restore with AES-GCM encrypted on-tape JSON descriptors and SHA-256 file verification
- SchemaVersion 2 layout: BOT index (
MLTI), payload, filemark, descriptor preamble (MLTD), ciphertext, double filemark - Overwrite or Append write modes (multiple sets per cartridge via BOT set table)
- Fail-fast restore on checksum mismatch (
FailFastOnChecksumMismatch) - MAM summary attributes written after backup (app name/version, host, text label)
- Local paths on all platforms; SMB source/destination on Windows (WNet) and Linux (SMB2 staging via SMBLibrary)
- LTO generation block sizes (LTO1–LTO9) and media-capacity checks
- Drive ops: load, eject, short erase, status, cartridge memory (MAM) / barcode
- Library ops: inventory, move medium (emulated; physical Windows
IOCTL_CHANGER_*and Linux SG_IO) - Shared library used by console, Avalonia UI, and Worker
- Live Avalonia monitor for drive status and library inventory
- Avalonia Service tab: install / uninstall / start / stop Worker (Windows + Linux)
- Emulator-backed unit / E2E tests without a physical tape (
{tapeId}.drive.json+{tapeId}.blocks.bin)
Requirements
- .NET 10 SDK
- Windows: physical tape/changer IOCTLs; Avalonia UI; SMB via WNet
- Linux: physical tape (
/dev/nst*) and changer (/dev/sg*); Avalonia UI; SMB via managed SMB2 client; orDeviceMode=Emulated - Optional: PowerShell 7+ for RepoUtils test/release engines under
utils/
Solution layout
utils/ # RepoUtils release / test engines
src/
MaksIT.LTO.slnx
MaksIT.LTO.Core/ # tape/library APIs + emulators
MaksIT.LTO.Backup.Shared/ # shared config, orchestrators, host helpers
MaksIT.LTO.Backup/ # console host
MaksIT.LTO.Backup.UI/ # Avalonia host (Windows + Linux)
MaksIT.LTO.Backup.Service/ # scheduled Worker (Windows / Linux)
MaksIT.LTO.Tests/ # emulator tests
Scheduled backups (library only)
Automatic scheduling runs only when Topology is TapeLibrary. StandaloneDrive remains manual (console / UI).
Backup jobs define what (source, Tapes pool, LTO gen). Drive selection is automatic.
Aggregations define when and in what order jobs run as one wave (e.g. Night / Afternoon):
JobNames— ordered list of backup job namesSchedule— UScheduler-compatible calendar with overdue catch-up (RunMonth,RunWeekday,RunTimeUTCHH:mm,MinIntervalMinutes)Disabled/LastRunUtc— wave-level
The Worker holds an exclusive lease for the whole aggregation (no mid-wave collisions). If another wave is busy, the due wave retries on the next poll without consuming LastRunUtc. After each job: unload cartridge so the next job can claim any empty drive.
Worker lifecycle (also from Avalonia Service tab):
MaksIT.LTO.Backup.Service --install
MaksIT.LTO.Backup.Service --start
MaksIT.LTO.Backup.Service --stop
MaksIT.LTO.Backup.Service --uninstall
Console:
MaksIT.LTO.Backup backup --name "Normal test"
MaksIT.LTO.Backup backup --aggregation "Night"
MaksIT.LTO.Backup backup --aggregation "Night" --scheduled
Install/uninstall typically require administrator (Windows) or root (Linux systemd).
Configuration
One shared model and one shared JSON for the whole solution:
- Model:
src/MaksIT.LTO.Backup.Shared/Models/Configuration.cs - File:
src/MaksIT.LTO.Backup.Shared/configuration.json - Loader / workflows:
ConfigurationFileService,BackupOrchestrator,LibraryOrchestratorinMaksIT.LTO.Backup.Shared
Both console and Avalonia UI link this file into their output. Edit the Shared copy only, or use the UI Settings tab and click Save configuration.
DeviceMode selects the backend (Physical / Emulated).
Topology selects how hardware is used:
StandaloneDrive— Drive tab only (load/eject/status/MAM). Backup tab runs against that drive.TapeLibrary— Library tab (inventory, move into drive bays, MAM on loaded cartridge). Backup tab runs only when a cartridge is in a drive bay.
{
"Configuration": {
"DeviceMode": "Emulated",
"Topology": "StandaloneDrive",
"TapePath": "\\\\.\\Tape0",
"LibraryPath": "\\\\.\\Changer0",
"WriteDelay": 100,
"FailFastOnChecksumMismatch": true,
"Emulator": {
"DataDirectory": ".\\emulator-data",
"Library": { "SlotCount": 24, "DriveCount": 2, "IePortCount": 1 }
},
"Backups": [
{
"Name": "Normal test",
"Barcode": "LTO001",
"LTOGen": "LTO5",
"WriteMode": "Overwrite",
"Source": { "LocalPath": { "Path": "F:\\LTO\\Backup" } },
"Destination": { "LocalPath": { "Path": "F:\\LTO\\Restore" } }
}
]
}
}
On-tape format (SchemaVersion 2)
[BOT index MLTI] → [file blocks…] → FM → [MLTD preamble] → [length-prefixed AES-GCM descriptor] → FM FM
Append adds another payload+descriptor set at EOD and rewrites the BOT set table (max 16 sets).
Descriptor secret
On first run a secret.txt is created for descriptor encryption (first line = current key). Keep a safe copy.
To rotate: put the new key on line 1 of secret.txt and move the old key to line 2+ or into secret.history.txt. Decrypt tries the current key then prior keys.
To avoid leaving the file on disk, set a machine environment variable (used as the preferred encrypt/decrypt key):
[System.Environment]::SetEnvironmentVariable(
"LTO_BACKUP_SECRET",
"<secret.txt content here>",
[System.EnvironmentVariableTarget]::Machine
)
Console menu
- Load tape
- Backup
- Restore
- Eject tape
- Get device status
- Tape erase (short)
- Read cartridge memory
- Move medium (library)
- Library inventory
- Exit
Build and run
cd src
dotnet build MaksIT.LTO.slnx
dotnet run --project .\MaksIT.LTO.Backup
Avalonia UI (Windows or Linux):
dotnet run --project .\MaksIT.LTO.Backup.UI
Publish hosts (or use RepoUtils release below):
cd src
dotnet publish .\MaksIT.LTO.Backup -c Release -o ..\releases\console
dotnet publish .\MaksIT.LTO.Backup.UI -c Release -o ..\releases\ui
dotnet publish .\MaksIT.LTO.Backup.Service -c Release -o ..\releases\service
Physical mode: Windows needs elevation; Linux needs access to /dev/nst* / /dev/sg* (often tape/disk group). Discover changer nodes with lsscsi -g.
Tests and coverage
Emulator-backed tests (no tape required):
cd src
dotnet test .\MaksIT.LTO.Tests
With Cobertura coverage:
cd src
dotnet test .\MaksIT.LTO.Tests --collect:"XPlat Code Coverage" --results-directory .\TestResults
Coverage shields at the top of this README use shields.io and are rewritten by the CoverageBadges plugin when utils\Invoke-TestEngine.bat runs.
RepoUtils release pipeline
Vendored under utils/ (from MaksIT RepoUtils community):
| Action | Entry |
|---|---|
| Test | utils\Invoke-TestEngine.bat |
| Release | utils\Invoke-ReleasePackage.bat |
Settings:
utils/engines/test/scriptSettings.jsonutils/engines/release/scriptSettings.json
License
GPLv2 — see LICENSE.md.
© Maksym Sadovnychyy (MAKS-IT)
