Skip to content

Commit 431ee60

Browse files
committed
feat: make folder decrypter aware of archived versions
Signed-off-by: Tommy van der Vorst <tommy@pixelspark.nl>
1 parent 403b4d4 commit 431ee60

File tree

4 files changed

+135
-19
lines changed

4 files changed

+135
-19
lines changed

Localizable.xcstrings

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9522,6 +9522,52 @@
95229522
}
95239523
}
95249524
},
9525+
"Current version" : {
9526+
"localizations" : {
9527+
"de" : {
9528+
"stringUnit" : {
9529+
"state" : "translated",
9530+
"value" : "Aktuelle Version"
9531+
}
9532+
},
9533+
"es" : {
9534+
"stringUnit" : {
9535+
"state" : "translated",
9536+
"value" : "Versión actual"
9537+
}
9538+
},
9539+
"it" : {
9540+
"stringUnit" : {
9541+
"state" : "translated",
9542+
"value" : "Versione corrente"
9543+
}
9544+
},
9545+
"ja" : {
9546+
"stringUnit" : {
9547+
"state" : "translated",
9548+
"value" : "現在のバージョン"
9549+
}
9550+
},
9551+
"nl" : {
9552+
"stringUnit" : {
9553+
"state" : "translated",
9554+
"value" : "Huidige versie"
9555+
}
9556+
},
9557+
"uk" : {
9558+
"stringUnit" : {
9559+
"state" : "translated",
9560+
"value" : "Поточна версія"
9561+
}
9562+
},
9563+
"zh-Hans" : {
9564+
"stringUnit" : {
9565+
"state" : "translated",
9566+
"value" : "当前版本"
9567+
}
9568+
}
9569+
}
9570+
},
95259571
"Currently no files are being downloaded from other devices." : {
95269572
"localizations" : {
95279573
"de" : {

Sushitrain/DecrypterView.swift

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import SwiftUI
2929
@State private var folderPassword: String = ""
3030
@State private var searchText: String = ""
3131
@State private var destURL: URL? = nil
32-
@State private var selectedDecryptedPaths: Set<String> = []
32+
@State private var selectedURLs: Set<String> = []
3333
@State private var showSuccessMessage = false
3434
@State private var keepFolderStructure = true
3535

@@ -63,7 +63,7 @@ import SwiftUI
6363
.disabled(folderPassword.isEmpty || folderID.isEmpty || sourceURL == nil)
6464
}
6565

66-
Section("\(selectedDecryptedPaths.count) files selected") {
66+
Section("\(selectedURLs.count) files selected") {
6767
LabeledContent("Folder") {
6868
HStack {
6969
Text(destURL?.lastPathComponent ?? "")
@@ -78,12 +78,12 @@ import SwiftUI
7878
Text("Recreate folder structure")
7979
}
8080

81-
Button("Decrypt \(selectedDecryptedPaths.count) files") {
81+
Button("Decrypt \(selectedURLs.count) files") {
8282
Task {
8383
await self.decryptSelection()
8484
}
8585
}.disabled(destURL == nil)
86-
}.disabled(folderPassword.isEmpty || folderID.isEmpty || sourceURL == nil || selectedDecryptedPaths.isEmpty)
86+
}.disabled(folderPassword.isEmpty || folderID.isEmpty || sourceURL == nil || selectedURLs.isEmpty)
8787
}
8888
.formStyle(.grouped)
8989
.disabled(loading)
@@ -106,7 +106,7 @@ import SwiftUI
106106
DecrypterItemsView(
107107
folderID: folderID, folderPassword: folderPassword,
108108
entries: searchText.isEmpty ? allEntries : foundEntries,
109-
selectedDecryptedPaths: $selectedDecryptedPaths
109+
selectedURLs: $selectedURLs
110110
)
111111
.id(searchText)
112112
}
@@ -176,11 +176,13 @@ import SwiftUI
176176
if trimmedPath.starts(with: ".stfolder") {
177177
continue
178178
}
179-
let decryptedPath = folderKey.decryptedFilePath(trimmedPath, error: &error)
179+
180+
let encryptedFilePath = EncryptedFilePath(trimmedPath)
181+
let decryptedPath = folderKey.decryptedFilePath(encryptedFilePath.pathWithoutVersion, error: &error)
180182
if let e = error {
181183
throw e
182184
}
183-
entries.append(EncryptedFileEntry(decryptedPath: decryptedPath, url: c))
185+
entries.append(EncryptedFileEntry(decryptedPath: decryptedPath, url: c, version: encryptedFilePath.version))
184186
}
185187
}
186188
}
@@ -193,7 +195,7 @@ import SwiftUI
193195
let folderID = self.folderID
194196
let folderPassword = self.folderPassword
195197
let destURL = self.destURL
196-
let selectedDecryptedPaths = self.selectedDecryptedPaths
198+
let selectedURLs = self.selectedURLs
197199
let sourceURL = self.sourceURL
198200
let keepFolderStructure = self.keepFolderStructure
199201
let entries = self.allEntries
@@ -204,16 +206,19 @@ import SwiftUI
204206
try withExtendedLifetime(try BookmarkManager.Accessor(url: destURL!)) {
205207
let folderKey = SushitrainNewFolderKey(folderID, folderPassword)
206208
for entry in entries {
207-
if !selectedDecryptedPaths.contains(entry.decryptedPath) {
209+
if !selectedURLs.contains(entry.url.absoluteString) {
208210
continue
209211
}
210212

211213
let rootPath = sourceURL!.path(percentEncoded: false)
212214
let filePath = entry.url.path(percentEncoded: false)
213215
let trimmedPath = String(filePath.trimmingPrefix(rootPath))
214-
Log.info("Decrypt \(trimmedPath) \(sourceURL!) \(destURL!)")
216+
let encryptedPath = EncryptedFilePath(trimmedPath)
217+
Log.info("Decrypt \(trimmedPath) \(sourceURL!) \(destURL!) \(entry.version ?? "current version")")
215218
try folderKey?.decryptFile(
216-
sourceURL?.path(percentEncoded: false), encryptedPath: trimmedPath,
219+
sourceURL?.path(percentEncoded: false),
220+
encryptedPathWithVersion: encryptedPath.actualPath,
221+
encryptedPathWithoutVersion: encryptedPath.pathWithoutVersion,
217222
destRoot: destURL?.path(percentEncoded: false),
218223
keepFolderStructure: keepFolderStructure
219224
)
@@ -291,20 +296,50 @@ import SwiftUI
291296

292297
var decryptedPath: String
293298
var url: URL
299+
var version: String?
294300
}
295301

296302
private struct DecrypterItemsView: View {
297303
let folderID: String
298304
let folderPassword: String
299305
let entries: [EncryptedFileEntry]
300306
@State private var decryptedPaths: [String] = []
307+
@State private var entriesByDecryptedPath: [String: [EncryptedFileEntry]] = [:]
301308

302-
@Binding var selectedDecryptedPaths: Set<String>
309+
@Binding var selectedURLs: Set<String>
303310

304311
var body: some View {
305-
List(selection: $selectedDecryptedPaths) {
312+
List(selection: $selectedURLs) {
306313
PathsOutlineGroup(paths: decryptedPaths, disableIntermediateSelection: true) { decryptedPath, isIntermediate in
307-
Text(decryptedPath.lastPathComponent)
314+
if let entries = self.entriesByDecryptedPath[decryptedPath] {
315+
if entries.count > 1 || (entries.count == 1 && entries[0].version != nil) {
316+
DisclosureGroup {
317+
ForEach(entries) { entry in
318+
if let version = entry.version {
319+
if let date = dateFromVersionString(version) {
320+
Label(
321+
date.formatted(date: .abbreviated, time: .complete),
322+
systemImage: "clock.arrow.trianglehead.2.counterclockwise.rotate.90"
323+
).tag(entry.url)
324+
}
325+
else {
326+
Label(version, systemImage: "clock.arrow.trianglehead.2.counterclockwise.rotate.90").tag(entry.url)
327+
}
328+
}
329+
else {
330+
Label("Current version", systemImage: "clock.fill").tag(entry.url)
331+
}
332+
}
333+
} label: {
334+
let currentEntry = entries.first(where: { $0.version == nil })
335+
Label(decryptedPath.lastPathComponent, systemImage: "doc").tag(currentEntry?.url).disabled(currentEntry == nil)
336+
}
337+
}
338+
else {
339+
let entry = entries[0]
340+
Label(decryptedPath.lastPathComponent, systemImage: "doc").tag(entry.url)
341+
}
342+
}
308343
}
309344
}
310345
.task {
@@ -314,7 +349,32 @@ import SwiftUI
314349

315350
private func update() async {
316351
self.decryptedPaths = self.entries.map { $0.decryptedPath }
352+
self.entriesByDecryptedPath = Dictionary(grouping: self.entries, by: { $0.decryptedPath })
317353
}
318354
}
319355

320356
#endif
357+
358+
private struct EncryptedFilePath {
359+
let version: String?
360+
let pathWithoutVersion: String
361+
let actualPath: String
362+
363+
// A versioned encrypted path will look like:
364+
// .stversions/A.syncthing-enc/BC/DEFXYZ~20260102-030405
365+
init(_ originalPath: String) {
366+
self.actualPath = originalPath
367+
if originalPath.starts(with: ".stversions/") {
368+
// Trim off '.stversions/', decrypt the path, and store the version
369+
if let tildeIndex = originalPath.lastIndex(of: "~"), tildeIndex != originalPath.endIndex {
370+
self.version = String(originalPath[originalPath.index(after: tildeIndex)...])
371+
self.pathWithoutVersion = String(
372+
String(originalPath[...originalPath.index(before: tildeIndex)]).trimmingPrefix(".stversions/"))
373+
return
374+
}
375+
}
376+
377+
self.pathWithoutVersion = originalPath
378+
self.version = nil
379+
}
380+
}

Sushitrain/Utils.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,3 +1251,13 @@ extension Binding where Value == Bool {
12511251
get: { underlying.wrappedValue != nil }, set: { underlying.wrappedValue = $0 ? underlying.wrappedValue : nil })
12521252
}
12531253
}
1254+
1255+
/// Translate version tags appended by Syncthing in a .stversions folder to a date
1256+
/// These look like '20260102-030405' and are in *local time* at the point of creation
1257+
/// So we can only assume here that that matches our local time.
1258+
func dateFromVersionString(_ version: String) -> Date? {
1259+
let dateFormatter = DateFormatter()
1260+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
1261+
dateFormatter.dateFormat = "yyyyMMdd-HHmmss"
1262+
return dateFormatter.date(from: version)
1263+
}

SushitrainCore/src/encryption.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ func (fk *FolderKey) DecryptedFilePath(path string) (string, error) {
125125
return decryptName(path, fk.key)
126126
}
127127

128-
func (fk *FolderKey) DecryptFile(encryptedRoot string, encryptedPath string, destRoot string, keepFolderStructure bool) error {
129-
destPath, err := fk.DecryptedFilePath(encryptedPath)
128+
func (fk *FolderKey) DecryptFile(encryptedRoot string, encryptedPathWithVersion string, encryptedPathWithoutVersion string, destRoot string, keepFolderStructure bool) error {
129+
destPath, err := fk.DecryptedFilePath(encryptedPathWithoutVersion)
130130
if err != nil {
131131
return err
132132
}
@@ -144,7 +144,7 @@ func (fk *FolderKey) DecryptFile(encryptedRoot string, encryptedPath string, des
144144

145145
// Load encrypted file info
146146
srcFs := fs.NewFilesystem(fs.FilesystemTypeBasic, encryptedRoot)
147-
encFd, err := srcFs.Open(encryptedPath)
147+
encFd, err := srcFs.Open(encryptedPathWithVersion)
148148
if err != nil {
149149
return err
150150
}
@@ -159,13 +159,13 @@ func (fk *FolderKey) DecryptFile(encryptedRoot string, encryptedPath string, des
159159

160160
encryptedBlocks, encryptedFileInfoBytes, err := loadBlocks(encFd)
161161
if err != nil {
162-
return fmt.Errorf("%s: loading metadata trailer: %w", encryptedPath, err)
162+
return fmt.Errorf("%s: loading metadata trailer: %w", encryptedPathWithVersion, err)
163163
}
164164

165165
// Construct a fake FileInfo object that satisfies protocol.DecryptFileInfo just enough to trick it into decrypting
166166
// the Encrypted field.
167167
encryptedFileInfo := protocol.FileInfo{
168-
Name: encryptedPath,
168+
Name: encryptedPathWithoutVersion,
169169
Encrypted: encryptedFileInfoBytes,
170170
}
171171

0 commit comments

Comments
 (0)