Files
Radio/app/src/main/java/com/michatec/radio/helpers/BackupHelper.kt
2025-04-27 15:07:05 +02:00

180 lines
7.3 KiB
Kotlin

/*
* BackupHelper.kt
* Implements the BackupHelper object
* A BackupHelper provides helper methods for backing up and restoring the radio station collection
*
* This file is part of
* TRANSISTOR - Radio App for Android
*
* Copyright (c) 2015-22 - Y20K.org
* Licensed under the MIT-License
* http://opensource.org/licenses/MIT
*/
package com.michatec.radio.helpers
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.Build
import android.util.Log
import android.view.View
import com.google.android.material.snackbar.Snackbar
import com.michatec.radio.R
import java.io.*
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
object BackupHelper {
/* Define log tag */
private val TAG: String = BackupHelper::class.java.simpleName
/* Compresses all files in the app's external files directory into destination zip file */
fun backup(view: View, context: Context, destinationUri: Uri) {
val sourceFolder: File? = context.getExternalFilesDir("")
if (sourceFolder != null && sourceFolder.isDirectory) {
Snackbar.make(
view,
"${
FileHelper.getFileName(
context,
destinationUri
)
} ${context.getString(R.string.toastmessage_backed_up)}",
Snackbar.LENGTH_LONG
).show()
val resolver: ContentResolver = context.contentResolver
val outputStream: OutputStream? = resolver.openOutputStream(destinationUri)
ZipOutputStream(BufferedOutputStream(outputStream)).use { zipOutputStream ->
zipOutputStream.use {
zipFolder(it, sourceFolder, "")
}
}
} else {
Log.e(TAG, "Unable to access External Storage.")
}
}
/* Extracts zip backup file and restores files and folders - Credit: https://www.baeldung.com/java-compress-and-uncompress*/
fun restore(view: View, context: Context, sourceUri: Uri) {
Snackbar.make(view, R.string.toastmessage_restored, Snackbar.LENGTH_LONG).show()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// bypass "ZipException" for Android 14 or above applications when zip file names contain ".." or start with "/"
dalvik.system.ZipPathValidator.clearCallback()
}
val resolver: ContentResolver = context.contentResolver
val sourceInputStream: InputStream? = resolver.openInputStream(sourceUri)
val destinationFolder: File? = context.getExternalFilesDir("")
val buffer = ByteArray(1024)
val zipInputStream = ZipInputStream(sourceInputStream)
var zipEntry: ZipEntry? = zipInputStream.nextEntry
// iterate through ZipInputStream until last ZipEntry
while (zipEntry != null) {
try {
val newFile: File = getFile(destinationFolder!!, zipEntry)
when (zipEntry.isDirectory) {
// CASE: Folder
true -> {
// create folder if zip entry is a folder
if (!newFile.isDirectory && !newFile.mkdirs()) {
Log.w(TAG, "Failed to create directory $newFile")
}
}
// CASE: File
false -> {
// create parent directory, if necessary
val parent: File? = newFile.parentFile
if (parent != null && !parent.isDirectory && !parent.mkdirs()) {
Log.w(TAG, "Failed to create directory $parent")
}
// write file content
val fileOutputStream = FileOutputStream(newFile)
var len: Int
while (zipInputStream.read(buffer).also { len = it } > 0) {
fileOutputStream.write(buffer, 0, len)
}
fileOutputStream.close()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to safely create file. $e")
}
// get next entry - zipEntry will be null, when zipInputStream has no more entries left
zipEntry = zipInputStream.nextEntry
}
zipInputStream.closeEntry()
zipInputStream.close()
// notify CollectionViewModel that collection has changed
CollectionHelper.sendCollectionBroadcast(
context,
modificationDate = Calendar.getInstance().time
)
}
/* Compresses folder into ZIP file - Credit: https://stackoverflow.com/a/52216574 */
private fun zipFolder(zipOutputStream: ZipOutputStream, source: File, parentDirPath: String) {
// source.listFiles() will return null, if source is not a directory
if (source.isDirectory) {
val data = ByteArray(2048)
// get all File objects in folder
for (file in source.listFiles()!!) {
// make sure that path does not start with a separator (/)
val path: String = if (parentDirPath.isEmpty()) file.name else parentDirPath + File.separator + file.name
when (file.isDirectory) {
// CASE: Folder
true -> {
// call zipFolder recursively to add files within this folder
zipFolder(zipOutputStream, file, path)
}
// CASE: File
false -> {
FileInputStream(file).use { fileInputStream ->
BufferedInputStream(fileInputStream).use { bufferedInputStream ->
val entry = ZipEntry(path)
entry.time = file.lastModified()
entry.size = file.length()
zipOutputStream.putNextEntry(entry)
while (true) {
val readBytes = bufferedInputStream.read(data)
if (readBytes == -1) {
break
}
zipOutputStream.write(data, 0, readBytes)
}
}
}
}
}
}
}
}
/* Normalize file path - protects against zip slip attack */
@Throws(IOException::class)
private fun getFile(destinationFolder: File, zipEntry: ZipEntry): File {
val destinationFile = File(destinationFolder, zipEntry.name)
val destinationFolderPath = destinationFolder.canonicalPath
val destinationFilePath = destinationFile.canonicalPath
// make sure that zipEntry path is in the destination folder
if (!destinationFilePath.startsWith(destinationFolderPath + File.separator)) {
throw IOException("ZIP entry is not within of the destination folder: " + zipEntry.name)
}
return destinationFile
}
}