Skip to content

Commit

Permalink
Merge pull request #31 from SecUSo/feature/export_multiple
Browse files Browse the repository at this point in the history
Feature: Export and import multiple backups at once
  • Loading branch information
udenr authored Nov 12, 2024
2 parents 064bf80 + 7d22719 commit 2b43f61
Show file tree
Hide file tree
Showing 15 changed files with 709 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,68 @@ import android.util.JsonWriter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.secuso.privacyfriendlybackup.data.BackupDataStorageRepository
import java.io.BufferedWriter
import java.io.IOException
import java.io.OutputStreamWriter
import java.text.FieldPosition
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

class DataExporter {
companion object {
suspend fun exportData(context: Context, uri: Uri, data: BackupDataStorageRepository.BackupData) : Boolean{
suspend fun exportDataZip(context: Context, uri: Uri, data: Set<BackupDataStorageRepository.BackupData>): Boolean {
return withContext(Dispatchers.IO) {
try {
ParcelFileDescriptor.AutoCloseOutputStream(
context.contentResolver.openFileDescriptor(uri, "w")
).use { out ->
JsonWriter(OutputStreamWriter(out, Charsets.UTF_8)).use { writer ->
writer.setIndent(" ")
writer.beginObject()
writeData(writer, data)
writer.endObject()
ZipOutputStream(ParcelFileDescriptor.AutoCloseOutputStream(context.contentResolver.openFileDescriptor(uri, "w"))).apply {
setLevel(5)
setComment("PFA Backup Export")
}.use { zipOut ->
data.forEach { backupData ->
val zipEntry = ZipEntry(getSingleExportFileName(backupData, backupData.encrypted))
zipOut.putNextEntry(zipEntry)

val osw = OutputStreamWriter(zipOut, Charsets.UTF_8)
val bw = BufferedWriter(osw)
val jw = JsonWriter(bw)
jw.let { writer ->
writer.setIndent(" ")
writer.beginObject()
writeData(writer, backupData)
writer.endObject()
}
jw.flush()
bw.flush()
osw.flush()

zipOut.closeEntry()
}
}
} catch (e : IOException) {
} catch (e: IOException) {
e.printStackTrace()
return@withContext false
}
return@withContext true
}
}

suspend fun exportData(context: Context, uri: Uri, data: BackupDataStorageRepository.BackupData): Boolean {
return withContext(Dispatchers.IO) {
try {
JsonWriter(
OutputStreamWriter(
ParcelFileDescriptor.AutoCloseOutputStream(context.contentResolver.openFileDescriptor(uri, "w")),
Charsets.UTF_8
)
).use { writer ->
writer.setIndent(" ")
writer.beginObject()
writeData(writer, data)
writer.endObject()
}
} catch (e: IOException) {
return@withContext false
}
return@withContext true
Expand All @@ -39,5 +82,40 @@ class DataExporter {
writer.name("encrypted").value(metaData.encrypted)
writer.name("data").value(String(metaData.data!!))
}

/**
* Filename for export containing multiple backups (.zip)
*/
fun getMultipleExportFileName(): String {
val sb = StringBuffer()
sb.append("PfaBackup_")
val sdf = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.getDefault())
sdf.format(Calendar.getInstance().time, sb, FieldPosition(SimpleDateFormat.DATE_FIELD))
sb.append(".zip")
return sb.toString()
}

/**
* Filename for single Backup export (.backup)
*/
fun getSingleExportFileName(backupMetaData: BackupDataStorageRepository.BackupData, encrypted: Boolean): String {
return if (backupMetaData.encrypted && encrypted) {
getEncryptedFilename(backupMetaData.filename)
} else {
getUnencryptedFilename(backupMetaData.filename)
}
}


private fun getUnencryptedFilename(filename: String) =
filename.replace("_encrypted.backup", ".backup")

private fun getEncryptedFilename(filename: String): String {
return if (filename.contains("_encrypted.backup", true)) {
filename
} else {
filename.replace(".backup", "_encrypted.backup", true)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,66 @@ import org.secuso.privacyfriendlybackup.data.BackupDataStorageRepository
import org.secuso.privacyfriendlybackup.data.room.model.enums.StorageType
import java.io.IOException
import java.io.InputStreamReader
import java.util.*
import java.util.Date
import java.util.LinkedList
import java.util.zip.ZipInputStream


class DataImporter {

companion object {
suspend fun importData(context: Context, uri: Uri) : Pair<Boolean, BackupDataStorageRepository.BackupData?> {
suspend fun importDataZip(context: Context, uri: Uri): List<Pair<Boolean, Long?>>? {
return withContext(IO) {
var backupData : BackupDataStorageRepository.BackupData? = null
val descriptor = context.contentResolver.openFileDescriptor(uri, "r")
val inStream = ParcelFileDescriptor.AutoCloseInputStream(descriptor)
val zipInStream = ZipInputStream(inStream)
val result: MutableList<Pair<Boolean, Long?>> = LinkedList()

try {
zipInStream.use { zipInputStream ->
generateSequence { zipInputStream.nextEntry }
.filterNot { it.isDirectory }
.forEach { _ ->
var backupData: BackupDataStorageRepository.BackupData? = null
val isr = InputStreamReader(zipInputStream, Charsets.UTF_8)
val jr = JsonReader(isr)
jr.let { reader ->
reader.beginObject()
backupData = readData(reader)
reader.endObject()
}
if (backupData != null) {

if (!backupData!!.encrypted) {
// short validation check if the json is valid
if (!isValidJSON(String(backupData!!.data!!))) {
result.add(false to null)
}
}

result.add(
BackupDataStorageRepository.getInstance(context).storeFile(
context,
backupData!!
)
)
} else {
result.add(false to null)
}
}
}
} catch (e: MalformedJsonException) {
return@withContext null
} catch (e: IOException) {
return@withContext null
}
return@withContext result
}
}

suspend fun importData(context: Context, uri: Uri): Pair<Boolean, BackupDataStorageRepository.BackupData?> {
return withContext(IO) {
var backupData: BackupDataStorageRepository.BackupData? = null

val descriptor = context.contentResolver.openFileDescriptor(uri, "r")
val inStream = ParcelFileDescriptor.AutoCloseInputStream(descriptor)
Expand All @@ -40,13 +91,13 @@ class DataImporter {
return@withContext false to null
}

val result : Pair<Boolean, Long>
val result: Pair<Boolean, Long>

if(backupData != null) {
if (backupData != null) {

if(!backupData!!.encrypted) {
if (!backupData!!.encrypted) {
// short validation check if the json is valid
if(!isValidJSON(String(backupData!!.data!!))) {
if (!isValidJSON(String(backupData!!.data!!))) {
return@withContext false to null
}
}
Expand Down Expand Up @@ -75,17 +126,17 @@ class DataImporter {
}
}

private fun readData(reader: JsonReader) : BackupDataStorageRepository.BackupData? {
var filename : String? = null
var packageName : String? = null
var timestamp : Long? = null
var encrypted : Boolean? = null
var data : String? = null
private fun readData(reader: JsonReader): BackupDataStorageRepository.BackupData? {
var filename: String? = null
var packageName: String? = null
var timestamp: Long? = null
var encrypted: Boolean? = null
var data: String? = null

while (reader.hasNext()) {
val nextName = reader.nextName()

when(nextName) {
when (nextName) {
"filename" -> filename = reader.nextString()
"packageName" -> packageName = reader.nextString()
"timestamp" -> timestamp = reader.nextLong()
Expand All @@ -95,11 +146,12 @@ class DataImporter {
}
}

if(TextUtils.isEmpty(filename)
if (TextUtils.isEmpty(filename)
|| TextUtils.isEmpty(packageName)
|| TextUtils.isEmpty(data)
|| timestamp == null
|| encrypted == null) {
|| encrypted == null
) {
return null
}

Expand Down
Loading

0 comments on commit 2b43f61

Please sign in to comment.