@@ -12,8 +12,8 @@ import com.aicode.feature.agent.domain.mcp.McpManager
1212import com.aicode.feature.agent.domain.permission.PermissionRulesRepository
1313import com.aicode.feature.backup.domain.AgentMessageDto
1414import com.aicode.feature.backup.domain.BackupCrypto
15- import com.aicode.feature.backup.domain.BackupFormat
1615import com.aicode.feature.backup.domain.BackupManager
16+ import com.aicode.feature.backup.domain.BackupOptions
1717import com.aicode.feature.backup.domain.BackupSnapshot
1818import com.aicode.feature.backup.domain.ChatSessionDto
1919import com.aicode.feature.backup.domain.GitCredentialDto
@@ -34,11 +34,18 @@ import com.aicode.feature.settings.data.repository.VisionModelSettingsRepository
3434import com.aicode.feature.workspace.data.local.dao.RemoteConnectionDao
3535import com.aicode.feature.workspace.data.local.entity.RemoteConnectionEntity
3636import com.aicode.feature.workspace.data.local.entity.RemoteMountEntity
37+ import com.aicode.feature.workspace.data.repository.WorkspaceRepository
3738import com.aicode.feature.workspace.domain.model.RemoteProtocol
3839import dagger.hilt.android.qualifiers.ApplicationContext
3940import kotlinx.coroutines.Dispatchers
4041import kotlinx.coroutines.withContext
4142import kotlinx.serialization.json.Json
43+ import org.apache.commons.compress.archivers.tar.TarArchiveEntry
44+ import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
45+ import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
46+ import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
47+ import java.io.ByteArrayInputStream
48+ import java.io.ByteArrayOutputStream
4249import javax.inject.Inject
4350import javax.inject.Singleton
4451
@@ -58,7 +65,8 @@ class BackupManagerImpl @Inject constructor(
5865 private val keepaliveSettingsRepository : KeepaliveSettingsRepository ,
5966 private val logSettingsRepository : LogSettingsRepository ,
6067 private val visionModelSettingsRepository : VisionModelSettingsRepository ,
61- private val syncSettingsRepository : SyncSettingsRepository
68+ private val syncSettingsRepository : SyncSettingsRepository ,
69+ private val workspaceRepository : WorkspaceRepository
6270) : BackupManager {
6371
6472 private val json = Json {
@@ -73,22 +81,22 @@ class BackupManagerImpl @Inject constructor(
7381 context.packageManager.getPackageInfo(context.packageName, 0 ).versionName ? : " "
7482 }.getOrDefault(" " )
7583
76- override suspend fun export (password : CharArray ): ByteArray = withContext(Dispatchers .IO ) {
77- val providers = aiProviderDao.getAllProvidersOnce().map { it.toDto() }
78- val credentials = gitCredentialDao.getAllOnce().map { it.toDto() }
79- val connections = remoteConnectionDao.getAllConnectionsOnce().map { it.toDto() }
80- val mounts = remoteConnectionDao.getAllMountsOnce().map { it.toDto() }
81- val sessions = chatSessionDao.getAllOnce().map { it.toDto() }
82- val messages = agentMessageDao.getAllOnce().map { it.toDto() }
83- val todos = todoItemDao.getAllOnce().map { it.toDto() }
84- val mcpServers = mcpConfigRepository.getServers()
85- val globalRules = permissionRulesRepository.getGlobalRulesOnce()
86- val themeMode = themeSettingsRepository.snapshot()
87- val keepalive = keepaliveSettingsRepository.snapshot()
88- val logLevel = logSettingsRepository.snapshot()
89- val visionProviderId = visionModelSettingsRepository.getVisionProviderId()
90- val visionModel = visionModelSettingsRepository.getVisionModel()
91- val syncSettings = syncSettingsRepository.snapshot()
84+ override suspend fun export (password : CharArray? , options : BackupOptions ): ByteArray = withContext(Dispatchers .IO ) {
85+ val providers = if (options.providers) aiProviderDao.getAllProvidersOnce().map { it.toDto() } else emptyList()
86+ val credentials = if (options.gitCredentials) gitCredentialDao.getAllOnce().map { it.toDto() } else emptyList()
87+ val connections = if (options.remoteConnections) remoteConnectionDao.getAllConnectionsOnce().map { it.toDto() } else emptyList()
88+ val mounts = if (options.remoteConnections) remoteConnectionDao.getAllMountsOnce().map { it.toDto() } else emptyList()
89+ val sessions = if (options.chatHistory) chatSessionDao.getAllOnce().map { it.toDto() } else emptyList()
90+ val messages = if (options.chatHistory) agentMessageDao.getAllOnce().map { it.toDto() } else emptyList()
91+ val todos = if (options.chatHistory) todoItemDao.getAllOnce().map { it.toDto() } else emptyList()
92+ val mcpServers = if (options.mcpServers) mcpConfigRepository.getServers() else emptyList ()
93+ val globalRules = if (options.permissionRules) permissionRulesRepository.getGlobalRulesOnce() else emptyList ()
94+ val themeMode = if (options.appSettings) themeSettingsRepository.snapshot() else null
95+ val keepalive = if (options.appSettings) keepaliveSettingsRepository.snapshot() else false
96+ val logLevel = if (options.appSettings) logSettingsRepository.snapshot() else null
97+ val visionProviderId = if (options.appSettings) visionModelSettingsRepository.getVisionProviderId() else " "
98+ val visionModel = if (options.appSettings) visionModelSettingsRepository.getVisionModel() else " "
99+ val syncSettings = if (options.appSettings) syncSettingsRepository.snapshot() else null
92100
93101 val snapshot = BackupSnapshot (
94102 schemaVersion = currentSchemaVersion(),
@@ -111,21 +119,31 @@ class BackupManagerImpl @Inject constructor(
111119 syncSettings = syncSettings
112120 )
113121 val plain = json.encodeToString(BackupSnapshot .serializer(), snapshot).toByteArray(Charsets .UTF_8 )
114- val salt = BackupCrypto .newSalt()
115- val iv = BackupCrypto .newIv()
116- val ciphertext = BackupCrypto .encrypt(plain, password, salt, iv)
117- BackupFormat .pack(BackupSnapshot .FORMAT_VERSION , salt, iv, ciphertext)
122+ val tarGz = tarGz(plain)
123+ if (password != null && password.isNotEmpty()) {
124+ val salt = BackupCrypto .newSalt()
125+ val iv = BackupCrypto .newIv()
126+ BackupCrypto .encrypt(tarGz, password, salt, iv)
127+ } else {
128+ tarGz
129+ }
118130 }
119131
120- override suspend fun import (data : ByteArray , password : CharArray ): Result <RestoreStats > = withContext(Dispatchers .IO ) {
132+ override suspend fun import (data : ByteArray , password : CharArray? ): Result <RestoreStats > = withContext(Dispatchers .IO ) {
121133 runCatching {
122- val header = BackupFormat .unpack(data)
123- ? : error(" 不是有效的 AiCode 备份文件" )
124- if (header.formatVersion > BackupSnapshot .FORMAT_VERSION ) {
125- error(" 备份格式版本 ${header.formatVersion} 高于本应用支持版本,请升级应用" )
134+ val tarGz = if (password != null && password.isNotEmpty()) {
135+ val salt = ByteArray (BackupCrypto .SALT_LEN )
136+ val iv = ByteArray (BackupCrypto .IV_LEN )
137+ require(data.size >= salt.size + iv.size) { " 不是有效的 AiCode 备份文件" }
138+ System .arraycopy(data, 0 , salt, 0 , salt.size)
139+ System .arraycopy(data, salt.size, iv, 0 , iv.size)
140+ val ciphertext = data.copyOfRange(salt.size + iv.size, data.size)
141+ BackupCrypto .decrypt(ciphertext, password, salt, iv)
142+ } else {
143+ data
126144 }
127- val ciphertext = data.copyOfRange(header.ciphertextOffset, data.size )
128- val plain = BackupCrypto .decrypt(ciphertext, password, header.salt, header.iv )
145+ val plain = unTarGz(tarGz )
146+ ? : error( " 不是有效的 AiCode 备份文件 " )
129147 val snapshot = json.decodeFromString(BackupSnapshot .serializer(), String (plain, Charsets .UTF_8 ))
130148 if (snapshot.schemaVersion > currentSchemaVersion()) {
131149 error(" 备份的数据库版本 v${snapshot.schemaVersion} 高于本应用 v${currentSchemaVersion()} ,请升级应用" )
@@ -134,6 +152,33 @@ class BackupManagerImpl @Inject constructor(
134152 }
135153 }
136154
155+ private fun tarGz (content : ByteArray ): ByteArray {
156+ val baos = ByteArrayOutputStream ()
157+ GzipCompressorOutputStream (baos).use { gz ->
158+ TarArchiveOutputStream (gz).use { tar ->
159+ tar.putArchiveEntry(TarArchiveEntry (" snapshot.json" ).apply { size = content.size.toLong() })
160+ tar.write(content)
161+ tar.closeArchiveEntry()
162+ }
163+ }
164+ return baos.toByteArray()
165+ }
166+
167+ private fun unTarGz (data : ByteArray ): ByteArray? = runCatching {
168+ GzipCompressorInputStream (ByteArrayInputStream (data)).use { gz ->
169+ org.apache.commons.compress.archivers.tar.TarArchiveInputStream (gz).use { tar ->
170+ var entry = tar.nextEntry
171+ while (entry != null ) {
172+ if (entry.name == " snapshot.json" ) {
173+ return @use tar.readBytes()
174+ }
175+ entry = tar.nextEntry
176+ }
177+ null
178+ }
179+ }
180+ }.getOrNull()
181+
137182 private suspend fun restore (snapshot : BackupSnapshot ): RestoreStats {
138183 if (snapshot.providers.isNotEmpty()) {
139184 aiProviderDao.insertAllProviders(snapshot.providers.map { it.toEntity() })
@@ -148,7 +193,8 @@ class BackupManagerImpl @Inject constructor(
148193 remoteConnectionDao.insertAllMounts(snapshot.remoteMounts.map { it.toEntity() })
149194 }
150195 if (snapshot.chatSessions.isNotEmpty()) {
151- chatSessionDao.upsertAll(snapshot.chatSessions.map { it.toEntity() })
196+ val currentWorkspacePath = workspaceRepository.currentPath()
197+ chatSessionDao.upsertAll(snapshot.chatSessions.map { it.copy(workspacePath = currentWorkspacePath).toEntity() })
152198 }
153199 if (snapshot.agentMessages.isNotEmpty()) {
154200 agentMessageDao.insertAll(snapshot.agentMessages.map { it.toEntity() })
0 commit comments