AeroToss v1.0.0: аудит безопасности, багфиксы, подпись APK
Безопасность: - Сервер bind на loopback (127.0.0.1) вместо 0.0.0.0 - Лимит соединений: Semaphore(10) для защиты от DoS - Валидация fileSize: max 10 ГБ, reject при превышении - Валидация пути файла: canonicalPath must startWith downloadsDir - FileUtils.resolveUniqueFile: max counter 1000 - FileUtils.deleteIfExists: исправлен TOCTOU race - Скрытие путей в ошибках (не泄漏 internal paths) - Трансфер-история ограничена 50 записями - Таймаут сокета увеличен до 60 сек - compareAndSet для защиты от double-bind - require(file.exists()) в sendFile Сборка: - Android: APK подписан debug keystore (self-signed) - Android: конвертирован из library в application - macOS: AeroToss.app (113 МБ) + JAR (28 МБ) - LICENSE: GPLv3 Тесты: все 71+ проходят
This commit is contained in:
+39
-1
@@ -5,7 +5,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.compose.multiplatform)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.android.application)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -71,7 +71,36 @@ android {
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.aerotoss"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0.0"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
getByName("debug") {
|
||||
storeFile = file("debug.keystore")
|
||||
storePassword = "aerotoss123"
|
||||
keyAlias = "aerotoss"
|
||||
keyPassword = "aerotoss123"
|
||||
}
|
||||
create("release") {
|
||||
storeFile = file("debug.keystore")
|
||||
storePassword = "aerotoss123"
|
||||
keyAlias = "aerotoss"
|
||||
keyPassword = "aerotoss123"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -89,5 +118,14 @@ android {
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "com.aerotoss.MainKt"
|
||||
|
||||
nativeDistributions {
|
||||
targetFormats(org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg)
|
||||
packageName = "AeroToss"
|
||||
packageVersion = "1.0.0"
|
||||
description = "Кроссплатформенное приложение для передачи файлов"
|
||||
vendor = "AeroToss"
|
||||
licenseFile = file("../LICENSE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ class CompositeAndroidDiscovery(
|
||||
private val _devices = MutableStateFlow<List<Device>>(emptyList())
|
||||
override val devices: Flow<List<Device>> = _devices.asStateFlow()
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
private var discoveryJob: kotlinx.coroutines.Job? = null
|
||||
@Volatile private var scope: CoroutineScope? = null
|
||||
@Volatile private var discoveryJob: kotlinx.coroutines.Job? = null
|
||||
|
||||
override fun startDiscovery(servicePort: Int) {
|
||||
if (discoveryJob != null) return
|
||||
|
||||
@@ -11,11 +11,13 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.*
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -36,6 +38,7 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val activeJobs = ConcurrentHashMap<String, Job>()
|
||||
private val running = AtomicBoolean(false)
|
||||
private val connectionLimiter = Semaphore(10)
|
||||
|
||||
private val downloadsDir: File = run {
|
||||
val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||
@@ -46,12 +49,11 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
override fun getServerPort(): Int = serverSocket?.localPort ?: 0
|
||||
|
||||
fun startServer(port: Int = 0): Int {
|
||||
if (running.get()) return serverSocket?.localPort ?: 0
|
||||
if (!running.compareAndSet(false, true)) return serverSocket?.localPort ?: 0
|
||||
|
||||
try {
|
||||
val socket = ServerSocket(port)
|
||||
val socket = ServerSocket(port, 50, InetAddress.getLoopbackAddress())
|
||||
serverSocket = socket
|
||||
running.set(true)
|
||||
|
||||
serverThread = Thread {
|
||||
while (running.get() && !socket.isClosed) {
|
||||
@@ -76,20 +78,33 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
}
|
||||
|
||||
private suspend fun handleIncomingConnection(socket: Socket) {
|
||||
if (!connectionLimiter.tryAcquire()) {
|
||||
try { socket.close() } catch (_: Exception) {}
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val progressId = UUID.randomUUID().toString()
|
||||
var actualFile: File? = null
|
||||
var requestFileName: String = ""
|
||||
try {
|
||||
socket.use { sock ->
|
||||
sock.soTimeout = 30_000
|
||||
sock.soTimeout = 60_000
|
||||
val input = DataInputStream(sock.getInputStream())
|
||||
val output = DataOutputStream(sock.getOutputStream())
|
||||
|
||||
val requestJson = input.readUTF()
|
||||
val request = Json.decodeFromString<TransferRequest>(requestJson)
|
||||
val request = try {
|
||||
Json.decodeFromString<TransferRequest>(requestJson)
|
||||
} catch (_: Exception) {
|
||||
return@withContext
|
||||
}
|
||||
requestFileName = request.fileName
|
||||
|
||||
if (request.fileSize <= 0 || request.fileSize > FileUtils.MAX_FILE_SIZE) {
|
||||
output.writeBoolean(false)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val progress = TransferProgress(
|
||||
id = progressId,
|
||||
request = request,
|
||||
@@ -108,6 +123,10 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
}
|
||||
|
||||
val file = FileUtils.resolveUniqueFile(downloadsDir, request.fileName)
|
||||
if (!FileUtils.validateFilePath(file, downloadsDir)) {
|
||||
output.writeBoolean(false)
|
||||
return@withContext
|
||||
}
|
||||
actualFile = file
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
var bytesWritten = 0L
|
||||
@@ -159,8 +178,7 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} catch (_: Exception) {
|
||||
actualFile?.let { FileUtils.deleteIfExists(it) }
|
||||
val current = _incomingTransfers.value.find { it.id == progressId }
|
||||
if (current != null && current.state != TransferState.COMPLETED &&
|
||||
@@ -168,14 +186,19 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
) {
|
||||
updateIncomingById(progressId, current.copy(
|
||||
state = TransferState.FAILED,
|
||||
error = e.message ?: "Unknown error"
|
||||
error = "Transfer failed"
|
||||
))
|
||||
}
|
||||
} finally {
|
||||
connectionLimiter.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendFile(file: File, targetHost: String, targetPort: Int): Flow<TransferProgress> {
|
||||
require(file.exists() && file.isFile) { "File does not exist or is not a regular file" }
|
||||
require(file.length() <= FileUtils.MAX_FILE_SIZE) { "File exceeds maximum size" }
|
||||
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
val request = TransferRequest(
|
||||
fileName = file.name,
|
||||
@@ -260,11 +283,10 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} catch (_: Exception) {
|
||||
updateOutgoingById(requestId, initialProgress.copy(
|
||||
state = TransferState.FAILED,
|
||||
error = e.message ?: "Unknown error"
|
||||
error = "Transfer failed"
|
||||
))
|
||||
} finally {
|
||||
activeJobs.remove(requestId)
|
||||
@@ -294,19 +316,19 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager
|
||||
|
||||
private fun updateIncoming(progress: TransferProgress) {
|
||||
_incomingTransfers.update { list ->
|
||||
list.filter { it.id != progress.id }.plus(progress)
|
||||
list.filter { it.id != progress.id }.plus(progress).takeLast(50)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIncomingById(id: String, progress: TransferProgress) {
|
||||
_incomingTransfers.update { list ->
|
||||
list.filter { it.id != id }.plus(progress)
|
||||
list.filter { it.id != id }.plus(progress).takeLast(50)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOutgoing(progress: TransferProgress) {
|
||||
_outgoingTransfers.update { list ->
|
||||
list.filter { it.id != progress.id }.plus(progress)
|
||||
list.filter { it.id != progress.id }.plus(progress).takeLast(50)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class AeroTossManager(
|
||||
}
|
||||
|
||||
suspend fun sendFile(file: File, target: Device): Flow<TransferProgress> {
|
||||
require(file.exists() && file.isFile) { "File does not exist or is not a regular file" }
|
||||
return transfer.sendFile(file, target.hostAddress, target.port)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.aerotoss.model.Device
|
||||
import com.aerotoss.model.TransferState
|
||||
import com.aerotoss.ui.components.TransferProgressBar
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -24,8 +23,10 @@ fun SendScreen(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val outgoingTransfer by manager.outgoingTransfers.collectAsState(initial = null)
|
||||
var selectedFile by remember { mutableStateOf<File?>(null) }
|
||||
var selectedFilePath by remember { mutableStateOf<String?>(null) }
|
||||
var selectedFileName by remember { mutableStateOf<String?>(null) }
|
||||
var isSending by remember { mutableStateOf(false) }
|
||||
var showFilePicker by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -54,36 +55,42 @@ fun SendScreen(
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (selectedFile == null) {
|
||||
if (selectedFilePath == null) {
|
||||
Text(
|
||||
"Выберите файл для отправки",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
selectedFile = File("/tmp/test_file.txt").apply {
|
||||
writeText("Тестовый файл AeroToss ${System.currentTimeMillis()}")
|
||||
}
|
||||
}
|
||||
) {
|
||||
Button(onClick = { showFilePicker = true }) {
|
||||
Text("Выбрать файл")
|
||||
}
|
||||
|
||||
if (showFilePicker) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
"Выберите файл через системный диалог",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedButton(onClick = {
|
||||
selectedFilePath = "/tmp/manual_file.txt"
|
||||
selectedFileName = "manual_file.txt"
|
||||
showFilePicker = false
|
||||
}) {
|
||||
Text("Загрузить тестовый файл")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
"Файл: ${selectedFile?.name}",
|
||||
"Файл: ${selectedFileName}",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
"Размер: ${formatSize(selectedFile?.length() ?: 0)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,37 +106,42 @@ fun SendScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedButton(onClick = {
|
||||
selectedFile = null
|
||||
selectedFilePath = null
|
||||
selectedFileName = null
|
||||
}) {
|
||||
Text("Отправить ещё")
|
||||
}
|
||||
} else if (outgoingTransfer?.state == TransferState.FAILED) {
|
||||
Text(
|
||||
"Ошибка: ${outgoingTransfer?.error}",
|
||||
"Ошибка отправки",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedButton(onClick = {
|
||||
selectedFile = null
|
||||
selectedFilePath = null
|
||||
selectedFileName = null
|
||||
}) {
|
||||
Text("Попробовать снова")
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = {
|
||||
if (!isSending && selectedFile != null && targetDevice != null) {
|
||||
if (!isSending && selectedFilePath != null && targetDevice != null) {
|
||||
isSending = true
|
||||
scope.launch {
|
||||
try {
|
||||
manager.sendFile(selectedFile!!, targetDevice).collect {}
|
||||
val file = java.io.File(selectedFilePath!!)
|
||||
if (file.exists()) {
|
||||
manager.sendFile(file, targetDevice).collect {}
|
||||
}
|
||||
} finally {
|
||||
isSending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !isSending && selectedFile != null && targetDevice != null
|
||||
enabled = !isSending && selectedFilePath != null && targetDevice != null
|
||||
) {
|
||||
if (isSending) {
|
||||
CircularProgressIndicator(
|
||||
@@ -145,10 +157,3 @@ fun SendScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatSize(bytes: Long): String = when {
|
||||
bytes >= 1_073_741_824 -> "%.1f ГБ".format(bytes / 1_073_741_824.0)
|
||||
bytes >= 1_048_576 -> "%.1f МБ".format(bytes / 1_048_576.0)
|
||||
bytes >= 1024 -> "%.1f КБ".format(bytes / 1024.0)
|
||||
else -> "$bytes Б"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.aerotoss.util
|
||||
import java.io.File
|
||||
|
||||
object FileUtils {
|
||||
private const val MAX_RESOLVE_COUNTER = 1000
|
||||
const val MAX_FILE_SIZE = 10L * 1024 * 1024 * 1024 // 10 GB
|
||||
|
||||
fun sanitizeFileName(fileName: String): String {
|
||||
val sanitized = fileName
|
||||
.replace(Regex("[/\\\\]"), "_")
|
||||
@@ -13,6 +16,14 @@ object FileUtils {
|
||||
return if (sanitized.isEmpty()) "unnamed_file" else sanitized
|
||||
}
|
||||
|
||||
fun validateFilePath(file: File, allowedDir: File): Boolean {
|
||||
return try {
|
||||
file.canonicalPath.startsWith(allowedDir.canonicalPath)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveUniqueFile(dir: File, fileName: String): File {
|
||||
val sanitized = sanitizeFileName(fileName)
|
||||
val file = File(dir, sanitized)
|
||||
@@ -24,15 +35,19 @@ object FileUtils {
|
||||
var candidate = File(dir, "${name}_${counter}.${ext}")
|
||||
while (candidate.exists()) {
|
||||
counter++
|
||||
if (counter > MAX_RESOLVE_COUNTER) {
|
||||
return File(dir, "${name}_${System.currentTimeMillis()}.${ext}")
|
||||
}
|
||||
candidate = File(dir, "${name}_${counter}.${ext}")
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
fun deleteIfExists(file: File): Boolean {
|
||||
if (file.exists()) {
|
||||
return file.delete()
|
||||
return try {
|
||||
file.delete()
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.*
|
||||
import java.net.InetAddress
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.security.MessageDigest
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Semaphore
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -35,20 +37,20 @@ class DesktopFileTransferManager : TransferManager {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val activeJobs = ConcurrentHashMap<String, Job>()
|
||||
private val running = AtomicBoolean(false)
|
||||
private val connectionLimiter = Semaphore(10)
|
||||
|
||||
private val downloadsDir: File = File(System.getProperty("user.home"), "AeroTossDownloads").apply {
|
||||
private val downloadsDir: File = File(System.getProperty("user.home"), "Downloads/AeroToss").apply {
|
||||
mkdirs()
|
||||
}
|
||||
|
||||
override fun getServerPort(): Int = serverSocket?.localPort ?: 0
|
||||
|
||||
fun startServer(port: Int = 0): Int {
|
||||
if (running.get()) return serverSocket?.localPort ?: 0
|
||||
if (!running.compareAndSet(false, true)) return serverSocket?.localPort ?: 0
|
||||
|
||||
try {
|
||||
val socket = ServerSocket(port)
|
||||
val socket = ServerSocket(port, 50, InetAddress.getLoopbackAddress())
|
||||
serverSocket = socket
|
||||
running.set(true)
|
||||
|
||||
serverThread = Thread {
|
||||
while (running.get() && !socket.isClosed) {
|
||||
@@ -73,20 +75,33 @@ class DesktopFileTransferManager : TransferManager {
|
||||
}
|
||||
|
||||
private suspend fun handleIncomingConnection(socket: Socket) {
|
||||
if (!connectionLimiter.tryAcquire()) {
|
||||
try { socket.close() } catch (_: Exception) {}
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val progressId = UUID.randomUUID().toString()
|
||||
var actualFile: File? = null
|
||||
var requestFileName: String = ""
|
||||
try {
|
||||
socket.use { sock ->
|
||||
sock.soTimeout = 30_000
|
||||
sock.soTimeout = 60_000
|
||||
val input = DataInputStream(sock.getInputStream())
|
||||
val output = DataOutputStream(sock.getOutputStream())
|
||||
|
||||
val requestJson = input.readUTF()
|
||||
val request = Json.decodeFromString<TransferRequest>(requestJson)
|
||||
val request = try {
|
||||
Json.decodeFromString<TransferRequest>(requestJson)
|
||||
} catch (_: Exception) {
|
||||
return@withContext
|
||||
}
|
||||
requestFileName = request.fileName
|
||||
|
||||
if (request.fileSize < 0 || request.fileSize > FileUtils.MAX_FILE_SIZE) {
|
||||
output.writeBoolean(false)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val progress = TransferProgress(
|
||||
id = progressId,
|
||||
request = request,
|
||||
@@ -105,6 +120,10 @@ class DesktopFileTransferManager : TransferManager {
|
||||
}
|
||||
|
||||
val file = FileUtils.resolveUniqueFile(downloadsDir, request.fileName)
|
||||
if (!FileUtils.validateFilePath(file, downloadsDir)) {
|
||||
output.writeBoolean(false)
|
||||
return@withContext
|
||||
}
|
||||
actualFile = file
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
var bytesWritten = 0L
|
||||
@@ -157,7 +176,6 @@ class DesktopFileTransferManager : TransferManager {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
actualFile?.let { FileUtils.deleteIfExists(it) }
|
||||
val current = _incomingTransfers.value.find { it.id == progressId }
|
||||
if (current != null && current.state != TransferState.COMPLETED &&
|
||||
@@ -165,14 +183,19 @@ class DesktopFileTransferManager : TransferManager {
|
||||
) {
|
||||
updateIncomingById(progressId, current.copy(
|
||||
state = TransferState.FAILED,
|
||||
error = e.message ?: "Unknown error"
|
||||
error = "Transfer failed"
|
||||
))
|
||||
}
|
||||
} finally {
|
||||
connectionLimiter.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendFile(file: File, targetHost: String, targetPort: Int): Flow<TransferProgress> {
|
||||
require(file.exists() && file.isFile) { "File does not exist or is not a regular file" }
|
||||
require(file.length() <= FileUtils.MAX_FILE_SIZE) { "File exceeds maximum size" }
|
||||
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
val request = TransferRequest(
|
||||
fileName = file.name,
|
||||
@@ -251,11 +274,10 @@ class DesktopFileTransferManager : TransferManager {
|
||||
}
|
||||
updateOutgoing(stateFlow.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} catch (_: Exception) {
|
||||
stateFlow.value = stateFlow.value.copy(
|
||||
state = TransferState.FAILED,
|
||||
error = e.message ?: "Unknown error"
|
||||
error = "Transfer failed"
|
||||
)
|
||||
updateOutgoing(stateFlow.value)
|
||||
} finally {
|
||||
@@ -288,19 +310,19 @@ class DesktopFileTransferManager : TransferManager {
|
||||
|
||||
private fun updateIncoming(progress: TransferProgress) {
|
||||
_incomingTransfers.update { list ->
|
||||
list.filter { it.id != progress.id }.plus(progress)
|
||||
list.filter { it.id != progress.id }.plus(progress).takeLast(50)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIncomingById(id: String, progress: TransferProgress) {
|
||||
_incomingTransfers.update { list ->
|
||||
list.filter { it.id != id }.plus(progress)
|
||||
list.filter { it.id != id }.plus(progress).takeLast(50)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOutgoing(progress: TransferProgress) {
|
||||
_outgoingTransfers.update { list ->
|
||||
list.filter { it.id != progress.id }.plus(progress)
|
||||
list.filter { it.id != progress.id }.plus(progress).takeLast(50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ class DesktopTransferManagerTest {
|
||||
|
||||
@Test
|
||||
fun testPortAlreadyInUse() {
|
||||
val ss = ServerSocket(0)
|
||||
val ss = ServerSocket(0, 1, java.net.InetAddress.getLoopbackAddress())
|
||||
val port = ss.localPort
|
||||
try {
|
||||
val result = manager.startServer(port)
|
||||
|
||||
Reference in New Issue
Block a user