Skip to content

Commit 007901f

Browse files
committed
feat: major CLI enhancements with batch operations, progress indicators, status checking, and improved UX
1 parent 6ed7615 commit 007901f

File tree

8 files changed

+1258
-13
lines changed

8 files changed

+1258
-13
lines changed

cmd/tle/commands/batch.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"time"
9+
10+
"github.com/drand/tlock"
11+
"github.com/drand/tlock/networks/http"
12+
)
13+
14+
// BatchResult represents the result of a batch operation
15+
type BatchResult struct {
16+
File string
17+
Success bool
18+
Error error
19+
Duration time.Duration
20+
}
21+
22+
// BatchEncrypt encrypts multiple files in a directory
23+
func BatchEncrypt(flags Flags, network *http.Network) error {
24+
LogMessage(flags.Quiet, flags.Verbose, "Starting batch encryption in directory: %s", flags.InputDir)
25+
26+
// Create output directory if it doesn't exist
27+
if err := os.MkdirAll(flags.OutputDir, 0755); err != nil {
28+
return fmt.Errorf("failed to create output directory: %w", err)
29+
}
30+
31+
// Find files matching the pattern
32+
files, err := findMatchingFiles(flags.InputDir, flags.Pattern)
33+
if err != nil {
34+
return fmt.Errorf("failed to find files: %w", err)
35+
}
36+
37+
if len(files) == 0 {
38+
LogMessage(flags.Quiet, flags.Verbose, "No files found matching the pattern")
39+
return nil
40+
}
41+
42+
LogMessage(flags.Quiet, flags.Verbose, "Found %d files to encrypt", len(files))
43+
44+
// Process files
45+
results := make([]BatchResult, 0, len(files))
46+
successCount := 0
47+
48+
// Create progress bar
49+
progressBar := NewProgressBar(len(files), flags.Quiet, flags.Verbose)
50+
51+
for i, file := range files {
52+
start := time.Now()
53+
54+
LogMessage(flags.Quiet, flags.Verbose, "Encrypting %d/%d: %s", i+1, len(files), file)
55+
56+
result := BatchResult{File: file}
57+
58+
// Determine output file path
59+
relPath, err := filepath.Rel(flags.InputDir, file)
60+
if err != nil {
61+
result.Error = fmt.Errorf("failed to get relative path: %w", err)
62+
results = append(results, result)
63+
continue
64+
}
65+
66+
outputFile := filepath.Join(flags.OutputDir, relPath)
67+
68+
// Add .tle extension if not present
69+
if !strings.HasSuffix(outputFile, ".tle") {
70+
outputFile += ".tle"
71+
}
72+
73+
// Create output directory for this file
74+
if err := os.MkdirAll(filepath.Dir(outputFile), 0755); err != nil {
75+
result.Error = fmt.Errorf("failed to create output directory: %w", err)
76+
results = append(results, result)
77+
continue
78+
}
79+
80+
// Encrypt the file
81+
if err := encryptFile(file, outputFile, flags, network); err != nil {
82+
result.Error = err
83+
} else {
84+
result.Success = true
85+
successCount++
86+
}
87+
88+
result.Duration = time.Since(start)
89+
results = append(results, result)
90+
91+
if result.Success {
92+
LogMessage(flags.Quiet, flags.Verbose, "✓ Encrypted %s in %v", file, result.Duration)
93+
} else {
94+
LogError(flags.Quiet, "Failed to encrypt %s: %v", file, result.Error)
95+
}
96+
97+
// Update progress bar
98+
progressBar.Increment()
99+
}
100+
101+
// Finish progress bar
102+
progressBar.Finish()
103+
104+
// Print summary
105+
LogMessage(flags.Quiet, flags.Verbose, "Batch encryption completed: %d/%d files successful", successCount, len(files))
106+
107+
if successCount < len(files) {
108+
LogMessage(flags.Quiet, flags.Verbose, "Failed files:")
109+
for _, result := range results {
110+
if !result.Success {
111+
LogError(flags.Quiet, " %s: %v", result.File, result.Error)
112+
}
113+
}
114+
}
115+
116+
return nil
117+
}
118+
119+
// BatchDecrypt decrypts multiple files in a directory
120+
func BatchDecrypt(flags Flags, network *http.Network) error {
121+
LogMessage(flags.Quiet, flags.Verbose, "Starting batch decryption in directory: %s", flags.InputDir)
122+
123+
// Create output directory if it doesn't exist
124+
if err := os.MkdirAll(flags.OutputDir, 0755); err != nil {
125+
return fmt.Errorf("failed to create output directory: %w", err)
126+
}
127+
128+
// Find files matching the pattern
129+
files, err := findMatchingFiles(flags.InputDir, flags.Pattern)
130+
if err != nil {
131+
return fmt.Errorf("failed to find files: %w", err)
132+
}
133+
134+
if len(files) == 0 {
135+
LogMessage(flags.Quiet, flags.Verbose, "No files found matching the pattern")
136+
return nil
137+
}
138+
139+
LogMessage(flags.Quiet, flags.Verbose, "Found %d files to decrypt", len(files))
140+
141+
// Process files
142+
results := make([]BatchResult, 0, len(files))
143+
successCount := 0
144+
145+
// Create progress bar
146+
progressBar := NewProgressBar(len(files), flags.Quiet, flags.Verbose)
147+
148+
for i, file := range files {
149+
start := time.Now()
150+
151+
LogMessage(flags.Quiet, flags.Verbose, "Decrypting %d/%d: %s", i+1, len(files), file)
152+
153+
result := BatchResult{File: file}
154+
155+
// Determine output file path
156+
relPath, err := filepath.Rel(flags.InputDir, file)
157+
if err != nil {
158+
result.Error = fmt.Errorf("failed to get relative path: %w", err)
159+
results = append(results, result)
160+
continue
161+
}
162+
163+
outputFile := filepath.Join(flags.OutputDir, relPath)
164+
165+
// Remove .tle extension if present
166+
outputFile = strings.TrimSuffix(outputFile, ".tle")
167+
168+
// Create output directory for this file
169+
if err := os.MkdirAll(filepath.Dir(outputFile), 0755); err != nil {
170+
result.Error = fmt.Errorf("failed to create output directory: %w", err)
171+
results = append(results, result)
172+
continue
173+
}
174+
175+
// Decrypt the file
176+
if err := decryptFile(file, outputFile, network); err != nil {
177+
result.Error = err
178+
} else {
179+
result.Success = true
180+
successCount++
181+
}
182+
183+
result.Duration = time.Since(start)
184+
results = append(results, result)
185+
186+
if result.Success {
187+
LogMessage(flags.Quiet, flags.Verbose, "✓ Decrypted %s in %v", file, result.Duration)
188+
} else {
189+
LogError(flags.Quiet, "Failed to decrypt %s: %v", file, result.Error)
190+
}
191+
192+
// Update progress bar
193+
progressBar.Increment()
194+
}
195+
196+
// Finish progress bar
197+
progressBar.Finish()
198+
199+
// Print summary
200+
LogMessage(flags.Quiet, flags.Verbose, "Batch decryption completed: %d/%d files successful", successCount, len(files))
201+
202+
if successCount < len(files) {
203+
LogMessage(flags.Quiet, flags.Verbose, "Failed files:")
204+
for _, result := range results {
205+
if !result.Success {
206+
LogError(flags.Quiet, " %s: %v", result.File, result.Error)
207+
}
208+
}
209+
}
210+
211+
return nil
212+
}
213+
214+
// findMatchingFiles finds files matching the given pattern in the directory
215+
func findMatchingFiles(dir, pattern string) ([]string, error) {
216+
var files []string
217+
218+
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
219+
if err != nil {
220+
return err
221+
}
222+
223+
if info.IsDir() {
224+
return nil
225+
}
226+
227+
// If no pattern specified, include all files
228+
if pattern == "" {
229+
files = append(files, path)
230+
return nil
231+
}
232+
233+
// Simple pattern matching (supports * wildcard)
234+
matched, err := filepath.Match(pattern, filepath.Base(path))
235+
if err != nil {
236+
return err
237+
}
238+
239+
if matched {
240+
files = append(files, path)
241+
}
242+
243+
return nil
244+
})
245+
246+
return files, err
247+
}
248+
249+
// encryptFile encrypts a single file
250+
func encryptFile(inputFile, outputFile string, flags Flags, network *http.Network) error {
251+
// Open input file
252+
input, err := os.Open(inputFile)
253+
if err != nil {
254+
return fmt.Errorf("failed to open input file: %w", err)
255+
}
256+
defer input.Close()
257+
258+
// Create output file
259+
output, err := os.Create(outputFile)
260+
if err != nil {
261+
return fmt.Errorf("failed to create output file: %w", err)
262+
}
263+
defer output.Close()
264+
265+
// Create tlock instance
266+
tlock := tlock.New(network)
267+
268+
// Determine round number
269+
var roundNumber uint64
270+
if flags.Round != 0 {
271+
roundNumber = flags.Round
272+
} else if flags.Duration != "" {
273+
start := time.Now()
274+
totalDuration, err := parseDurationsAsSeconds(start, flags.Duration)
275+
if err != nil {
276+
return fmt.Errorf("failed to parse duration: %w", err)
277+
}
278+
decryptionTime := start.Add(totalDuration)
279+
roundNumber = network.RoundNumber(decryptionTime)
280+
} else {
281+
return fmt.Errorf("no round or duration specified")
282+
}
283+
284+
// Encrypt the file
285+
return tlock.Encrypt(output, input, roundNumber)
286+
}
287+
288+
// decryptFile decrypts a single file
289+
func decryptFile(inputFile, outputFile string, network *http.Network) error {
290+
// Open input file
291+
input, err := os.Open(inputFile)
292+
if err != nil {
293+
return fmt.Errorf("failed to open input file: %w", err)
294+
}
295+
defer input.Close()
296+
297+
// Create output file
298+
output, err := os.Create(outputFile)
299+
if err != nil {
300+
return fmt.Errorf("failed to create output file: %w", err)
301+
}
302+
defer output.Close()
303+
304+
// Create tlock instance
305+
tlock := tlock.New(network)
306+
307+
// Decrypt the file
308+
return tlock.Decrypt(output, input)
309+
}

0 commit comments

Comments
 (0)