From 44954f3bee28e143b01f6daf3aef6107806173a7 Mon Sep 17 00:00:00 2001 From: fatih ergin Date: Tue, 26 Sep 2023 22:43:42 +0300 Subject: [PATCH 1/5] update minimum sdk to 26: apache commons compress requirement --- app/build.gradle.kts | 2 ++ gradle/libs.versions.toml | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3e4621cd..0d6d28a21 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -97,6 +97,8 @@ dependencies { implementation(libs.androidx.documentfile) implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidpdfviewer) + implementation(libs.apache.commons.compress) + implementation(libs.tukaanixz) implementation(libs.roottools) implementation(libs.rootshell) implementation(libs.gestureviews) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 30c4e7b5a..87a20de1d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,12 +13,14 @@ androidpdfviewer = "e6a533125b" rootshell = "1.6" roottools = "df729dcb13" zip4j = "2.11.5" +apachecommonscompress = "1.22" +tukaanixz = "1.9" #Gradle gradlePlugins-agp = "8.1.1" #build app-build-compileSDKVersion = "34" app-build-targetSDK = "34" -app-build-minimumSDK = "23" +app-build-minimumSDK = "26" app-build-javaVersion = "VERSION_17" app-build-kotlinJVMTarget = "17" #versioning @@ -38,6 +40,8 @@ gestureviews = { module = "com.alexvasilkov:gesture-views", version.ref = "gestu rootshell = { module = "com.github.Stericson:RootShell", version.ref = "rootshell" } roottools = { module = "com.github.Stericson:RootTools", version.ref = "roottools" } zip4j = { module = "net.lingala.zip4j:zip4j", version.ref = "zip4j" } +apache-commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "apachecommonscompress" } +tukaanixz = { module = "org.tukaani:xz", version.ref = "tukaanixz" } [plugins] android = { id = "com.android.application", version.ref = "gradlePlugins-agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From 6ab71cf83f33e5ed4417d59dbf0cc7ea5d848fce Mon Sep 17 00:00:00 2001 From: fatih ergin Date: Tue, 26 Sep 2023 22:51:25 +0300 Subject: [PATCH 2/5] implement compressing .7z, .tar.gz, .tar.xz --- .../filemanager/pro/adapters/ItemsAdapter.kt | 91 +------ .../pro/helpers/CompressionFormat.kt | 31 +++ .../pro/helpers/CompressionHelper.kt | 223 ++++++++++++++++++ 3 files changed, 256 insertions(+), 89 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt create mode 100644 app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/adapters/ItemsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/adapters/ItemsAdapter.kt index 0c0932e1e..24a2e6ea1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/adapters/ItemsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/adapters/ItemsAdapter.kt @@ -46,12 +46,8 @@ import com.simplemobiletools.filemanager.pro.models.ListItem import com.stericson.RootTools.RootTools import net.lingala.zip4j.exception.ZipException import net.lingala.zip4j.io.inputstream.ZipInputStream -import net.lingala.zip4j.io.outputstream.ZipOutputStream import net.lingala.zip4j.model.LocalFileHeader -import net.lingala.zip4j.model.ZipParameters -import net.lingala.zip4j.model.enums.EncryptionMethod import java.io.BufferedInputStream -import java.io.Closeable import java.io.File import java.util.* @@ -476,7 +472,7 @@ class ItemsAdapter( return } - CompressAsDialog(activity, firstPath) { destination, password -> + CompressAsDialog(activity, firstPath) { destination, compressionFormat, password -> activity.handleAndroidSAFDialog(firstPath) { granted -> if (!granted) { return@handleAndroidSAFDialog @@ -489,7 +485,7 @@ class ItemsAdapter( activity.toast(R.string.compressing) val paths = getSelectedFileDirItems().map { it.path } ensureBackgroundThread { - if (compressPaths(paths, destination, password)) { + if (CompressionHelper.compress(activity, paths, destination, compressionFormat, password)) { activity.runOnUiThread { activity.toast(R.string.compression_successful) listener?.refreshFragment() @@ -638,89 +634,6 @@ class ItemsAdapter( } } - @SuppressLint("NewApi") - private fun compressPaths(sourcePaths: List, targetPath: String, password: String? = null): Boolean { - val queue = LinkedList() - val fos = activity.getFileOutputStreamSync(targetPath, "application/zip") ?: return false - - val zout = password?.let { ZipOutputStream(fos, password.toCharArray()) } ?: ZipOutputStream(fos) - var res: Closeable = fos - - fun zipEntry(name: String) = ZipParameters().also { - it.fileNameInZip = name - if (password != null) { - it.isEncryptFiles = true - it.encryptionMethod = EncryptionMethod.AES - } - } - - try { - sourcePaths.forEach { currentPath -> - var name: String - var mainFilePath = currentPath - val base = "${mainFilePath.getParentPath()}/" - res = zout - queue.push(mainFilePath) - if (activity.getIsPathDirectory(mainFilePath)) { - name = "${mainFilePath.getFilenameFromPath()}/" - zout.putNextEntry( - ZipParameters().also { - it.fileNameInZip = name - } - ) - } - - while (!queue.isEmpty()) { - mainFilePath = queue.pop() - if (activity.getIsPathDirectory(mainFilePath)) { - if (activity.isRestrictedSAFOnlyRoot(mainFilePath)) { - activity.getAndroidSAFFileItems(mainFilePath, true) { files -> - for (file in files) { - name = file.path.relativizeWith(base) - if (activity.getIsPathDirectory(file.path)) { - queue.push(file.path) - name = "${name.trimEnd('/')}/" - zout.putNextEntry(zipEntry(name)) - } else { - zout.putNextEntry(zipEntry(name)) - activity.getFileInputStreamSync(file.path)!!.copyTo(zout) - zout.closeEntry() - } - } - } - } else { - val mainFile = File(mainFilePath) - for (file in mainFile.listFiles()) { - name = file.path.relativizeWith(base) - if (activity.getIsPathDirectory(file.absolutePath)) { - queue.push(file.absolutePath) - name = "${name.trimEnd('/')}/" - zout.putNextEntry(zipEntry(name)) - } else { - zout.putNextEntry(zipEntry(name)) - activity.getFileInputStreamSync(file.path)!!.copyTo(zout) - zout.closeEntry() - } - } - } - - } else { - name = if (base == currentPath) currentPath.getFilenameFromPath() else mainFilePath.relativizeWith(base) - zout.putNextEntry(zipEntry(name)) - activity.getFileInputStreamSync(mainFilePath)!!.copyTo(zout) - zout.closeEntry() - } - } - } - } catch (exception: Exception) { - activity.showErrorToast(exception) - return false - } finally { - res.close() - } - return true - } - private fun askConfirmDelete() { activity.handleDeletePasswordProtection { val itemsCnt = selectedKeys.size diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt new file mode 100644 index 000000000..b280dd1f3 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt @@ -0,0 +1,31 @@ +package com.simplemobiletools.filemanager.pro.helpers + +import org.apache.commons.compress.compressors.CompressorStreamFactory + +enum class CompressionFormat( + val extension: String, + val mimeType: String, + val compressorStreamFactory: String, + val canReadEncryptedArchive: Boolean, + val canCreateEncryptedArchive: Boolean +) { + ZIP(".zip", "application/zip", "", true, true), + SEVEN_ZIP(".7z", "application/x-7z-compressed", "", true, false), + TAR_GZ(".tar.gz", "application/gzip", CompressorStreamFactory.GZIP, false, false), + + // TAR_SZ(".tar.sz", "application/x-snappy-framed", CompressorStreamFactory.SNAPPY_FRAMED, false, false), // FIXME: ask for enabling it. it's not so common + TAR_XZ(".tar.xz", "application/x-xz", CompressorStreamFactory.XZ, false, false); + + companion object { + fun fromExtension(extension: String): CompressionFormat { + return when (extension.lowercase()) { + ZIP.extension -> ZIP + SEVEN_ZIP.extension -> SEVEN_ZIP + TAR_GZ.extension -> TAR_GZ +// TAR_SZ.extension -> TAR_SZ + TAR_XZ.extension -> TAR_XZ + else -> ZIP + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt new file mode 100644 index 000000000..a5ccbe940 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt @@ -0,0 +1,223 @@ +package com.simplemobiletools.filemanager.pro.helpers + +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.* +import net.lingala.zip4j.io.outputstream.ZipOutputStream +import net.lingala.zip4j.model.ZipParameters +import net.lingala.zip4j.model.enums.EncryptionMethod +import org.apache.commons.compress.archivers.ArchiveEntry +import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile +import org.apache.commons.compress.archivers.tar.TarArchiveEntry +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream +import org.apache.commons.compress.compressors.CompressorStreamFactory +import org.apache.commons.compress.utils.IOUtils +import java.io.Closeable +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.util.LinkedList + + +class CompressionHelper { + + companion object { + fun compress( + activity: BaseSimpleActivity, + sourcePaths: List, + targetPath: String, + compressionFormat: CompressionFormat, + password: String? = null + ): Boolean { + return when (compressionFormat) { + CompressionFormat.ZIP -> compressToZip(activity, sourcePaths, targetPath, password) + CompressionFormat.SEVEN_ZIP -> compressToSevenZip(activity, sourcePaths, targetPath) + CompressionFormat.TAR_GZ, +// CompressionFormat.TAR_SZ, + CompressionFormat.TAR_XZ -> compressToTarVariants(activity, sourcePaths, targetPath, compressionFormat) + } + } + + private fun compressToZip( + activity: BaseSimpleActivity, + sourcePaths: List, + targetPath: String, + password: String? = null + ): Boolean { + val queue = LinkedList() + val fos = activity.getFileOutputStreamSync(targetPath, CompressionFormat.ZIP.mimeType) ?: return false + + val zout = password?.let { ZipOutputStream(fos, password.toCharArray()) } ?: ZipOutputStream(fos) + var res: Closeable = fos + + fun zipEntry(name: String) = ZipParameters().also { + it.fileNameInZip = name + if (password != null) { + it.isEncryptFiles = true + it.encryptionMethod = EncryptionMethod.AES + } + } + + try { + sourcePaths.forEach { currentPath -> + var name: String + var mainFilePath = currentPath + val base = "${mainFilePath.getParentPath()}/" + res = zout + queue.push(mainFilePath) + if (activity.getIsPathDirectory(mainFilePath)) { + name = "${mainFilePath.getFilenameFromPath()}/" + zout.putNextEntry( + ZipParameters().also { + it.fileNameInZip = name + } + ) + } + + while (!queue.isEmpty()) { + mainFilePath = queue.pop() + if (activity.getIsPathDirectory(mainFilePath)) { + if (activity.isRestrictedSAFOnlyRoot(mainFilePath)) { + activity.getAndroidSAFFileItems(mainFilePath, true) { files -> + for (file in files) { + name = file.path.relativizeWith(base) + if (activity.getIsPathDirectory(file.path)) { + queue.push(file.path) + name = "${name.trimEnd('/')}/" + zout.putNextEntry(zipEntry(name)) + } else { + zout.putNextEntry(zipEntry(name)) + activity.getFileInputStreamSync(file.path)!!.copyTo(zout) + zout.closeEntry() + } + } + } + } else { + val mainFile = File(mainFilePath) + for (file in mainFile.listFiles()) { + name = file.path.relativizeWith(base) + if (activity.getIsPathDirectory(file.absolutePath)) { + queue.push(file.absolutePath) + name = "${name.trimEnd('/')}/" + zout.putNextEntry(zipEntry(name)) + } else { + zout.putNextEntry(zipEntry(name)) + activity.getFileInputStreamSync(file.path)!!.copyTo(zout) + zout.closeEntry() + } + } + } + + } else { + name = if (base == currentPath) currentPath.getFilenameFromPath() else mainFilePath.relativizeWith(base) + zout.putNextEntry(zipEntry(name)) + activity.getFileInputStreamSync(mainFilePath)!!.copyTo(zout) + zout.closeEntry() + } + } + } + } catch (exception: Exception) { + activity.showErrorToast(exception) + return false + } finally { + res.close() + } + return true + } + + private fun compressToSevenZip( + activity: BaseSimpleActivity, + sourcePaths: List, + targetPath: String, + ): Boolean { + try { + SevenZOutputFile(File(targetPath)).use { sevenZOutput -> + sourcePaths.forEach { sourcePath -> + Files.walk(File(sourcePath).toPath()).forEach { path -> + val file = path.toFile() + val basePath = "${sourcePath.getParentPath()}/" + + if (!activity.getIsPathDirectory(file.absolutePath)) { + FileInputStream(file).use { _ -> + val entryName = if (basePath == sourcePath) { + sourcePath.getFilenameFromPath() + } else { + path.toString().relativizeWith(basePath) + } + + val sevenZArchiveEntry = sevenZOutput.createArchiveEntry(file, entryName) + sevenZOutput.putArchiveEntry(sevenZArchiveEntry) + sevenZOutput.write(Files.readAllBytes(file.toPath())) + sevenZOutput.closeArchiveEntry() + } + } + } + } + + sevenZOutput.finish() + } + } catch (exception: IOException) { + activity.showErrorToast(exception) + return false + } + return true + } + + private fun compressToTarVariants( + activity: BaseSimpleActivity, + sourcePaths: List, + outFilePath: String, + format: CompressionFormat + ): Boolean { + if (!listOf( + CompressionFormat.TAR_GZ, +// CompressionFormat.TAR_SZ, + CompressionFormat.TAR_XZ + ).contains(format) + ) { + return false + } + + val fos = activity.getFileOutputStreamSync(outFilePath, format.mimeType) + try { + fos.use { fileOutputStream -> + CompressorStreamFactory() + .createCompressorOutputStream(format.compressorStreamFactory, fileOutputStream).use { compressedOut -> + TarArchiveOutputStream(compressedOut).use { archive -> + archive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX) + sourcePaths.forEach { sourcePath -> + val basePath = "${sourcePath.getParentPath()}/" + Files.walk(File(sourcePath).toPath()).forEach { path: Path -> + val file = path.toFile() + + if (!activity.getIsPathDirectory(file.absolutePath)) { + val entryName = if (basePath == sourcePath) { + sourcePath.getFilenameFromPath() + } else { + path.toString().relativizeWith(basePath) + } + + val tarArchiveEntry: ArchiveEntry = TarArchiveEntry(file, entryName) + FileInputStream(file).use { fis -> + archive.putArchiveEntry(tarArchiveEntry) + IOUtils.copy(fis, archive) + archive.closeArchiveEntry() + } + } + } + } + + archive.finish() + } + } + } + } catch (exception: IOException) { + activity.showErrorToast(exception) + return false + } + return true + } + + } +} From aa247e90a0bb4fa714fa3fdf9198b2053255b07a Mon Sep 17 00:00:00 2001 From: fatih ergin Date: Tue, 26 Sep 2023 22:57:40 +0300 Subject: [PATCH 3/5] add new compression formats to compression dialog --- .../pro/dialogs/CompressAsDialog.kt | 29 +++++++++++++++++-- .../main/res/layout/dialog_compress_as.xml | 20 ++++++++++++- app/src/main/res/values/strings.xml | 4 +++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt index ce2c69726..3d78e6311 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt @@ -1,6 +1,7 @@ package com.simplemobiletools.filemanager.pro.dialogs import android.view.View +import android.widget.ArrayAdapter import androidx.appcompat.app.AlertDialog import com.simplemobiletools.commons.activities.BaseSimpleActivity import com.simplemobiletools.commons.dialogs.FilePickerDialog @@ -8,8 +9,13 @@ import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.filemanager.pro.R import com.simplemobiletools.filemanager.pro.databinding.DialogCompressAsBinding import com.simplemobiletools.filemanager.pro.extensions.config +import com.simplemobiletools.filemanager.pro.helpers.CompressionFormat -class CompressAsDialog(val activity: BaseSimpleActivity, val path: String, val callback: (destination: String, password: String?) -> Unit) { +class CompressAsDialog( + val activity: BaseSimpleActivity, + val path: String, + val callback: (destination: String, compressionFormat: CompressionFormat, password: String?) -> Unit +) { private val binding = DialogCompressAsBinding.inflate(activity.layoutInflater) init { @@ -29,6 +35,21 @@ class CompressAsDialog(val activity: BaseSimpleActivity, val path: String, val c } } + compressionFormatValue.apply { + setOnClickListener { + activity.hideKeyboard(filenameValue) + } + val adapter = ArrayAdapter(activity, android.R.layout.simple_dropdown_item_1line, CompressionFormat.values().map { it.extension }) + setAdapter(adapter) + setText(adapter.getItem(0), false) + + setOnItemClickListener { _, _, i, _ -> + val compressionFormat = CompressionFormat.entries[i] + filenameHint.hint = String.format(activity.getString(R.string.filename_without_extension), compressionFormat.extension) + passwordProtect.beVisibleIf(compressionFormat.canCreateEncryptedArchive) + enterPasswordHint.beVisibleIf(compressionFormat.canCreateEncryptedArchive && passwordProtect.isChecked) + } + } passwordProtect.setOnCheckedChangeListener { _, _ -> enterPasswordHint.beVisibleIf(passwordProtect.isChecked) } @@ -53,14 +74,14 @@ class CompressAsDialog(val activity: BaseSimpleActivity, val path: String, val c when { name.isEmpty() -> activity.toast(R.string.empty_name) name.isAValidFilename() -> { - val newPath = "$realPath/$name.zip" + val newPath = "$realPath/$name${getSelectedCompressionFormat().extension}" if (activity.getDoesFilePathExist(newPath)) { activity.toast(R.string.name_taken) return@OnClickListener } alertDialog.dismiss() - callback(newPath, password) + callback(newPath, getSelectedCompressionFormat(), password) } else -> activity.toast(R.string.invalid_name) @@ -69,4 +90,6 @@ class CompressAsDialog(val activity: BaseSimpleActivity, val path: String, val c } } } + + private fun getSelectedCompressionFormat() = CompressionFormat.fromExtension(binding.compressionFormatValue.text.toString()) } diff --git a/app/src/main/res/layout/dialog_compress_as.xml b/app/src/main/res/layout/dialog_compress_as.xml index f5229ff18..48e6cad66 100644 --- a/app/src/main/res/layout/dialog_compress_as.xml +++ b/app/src/main/res/layout/dialog_compress_as.xml @@ -24,10 +24,28 @@ + + + + + + Decompression successful Compressing failed Decompressing failed + + Compression format + + Filename (without %s) Manage favorites From f885aaaca158c60799366a0b89bef029997f2e7c Mon Sep 17 00:00:00 2001 From: fatih ergin Date: Fri, 6 Oct 2023 21:50:03 +0300 Subject: [PATCH 4/5] add proguard rules for apache commons compress --- app/proguard-rules.pro | 1 + 1 file changed, 1 insertion(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f3d41e79b..f284f0287 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -2,3 +2,4 @@ -dontnote org.apache.http.** -keep class com.simplemobiletools.** { *; } -dontwarn com.simplemobiletools.** +-dontwarn com.github.luben.zstd.ZstdOutputStream From edd3f937ff27f0d3bd7140f0763d1de26e5b4812 Mon Sep 17 00:00:00 2001 From: fatih ergin Date: Fri, 6 Oct 2023 21:51:07 +0300 Subject: [PATCH 5/5] add "unknown" compression format for proper cases --- .../pro/dialogs/CompressAsDialog.kt | 5 +- .../pro/helpers/CompressionFormat.kt | 16 +- .../pro/helpers/CompressionHelper.kt | 315 +++++++++--------- 3 files changed, 169 insertions(+), 167 deletions(-) diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt index 3d78e6311..ccc5101c2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/dialogs/CompressAsDialog.kt @@ -39,7 +39,10 @@ class CompressAsDialog( setOnClickListener { activity.hideKeyboard(filenameValue) } - val adapter = ArrayAdapter(activity, android.R.layout.simple_dropdown_item_1line, CompressionFormat.values().map { it.extension }) + val adapter = ArrayAdapter( + activity, + android.R.layout.simple_dropdown_item_1line, + CompressionFormat.entries.take(CompressionFormat.entries.size - 1).map { it.extension }) setAdapter(adapter) setText(adapter.getItem(0), false) diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt index b280dd1f3..a1ab5aab8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionFormat.kt @@ -12,19 +12,23 @@ enum class CompressionFormat( ZIP(".zip", "application/zip", "", true, true), SEVEN_ZIP(".7z", "application/x-7z-compressed", "", true, false), TAR_GZ(".tar.gz", "application/gzip", CompressorStreamFactory.GZIP, false, false), - - // TAR_SZ(".tar.sz", "application/x-snappy-framed", CompressorStreamFactory.SNAPPY_FRAMED, false, false), // FIXME: ask for enabling it. it's not so common - TAR_XZ(".tar.xz", "application/x-xz", CompressorStreamFactory.XZ, false, false); + TAR_XZ(".tar.xz", "application/x-xz", CompressorStreamFactory.XZ, false, false), + UNKNOWN("", "", "", false, false); companion object { fun fromExtension(extension: String): CompressionFormat { - return when (extension.lowercase()) { + val normalizedExtension = if (extension.startsWith(".")) { + extension + } else { + ".$extension" + } + + return when (normalizedExtension.lowercase()) { ZIP.extension -> ZIP SEVEN_ZIP.extension -> SEVEN_ZIP TAR_GZ.extension -> TAR_GZ -// TAR_SZ.extension -> TAR_SZ TAR_XZ.extension -> TAR_XZ - else -> ZIP + else -> UNKNOWN } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt index a5ccbe940..1cebb9376 100644 --- a/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/filemanager/pro/helpers/CompressionHelper.kt @@ -19,86 +19,69 @@ import java.nio.file.Files import java.nio.file.Path import java.util.LinkedList +object CompressionHelper { + fun compress( + activity: BaseSimpleActivity, + sourcePaths: List, + targetPath: String, + compressionFormat: CompressionFormat, + password: String? = null + ): Boolean { + return when (compressionFormat) { + CompressionFormat.ZIP -> compressToZip(activity, sourcePaths, targetPath, password) + CompressionFormat.SEVEN_ZIP -> compressToSevenZip(activity, sourcePaths, targetPath) + CompressionFormat.TAR_GZ, + CompressionFormat.TAR_XZ -> compressToTarVariants(activity, sourcePaths, targetPath, compressionFormat) + + CompressionFormat.UNKNOWN -> false + } + } -class CompressionHelper { - - companion object { - fun compress( - activity: BaseSimpleActivity, - sourcePaths: List, - targetPath: String, - compressionFormat: CompressionFormat, - password: String? = null - ): Boolean { - return when (compressionFormat) { - CompressionFormat.ZIP -> compressToZip(activity, sourcePaths, targetPath, password) - CompressionFormat.SEVEN_ZIP -> compressToSevenZip(activity, sourcePaths, targetPath) - CompressionFormat.TAR_GZ, -// CompressionFormat.TAR_SZ, - CompressionFormat.TAR_XZ -> compressToTarVariants(activity, sourcePaths, targetPath, compressionFormat) + private fun compressToZip( + activity: BaseSimpleActivity, + sourcePaths: List, + targetPath: String, + password: String? = null + ): Boolean { + val queue = LinkedList() + val fos = activity.getFileOutputStreamSync(targetPath, CompressionFormat.ZIP.mimeType) ?: return false + + val zout = password?.let { ZipOutputStream(fos, password.toCharArray()) } ?: ZipOutputStream(fos) + var res: Closeable = fos + + fun zipEntry(name: String) = ZipParameters().also { + it.fileNameInZip = name + if (password != null) { + it.isEncryptFiles = true + it.encryptionMethod = EncryptionMethod.AES } } - private fun compressToZip( - activity: BaseSimpleActivity, - sourcePaths: List, - targetPath: String, - password: String? = null - ): Boolean { - val queue = LinkedList() - val fos = activity.getFileOutputStreamSync(targetPath, CompressionFormat.ZIP.mimeType) ?: return false - - val zout = password?.let { ZipOutputStream(fos, password.toCharArray()) } ?: ZipOutputStream(fos) - var res: Closeable = fos - - fun zipEntry(name: String) = ZipParameters().also { - it.fileNameInZip = name - if (password != null) { - it.isEncryptFiles = true - it.encryptionMethod = EncryptionMethod.AES + try { + sourcePaths.forEach { currentPath -> + var name: String + var mainFilePath = currentPath + val base = "${mainFilePath.getParentPath()}/" + res = zout + queue.push(mainFilePath) + if (activity.getIsPathDirectory(mainFilePath)) { + name = "${mainFilePath.getFilenameFromPath()}/" + zout.putNextEntry( + ZipParameters().also { + it.fileNameInZip = name + } + ) } - } - try { - sourcePaths.forEach { currentPath -> - var name: String - var mainFilePath = currentPath - val base = "${mainFilePath.getParentPath()}/" - res = zout - queue.push(mainFilePath) + while (!queue.isEmpty()) { + mainFilePath = queue.pop() if (activity.getIsPathDirectory(mainFilePath)) { - name = "${mainFilePath.getFilenameFromPath()}/" - zout.putNextEntry( - ZipParameters().also { - it.fileNameInZip = name - } - ) - } - - while (!queue.isEmpty()) { - mainFilePath = queue.pop() - if (activity.getIsPathDirectory(mainFilePath)) { - if (activity.isRestrictedSAFOnlyRoot(mainFilePath)) { - activity.getAndroidSAFFileItems(mainFilePath, true) { files -> - for (file in files) { - name = file.path.relativizeWith(base) - if (activity.getIsPathDirectory(file.path)) { - queue.push(file.path) - name = "${name.trimEnd('/')}/" - zout.putNextEntry(zipEntry(name)) - } else { - zout.putNextEntry(zipEntry(name)) - activity.getFileInputStreamSync(file.path)!!.copyTo(zout) - zout.closeEntry() - } - } - } - } else { - val mainFile = File(mainFilePath) - for (file in mainFile.listFiles()) { + if (activity.isRestrictedSAFOnlyRoot(mainFilePath)) { + activity.getAndroidSAFFileItems(mainFilePath, true) { files -> + for (file in files) { name = file.path.relativizeWith(base) - if (activity.getIsPathDirectory(file.absolutePath)) { - queue.push(file.absolutePath) + if (activity.getIsPathDirectory(file.path)) { + queue.push(file.path) name = "${name.trimEnd('/')}/" zout.putNextEntry(zipEntry(name)) } else { @@ -108,116 +91,128 @@ class CompressionHelper { } } } - } else { - name = if (base == currentPath) currentPath.getFilenameFromPath() else mainFilePath.relativizeWith(base) - zout.putNextEntry(zipEntry(name)) - activity.getFileInputStreamSync(mainFilePath)!!.copyTo(zout) - zout.closeEntry() + val mainFile = File(mainFilePath) + for (file in mainFile.listFiles()) { + name = file.path.relativizeWith(base) + if (activity.getIsPathDirectory(file.absolutePath)) { + queue.push(file.absolutePath) + name = "${name.trimEnd('/')}/" + zout.putNextEntry(zipEntry(name)) + } else { + zout.putNextEntry(zipEntry(name)) + activity.getFileInputStreamSync(file.path)!!.copyTo(zout) + zout.closeEntry() + } + } } + + } else { + name = if (base == currentPath) currentPath.getFilenameFromPath() else mainFilePath.relativizeWith(base) + zout.putNextEntry(zipEntry(name)) + activity.getFileInputStreamSync(mainFilePath)!!.copyTo(zout) + zout.closeEntry() } } - } catch (exception: Exception) { - activity.showErrorToast(exception) - return false - } finally { - res.close() } - return true + } catch (exception: Exception) { + activity.showErrorToast(exception) + return false + } finally { + res.close() } + return true + } - private fun compressToSevenZip( - activity: BaseSimpleActivity, - sourcePaths: List, - targetPath: String, - ): Boolean { - try { - SevenZOutputFile(File(targetPath)).use { sevenZOutput -> - sourcePaths.forEach { sourcePath -> - Files.walk(File(sourcePath).toPath()).forEach { path -> - val file = path.toFile() - val basePath = "${sourcePath.getParentPath()}/" - - if (!activity.getIsPathDirectory(file.absolutePath)) { - FileInputStream(file).use { _ -> - val entryName = if (basePath == sourcePath) { - sourcePath.getFilenameFromPath() - } else { - path.toString().relativizeWith(basePath) - } - - val sevenZArchiveEntry = sevenZOutput.createArchiveEntry(file, entryName) - sevenZOutput.putArchiveEntry(sevenZArchiveEntry) - sevenZOutput.write(Files.readAllBytes(file.toPath())) - sevenZOutput.closeArchiveEntry() + private fun compressToSevenZip( + activity: BaseSimpleActivity, + sourcePaths: List, + targetPath: String, + ): Boolean { + try { + SevenZOutputFile(File(targetPath)).use { sevenZOutput -> + sourcePaths.forEach { sourcePath -> + Files.walk(File(sourcePath).toPath()).forEach { path -> + val file = path.toFile() + val basePath = "${sourcePath.getParentPath()}/" + + if (!activity.getIsPathDirectory(file.absolutePath)) { + FileInputStream(file).use { _ -> + val entryName = if (basePath == sourcePath) { + sourcePath.getFilenameFromPath() + } else { + path.toString().relativizeWith(basePath) } + + val sevenZArchiveEntry = sevenZOutput.createArchiveEntry(file, entryName) + sevenZOutput.putArchiveEntry(sevenZArchiveEntry) + sevenZOutput.write(Files.readAllBytes(file.toPath())) + sevenZOutput.closeArchiveEntry() } } } - - sevenZOutput.finish() } - } catch (exception: IOException) { - activity.showErrorToast(exception) - return false + + sevenZOutput.finish() } - return true + } catch (exception: IOException) { + activity.showErrorToast(exception) + return false } + return true + } - private fun compressToTarVariants( - activity: BaseSimpleActivity, - sourcePaths: List, - outFilePath: String, - format: CompressionFormat - ): Boolean { - if (!listOf( - CompressionFormat.TAR_GZ, -// CompressionFormat.TAR_SZ, - CompressionFormat.TAR_XZ - ).contains(format) - ) { - return false - } + private fun compressToTarVariants( + activity: BaseSimpleActivity, + sourcePaths: List, + outFilePath: String, + format: CompressionFormat + ): Boolean { + if (!listOf( + CompressionFormat.TAR_GZ, + CompressionFormat.TAR_XZ + ).contains(format) + ) { + return false + } - val fos = activity.getFileOutputStreamSync(outFilePath, format.mimeType) - try { - fos.use { fileOutputStream -> - CompressorStreamFactory() - .createCompressorOutputStream(format.compressorStreamFactory, fileOutputStream).use { compressedOut -> - TarArchiveOutputStream(compressedOut).use { archive -> - archive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX) - sourcePaths.forEach { sourcePath -> - val basePath = "${sourcePath.getParentPath()}/" - Files.walk(File(sourcePath).toPath()).forEach { path: Path -> - val file = path.toFile() - - if (!activity.getIsPathDirectory(file.absolutePath)) { - val entryName = if (basePath == sourcePath) { - sourcePath.getFilenameFromPath() - } else { - path.toString().relativizeWith(basePath) - } - - val tarArchiveEntry: ArchiveEntry = TarArchiveEntry(file, entryName) - FileInputStream(file).use { fis -> - archive.putArchiveEntry(tarArchiveEntry) - IOUtils.copy(fis, archive) - archive.closeArchiveEntry() - } + val fos = activity.getFileOutputStreamSync(outFilePath, format.mimeType) + try { + fos.use { fileOutputStream -> + CompressorStreamFactory() + .createCompressorOutputStream(format.compressorStreamFactory, fileOutputStream).use { compressedOut -> + TarArchiveOutputStream(compressedOut).use { archive -> + archive.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX) + sourcePaths.forEach { sourcePath -> + val basePath = "${sourcePath.getParentPath()}/" + Files.walk(File(sourcePath).toPath()).forEach { path: Path -> + val file = path.toFile() + + if (!activity.getIsPathDirectory(file.absolutePath)) { + val entryName = if (basePath == sourcePath) { + sourcePath.getFilenameFromPath() + } else { + path.toString().relativizeWith(basePath) + } + + val tarArchiveEntry: ArchiveEntry = TarArchiveEntry(file, entryName) + FileInputStream(file).use { fis -> + archive.putArchiveEntry(tarArchiveEntry) + IOUtils.copy(fis, archive) + archive.closeArchiveEntry() } } } - - archive.finish() } + + archive.finish() } - } - } catch (exception: IOException) { - activity.showErrorToast(exception) - return false + } } - return true + } catch (exception: IOException) { + activity.showErrorToast(exception) + return false } - + return true } }