Skip to content

Commit b80a871

Browse files
authored
feat: cross-platform backup - WPB-16658 (#2889)
1 parent 96329d0 commit b80a871

File tree

104 files changed

+1613
-316
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+1613
-316
lines changed

WireBackup/Sources/WireBackup/KaliumBackupAdapters/Models/BackupConversation+initWithBackupConversationModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import KaliumBackup
2020

2121
extension BackupConversation {
2222

23-
convenience init(_ conversation: BackupConversationModel) {
23+
convenience init(_ conversation: ConversationBackupModel) {
2424
self.init(
25-
id: BackupQualifiedId(conversation.id),
25+
id: BackupQualifiedId(conversation.qualifiedID),
2626
name: conversation.name
2727
)
2828
}

WireBackup/Sources/WireBackup/KaliumBackupAdapters/Models/BackupMessage+initWithBackupMessageModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import KaliumBackup
2121

2222
extension BackupMessage {
2323

24-
convenience init(_ message: BackupMessageModel) {
24+
convenience init(_ message: MessageBackupModel) {
2525
self.init(
2626
id: message.id.lowercased(),
2727
conversationId: BackupQualifiedId(message.conversationID),

WireBackup/Sources/WireBackup/KaliumBackupAdapters/Models/BackupUser+initWithBackupUserModel.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
//
1818

1919
import KaliumBackup
20+
import WireFoundation
2021

2122
extension BackupUser {
2223

23-
convenience init(_ user: BackupUserModel) {
24+
convenience init(_ user: UserBackupModel) {
2425
self.init(
25-
id: BackupQualifiedId(user.id),
26+
id: BackupQualifiedId(user.qualifiedID),
2627
name: user.name,
2728
handle: user.handle
2829
)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2025 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import Foundation
20+
import KaliumBackup
21+
import WireFoundation
22+
23+
/// Abstraction around the multi-platform framework, attempting to improve the interface by using proper types and Swift
24+
/// concurrency and hide the NSObject API.
25+
struct BackupCreator {
26+
27+
private let mpBackupCreator: MPBackupExporter
28+
29+
init(
30+
selfUserID: QualifiedID,
31+
workDirectoryURL: URL,
32+
outputDirectoryURL: URL,
33+
fileArchiver: some FileArchiverProtocol
34+
) {
35+
self.mpBackupCreator = MPBackupExporter(
36+
selfUserId: BackupQualifiedId(selfUserID),
37+
workDirectory: workDirectoryURL.path(),
38+
outputDirectory: outputDirectoryURL.path(),
39+
fileZipper: FileArchiverToFileZipperAdapter(fileArchiver)
40+
)
41+
}
42+
43+
func addUser(_ user: UserBackupModel) {
44+
mpBackupCreator.add(user: BackupUser(user))
45+
}
46+
47+
func addConversation(_ conversation: ConversationBackupModel) {
48+
mpBackupCreator.add(conversation: BackupConversation(conversation))
49+
}
50+
51+
func addMessage(_ message: MessageBackupModel) {
52+
mpBackupCreator.add(message: BackupMessage(message))
53+
}
54+
55+
func finalize(password: String) async throws -> URL {
56+
57+
let result: any BackupExportResult = try await mpBackupCreator.finalize(password: password)
58+
59+
switch result {
60+
case let success as BackupExportResultSuccess:
61+
return URL(filePath: success.pathToOutputFile, directoryHint: .notDirectory)
62+
case let ioError as BackupExportResultFailureIOError:
63+
throw FinalizeBackupFileError.ioError(ioError.message)
64+
case let zipError as BackupExportResultFailureZipError:
65+
throw FinalizeBackupFileError.zipError(zipError.message)
66+
default:
67+
throw FinalizeBackupFileError.unexpectedResultType
68+
}
69+
70+
}
71+
72+
// MARK: -
73+
74+
enum FinalizeBackupFileError: Error {
75+
76+
case success(_ outputFile: String)
77+
case ioError(_ message: String)
78+
case zipError(_ message: String)
79+
case unexpectedResultType
80+
81+
}
82+
83+
}
84+
85+
// MARK: -
86+
87+
private final class FileArchiverToFileZipperAdapter<FileArchiver>: FileZipper
88+
where FileArchiver: FileArchiverProtocol {
89+
90+
let fileArchiver: FileArchiver
91+
92+
init(_ fileArchiver: FileArchiver) {
93+
self.fileArchiver = fileArchiver
94+
}
95+
96+
func zip(entries: [String], outputDirectory: OkioPath) throws -> String {
97+
98+
let outputDirectory = outputDirectory.segments.reduce(URL(fileURLWithPath: "/")) { url, component in
99+
url.appendingPathComponent(component)
100+
}
101+
let targetURLs = entries.map { entry in
102+
URL(filePath: entry, directoryHint: .notDirectory)
103+
}
104+
let destinationURL = outputDirectory.appendingPathComponent("backup.zip", isDirectory: false)
105+
106+
try fileArchiver.zipResources(at: targetURLs, into: destinationURL)
107+
108+
return destinationURL.path()
109+
110+
}
111+
112+
}

WireBackup/Sources/WireBackup/Models/BackupConversationModel.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,16 @@
1818

1919
public import WireFoundation
2020

21-
public struct BackupConversationModel {
22-
public typealias ID = QualifiedID
21+
public struct ConversationBackupModel: Codable, Hashable, Sendable {
2322

24-
public var id: ID
23+
public var qualifiedID: QualifiedID
2524
public var name: String
2625

2726
public init(
28-
id: ID,
27+
qualifiedID: QualifiedID,
2928
name: String
3029
) {
31-
self.id = id
30+
self.qualifiedID = qualifiedID
3231
self.name = name
3332
}
3433

WireBackup/Sources/WireBackup/Models/BackupMessageModel.swift

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,17 @@
1919
public import Foundation
2020
public import WireFoundation
2121

22-
public struct BackupMessageModel {
23-
public typealias ID = String
22+
public struct MessageBackupModel: Codable, Hashable, Sendable {
2423

25-
public var id: ID
24+
public var id: String
2625
public var conversationID: QualifiedID
2726
public var senderUserID: QualifiedID
2827
public var senderClientID: String?
2928
public var creationDate: Date
3029
public var content: MessageContent
3130

3231
public init(
33-
id: ID,
32+
id: String,
3433
conversationID: QualifiedID,
3534
senderUserID: QualifiedID,
3635
senderClientID: String? = nil,
@@ -52,7 +51,7 @@ public struct BackupMessageModel {
5251
// The following types replicate the API of the multi-platform backup library in a Swift friendlier way.
5352
// (e.g. enums instead of class hierarchy)
5453

55-
public enum MessageContent {
54+
public enum MessageContent: Codable, Hashable, Sendable {
5655

5756
case text(TextContent)
5857
case location(LocationContent)
@@ -64,18 +63,18 @@ public enum MessageContent {
6463

6564
public extension MessageContent {
6665

67-
struct TextContent {
66+
struct TextContent: Codable, Hashable, Sendable {
6867
public var text: String
6968
}
7069

71-
struct LocationContent {
70+
struct LocationContent: Codable, Hashable, Sendable {
7271
public var longitude: Float
7372
public var latitude: Float
7473
public var name: String?
7574
public var zoom: Int32?
7675
}
7776

78-
struct AssetContent {
77+
struct AssetContent: Codable, Hashable, Sendable {
7978
public var mimeType: String
8079
public var size: UInt64
8180
public var name: String?
@@ -87,16 +86,17 @@ public extension MessageContent {
8786
public var encryption: EncryptionAlgorithm?
8887
public var metadata: Metadata?
8988

90-
public enum EncryptionAlgorithm {
89+
public enum EncryptionAlgorithm: Codable, Hashable, Sendable {
9190
case aesCBC
9291
case aesGCM
9392
}
9493

95-
public enum Metadata {
94+
public enum Metadata: Codable, Hashable, Sendable {
9695

9796
case image(ImageMetadata)
9897
case video(VideoMetadata)
9998
case audio(AudioMetadata)
99+
// TODO: [WPB-16658] check if the `.generic` case needs to be used
100100
case generic(GenericMetadata)
101101

102102
}
@@ -106,24 +106,24 @@ public extension MessageContent {
106106

107107
public extension MessageContent.AssetContent.Metadata {
108108

109-
struct ImageMetadata {
109+
struct ImageMetadata: Codable, Hashable, Sendable {
110110
public var width: Int32
111111
public var height: Int32
112112
public var tag: String?
113113
}
114114

115-
struct VideoMetadata {
115+
struct VideoMetadata: Codable, Hashable, Sendable {
116116
public var width: Int32?
117117
public var height: Int32?
118118
public var duration: UInt64?
119119
}
120120

121-
struct AudioMetadata {
121+
struct AudioMetadata: Codable, Hashable, Sendable {
122122
public var normalization: Data?
123123
public var duration: UInt64?
124124
}
125125

126-
struct GenericMetadata {
126+
struct GenericMetadata: Codable, Hashable, Sendable {
127127
public var name: String?
128128
}
129129

WireBackup/Sources/WireBackup/Models/BackupUserModel.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,18 @@
1818

1919
public import WireFoundation
2020

21-
public struct BackupUserModel {
22-
public typealias ID = QualifiedID
21+
public struct UserBackupModel: Codable, Hashable, Sendable {
2322

24-
public var id: ID
23+
public var qualifiedID: QualifiedID
2524
public var name: String
2625
public var handle: String
2726

2827
public init(
29-
id: ID,
28+
qualifiedID: QualifiedID,
3029
name: String,
3130
handle: String
3231
) {
33-
self.id = id
32+
self.qualifiedID = qualifiedID
3433
self.name = name
3534
self.handle = handle
3635
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2025 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
import WireFoundation
20+
21+
// sourcery: AutoMockable
22+
public protocol BackupLocalStoreProtocol: Sendable {
23+
24+
/// Returns the number of all stored users, conversations and messages in the local data store, including deleted
25+
/// ones.
26+
func countModels() async throws -> (userCount: Int, conversationCount: Int, messageCount: Int)
27+
28+
// MARK: -
29+
30+
/// Returns all users stored in the local database, including deleted ones.
31+
func fetchAllUsers() -> AsyncThrowingStream<UserBackupModel, any Error>
32+
33+
// MARK: -
34+
35+
/// Returns all conversations stored in the local database, including deleted ones.
36+
func fetchAllConversations() -> AsyncThrowingStream<ConversationBackupModel, any Error>
37+
38+
// MARK: -
39+
40+
/// Returns all messages stored in the local database, including deleted ones.
41+
func fetchAllMessages() -> AsyncThrowingStream<MessageBackupModel, any Error>
42+
43+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2025 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
public enum BackupFileExtension: String, CaseIterable {
20+
21+
case crossPlatform = "wbu"
22+
23+
// MARK: Legacy
24+
25+
// There are some external apps that users can use to transfer backup files, which can modify their attachments and
26+
// change the underscore with a dash. For this reason, we accept 2 types of file extensions to restore
27+
// conversations.
28+
29+
case fileExtensionWithUnderscore = "ios_wbu"
30+
case fileExtensionWithHyphen = "ios-wbu"
31+
32+
}

0 commit comments

Comments
 (0)