Skip to content

Commit b709a98

Browse files
committed
Add support for downloading entire folders at a time.
1 parent 6948368 commit b709a98

2 files changed

Lines changed: 129 additions & 2 deletions

File tree

main.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ func main() {
254254
var downloadCmd = &cobra.Command{
255255
Use: "download [vault-path] [local-path]",
256256
Aliases: []string{"down", "d", "get"},
257-
Short: "Download a file from the vault",
257+
Short: "Download a file or directory from the vault",
258258
Args: cobra.RangeArgs(1, 2),
259259
Run: func(cmd *cobra.Command, args []string) {
260260
vaultPath := args[0]
@@ -283,11 +283,26 @@ func main() {
283283
return
284284
}
285285

286-
err = utils.DownloadFile(vaultPath, localPath, session)
286+
// Check if the vault path is a directory or file
287+
entry, err := session.Index.FindEntry(vaultPath)
287288
if err != nil {
288289
fmt.Printf("❌ Download failed: %v\n", err)
289290
return
290291
}
292+
293+
var downloadErr error
294+
if entry.Type == "folder" {
295+
// Directory download
296+
downloadErr = utils.DownloadDirectory(vaultPath, localPath, session)
297+
} else {
298+
// Single file download
299+
downloadErr = utils.DownloadFile(vaultPath, localPath, session)
300+
}
301+
302+
if downloadErr != nil {
303+
fmt.Printf("❌ Download failed: %v\n", downloadErr)
304+
return
305+
}
291306
fmt.Println("✔ Download successful.")
292307
},
293308
}

utils/download.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"os"
9+
"path/filepath"
910
"strings"
1011
"time"
1112
)
@@ -65,6 +66,117 @@ func DownloadFile(vaultPath string, outputPath string, session *Session) error {
6566
return nil
6667
}
6768

69+
// DownloadDirectory downloads an entire directory recursively from the vault
70+
func DownloadDirectory(vaultPath string, outputPath string, session *Session) error {
71+
// 1. Verify the path is a directory
72+
PrintProgressStep(1, 3, "Locating directory in vault...")
73+
entry, err := session.Index.FindEntry(vaultPath)
74+
if err != nil {
75+
return fmt.Errorf("could not find directory in vault: %w", err)
76+
}
77+
78+
// 2. Safety check: Ensure we're downloading a folder
79+
if entry.Type != "folder" {
80+
return fmt.Errorf("'%s' is a file, not a directory. Use download command for files", vaultPath)
81+
}
82+
PrintCompletionLine("Directory located")
83+
84+
fmt.Printf("Downloading directory from vault: %s\n", vaultPath)
85+
86+
// 3. Create output directory if it doesn't exist
87+
err = os.MkdirAll(outputPath, 0755)
88+
if err != nil {
89+
return fmt.Errorf("failed to create output directory: %w", err)
90+
}
91+
92+
fileCount := 0
93+
94+
// 4. Recursively download all files in the directory
95+
var downloadFiles func(currentEntry Entry, currentVaultPath string, currentLocalPath string) error
96+
downloadFiles = func(currentEntry Entry, currentVaultPath string, currentLocalPath string) error {
97+
// Process all entries in the current folder
98+
for name, subEntry := range currentEntry.Contents {
99+
var nextVaultPath string
100+
if currentVaultPath == "" {
101+
nextVaultPath = name
102+
} else {
103+
nextVaultPath = currentVaultPath + "/" + name
104+
}
105+
106+
nextLocalPath := filepath.Join(currentLocalPath, name)
107+
108+
if subEntry.Type == "file" {
109+
fileCount++
110+
fmt.Printf("Downloading file (%d): %s\n", fileCount, name)
111+
112+
// 5. Fetch the encrypted file from GitHub
113+
encryptedData, err := FetchRaw(session.Username, subEntry.RealName)
114+
if err != nil {
115+
return fmt.Errorf("failed to fetch file %s: %w", nextVaultPath, err)
116+
}
117+
118+
// 6. Decrypt the file key from the index
119+
encryptedKey, err := hex.DecodeString(subEntry.FileKey)
120+
if err != nil {
121+
return fmt.Errorf("invalid file key in index for %s: %w", nextVaultPath, err)
122+
}
123+
fileKey, err := Decrypt(encryptedKey, session.Password)
124+
if err != nil {
125+
return fmt.Errorf("failed to decrypt file key for %s: %w", nextVaultPath, err)
126+
}
127+
128+
// 7. Decrypt the file data with the file key
129+
decryptedData, err := DecryptWithKey(encryptedData, fileKey)
130+
if err != nil {
131+
return fmt.Errorf("decryption failed for %s: %w", nextVaultPath, err)
132+
}
133+
134+
// 8. Save to local path
135+
err = os.WriteFile(nextLocalPath, decryptedData, 0644)
136+
if err != nil {
137+
return fmt.Errorf("failed to save file %s: %w", nextLocalPath, err)
138+
}
139+
140+
fmt.Printf(" → Saved: %s\n", nextLocalPath)
141+
142+
} else if subEntry.Type == "folder" {
143+
// Create subdirectory
144+
err := os.MkdirAll(nextLocalPath, 0755)
145+
if err != nil {
146+
return fmt.Errorf("failed to create directory %s: %w", nextLocalPath, err)
147+
}
148+
149+
// Recursively download contents
150+
err = downloadFiles(subEntry, nextVaultPath, nextLocalPath)
151+
if err != nil {
152+
return err
153+
}
154+
}
155+
}
156+
157+
return nil
158+
}
159+
160+
// Start recursive download from the directory entry
161+
err = downloadFiles(*entry, vaultPath, outputPath)
162+
if err != nil {
163+
return err
164+
}
165+
166+
if fileCount == 0 {
167+
return fmt.Errorf("no files found in directory: %s", vaultPath)
168+
}
169+
170+
PrintProgressStep(2, 3, "Finalizing download...")
171+
PrintCompletionLine("Decryption complete")
172+
173+
PrintProgressStep(3, 3, "Writing files to disk...")
174+
PrintCompletionLine("Files written successfully")
175+
176+
fmt.Printf("✔ Successfully downloaded %d files from directory\n", fileCount)
177+
return nil
178+
}
179+
68180
// DownloadSharedFile downloads a file using a share string (username:reference:sharepassword:base64filename)
69181
func DownloadSharedFile(shareString string, outputPath string) error {
70182
// 1. Parse the share string (supports both old 3-part and new 4-part formats)

0 commit comments

Comments
 (0)