@@ -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+ }
0 commit comments