Skip to content

Commit c2e301b

Browse files
feat: Implement atomic operations with rollback on error (#125)
* feat: Implement atomic operations with rollback on error * fix: format * fix: lint * fix: format n lint * chore: updated requirements
1 parent 470bba6 commit c2e301b

File tree

12 files changed

+1085
-68
lines changed

12 files changed

+1085
-68
lines changed

README.md

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
### Atomic Transactions (Experimental)
2+
3+
[Detailed design and usage docs](internal/atomic/README.md)
4+
5+
Add and delete operations are executed via a lightweight transactional journal to prevent partial state (half-written manifests or orphaned chunks).
6+
7+
Workflow:
8+
9+
1. Start a transaction: new/replacement files written under `.txn/<id>/new/`.
10+
2. Deletions move originals to `.txn/<id>/trash/`.
11+
3. Commit promotes all staged files (atomic renames) and removes trash.
12+
4. Rollback removes staged files and restores trash originals.
13+
14+
Manual recovery:
15+
16+
```
17+
sietch recover --retention 24h
18+
```
19+
20+
Scans `.txn/` for incomplete transactions and resumes or rolls them back. Completed journals older than the retention window are purged.
21+
22+
Limitations / Next Steps:
23+
24+
- Current scope covers `add` and `delete` commands.
25+
- Further commands (sync, sneakernet transfer) can adopt the same API later.
26+
- Fault injection hooks for deterministic testing are planned.
27+
128
# Sietch Vault
229

330
[![CI](https://github.com/substantialcattle5/sietch/actions/workflows/ci.yml/badge.svg)](https://github.com/substantialcattle5/sietch/actions/workflows/ci.yml)
@@ -11,9 +38,9 @@ Sietch creates self-contained encrypted vaults that can sync over LAN, sneakerne
1138

1239
Sietch Vault is designed for environments where:
1340

14-
* Internet is scarce, censored, or unreliable
15-
* Data privacy is a necessity, not an optional feature
16-
* People work nomadically—researchers, journalists, activists
41+
- Internet is scarce, censored, or unreliable
42+
- Data privacy is a necessity, not an optional feature
43+
- People work nomadically—researchers, journalists, activists
1744

1845
## Quick Start
1946

@@ -28,12 +55,14 @@ make build
2855
### Basic Usage
2956

3057
**Create a vault**
58+
3159
```bash
3260
sietch init --name dune --key-type aes # AES-256-GCM encryption
3361
sietch init --name dune --key-type chacha20 # ChaCha20-Poly1305 encryption
3462
```
3563

3664
**Add files**
65+
3766
```bash
3867
# Single file
3968
sietch add ./secrets/thumper-plans.pdf documents/
@@ -46,13 +75,15 @@ sietch add ~/photos/img1.jpg ~/photos/img2.jpg vault/photos/
4675
```
4776

4877
**Sync over LAN**
78+
4979
```bash
5080
sietch sync /ip4/192.168.1.42/tcp/4001/p2p/QmPeerID
5181
# or auto-discover peers
5282
sietch sync
5383
```
5484

5585
**Retrieve files**
86+
5687
```bash
5788
sietch get thumper-plans.pdf ./retrieved/
5889
```
@@ -70,30 +101,38 @@ sietch get thumper-plans.pdf ./retrieved/
70101
## How It Works
71102

72103
### Chunking & Deduplication
73-
* Files are split into configurable chunks (default: 4MB)
74-
* Identical chunks across files are deduplicated to save space
75-
* Please Refer [this](internal/deduplication/README.md) documentation to understand how Deduplication works.
104+
105+
- Files are split into configurable chunks (default: 4MB)
106+
- Identical chunks across files are deduplicated to save space
107+
- Please Refer [this](internal/deduplication/README.md) documentation to understand how Deduplication works.
76108

77109
### Encryption
110+
78111
Each chunk is encrypted before storage using:
79-
* **Symmetric**: AES-256-GCM or ChaCha20-Poly1305 with passphrase
80-
* **Asymmetric**: GPG-compatible public/private keypairs
112+
113+
- **Symmetric**: AES-256-GCM or ChaCha20-Poly1305 with passphrase
114+
- **Asymmetric**: GPG-compatible public/private keypairs
81115

82116
### Peer Discovery
117+
83118
Peers discover each other via:
84-
* LAN gossip (UDP broadcast)
85-
* Manual IP whitelisting
86-
* QR-code sharing *(coming soon)*
119+
120+
- LAN gossip (UDP broadcast)
121+
- Manual IP whitelisting
122+
- QR-code sharing _(coming soon)_
87123

88124
### Syncing
125+
89126
Inspired by rsync, Sietch only transfers:
90-
* Missing chunks
91-
* Changed metadata
92-
* Over encrypted TCP connections with optional compression
127+
128+
- Missing chunks
129+
- Changed metadata
130+
- Over encrypted TCP connections with optional compression
93131

94132
## Available Commands
95133

96134
### Core Operations
135+
97136
```bash
98137
sietch init [flags] # Initialize a new vault
99138
sietch add <source> <destination> [args...] # Add files to vault (multiple file support)
@@ -103,13 +142,15 @@ sietch delete <filename> # Delete files from vault
103142
```
104143

105144
### Network Operations
145+
106146
```bash
107147
sietch discover [flags] # Discover peers on local network
108148
sietch sync [peer-address] # Sync with other vaults
109149
sietch sneak [flags] # Transfer via sneakernet (USB)
110150
```
111151

112152
### Management
153+
113154
```bash
114155
sietch dedup stats # Show deduplication statistics
115156
sietch dedup gc # Run garbage collection
@@ -120,27 +161,31 @@ sietch scaffold [flags] # Create vault from template
120161
## Advanced Usage
121162

122163
**View vault contents**
164+
123165
```bash
124166
sietch ls # List all files
125167
sietch ls docs/ # List files in specific directory
126168
sietch ls --long # Show detailed information
127169
```
128170

129171
**Network synchronization**
172+
130173
```bash
131174
sietch discover # Find peers automatically
132175
sietch sync # Auto-discover and sync
133176
sietch sync /ip4/192.168.1.5/tcp/4001/p2p/QmPeerID # Sync with specific peer
134177
```
135178

136179
**Sneakernet transfer**
180+
137181
```bash
138182
sietch sneak # Interactive mode
139183
sietch sneak --source /media/usb/vault # Transfer from USB vault
140184
sietch sneak --dry-run --source /backup/vault # Preview transfer
141185
```
142186

143187
**Deduplication management**
188+
144189
```bash
145190
sietch dedup stats # Show statistics
146191
sietch dedup gc # Clean unreferenced chunks
@@ -168,26 +213,29 @@ sietch manifest
168213
## Development
169214

170215
### Prerequisites
171-
* **Go 1.23+**[Download](https://golang.org/dl/)
172-
* **Git** – Version control
216+
217+
- **Go 1.23+**[Download](https://golang.org/dl/)
218+
- **Git** – Version control
173219

174220
### Quick Development Setup
175221

176222
1. **Clone and setup**
177-
```bash
178-
git clone https://github.com/substantialcattle5/sietch.git
179-
cd sietch
180-
./scripts/setup-hooks.sh
181-
```
223+
224+
```bash
225+
git clone https://github.com/substantialcattle5/sietch.git
226+
cd sietch
227+
./scripts/setup-hooks.sh
228+
```
182229

183230
2. **Verify installation**
184-
```bash
185-
make check-versions
186-
make build
187-
make test
188-
```
231+
```bash
232+
make check-versions
233+
make build
234+
make test
235+
```
189236

190237
### Available Commands
238+
191239
```bash
192240
make help # List all commands
193241
make dev # Format, test, build
@@ -203,13 +251,15 @@ For detailed development guidelines, see [CONTRIBUTING.md](CONTRIBUTING.md).
203251
We welcome contributions of all kinds! Whether you're fixing bugs, adding features, improving documentation, or enhancing UX.
204252

205253
**Quick contribution steps:**
254+
206255
1. Fork the repository
207256
2. Create a feature branch: `git checkout -b feature/stillsuit`
208257
3. Make your changes following our [style guidelines](CONTRIBUTING.md#styleguides)
209258
4. Commit using [conventional commits](CONTRIBUTING.md#commit-messages)
210259
5. Push and open a Pull Request
211260

212261
See our [Contributing Guide](CONTRIBUTING.md) for detailed information about:
262+
213263
- Development environment setup
214264
- Code style guidelines
215265
- Testing requirements
@@ -218,9 +268,10 @@ See our [Contributing Guide](CONTRIBUTING.md) for detailed information about:
218268
## Inspiration & Credits
219269

220270
Sietch draws inspiration from:
221-
* **Syncthing** - Decentralized file synchronization
222-
* **IPFS** - Content-addressed storage
223-
* **Obsidian Sync** - Seamless cross-device syncing
271+
272+
- **Syncthing** - Decentralized file synchronization
273+
- **IPFS** - Content-addressed storage
274+
- **Obsidian Sync** - Seamless cross-device syncing
224275

225276
Built with ❤️ in Go by the open source community.
226277

@@ -308,4 +359,4 @@ Licensed under the **MIT License** – see the [LICENSE](LICENSE) file for detai
308359

309360
---
310361

311-
> *"When you live in the desert, you develop a very strong survival instinct."* – Chani, *Dune*
362+
> _"When you live in the desert, you develop a very strong survival instinct."_ – Chani, _Dune_

cmd/add.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,25 @@ package cmd
66
import (
77
"context"
88
"fmt"
9+
"io"
910
"os"
1011
"path/filepath"
1112
"strings"
1213
"time"
1314

1415
"github.com/spf13/cobra"
1516

17+
"github.com/substantialcattle5/sietch/internal/atomic"
1618
"github.com/substantialcattle5/sietch/internal/chunk"
1719
"github.com/substantialcattle5/sietch/internal/config"
1820
"github.com/substantialcattle5/sietch/internal/constants"
1921
"github.com/substantialcattle5/sietch/internal/fs"
20-
"github.com/substantialcattle5/sietch/internal/manifest"
22+
23+
// manifest raw storage removed in favor of transactional helper
2124
"github.com/substantialcattle5/sietch/internal/progress"
2225
"github.com/substantialcattle5/sietch/internal/ui"
2326
"github.com/substantialcattle5/sietch/util"
27+
"gopkg.in/yaml.v3"
2428
)
2529

2630
// SpaceSavings represents space savings statistics for a file
@@ -140,6 +144,20 @@ Examples:
140144
fmt.Printf("Starting batch processing of %d files...\n\n", len(filePairs))
141145
}
142146

147+
// Begin transaction encompassing entire add set for atomicity
148+
// vaultRoot already resolved earlier; reuse variable
149+
txn, err := atomic.Begin(vaultRoot, map[string]any{"command": "add", "fileCount": len(filePairs)})
150+
if err != nil {
151+
return fmt.Errorf("begin transaction: %w", err)
152+
}
153+
committed := false
154+
defer func() {
155+
if !committed {
156+
_ = txn.Rollback()
157+
fmt.Println("txn rollback; add operation did not complete")
158+
}
159+
}()
160+
143161
for i, pair := range filePairs {
144162
// Enhanced progress display for multiple files
145163
if len(filePairs) > 1 {
@@ -220,7 +238,8 @@ Examples:
220238

221239
// Process the file and store chunks - using the appropriate chunking function
222240
var chunkRefs []config.ChunkRef
223-
chunkRefs, err = chunk.ChunkFile(ctx, actualSourcePath, chunkSize, vaultRoot, passphrase, progressMgr)
241+
// Use transactional chunking to stage new chunks
242+
chunkRefs, err = chunk.ChunkFileTransactional(ctx, actualSourcePath, chunkSize, vaultRoot, passphrase, progressMgr, txn)
224243

225244
if err != nil {
226245
errorMsg := fmt.Sprintf("✗ %s: chunking failed - %v", filepath.Base(pair.Source), err)
@@ -254,8 +273,8 @@ Examples:
254273
}
255274

256275
// Save the manifest
257-
err = manifest.StoreFileManifest(vaultRoot, filepath.Base(pair.Source), fileManifest)
258-
if err != nil {
276+
// Store manifest via transaction (stage create)
277+
if err := storeManifestTransactional(txn, vaultRoot, filepath.Base(pair.Source), fileManifest); err != nil {
259278
if err.Error() == "skipped" {
260279
errorMsg := fmt.Sprintf("✗ '%s': skipped", fileManifest.Destination+fileManifest.FilePath)
261280
fmt.Println(errorMsg)
@@ -351,11 +370,15 @@ Examples:
351370
}
352371
}
353372

354-
// Return error only if all files failed
373+
// Commit transaction if we had any successes
355374
if successCount == 0 {
356375
return fmt.Errorf("all files failed to process")
357376
}
358-
377+
if err := txn.Commit(); err != nil {
378+
return fmt.Errorf("commit transaction: %w", err)
379+
}
380+
committed = true
381+
fmt.Println("txn successful; add committed")
359382
return nil
360383
},
361384
}
@@ -518,5 +541,48 @@ func init() {
518541
addCmd.Flags().String("passphrase-file", "", "Read passphrase from file (file should have 0600 permissions)")
519542
}
520543

544+
// storeManifestTransactional writes a manifest yaml via the transaction staging new file.
545+
func storeManifestTransactional(txn *atomic.Transaction, vaultRoot string, fileName string, m *config.FileManifest) error {
546+
// Mirror logic from manifest.StoreFileManifest but stage instead of direct write.
547+
manifestsDir := filepath.Join(vaultRoot, ".sietch", "manifests")
548+
if err := os.MkdirAll(manifestsDir, 0o755); err != nil {
549+
return fmt.Errorf("failed to create manifests directory: %v", err)
550+
}
551+
destination := strings.ReplaceAll(m.Destination, "/", ".")
552+
uniqueFileIdentifier := destination + fileName + ".yaml"
553+
relPath := filepath.ToSlash(filepath.Join(".sietch", "manifests", uniqueFileIdentifier))
554+
// Prompt overwrite if exists in final location
555+
finalPath := filepath.Join(manifestsDir, uniqueFileIdentifier)
556+
if _, err := os.Stat(finalPath); err == nil {
557+
message := fmt.Sprintf("'%s' exists. Overwrite? ", m.Destination+fileName)
558+
response, err2 := util.ConfirmOverwrite(message, os.Stdin, os.Stdout)
559+
if err2 != nil || !response {
560+
return fmt.Errorf("skipped")
561+
}
562+
// Stage replace instead of create
563+
w, err2 := txn.StageReplace(relPath)
564+
if err2 != nil {
565+
return err2
566+
}
567+
defer w.Close()
568+
return writeManifestYAML(w, m)
569+
}
570+
w, err := txn.StageCreate(relPath)
571+
if err != nil {
572+
return err
573+
}
574+
defer w.Close()
575+
return writeManifestYAML(w, m)
576+
}
577+
578+
func writeManifestYAML(w io.Writer, m *config.FileManifest) error {
579+
enc := yaml.NewEncoder(w)
580+
enc.SetIndent(2)
581+
if err := enc.Encode(m); err != nil {
582+
return fmt.Errorf("encode manifest: %w", err)
583+
}
584+
return nil
585+
}
586+
521587
//TODO: Need to check how symlinks will be handled
522588
//TODO: Interactive mode with real time progress indicators

0 commit comments

Comments
 (0)