DroidFS/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt

514 lines
20 KiB
Kotlin
Raw Normal View History

package sushi.hardcore.droidfs.file_operations
2020-12-29 17:05:02 +01:00
2022-03-06 14:45:52 +01:00
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
2020-12-29 17:05:02 +01:00
import android.content.Intent
import android.net.Uri
2022-03-06 14:45:52 +01:00
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
2021-11-09 16:27:59 +01:00
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
2020-12-29 17:05:02 +01:00
import androidx.documentfile.provider.DocumentFile
2023-05-09 23:13:46 +02:00
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
2023-05-11 21:58:07 +02:00
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
2023-05-09 23:13:46 +02:00
import kotlinx.coroutines.yield
2023-02-06 10:52:51 +01:00
import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R
2023-05-09 23:13:46 +02:00
import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp
2020-12-29 17:05:02 +01:00
import sushi.hardcore.droidfs.explorers.ExplorerElement
2022-06-18 21:13:16 +02:00
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.ObjRef
2020-12-29 17:05:02 +01:00
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
import java.io.FileNotFoundException
class FileOperationService : Service() {
2020-12-29 18:39:43 +01:00
companion object {
const val NOTIFICATION_CHANNEL_ID = "FileOperations"
const val ACTION_CANCEL = "file_operation_cancel"
2020-12-29 18:39:43 +01:00
}
2020-12-29 17:05:02 +01:00
private val binder = LocalBinder()
2023-05-09 23:13:46 +02:00
private lateinit var volumeManger: VolumeManager
private var serviceScope = MainScope()
2021-11-09 16:27:59 +01:00
private lateinit var notificationManager: NotificationManagerCompat
2022-04-20 15:17:33 +02:00
private val tasks = HashMap<Int, Job>()
private var lastNotificationId = 0
2020-12-29 17:05:02 +01:00
inner class LocalBinder : Binder() {
fun getService(): FileOperationService = this@FileOperationService
}
override fun onBind(p0: Intent?): IBinder {
2023-05-09 23:13:46 +02:00
volumeManger = (application as VolumeManagerApp).volumeManager
2020-12-29 17:05:02 +01:00
return binder
}
private fun showNotification(message: Int, total: Int?): FileOperationNotification {
++lastNotificationId
2020-12-29 18:39:43 +01:00
if (!::notificationManager.isInitialized){
2021-11-09 16:27:59 +01:00
notificationManager = NotificationManagerCompat.from(this)
2020-12-29 18:39:43 +01:00
}
2021-11-09 16:27:59 +01:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANNEL_ID,
getString(R.string.file_operations),
NotificationManager.IMPORTANCE_LOW
)
)
}
2021-11-09 16:27:59 +01:00
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
notificationBuilder
.setContentTitle(getString(message))
2023-02-05 14:43:30 +01:00
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
2021-11-09 16:27:59 +01:00
.addAction(NotificationCompat.Action(
R.drawable.icon_close,
getString(R.string.cancel),
PendingIntent.getBroadcast(
this,
0,
Intent(this, NotificationBroadcastReceiver::class.java).apply {
val bundle = Bundle()
bundle.putBinder("binder", LocalBinder())
bundle.putInt("notificationId", lastNotificationId)
putExtra("bundle", bundle)
action = ACTION_CANCEL
},
2023-04-18 14:59:05 +02:00
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
2021-11-09 16:27:59 +01:00
)
))
if (total != null) {
notificationBuilder
.setContentText("0/$total")
.setProgress(total, 0, false)
} else {
notificationBuilder
.setContentText(getString(R.string.discovering_files))
.setProgress(0, 0, true)
}
notificationManager.notify(lastNotificationId, notificationBuilder.build())
return FileOperationNotification(notificationBuilder, lastNotificationId)
2020-12-29 18:39:43 +01:00
}
private fun updateNotificationProgress(notification: FileOperationNotification, progress: Int, total: Int){
notification.notificationBuilder
.setProgress(total, progress, false)
.setContentText("$progress/$total")
notificationManager.notify(notification.notificationId, notification.notificationBuilder.build())
2020-12-29 18:39:43 +01:00
}
private fun cancelNotification(notification: FileOperationNotification){
notificationManager.cancel(notification.notificationId)
2020-12-29 18:39:43 +01:00
}
fun cancelOperation(notificationId: Int){
2022-04-20 15:17:33 +02:00
tasks[notificationId]?.cancel()
}
2023-05-09 23:13:46 +02:00
private fun getEncryptedVolume(volumeId: Int): EncryptedVolume {
return volumeManger.getVolume(volumeId) ?: throw IllegalArgumentException("Invalid volumeId: $volumeId")
}
2022-04-20 15:17:33 +02:00
2023-05-09 23:13:46 +02:00
private suspend fun <T> waitForTask(notification: FileOperationNotification, task: Deferred<T>): TaskResult<out T> {
2022-04-20 15:17:33 +02:00
tasks[notification.notificationId] = task
2023-05-11 21:58:07 +02:00
return coroutineScope {
withContext(serviceScope.coroutineContext) {
try {
TaskResult.completed(task.await())
} catch (e: CancellationException) {
TaskResult.cancelled()
} catch (e: Throwable) {
e.printStackTrace()
TaskResult.error(e.localizedMessage)
} finally {
cancelNotification(notification)
}
2023-05-09 23:13:46 +02:00
}
2023-05-11 21:58:07 +02:00
}
2023-05-09 23:13:46 +02:00
}
private suspend fun <T> volumeTask(
volumeId: Int,
notification: FileOperationNotification,
task: suspend (encryptedVolume: EncryptedVolume) -> T
): TaskResult<out T> {
return waitForTask(
notification,
volumeManger.getCoroutineScope(volumeId).async {
task(getEncryptedVolume(volumeId))
}
)
}
2023-05-09 23:13:46 +02:00
private suspend fun copyFile(
encryptedVolume: EncryptedVolume,
srcPath: String,
dstPath: String,
srcEncryptedVolume: EncryptedVolume = encryptedVolume,
): Boolean {
2020-12-29 17:05:02 +01:00
var success = true
2023-05-09 23:13:46 +02:00
val srcFileHandle = srcEncryptedVolume.openFileReadMode(srcPath)
2022-06-18 21:13:16 +02:00
if (srcFileHandle != -1L) {
val dstFileHandle = encryptedVolume.openFileWriteMode(dstPath)
2022-06-18 21:13:16 +02:00
if (dstFileHandle != -1L) {
2020-12-29 17:05:02 +01:00
var offset: Long = 0
2023-02-06 10:52:51 +01:00
val ioBuffer = ByteArray(Constants.IO_BUFF_SIZE)
var length: Long
2023-05-09 23:13:46 +02:00
while (srcEncryptedVolume.read(srcFileHandle, offset, ioBuffer, 0, ioBuffer.size.toLong()).also { length = it.toLong() } > 0) {
yield()
val written = encryptedVolume.write(dstFileHandle, offset, ioBuffer, 0, length).toLong()
if (written == length) {
2020-12-29 17:05:02 +01:00
offset += written
} else {
success = false
break
}
}
encryptedVolume.truncate(dstPath, offset)
2022-06-18 21:13:16 +02:00
encryptedVolume.closeFile(dstFileHandle)
2020-12-29 17:05:02 +01:00
} else {
success = false
}
2023-05-09 23:13:46 +02:00
srcEncryptedVolume.closeFile(srcFileHandle)
2020-12-29 17:05:02 +01:00
} else {
success = false
}
return success
}
2022-04-20 15:17:33 +02:00
suspend fun copyElements(
2023-05-09 23:13:46 +02:00
volumeId: Int,
items: List<OperationFile>,
srcVolumeId: Int = volumeId,
): TaskResult<out String?> {
2022-04-20 15:17:33 +02:00
val notification = showNotification(R.string.file_op_copy_msg, items.size)
2023-05-09 23:13:46 +02:00
val srcEncryptedVolume = getEncryptedVolume(srcVolumeId)
return volumeTask(volumeId, notification) { encryptedVolume ->
2020-12-29 17:05:02 +01:00
var failedItem: String? = null
2023-05-09 23:13:46 +02:00
for (i in items.indices) {
yield()
if (items[i].isDirectory) {
if (!encryptedVolume.pathExists(items[i].dstPath!!)) {
if (!encryptedVolume.mkdir(items[i].dstPath!!)) {
2022-06-18 21:13:16 +02:00
failedItem = items[i].srcPath
2020-12-29 17:05:02 +01:00
}
}
2023-05-09 23:13:46 +02:00
} else if (!copyFile(encryptedVolume, items[i].srcPath, items[i].dstPath!!, srcEncryptedVolume)) {
failedItem = items[i].srcPath
2020-12-29 17:05:02 +01:00
}
2022-04-20 15:17:33 +02:00
if (failedItem == null) {
updateNotificationProgress(notification, i+1, items.size)
2020-12-29 18:39:43 +01:00
} else {
2020-12-29 17:05:02 +01:00
break
}
}
2022-04-20 15:17:33 +02:00
failedItem
}
2020-12-29 17:05:02 +01:00
}
2023-05-09 23:13:46 +02:00
suspend fun moveElements(volumeId: Int, toMove: List<OperationFile>, toClean: List<String>): TaskResult<out String?> {
2022-04-21 14:06:36 +02:00
val notification = showNotification(R.string.file_op_move_msg, toMove.size)
2023-05-09 23:13:46 +02:00
return volumeTask(volumeId, notification) { encryptedVolume ->
2022-04-21 14:06:36 +02:00
val total = toMove.size+toClean.size
2020-12-29 17:05:02 +01:00
var failedItem: String? = null
2022-04-21 14:06:36 +02:00
for ((i, item) in toMove.withIndex()) {
2022-06-18 21:13:16 +02:00
if (!encryptedVolume.rename(item.srcPath, item.dstPath!!)) {
failedItem = item.srcPath
2022-04-21 14:06:36 +02:00
break
} else {
updateNotificationProgress(notification, i+1, total)
2020-12-29 17:05:02 +01:00
}
2022-04-21 14:06:36 +02:00
}
if (failedItem == null) {
2022-06-18 21:13:16 +02:00
for ((i, folderPath) in toClean.asReversed().withIndex()) {
if (!encryptedVolume.rmdir(folderPath)) {
failedItem = folderPath
2022-04-21 14:06:36 +02:00
break
} else {
updateNotificationProgress(notification, toMove.size+i+1, total)
2020-12-29 17:05:02 +01:00
}
}
}
2022-04-20 15:17:33 +02:00
failedItem
}
2020-12-29 17:05:02 +01:00
}
2022-04-20 15:17:33 +02:00
private suspend fun importFilesFromUris(
2023-05-09 23:13:46 +02:00
encryptedVolume: EncryptedVolume,
2022-04-20 15:17:33 +02:00
dstPaths: List<String>,
uris: List<Uri>,
notification: FileOperationNotification,
): String? {
var failedIndex = -1
for (i in dstPaths.indices) {
2023-05-09 23:13:46 +02:00
yield()
try {
if (!encryptedVolume.importFile(this@FileOperationService, uris[i], dstPaths[i])) {
failedIndex = i
}
2023-05-09 23:13:46 +02:00
} catch (e: FileNotFoundException) {
failedIndex = i
}
if (failedIndex == -1) {
2022-04-20 15:17:33 +02:00
updateNotificationProgress(notification, i+1, dstPaths.size)
} else {
2022-04-20 15:17:33 +02:00
return uris[failedIndex].toString()
}
}
2022-04-20 15:17:33 +02:00
return null
}
2023-05-09 23:13:46 +02:00
suspend fun importFilesFromUris(volumeId: Int, dstPaths: List<String>, uris: List<Uri>): TaskResult<out String?> {
2022-04-20 15:17:33 +02:00
val notification = showNotification(R.string.file_op_import_msg, dstPaths.size)
2023-05-09 23:13:46 +02:00
return volumeTask(volumeId, notification) { encryptedVolume ->
importFilesFromUris(encryptedVolume, dstPaths, uris, notification)
2022-04-20 15:17:33 +02:00
}
}
/**
* Map the content of an unencrypted directory to prepare its import
*
* Contents of dstFiles and srcUris, at the same index, will match each other
*
* @return false if cancelled early, true otherwise.
*/
2023-05-09 23:13:46 +02:00
private suspend fun recursiveMapDirectoryForImport(
rootSrcDir: DocumentFile,
rootDstPath: String,
dstFiles: ArrayList<String>,
srcUris: ArrayList<Uri>,
dstDirs: ArrayList<String>,
2023-05-09 23:13:46 +02:00
) {
dstDirs.add(rootDstPath)
for (child in rootSrcDir.listFiles()) {
2023-05-09 23:13:46 +02:00
yield()
child.name?.let { name ->
val subPath = PathUtils.pathJoin(rootDstPath, name)
if (child.isDirectory) {
2023-05-09 23:13:46 +02:00
recursiveMapDirectoryForImport(child, subPath, dstFiles, srcUris, dstDirs)
} else if (child.isFile) {
srcUris.add(child.uri)
dstFiles.add(subPath)
}
}
}
}
2023-05-09 23:13:46 +02:00
class ImportDirectoryResult(val taskResult: TaskResult<out String?>, val uris: List<Uri>)
2022-04-20 15:17:33 +02:00
suspend fun importDirectory(
2023-05-09 23:13:46 +02:00
volumeId: Int,
2022-04-20 15:17:33 +02:00
rootDstPath: String,
rootSrcDir: DocumentFile,
2023-05-09 23:13:46 +02:00
): ImportDirectoryResult {
2022-04-20 15:17:33 +02:00
val notification = showNotification(R.string.file_op_import_msg, null)
val srcUris = arrayListOf<Uri>()
2023-05-09 23:13:46 +02:00
return ImportDirectoryResult(volumeTask(volumeId, notification) { encryptedVolume ->
2022-04-20 15:17:33 +02:00
var failedItem: String? = null
val dstFiles = arrayListOf<String>()
val dstDirs = arrayListOf<String>()
2023-05-09 23:13:46 +02:00
recursiveMapDirectoryForImport(rootSrcDir, rootDstPath, dstFiles, srcUris, dstDirs)
// create destination folders so the new files can use them
for (dir in dstDirs) {
if (!encryptedVolume.mkdir(dir)) {
failedItem = dir
break
2021-08-27 19:47:35 +02:00
}
2020-12-29 17:05:02 +01:00
}
2022-04-20 15:17:33 +02:00
if (failedItem == null) {
2023-05-09 23:13:46 +02:00
failedItem = importFilesFromUris(encryptedVolume, dstFiles, srcUris, notification)
2020-12-29 17:05:02 +01:00
}
2022-04-20 15:17:33 +02:00
failedItem
2023-05-09 23:13:46 +02:00
}, srcUris)
2020-12-29 17:05:02 +01:00
}
2023-05-09 23:13:46 +02:00
suspend fun wipeUris(uris: List<Uri>, rootFile: DocumentFile? = null): TaskResult<out String?> {
2022-04-20 15:17:33 +02:00
val notification = showNotification(R.string.file_op_wiping_msg, uris.size)
2023-05-09 23:13:46 +02:00
val task = serviceScope.async(Dispatchers.IO) {
2020-12-29 17:05:02 +01:00
var errorMsg: String? = null
2020-12-29 18:39:43 +01:00
for (i in uris.indices) {
2023-05-09 23:13:46 +02:00
yield()
errorMsg = Wiper.wipe(this@FileOperationService, uris[i])
2020-12-29 18:39:43 +01:00
if (errorMsg == null) {
2022-04-20 15:17:33 +02:00
updateNotificationProgress(notification, i+1, uris.size)
2020-12-29 18:39:43 +01:00
} else {
2020-12-29 17:05:02 +01:00
break
}
}
if (errorMsg == null) {
rootFile?.delete()
}
2022-04-20 15:17:33 +02:00
errorMsg
}
2023-05-09 23:13:46 +02:00
return waitForTask(notification, task)
2020-12-29 17:05:02 +01:00
}
2023-05-09 23:13:46 +02:00
private fun exportFileInto(encryptedVolume: EncryptedVolume, srcPath: String, treeDocumentFile: DocumentFile): Boolean {
2020-12-29 17:05:02 +01:00
val outputStream = treeDocumentFile.createFile("*/*", File(srcPath).name)?.uri?.let {
contentResolver.openOutputStream(it)
}
2020-12-29 18:39:43 +01:00
return if (outputStream == null) {
2020-12-29 17:05:02 +01:00
false
2020-12-29 18:39:43 +01:00
} else {
2022-06-18 21:13:16 +02:00
encryptedVolume.exportFile(srcPath, outputStream)
2020-12-29 17:05:02 +01:00
}
}
2023-05-09 23:13:46 +02:00
private suspend fun recursiveExportDirectory(
encryptedVolume: EncryptedVolume,
2022-04-20 15:17:33 +02:00
plain_directory_path: String,
treeDocumentFile: DocumentFile,
): String? {
2020-12-29 17:05:02 +01:00
treeDocumentFile.createDirectory(File(plain_directory_path).name)?.let { childTree ->
2022-06-18 21:13:16 +02:00
val explorerElements = encryptedVolume.readDir(plain_directory_path) ?: return null
2020-12-29 17:05:02 +01:00
for (e in explorerElements) {
2023-05-09 23:13:46 +02:00
yield()
2020-12-29 17:05:02 +01:00
val fullPath = PathUtils.pathJoin(plain_directory_path, e.name)
if (e.isDirectory) {
2023-05-09 23:13:46 +02:00
recursiveExportDirectory(encryptedVolume, fullPath, childTree)?.let { return it }
} else if (!exportFileInto(encryptedVolume, fullPath, childTree)) {
return fullPath
2020-12-29 17:05:02 +01:00
}
}
return null
}
return treeDocumentFile.name
}
2023-05-09 23:13:46 +02:00
suspend fun exportFiles(volumeId: Int, items: List<ExplorerElement>, uri: Uri): TaskResult<out String?> {
2022-04-20 15:17:33 +02:00
val notification = showNotification(R.string.file_op_export_msg, items.size)
2023-05-09 23:13:46 +02:00
return volumeTask(volumeId, notification) { encryptedVolume ->
2022-04-20 15:17:33 +02:00
val treeDocumentFile = DocumentFile.fromTreeUri(this@FileOperationService, uri)!!
var failedItem: String? = null
for (i in items.indices) {
2023-05-09 23:13:46 +02:00
yield()
failedItem = if (items[i].isDirectory) {
recursiveExportDirectory(encryptedVolume, items[i].fullPath, treeDocumentFile)
} else {
if (exportFileInto(encryptedVolume, items[i].fullPath, treeDocumentFile)) {
null
2020-12-29 17:05:02 +01:00
} else {
2023-05-09 23:13:46 +02:00
items[i].fullPath
2020-12-29 17:05:02 +01:00
}
}
2022-04-20 15:17:33 +02:00
if (failedItem == null) {
updateNotificationProgress(notification, i+1, items.size)
} else {
break
}
2020-12-29 17:05:02 +01:00
}
2022-04-20 15:17:33 +02:00
failedItem
}
2020-12-29 17:05:02 +01:00
}
2022-03-06 14:45:52 +01:00
2023-05-09 23:13:46 +02:00
private suspend fun recursiveRemoveDirectory(encryptedVolume: EncryptedVolume, path: String): String? {
encryptedVolume.readDir(path)?.let { elements ->
for (e in elements) {
yield()
val fullPath = PathUtils.pathJoin(path, e.name)
if (e.isDirectory) {
recursiveRemoveDirectory(encryptedVolume, fullPath)?.let { return it }
} else if (!encryptedVolume.deleteFile(fullPath)) {
return fullPath
}
}
}
return if (!encryptedVolume.rmdir(path)) {
path
} else {
null
}
}
suspend fun removeElements(volumeId: Int, items: List<ExplorerElement>): String? {
2022-06-29 15:09:37 +02:00
val notification = showNotification(R.string.file_op_delete_msg, items.size)
2023-05-09 23:13:46 +02:00
return volumeTask(volumeId, notification) { encryptedVolume ->
2022-06-29 15:09:37 +02:00
var failedItem: String? = null
for ((i, element) in items.withIndex()) {
2023-05-09 23:13:46 +02:00
yield()
2022-06-29 15:09:37 +02:00
if (element.isDirectory) {
2023-05-09 23:13:46 +02:00
recursiveRemoveDirectory(encryptedVolume, element.fullPath)?.let { failedItem = it }
} else if (!encryptedVolume.deleteFile(element.fullPath)) {
failedItem = element.fullPath
2022-06-29 15:09:37 +02:00
}
if (failedItem == null) {
updateNotificationProgress(notification, i + 1, items.size)
} else {
break
}
}
failedItem
2023-05-09 23:13:46 +02:00
}.failedItem // treat cancellation as success
2022-06-29 15:09:37 +02:00
}
2023-05-09 23:13:46 +02:00
private suspend fun recursiveCountChildElements(rootDirectory: DocumentFile, scope: CoroutineScope): Int {
yield()
2022-03-06 14:45:52 +01:00
val children = rootDirectory.listFiles()
var count = children.size
for (child in children) {
if (child.isDirectory) {
2022-04-20 15:17:33 +02:00
count += recursiveCountChildElements(child, scope)
2022-03-06 14:45:52 +01:00
}
}
return count
}
2023-05-09 23:13:46 +02:00
private suspend fun recursiveCopyVolume(
2022-03-06 14:45:52 +01:00
src: DocumentFile,
dst: DocumentFile,
dstRootDirectory: ObjRef<DocumentFile?>?,
notification: FileOperationNotification,
total: Int,
2022-04-20 15:17:33 +02:00
scope: CoroutineScope,
2022-03-06 14:45:52 +01:00
progress: ObjRef<Int> = ObjRef(0)
): DocumentFile? {
val dstDir = dst.createDirectory(src.name ?: return src) ?: return src
2022-04-20 15:17:33 +02:00
dstRootDirectory?.let { it.value = dstDir }
2022-03-06 14:45:52 +01:00
for (child in src.listFiles()) {
2023-05-09 23:13:46 +02:00
yield()
2022-03-06 14:45:52 +01:00
if (child.isFile) {
val dstFile = dstDir.createFile("", child.name ?: return child) ?: return child
val outputStream = contentResolver.openOutputStream(dstFile.uri)
val inputStream = contentResolver.openInputStream(child.uri)
if (outputStream == null || inputStream == null) return child
val written = inputStream.copyTo(outputStream)
outputStream.close()
inputStream.close()
if (written != child.length()) return child
} else {
2022-04-20 15:17:33 +02:00
recursiveCopyVolume(child, dstDir, null, notification, total, scope, progress)?.let { return it }
2022-03-06 14:45:52 +01:00
}
progress.value++
updateNotificationProgress(notification, progress.value, total)
}
return null
}
2023-05-09 23:13:46 +02:00
class CopyVolumeResult(val taskResult: TaskResult<out DocumentFile?>, val dstRootDirectory: DocumentFile?)
2022-04-20 15:17:33 +02:00
2023-05-09 23:13:46 +02:00
suspend fun copyVolume(src: DocumentFile, dst: DocumentFile): CopyVolumeResult {
2022-04-20 15:17:33 +02:00
val notification = showNotification(R.string.copy_volume_notification, null)
val dstRootDirectory = ObjRef<DocumentFile?>(null)
2023-05-09 23:13:46 +02:00
val task = serviceScope.async(Dispatchers.IO) {
2022-04-20 15:17:33 +02:00
val total = recursiveCountChildElements(src, this)
2023-05-09 23:13:46 +02:00
updateNotificationProgress(notification, 0, total)
recursiveCopyVolume(src, dst, dstRootDirectory, notification, total, this)
2022-04-20 15:17:33 +02:00
}
2023-05-09 23:13:46 +02:00
return CopyVolumeResult(waitForTask(notification, task), dstRootDirectory.value)
2022-03-06 14:45:52 +01:00
}
2020-12-29 17:05:02 +01:00
}