mirror of
https://github.com/Michatec/Radio.git
synced 2026-01-30 23:17:21 +00:00
Initial commit
This commit is contained in:
179
app/src/main/java/com/michatec/radio/helpers/BackupHelper.kt
Normal file
179
app/src/main/java/com/michatec/radio/helpers/BackupHelper.kt
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user