diff --git a/app/.gitignore b/app/.gitignore index e247f66..2c8fce9 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,2 @@ /build -/src/main/java/com/donut/mixfile/server/uploaders/hidden \ No newline at end of file +/src/main/java/com/donut/mixfile/server/core/uploaders/hidden \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6513692..5e12f85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,10 +13,10 @@ android { compileSdk = 35 defaultConfig { applicationId = "com.donut.mixfile" - minSdk = 24 + minSdk = 26 targetSdk = 35 versionCode = 87 - versionName = "1.12.4" + versionName = "1.12.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -69,7 +69,10 @@ val brotliVersion = "1.17.0" val operatingSystem: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem() dependencies { - implementation("androidx.compose.material:material-icons-extended:1.7.8") + implementation(libs.androidx.material.icons.extended) + implementation(libs.fastjson2.kotlin) + implementation(libs.kotlin.stdlib) + implementation(libs.kotlin.reflect) implementation(libs.mmkv) implementation(libs.mimetypes) implementation(libs.zoomable) @@ -80,7 +83,7 @@ dependencies { implementation(libs.ktor.server.default.headers) implementation(libs.ktor.server.cors) implementation(libs.ktor.server.netty) - implementation(libs.ktor.serialization.gson) +// implementation(libs.ktor.serialization.gson) implementation(libs.coil) implementation(libs.coil.compose) implementation(libs.coil.gif) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index d16cbef..5d45665 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -25,6 +25,7 @@ -keep class com.donut.mixfile.** { *; } -keep class com.tencent.mmkv.** {*;} -keep class io.netty.** {*;} +-keep class com.alibaba.** {*;} -dontwarn xyz.doikki.videoplayer.** -dontwarn java.lang.management.ManagementFactory -dontwarn java.lang.management.RuntimeMXBean diff --git a/app/src/main/java/com/donut/mixfile/App.kt b/app/src/main/java/com/donut/mixfile/App.kt index 23244e8..9ff9e5b 100644 --- a/app/src/main/java/com/donut/mixfile/App.kt +++ b/app/src/main/java/com/donut/mixfile/App.kt @@ -14,6 +14,7 @@ import coil.decode.SvgDecoder import coil.decode.VideoFrameDecoder import com.donut.mixfile.server.FileService import com.donut.mixfile.server.UPLOADERS +import com.donut.mixfile.server.core.utils.registerJson import com.donut.mixfile.util.objects.MixActivity import com.donut.mixfile.util.showError import com.donut.mixfile.util.showErrorDialog @@ -41,6 +42,7 @@ class App : Application(), ImageLoaderFactory { override fun onCreate() { super.onCreate() + registerJson() Thread.setDefaultUncaughtExceptionHandler { t, e -> showError(e) if (Looper.myLooper() == null) { diff --git a/app/src/main/java/com/donut/mixfile/activity/DialogActivity.kt b/app/src/main/java/com/donut/mixfile/activity/DialogActivity.kt index d52091e..ccafa66 100644 --- a/app/src/main/java/com/donut/mixfile/activity/DialogActivity.kt +++ b/app/src/main/java/com/donut/mixfile/activity/DialogActivity.kt @@ -24,10 +24,10 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.donut.mixfile.currentActivity +import com.donut.mixfile.server.core.utils.resolveMixShareInfo import com.donut.mixfile.ui.component.common.CommonColumn import com.donut.mixfile.ui.theme.MainTheme import com.donut.mixfile.ui.theme.colorScheme -import com.donut.mixfile.util.file.resolveMixShareInfo import com.donut.mixfile.util.file.showFileInfoDialog import com.donut.mixfile.util.objects.MixActivity diff --git a/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt b/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt index 8c67749..99ce556 100644 --- a/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt +++ b/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt @@ -160,9 +160,10 @@ fun VideoPlayerScreen( }, modifier = Modifier.fillMaxSize() ) + val currentMediaTitle = videoUris[currentMediaItem].fragment ?: "" TopControl( - title = "${currentMediaItem + 1} - ${videoUris[currentMediaItem].fragment ?: ""}", + title = if (videoUris.size > 1) "${currentMediaItem + 1} - ${currentMediaTitle}" else currentMediaTitle, visible = controlsVisible, modifier = Modifier.align(Alignment.TopCenter) ) diff --git a/app/src/main/java/com/donut/mixfile/activity/video/player/TopControl.kt b/app/src/main/java/com/donut/mixfile/activity/video/player/TopControl.kt index 82e0f23..4acd622 100644 --- a/app/src/main/java/com/donut/mixfile/activity/video/player/TopControl.kt +++ b/app/src/main/java/com/donut/mixfile/activity/video/player/TopControl.kt @@ -8,17 +8,25 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.donut.mixfile.currentActivity import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.formatTime import java.util.Date @@ -36,18 +44,45 @@ fun TopControl(title: String, visible: Boolean, modifier: Modifier) { visible = visible, modifier = modifier ) { - FlowRow( - verticalArrangement = Arrangement.Center, + Row( + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .fillMaxWidth() .background(Color.Black.copy(alpha = 0.1f)) .padding(10.dp, 15.dp), ) { - Text( - text = title, - color = Color.White, - ) + Row( + modifier = Modifier.weight(0.8f), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Row( + modifier = Modifier.width(40.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + modifier = Modifier.size(20.dp), + onClick = { + currentActivity.finish() + }, + ) { + Icon( + modifier = Modifier.size(100.dp), + imageVector = Icons.Default.ArrowBackIosNew, + contentDescription = "Exit", + tint = Color.White + ) + } + } + Text( + text = title, + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } val batteryManager = LocalContext.current.getSystemService(BATTERY_SERVICE) as BatteryManager diff --git a/app/src/main/java/com/donut/mixfile/server/Client.kt b/app/src/main/java/com/donut/mixfile/server/Client.kt deleted file mode 100644 index 2673933..0000000 --- a/app/src/main/java/com/donut/mixfile/server/Client.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.donut.mixfile.server - -import com.donut.mixfile.util.cachedMutableOf -import com.google.gson.GsonBuilder -import io.ktor.client.HttpClient -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.DefaultRequest -import io.ktor.client.plugins.HttpRequestRetry -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.http.ContentType -import io.ktor.http.content.OutgoingContent -import io.ktor.http.userAgent -import io.ktor.serialization.gson.GsonConverter -import io.ktor.serialization.gson.gson -import io.ktor.utils.io.ByteWriteChannel -import io.ktor.utils.io.jvm.javaio.toOutputStream -import okhttp3.Dispatcher -import java.io.InputStream - -var UPLOAD_RETRY_TIMES by cachedMutableOf(3, "UPLOAD_RETRY_TIMES") - -val uploadClient = HttpClient(OkHttp) { - engine { - config { - dispatcher(Dispatcher().apply { - maxRequestsPerHost = Int.MAX_VALUE - maxRequests = Int.MAX_VALUE - }) - } - } - install(ContentNegotiation) { - gson() - register(ContentType.Any, GsonConverter(GsonBuilder().create())) - } - install(HttpRequestRetry) { - maxRetries = UPLOAD_RETRY_TIMES.toInt() - retryOnException(retryOnTimeout = true) - retryOnServerErrors() - delayMillis { retry -> - retry * 100L - } - } - install(HttpTimeout) { - requestTimeoutMillis = 1000 * 120 - } - install(DefaultRequest) { - userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36") - } -} - -val localClient = HttpClient(OkHttp).config { - install(HttpTimeout) { - requestTimeoutMillis = 1000 * 60 * 60 * 24 * 30L - socketTimeoutMillis = 1000 * 60 * 60 - connectTimeoutMillis = 1000 * 60 * 60 - } -} - -class StreamContent(private val stream: InputStream, val length: Long = 0) : - OutgoingContent.WriteChannelContent() { - override suspend fun writeTo(channel: ByteWriteChannel) { - stream.copyTo(channel.toOutputStream()) - } - - override val contentLength: Long - get() = length - -} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/uploaders/CustomUploader.kt b/app/src/main/java/com/donut/mixfile/server/CustomUploader.kt similarity index 60% rename from app/src/main/java/com/donut/mixfile/server/uploaders/CustomUploader.kt rename to app/src/main/java/com/donut/mixfile/server/CustomUploader.kt index cc13571..ea07aa6 100644 --- a/app/src/main/java/com/donut/mixfile/server/uploaders/CustomUploader.kt +++ b/app/src/main/java/com/donut/mixfile/server/CustomUploader.kt @@ -1,8 +1,11 @@ -package com.donut.mixfile.server.uploaders +package com.donut.mixfile.server -import com.donut.mixfile.server.Uploader -import com.donut.mixfile.server.uploadClient +import com.donut.mixfile.server.core.Uploader +import com.donut.mixfile.server.core.uploaders.A3Uploader +import com.donut.mixfile.server.core.uploaders.hidden.A1Uploader +import com.donut.mixfile.server.core.uploaders.hidden.A2Uploader import com.donut.mixfile.util.cachedMutableOf +import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.client.request.put import io.ktor.client.request.setBody @@ -17,10 +20,16 @@ var CUSTOM_UPLOAD_URL by cachedMutableOf("", "CUSTOM_UPLOAD_URL") var CUSTOM_REFERER by cachedMutableOf("", "CUSTOM_REFERER") +val UPLOADERS = listOf(A1Uploader, A2Uploader, A3Uploader, CustomUploader) + +var currentUploader by cachedMutableOf(A1Uploader.name, "current_uploader") + +fun getCurrentUploader() = UPLOADERS.firstOrNull { it.name == currentUploader } ?: A1Uploader + object CustomUploader : Uploader("自定义") { - override suspend fun genHead(): ByteArray { - return uploadClient.get { + override suspend fun genHead(client: HttpClient): ByteArray { + return client.get { url(CUSTOM_UPLOAD_URL) }.also { val referer = it.headers["referer"] @@ -35,8 +44,8 @@ object CustomUploader : Uploader("自定义") { override val referer: String get() = CUSTOM_REFERER - override suspend fun doUpload(fileData: ByteArray): String { - val response = uploadClient.put { + override suspend fun doUpload(fileData: ByteArray, client: HttpClient): String { + val response = client.put { url(CUSTOM_UPLOAD_URL) setBody(fileData) } diff --git a/app/src/main/java/com/donut/mixfile/server/FileService.kt b/app/src/main/java/com/donut/mixfile/server/FileService.kt index 7e6e584..c6e524c 100644 --- a/app/src/main/java/com/donut/mixfile/server/FileService.kt +++ b/app/src/main/java/com/donut/mixfile/server/FileService.kt @@ -8,10 +8,98 @@ import android.app.Service import android.content.Intent import android.os.Build import android.os.IBinder +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.core.app.NotificationCompat +import com.alibaba.fastjson2.toJSONString import com.donut.mixfile.MainActivity import com.donut.mixfile.R +import com.donut.mixfile.app +import com.donut.mixfile.appScope +import com.donut.mixfile.server.core.MixFileServer +import com.donut.mixfile.server.core.Uploader +import com.donut.mixfile.server.core.utils.MixUploadTask +import com.donut.mixfile.server.image.createBlankBitmap +import com.donut.mixfile.server.image.toGif +import com.donut.mixfile.ui.routes.home.UploadTask import com.donut.mixfile.ui.routes.home.serverAddress +import com.donut.mixfile.ui.routes.increaseDownloadData +import com.donut.mixfile.ui.routes.increaseUploadData +import com.donut.mixfile.util.cachedMutableOf +import com.donut.mixfile.util.file.favorites +import com.donut.mixfile.util.showError +import io.ktor.server.application.ApplicationCall +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.io.FileNotFoundException +import java.io.InputStream + +var DOWNLOAD_TASK_COUNT by cachedMutableOf(5, "download_task_count") +var UPLOAD_TASK_COUNT by cachedMutableOf(10, "upload_task_count") +var enableAccessKey by cachedMutableOf(false, "enable_mix_file_access_key") +var UPLOAD_RETRY_TIMES by cachedMutableOf(10, "UPLOAD_RETRY_TIMES") + + +val mixFileServer = object : MixFileServer( + accessKeyTip = "网页端已被禁止访问,请到APP设置中开启", + enableAccessKey = enableAccessKey, +) { + + override fun onDownloadData(data: ByteArray) { + increaseDownloadData(data.size.toLong()) + } + + override fun onUploadData(data: ByteArray) { + increaseUploadData(data.size.toLong()) + } + + + override val downloadTaskCount: Int + get() = DOWNLOAD_TASK_COUNT.toInt() + override val uploadTaskCount: Int + get() = UPLOAD_TASK_COUNT.toInt() + override val requestRetryCount: Int + get() = UPLOAD_RETRY_TIMES.toInt() + + + override fun onError(error: Throwable) { + showError(error) + } + + override fun getUploader(): Uploader { + return getCurrentUploader() + } + + override fun getStaticFile(path: String): InputStream? { + try { + val fileStream = app.assets.open(path) + return fileStream + } catch (e: FileNotFoundException) { + return null + } + } + + override fun genDefaultImage(): ByteArray { + return createBlankBitmap().toGif() + } + + override fun getFileHistory(): String { + return favorites.takeLast(1000).toJSONString() + } + + override fun getUploadTask( + call: ApplicationCall, + name: String, + size: Long, + add: Boolean + ): MixUploadTask { + return UploadTask(call, name, size, add) + } + +} +var serverStarted by mutableStateOf(false) class FileService : Service() { @@ -28,7 +116,11 @@ class FileService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - startServer() + appScope.launch(Dispatchers.IO) { + mixFileServer.start() + delay(1000) + serverStarted = true + } return START_STICKY } diff --git a/app/src/main/java/com/donut/mixfile/server/Utils.kt b/app/src/main/java/com/donut/mixfile/server/Utils.kt new file mode 100644 index 0000000..aa70a81 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/Utils.kt @@ -0,0 +1,13 @@ +package com.donut.mixfile.server + +import com.donut.mixfile.server.core.utils.bean.MixShareInfo +import com.donut.mixfile.ui.routes.home.getLocalServerAddress +import com.donut.mixfile.ui.routes.home.serverAddress +import com.donut.mixfile.util.getFileAccessUrl + +val MixShareInfo.downloadUrl: String + get() = getFileAccessUrl(getLocalServerAddress(), this.toString(), fileName) + + +val MixShareInfo.lanUrl: String + get() = getFileAccessUrl(serverAddress, this.toString(), fileName) \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/core/Client.kt b/app/src/main/java/com/donut/mixfile/server/core/Client.kt new file mode 100644 index 0000000..6b8cde7 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/Client.kt @@ -0,0 +1,63 @@ +package com.donut.mixfile.server.core + +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.DefaultRequest +import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.http.content.OutgoingContent +import io.ktor.http.userAgent +import io.ktor.utils.io.ByteWriteChannel +import io.ktor.utils.io.jvm.javaio.toOutputStream +import okhttp3.Dispatcher +import java.io.InputStream + + +val MixFileServer.httpClient + get() = HttpClient(OkHttp) { + engine { + config { + dispatcher(Dispatcher().apply { + maxRequestsPerHost = Int.MAX_VALUE + maxRequests = Int.MAX_VALUE + }) + } + } + install(ContentNegotiation) { + + } + install(HttpRequestRetry) { + maxRetries = uploadTaskCount + retryOnException(retryOnTimeout = true) + retryOnServerErrors() + delayMillis { retry -> + retry * 100L + } + } + install(HttpTimeout) { + requestTimeoutMillis = 1000 * 120 + } + install(DefaultRequest) { + userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36") + } + } + +val localClient = HttpClient(OkHttp).config { + install(HttpTimeout) { + requestTimeoutMillis = 1000 * 60 * 60 * 24 * 30L + socketTimeoutMillis = 1000 * 60 * 60 + connectTimeoutMillis = 1000 * 60 * 60 + } +} + +class StreamContent(private val stream: InputStream, val length: Long = 0) : + OutgoingContent.WriteChannelContent() { + override suspend fun writeTo(channel: ByteWriteChannel) { + stream.copyTo(channel.toOutputStream()) + } + + override val contentLength: Long + get() = length + +} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/FileServer.kt b/app/src/main/java/com/donut/mixfile/server/core/MixFileServer.kt similarity index 56% rename from app/src/main/java/com/donut/mixfile/server/FileServer.kt rename to app/src/main/java/com/donut/mixfile/server/core/MixFileServer.kt index 87bc5d2..28aa2a3 100644 --- a/app/src/main/java/com/donut/mixfile/server/FileServer.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/MixFileServer.kt @@ -1,19 +1,16 @@ -package com.donut.mixfile.server +package com.donut.mixfile.server.core -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.donut.mixfile.appScope -import com.donut.mixfile.server.routes.getRoutes -import com.donut.mixfile.util.cachedMutableOf -import com.donut.mixfile.util.genRandomString -import com.donut.mixfile.util.ignoreError -import com.donut.mixfile.util.showError + +import com.donut.mixfile.server.core.routes.getRoutes +import com.donut.mixfile.server.core.utils.MixUploadTask +import com.donut.mixfile.server.core.utils.bean.MixShareInfo +import com.donut.mixfile.server.core.utils.genRandomString +import com.donut.mixfile.server.core.utils.ignoreError +import com.donut.mixfile.server.core.utils.registerJson import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode -import io.ktor.serialization.gson.gson +import io.ktor.server.application.ApplicationCall import io.ktor.server.application.ApplicationCallPipeline import io.ktor.server.application.call import io.ktor.server.application.install @@ -24,30 +21,75 @@ import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.plugins.statuspages.StatusPages import io.ktor.server.response.respondText import io.ktor.server.routing.routing -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.io.IOException +import java.io.InputStream import java.net.ServerSocket -var serverPort by mutableIntStateOf(4719) -val accessKey = genRandomString(32) -var enableAccessKey by cachedMutableOf(false, "enable_mix_file_access_key") -var serverStarted by mutableStateOf(false) -fun startServer() { - appScope.launch(Dispatchers.IO) { +abstract class MixFileServer( + var serverPort: Int = 4719, + var accessKey: String = genRandomString(32), + var enableAccessKey: Boolean = false, + var accessKeyTip: String = "Require Access Key", +) { + + init { + registerJson() + } + + abstract val downloadTaskCount: Int + abstract val uploadTaskCount: Int + abstract val requestRetryCount: Int + + abstract fun onError(error: Throwable) + + abstract fun getUploader(): Uploader + + abstract fun getStaticFile(path: String): InputStream? + + abstract fun genDefaultImage(): ByteArray + + abstract fun getFileHistory(): String + + open fun getUploadTask( + call: ApplicationCall, + name: String, + size: Long, + add: Boolean + ): MixUploadTask = object : MixUploadTask { + override var error: Throwable? = null + + override var stopped: Boolean = false + + override suspend fun complete(shareInfo: MixShareInfo) { + } + + override var onStop: () -> Unit = {} + override suspend fun updateProgress(size: Long, total: Long) { + } + + } + + open fun onDownloadData(data: ByteArray) { + + } + + open fun onUploadData(data: ByteArray) { + + } + + + fun start() { serverPort = findAvailablePort(serverPort) ?: serverPort embeddedServer(Netty, port = serverPort, watchPaths = emptyList()) { intercept(ApplicationCallPipeline.Call) { val key = call.request.queryParameters["accessKey"] if (enableAccessKey && !key.contentEquals(accessKey)) { - call.respondText("网页端已被禁止访问,请到APP设置中开启") + call.respondText(accessKeyTip) finish() } } install(ContentNegotiation) { - gson() + } install(CORS) { allowOrigins { true } @@ -66,19 +108,11 @@ fun startServer() { "发生错误: ${cause.message} ${cause.stackTraceToString()}", status = HttpStatusCode.InternalServerError ) - if (cause is IOException) { - return@exception - } - when (cause.message) { - "服务器达到并发限制" -> Unit - else -> showError(cause) - } + onError(cause) } } routing(getRoutes()) - }.start(wait = false) - delay(1000) - serverStarted = true + }.start(wait = true) } } diff --git a/app/src/main/java/com/donut/mixfile/server/Uploader.kt b/app/src/main/java/com/donut/mixfile/server/core/Uploader.kt similarity index 53% rename from app/src/main/java/com/donut/mixfile/server/Uploader.kt rename to app/src/main/java/com/donut/mixfile/server/core/Uploader.kt index 0be4d4d..e3be445 100644 --- a/app/src/main/java/com/donut/mixfile/server/Uploader.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/Uploader.kt @@ -1,29 +1,15 @@ -package com.donut.mixfile.server +package com.donut.mixfile.server.core -import com.donut.mixfile.server.uploaders.A3Uploader -import com.donut.mixfile.server.uploaders.CustomUploader -import com.donut.mixfile.server.uploaders.hidden.A1Uploader -import com.donut.mixfile.server.uploaders.hidden.A2Uploader -import com.donut.mixfile.server.utils.bean.hashMixSHA256 -import com.donut.mixfile.server.utils.createBlankBitmap -import com.donut.mixfile.server.utils.toGif -import com.donut.mixfile.ui.routes.increaseUploadData -import com.donut.mixfile.util.cachedMutableOf -import com.donut.mixfile.util.encryptAES - -val UPLOADERS = listOf(A1Uploader, A2Uploader, A3Uploader, CustomUploader) - -var currentUploader by cachedMutableOf(A1Uploader.name, "current_uploader") - - -fun getCurrentUploader() = UPLOADERS.firstOrNull { it.name == currentUploader } ?: A1Uploader +import com.donut.mixfile.server.core.aes.encryptAES +import com.donut.mixfile.server.core.utils.bean.hashMixSHA256 +import io.ktor.client.HttpClient abstract class Uploader(val name: String) { open val referer = "" open val chunkSize = 1024L * 1024L - abstract suspend fun doUpload(fileData: ByteArray): String + abstract suspend fun doUpload(fileData: ByteArray, client: HttpClient): String companion object { val urlTransforms = mutableMapOf String>() @@ -53,16 +39,24 @@ abstract class Uploader(val name: String) { } } - suspend fun upload(head: ByteArray, fileData: ByteArray, key: ByteArray): String { + suspend fun upload( + head: ByteArray, + fileData: ByteArray, + key: ByteArray, + mixFileServer: MixFileServer + ): String { val encryptedData = encryptBytes(head, fileData, key) try { - return doUpload(encryptedData) + "#${fileData.hashMixSHA256()}" + return doUpload( + encryptedData, + mixFileServer.httpClient + ) + "#${fileData.hashMixSHA256()}" } finally { - increaseUploadData(encryptedData.size.toLong()) + mixFileServer.onUploadData(encryptedData) } } - open suspend fun genHead() = createBlankBitmap().toGif() + open suspend fun genHead(client: HttpClient): ByteArray? = null private fun encryptBytes(head: ByteArray, fileData: ByteArray, key: ByteArray): ByteArray { return head + (encryptAES(fileData, key)) } diff --git a/app/src/main/java/com/donut/mixfile/util/AES.kt b/app/src/main/java/com/donut/mixfile/server/core/aes/AES.kt similarity index 97% rename from app/src/main/java/com/donut/mixfile/util/AES.kt rename to app/src/main/java/com/donut/mixfile/server/core/aes/AES.kt index dbb313a..a9feeb8 100644 --- a/app/src/main/java/com/donut/mixfile/util/AES.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/aes/AES.kt @@ -1,4 +1,4 @@ -package com.donut.mixfile.util +package com.donut.mixfile.server.core.aes import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.readRemaining diff --git a/app/src/main/java/com/donut/mixfile/server/routes/DownloadRoute.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/DownloadRoute.kt similarity index 82% rename from app/src/main/java/com/donut/mixfile/server/routes/DownloadRoute.kt rename to app/src/main/java/com/donut/mixfile/server/core/routes/DownloadRoute.kt index c69164a..1bf6420 100644 --- a/app/src/main/java/com/donut/mixfile/server/routes/DownloadRoute.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/DownloadRoute.kt @@ -1,11 +1,11 @@ -package com.donut.mixfile.server.routes +package com.donut.mixfile.server.core.routes -import com.donut.mixfile.server.utils.bean.MixShareInfo -import com.donut.mixfile.ui.routes.increaseDownloadData -import com.donut.mixfile.util.cachedMutableOf -import com.donut.mixfile.util.encodeURL -import com.donut.mixfile.util.file.resolveMixShareInfo -import com.donut.mixfile.util.objects.SortedTask +import com.donut.mixfile.server.core.MixFileServer +import com.donut.mixfile.server.core.httpClient +import com.donut.mixfile.server.core.utils.SortedTask +import com.donut.mixfile.server.core.utils.bean.MixShareInfo +import com.donut.mixfile.server.core.utils.encodeURL +import com.donut.mixfile.server.core.utils.resolveMixShareInfo import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.withCharset @@ -24,10 +24,9 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlin.io.encoding.ExperimentalEncodingApi -var DOWNLOAD_TASK_COUNT by cachedMutableOf(5, "download_task_count") @OptIn(ExperimentalEncodingApi::class) -fun getDownloadRoute(): RoutingHandler { +fun MixFileServer.getDownloadRoute(): RoutingHandler { return route@{ val shareInfoData = call.request.queryParameters["s"] val referer = call.request.queryParameters["referer"] @@ -40,7 +39,7 @@ fun getDownloadRoute(): RoutingHandler { call.respondText("解析文件失败", status = HttpStatusCode.InternalServerError) return@route } - val mixFile = shareInfo.fetchMixFile() + val mixFile = shareInfo.fetchMixFile(httpClient) if (mixFile == null) { call.respondText( "解析文件索引失败", @@ -66,11 +65,11 @@ fun getDownloadRoute(): RoutingHandler { } contentLength = mixFile.fileSize - range.first } - responseFileStream(call, fileList, contentLength, shareInfo, referer) + responseDownloadFileStream(call, fileList, contentLength, shareInfo, referer) } } -private suspend fun responseFileStream( +private suspend fun MixFileServer.responseDownloadFileStream( call: ApplicationCall, fileDataList: List>, contentLength: Long, @@ -83,7 +82,7 @@ private suspend fun responseFileStream( contentType = ContentType.parse(shareInfo.contentType()).withCharset(Charsets.UTF_8), contentLength = contentLength ) { - val sortedTask = SortedTask(DOWNLOAD_TASK_COUNT.toInt()) + val sortedTask = SortedTask(downloadTaskCount) val tasks = mutableListOf>() while (!isClosedForWrite && fileList.isNotEmpty()) { val currentMeta = fileList.removeAt(0) @@ -91,7 +90,8 @@ private suspend fun responseFileStream( sortedTask.prepareTask(taskOrder) tasks.add(async { val url = currentMeta.first - val dataBytes = shareInfo.fetchFile(url, referer ?: shareInfo.referer) + val dataBytes = + shareInfo.fetchFile(url, httpClient, referer ?: shareInfo.referer) val range = currentMeta.second if (dataBytes == null) { call.respondText( @@ -108,7 +108,7 @@ private suspend fun responseFileStream( } try { writeFully(dataToWrite) - increaseDownloadData(dataToWrite.size.toLong()) + onDownloadData(dataToWrite) } catch (e: Exception) { close(e) } diff --git a/app/src/main/java/com/donut/mixfile/server/routes/Routes.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt similarity index 60% rename from app/src/main/java/com/donut/mixfile/server/routes/Routes.kt rename to app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt index 0bba4ad..46a83e7 100644 --- a/app/src/main/java/com/donut/mixfile/server/routes/Routes.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt @@ -1,12 +1,10 @@ -package com.donut.mixfile.server.routes +package com.donut.mixfile.server.core.routes -import com.donut.mixfile.app -import com.donut.mixfile.util.file.favorites -import com.donut.mixfile.util.file.resolveMixShareInfo -import com.donut.mixfile.util.isNotNull -import com.donut.mixfile.util.parseFileMimeType -import com.donut.mixfile.util.toJsonString -import com.google.gson.JsonObject +import com.alibaba.fastjson2.toJSONString +import com.donut.mixfile.server.core.MixFileServer +import com.donut.mixfile.server.core.utils.isNotNull +import com.donut.mixfile.server.core.utils.parseFileMimeType +import com.donut.mixfile.server.core.utils.resolveMixShareInfo import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.server.request.header @@ -19,24 +17,19 @@ import io.ktor.server.routing.get import io.ktor.server.routing.put import io.ktor.server.routing.route import io.ktor.utils.io.jvm.javaio.toOutputStream -import java.io.FileNotFoundException -fun getRoutes(): Routing.() -> Unit { +fun MixFileServer.getRoutes(): Routing.() -> Unit { return { get("{param...}") { val file = call.request.path().substring(1).ifEmpty { "index.html" } - try { - val fileStream = app.assets.open(file) - call.respondBytesWriter( - contentType = ContentType.parse(file.parseFileMimeType()) - ) { - fileStream.copyTo(this.toOutputStream()) - } - } catch (e: FileNotFoundException) { - call.respond(HttpStatusCode.NotFound) + val fileStream = getStaticFile(file) ?: return@get call.respond(HttpStatusCode.NotFound) + call.respondBytesWriter( + contentType = ContentType.parse(file.parseFileMimeType()) + ) { + fileStream.copyTo(this.toOutputStream()) } } route("/api") { @@ -46,7 +39,7 @@ fun getRoutes(): Routing.() -> Unit { if (call.request.header("origin").isNotNull()) { return@get call.respondText("此接口禁止跨域", status = HttpStatusCode.Forbidden) } - call.respond(favorites.takeLast(1000).toJsonString()) + call.respond(getFileHistory()) } get("/file_info") { val shareInfoStr = call.request.queryParameters["s"] @@ -62,10 +55,10 @@ fun getRoutes(): Routing.() -> Unit { ) return@get } - call.respondText(JsonObject().apply { - addProperty("name", shareInfo.fileName) - addProperty("size", shareInfo.fileSize) - }.toString()) + call.respondText(object { + val name = shareInfo.fileName + val size = shareInfo.fileSize + }.toJSONString()) } } } diff --git a/app/src/main/java/com/donut/mixfile/server/routes/UploadRoute.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/UploadRoute.kt similarity index 75% rename from app/src/main/java/com/donut/mixfile/server/routes/UploadRoute.kt rename to app/src/main/java/com/donut/mixfile/server/core/routes/UploadRoute.kt index 0ac2ab4..9d3b5ad 100644 --- a/app/src/main/java/com/donut/mixfile/server/routes/UploadRoute.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/UploadRoute.kt @@ -1,12 +1,12 @@ -package com.donut.mixfile.server.routes +package com.donut.mixfile.server.core.routes -import com.donut.mixfile.server.Uploader -import com.donut.mixfile.server.getCurrentUploader -import com.donut.mixfile.server.utils.bean.MixFile -import com.donut.mixfile.server.utils.bean.MixShareInfo -import com.donut.mixfile.ui.routes.home.UploadTask -import com.donut.mixfile.util.cachedMutableOf -import com.donut.mixfile.util.generateRandomByteArray +import com.donut.mixfile.server.core.MixFileServer +import com.donut.mixfile.server.core.Uploader +import com.donut.mixfile.server.core.aes.generateRandomByteArray +import com.donut.mixfile.server.core.httpClient +import com.donut.mixfile.server.core.utils.MixUploadTask +import com.donut.mixfile.server.core.utils.bean.MixFile +import com.donut.mixfile.server.core.utils.bean.MixShareInfo import io.ktor.http.HttpStatusCode import io.ktor.server.request.contentLength import io.ktor.server.request.receiveChannel @@ -15,7 +15,6 @@ import io.ktor.server.routing.RoutingHandler import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.readRemaining import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel @@ -23,14 +22,11 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.job import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.withContext import kotlinx.io.readByteArray import kotlin.math.ceil -var UPLOAD_TASK_COUNT by cachedMutableOf(10, "upload_task_count") - -fun getUploadRoute(): RoutingHandler { +fun MixFileServer.getUploadRoute(): RoutingHandler { return route@{ val key = generateRandomByteArray(32) val name = call.request.queryParameters["name"] @@ -44,13 +40,13 @@ fun getUploadRoute(): RoutingHandler { call.respondText("文件大小不合法", status = HttpStatusCode.InternalServerError) return@route } - val uploadTask = UploadTask(call, name, size, add = add.toBoolean()) + val uploadTask = getUploadTask(call, name, size, add.toBoolean()) currentCoroutineContext().job.invokeOnCompletion { uploadTask.error = it uploadTask.stopped = true } - val uploader = getCurrentUploader() - val head = uploader.genHead() + val uploader = getUploader() + val head = uploader.genHead(httpClient) ?: genDefaultImage() val mixUrl = uploadFile(call.receiveChannel(), head, uploader, key, fileSize = size, uploadTask) if (mixUrl == null) { @@ -71,15 +67,15 @@ fun getUploadRoute(): RoutingHandler { } } -suspend fun uploadFile( +suspend fun MixFileServer.uploadFile( channel: ByteReadChannel, head: ByteArray, uploader: Uploader, secret: ByteArray, fileSize: Long, - uploadTask: UploadTask, + uploadTask: MixUploadTask, ): String? { - val semaphore = Semaphore(UPLOAD_TASK_COUNT.toInt()) + val semaphore = Semaphore(uploadTaskCount) return coroutineScope { val context = currentCoroutineContext() uploadTask.onStop = { @@ -99,11 +95,9 @@ suspend fun uploadFile( fileIndex++ tasks.add(async { try { - val url = uploader.upload(head, fileData, secret) + val url = uploader.upload(head, fileData, secret, this@uploadFile) fileList[currentIndex] = url - withContext(Dispatchers.Main) { - uploadTask.progress.increaseBytesWritten(fileData.size.toLong(), fileSize) - } + uploadTask.updateProgress(fileData.size.toLong(), fileSize) } finally { semaphore.release() } @@ -115,8 +109,9 @@ suspend fun uploadFile( } val mixFile = MixFile(chunkSize = chunkSize, version = 0, fileList = fileList, fileSize = fileSize) + val mixFileData = mixFile.toBytes() val mixFileUrl = - uploader.upload(head, mixFile.toBytes(), secret) + uploader.upload(head, mixFileData, secret, this@uploadFile) return@coroutineScope mixFileUrl } } \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/core/uploaders/A3Uploader.kt b/app/src/main/java/com/donut/mixfile/server/core/uploaders/A3Uploader.kt new file mode 100644 index 0000000..67fcf69 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/uploaders/A3Uploader.kt @@ -0,0 +1,29 @@ +package com.donut.mixfile.server.core.uploaders + +import com.alibaba.fastjson2.JSONObject +import com.alibaba.fastjson2.to +import com.donut.mixfile.server.core.Uploader +import com.donut.mixfile.server.core.utils.add +import com.donut.mixfile.server.core.utils.fileFormHeaders +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitFormWithBinaryData + +object A3Uploader : Uploader("线路A3") { + + override val referer: String + get() = "" + + override suspend fun doUpload(fileData: ByteArray, client: HttpClient): String { + val result = + client.submitFormWithBinaryData( + "https://chatbot.weixin.qq.com/weixinh5/webapp/pfnYYEumBeFN7Yb3TAxwrabYVOa4R9/cos/upload", + formData { + add("media", fileData, fileFormHeaders()) + }) { + }.body().to() + + return result.getString("url") + } +} diff --git a/app/src/main/java/com/donut/mixfile/util/Extension.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt similarity index 97% rename from app/src/main/java/com/donut/mixfile/util/Extension.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt index 06816da..f2c1ea6 100644 --- a/app/src/main/java/com/donut/mixfile/util/Extension.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt @@ -1,8 +1,10 @@ -package com.donut.mixfile.util +package com.donut.mixfile.server.core.utils import java.nio.ByteBuffer import kotlin.streams.toList +typealias UnitBlock = () -> Unit + inline fun T?.isNull(block: UnitBlock = {}): Boolean { if (this == null) { block() @@ -150,7 +152,5 @@ fun Int.toBytes(): ByteArray = fun ByteArray.toInt(): Int = ByteBuffer.wrap(this).int -fun T.toJsonString(): String = GSON.toJson(this) - infix fun T?.default(value: T) = this ?: value diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/Json.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/Json.kt new file mode 100644 index 0000000..f908faf --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/Json.kt @@ -0,0 +1,28 @@ +package com.donut.mixfile.server.core.utils + +import com.alibaba.fastjson2.JSON +import com.alibaba.fastjson2.JSONWriter +import com.alibaba.fastjson2.writer.ObjectWriter +import java.lang.reflect.Type +import java.util.Date + +object MillisDateWriter : ObjectWriter { + override fun write( + jsonWriter: JSONWriter, + `object`: Any?, + fieldName: Any?, + fieldType: Type?, + features: Long + ) { + if (`object` == null) { + jsonWriter.writeNull() + return + } + jsonWriter.writeInt64((`object` as Date).time) // 输出毫秒时间戳 + } + +} + +fun registerJson() { + JSON.register(Date::class.java, MillisDateWriter) +} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/MixUploadTask.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/MixUploadTask.kt new file mode 100644 index 0000000..68e3077 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/MixUploadTask.kt @@ -0,0 +1,11 @@ +package com.donut.mixfile.server.core.utils + +import com.donut.mixfile.server.core.utils.bean.MixShareInfo + +interface MixUploadTask { + var error: Throwable? + var stopped: Boolean + suspend fun complete(shareInfo: MixShareInfo) + var onStop: () -> Unit + suspend fun updateProgress(size: Long, total: Long) +} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/ShareCode.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/ShareCode.kt new file mode 100644 index 0000000..761f625 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/ShareCode.kt @@ -0,0 +1,103 @@ +package com.donut.mixfile.server.core.utils + +import com.donut.mixfile.server.core.utils.bean.MixShareInfo +import java.security.MessageDigest + +tailrec fun String.hashToMD5String(round: Int = 1): String { + val digest = hashMD5() + if (round > 1) { + return digest.toHex().hashToMD5String(round - 1) + } + return digest.toHex() +} + +fun ByteArray.toHex(): String { + val sb = StringBuilder() + for (b in this) { + sb.append(String.format("%02x", b)) + } + return sb.toString() +} + +fun String.hashMD5() = hashToHexString("MD5") + +fun String.hashSHA256() = hashToHexString("SHA-256") + +fun String.hashToHexString(algorithm: String): ByteArray { + val md = MessageDigest.getInstance(algorithm) + md.update(this.toByteArray()) + return md.digest() +} + +fun ByteArray.hashToHexString(algorithm: String): String { + return calcHash(algorithm).toHex() +} + +fun ByteArray.calcHash(algorithm: String): ByteArray { + val md = MessageDigest.getInstance(algorithm) + md.update(this) + return md.digest() +} + +fun ByteArray.hashSHA256() = calcHash("SHA-256") + +fun ByteArray.hashSHA256String() = hashToHexString("SHA-256") + +fun resolveMixShareInfo(value: String): MixShareInfo? { + return parseShareCode(value) +} + +private val encodeMap = run { + val map = mutableMapOf() + for (value in 0xfe00..0xfe0f) { + val key = (value - 0xfe00).toString(16) + map[key] = "${value.toChar()}" + } + map +} + +fun MixShareInfo.shareCode(useShortCode: Boolean): String { + if (useShortCode) { + return "mf://${encodeHex(this.toString())}${ + MixShareInfo.ENCODER.encode( + this.url.hashMD5().copyOf(6) + ) + }" + } + return "mf://$this" +} + +fun parseShareCode(code: String): MixShareInfo? { + val mf = code.substringAfter("mf://") + val encoded = decodeHex(mf) + val parsed = MixShareInfo.tryFromString(encoded) ?: MixShareInfo.tryFromString(mf) + return parsed +} + +fun encodeHex(data: String): String { + val sb = StringBuilder() + for (element in data.toByteArray().toHex()) { + if (encodeMap.containsKey(element.toString())) { + sb.append(encodeMap[element.toString()]) + } + } + return sb.toString() +} + +fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() +} + +fun decodeHex(data: String): String { + val sb = StringBuilder() + for (element in data) { + if (encodeMap.containsValue(element.toString())) { + sb.append(encodeMap.filterValues { it == element.toString() }.keys.first()) + } + } + return sb.toString().decodeHex().decodeToString() +} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/util/objects/SortedTask.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/SortedTask.kt similarity index 85% rename from app/src/main/java/com/donut/mixfile/util/objects/SortedTask.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/SortedTask.kt index a5260a8..dfafa2b 100644 --- a/app/src/main/java/com/donut/mixfile/util/objects/SortedTask.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/SortedTask.kt @@ -1,6 +1,5 @@ -package com.donut.mixfile.util.objects +package com.donut.mixfile.server.core.utils -import com.donut.mixfile.util.showError import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withLock @@ -31,9 +30,6 @@ class SortedTask(limit: Int) { taskMap.remove(task.key) try { block() - } catch (e: Exception) { - showError(e) - break } finally { semaphore.release() } diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt new file mode 100644 index 0000000..2c3eb34 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt @@ -0,0 +1,111 @@ +package com.donut.mixfile.server.core.utils + + +import com.donut.mixfile.server.core.aes.generateRandomByteArray +import com.github.amr.mimetypes.MimeTypes +import io.ktor.client.request.forms.FormBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.quote +import io.ktor.server.application.ApplicationCall +import io.ktor.util.pipeline.PipelineContext +import io.ktor.utils.io.InternalAPI +import kotlinx.coroutines.launch +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.net.URLEncoder +import java.util.concurrent.CopyOnWriteArrayList +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +fun String.getFileExtension(): String { + val index = this.lastIndexOf('.') + return if (index == -1) "" else this.substring(index + 1).lowercase() +} + +fun genRandomString( + length: Int = 32, + charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') +): String { + return (1..length) + .map { kotlin.random.Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") +} + + +fun fileFormHeaders( + suffix: String = ".gif", + mimeType: String = "image/gif", +): Headers { + return Headers.build { + append(HttpHeaders.ContentType, mimeType) + append( + HttpHeaders.ContentDisposition, + "filename=\"${genRandomString(5)}${suffix}\"" + ) + } +} + + +fun concurrencyLimit( + limit: Int, + route: suspend PipelineContext.(Unit) -> Unit, +): suspend PipelineContext.(Unit) -> Unit { + val tasks = CopyOnWriteArrayList<() -> Unit>() + return route@{ + while (tasks.size > limit) { + val remove = tasks.removeAt(0) + ignoreError { + remove() + } + } + val cancel: () -> Unit = { + launch { + throw Throwable("服务器达到并发限制") + } + } + tasks.add(cancel) + route(Unit) + tasks.remove(cancel) + } +} + +inline fun ignoreError(block: () -> T): T? { + try { + return block() + } catch (_: Exception) { + + } + return null +} + + +fun getRandomEncKey() = generateRandomByteArray(256) + +fun compressGzip(input: String): ByteArray { + val byteArrayOutputStream = ByteArrayOutputStream() + GZIPOutputStream(byteArrayOutputStream).use { gzip -> + gzip.write(input.toByteArray()) + } + return byteArrayOutputStream.toByteArray() +} + +fun decompressGzip(compressed: ByteArray): String { + val byteArrayInputStream = ByteArrayInputStream(compressed) + GZIPInputStream(byteArrayInputStream).use { gzip -> + return gzip.bufferedReader().use { it.readText() } + } +} + +fun String.encodeURL(): String? { + return URLEncoder.encode(this, "UTF-8") +} + +fun String.parseFileMimeType() = MimeTypes.getInstance() + .getByExtension(this.getFileExtension())?.mimeType ?: "application/octet-stream" + +@OptIn(InternalAPI::class) +fun FormBuilder.add(key: String, value: Any?, headers: Headers = Headers.Empty) { + append(key.quote(), value ?: "", headers) +} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/util/basen/Alphabet.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/Alphabet.kt similarity index 93% rename from app/src/main/java/com/donut/mixfile/util/basen/Alphabet.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/basen/Alphabet.kt index 8388a06..9f520ff 100644 --- a/app/src/main/java/com/donut/mixfile/util/basen/Alphabet.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/Alphabet.kt @@ -1,7 +1,4 @@ -package com.donut.mixmessage.util.encode.basen - -import com.donut.mixfile.util.basen.ALPHABETS -import com.donut.mixfile.util.basen.CodecDirection +package com.donut.mixfile.server.core.utils.basen class Alphabet private constructor(val key: String) { diff --git a/app/src/main/java/com/donut/mixfile/util/basen/BaseN.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/BaseN.kt similarity index 96% rename from app/src/main/java/com/donut/mixfile/util/basen/BaseN.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/basen/BaseN.kt index 80a8980..153b1e8 100644 --- a/app/src/main/java/com/donut/mixfile/util/basen/BaseN.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/BaseN.kt @@ -1,6 +1,5 @@ -package com.donut.mixfile.util.basen +package com.donut.mixfile.server.core.utils.basen -import com.donut.mixmessage.util.encode.basen.Alphabet import java.security.MessageDigest import kotlin.math.ln diff --git a/app/src/main/java/com/donut/mixfile/util/basen/BigIntBaseN.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/BigIntBaseN.kt similarity index 90% rename from app/src/main/java/com/donut/mixfile/util/basen/BigIntBaseN.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/basen/BigIntBaseN.kt index b0dd665..732e732 100644 --- a/app/src/main/java/com/donut/mixfile/util/basen/BigIntBaseN.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/BigIntBaseN.kt @@ -1,6 +1,5 @@ -package com.donut.mixfile.util.basen +package com.donut.mixfile.server.core.utils.basen -import com.donut.mixmessage.util.encode.basen.Alphabet import java.math.BigInteger class BigIntBaseN(override val alphabet: Alphabet) : BaseN() { diff --git a/app/src/main/java/com/donut/mixfile/util/basen/CodecDirection.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/CodecDirection.kt similarity index 91% rename from app/src/main/java/com/donut/mixfile/util/basen/CodecDirection.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/basen/CodecDirection.kt index d10cd7a..41b723f 100644 --- a/app/src/main/java/com/donut/mixfile/util/basen/CodecDirection.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/CodecDirection.kt @@ -1,4 +1,4 @@ -package com.donut.mixfile.util.basen +package com.donut.mixfile.server.core.utils.basen class CodecDirection(val fromBase: Int, val toBase: Int) { private val fromLog = logInt(fromBase) diff --git a/app/src/main/java/com/donut/mixfile/util/basen/LoopBaseN.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/LoopBaseN.kt similarity index 86% rename from app/src/main/java/com/donut/mixfile/util/basen/LoopBaseN.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/basen/LoopBaseN.kt index be2ea60..9637105 100644 --- a/app/src/main/java/com/donut/mixfile/util/basen/LoopBaseN.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/basen/LoopBaseN.kt @@ -1,6 +1,4 @@ -package com.donut.mixfile.util.basen - -import com.donut.mixmessage.util.encode.basen.Alphabet +package com.donut.mixfile.server.core.utils.basen class LoopBaseN(override val alphabet: Alphabet) : BaseN() { diff --git a/app/src/main/java/com/donut/mixfile/server/utils/bean/MixFile.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/bean/MixFile.kt similarity index 56% rename from app/src/main/java/com/donut/mixfile/server/utils/bean/MixFile.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/bean/MixFile.kt index ad7b654..f6f5dab 100644 --- a/app/src/main/java/com/donut/mixfile/server/utils/bean/MixFile.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/bean/MixFile.kt @@ -1,38 +1,35 @@ -package com.donut.mixfile.server.utils.bean +package com.donut.mixfile.server.core.utils.bean -import com.donut.mixfile.server.Uploader -import com.donut.mixfile.server.accessKey -import com.donut.mixfile.server.uploadClient -import com.donut.mixfile.ui.routes.home.getLocalServerAddress -import com.donut.mixfile.ui.routes.home.serverAddress -import com.donut.mixfile.util.GSON -import com.donut.mixfile.util.basen.BigIntBaseN -import com.donut.mixfile.util.compressGzip -import com.donut.mixfile.util.decompressGzip -import com.donut.mixfile.util.decryptAES -import com.donut.mixfile.util.encryptAES -import com.donut.mixfile.util.hashMD5 -import com.donut.mixfile.util.hashSHA256 -import com.donut.mixfile.util.parseFileMimeType -import com.donut.mixmessage.util.encode.basen.Alphabet -import com.google.gson.annotations.SerializedName +import com.alibaba.fastjson2.annotation.JSONField +import com.alibaba.fastjson2.to +import com.alibaba.fastjson2.toJSONString +import com.donut.mixfile.server.core.Uploader +import com.donut.mixfile.server.core.aes.decryptAES +import com.donut.mixfile.server.core.aes.encryptAES +import com.donut.mixfile.server.core.utils.basen.Alphabet +import com.donut.mixfile.server.core.utils.basen.BigIntBaseN +import com.donut.mixfile.server.core.utils.compressGzip +import com.donut.mixfile.server.core.utils.decompressGzip +import com.donut.mixfile.server.core.utils.hashMD5 +import com.donut.mixfile.server.core.utils.hashSHA256 +import com.donut.mixfile.server.core.utils.parseFileMimeType +import io.ktor.client.HttpClient import io.ktor.client.request.header import io.ktor.client.request.prepareGet import io.ktor.client.statement.bodyAsChannel import io.ktor.utils.io.discard -import java.net.URLEncoder fun ByteArray.hashMixSHA256() = MixShareInfo.ENCODER.encode(hashSHA256()) data class MixShareInfo( - @SerializedName("f") var fileName: String, - @SerializedName("s") val fileSize: Long, - @SerializedName("h") val headSize: Int, - @SerializedName("u") val url: String, - @SerializedName("k") val key: String, - @SerializedName("r") val referer: String, + @JSONField(name = "f") var fileName: String, + @JSONField(name = "s") val fileSize: Long, + @JSONField(name = "h") val headSize: Int, + @JSONField(name = "u") val url: String, + @JSONField(name = "k") val key: String, + @JSONField(name = "r") val referer: String, ) { companion object { @@ -49,7 +46,7 @@ data class MixShareInfo( } private fun fromJson(json: String): MixShareInfo = - GSON.fromJson(json, MixShareInfo::class.java) + json.to() private fun enc(input: String): String { val bytes = input.encodeToByteArray() @@ -62,38 +59,23 @@ data class MixShareInfo( val result = decryptAES(bytes, "123".hashMD5()) return result!!.decodeToString() } + } - val downloadUrl: String - get() { - return "${getLocalServerAddress()}/api/download?s=${ - URLEncoder.encode( - this.toString(), - "UTF-8" - ) - }&accessKey=${accessKey}#${fileName}" - } - - val lanUrl: String - get() { - return "$serverAddress/api/download?s=${ - URLEncoder.encode( - this.toString(), - "UTF-8" - ) - }&accessKey=${accessKey}#${fileName}" - } - override fun toString(): String { return enc(toJson()) } - private fun toJson(): String = GSON.toJson(this) + private fun toJson(): String = this.toJSONString() - suspend fun fetchFile(url: String, referer: String = this.referer): ByteArray? { + suspend fun fetchFile( + url: String, + client: HttpClient, + referer: String = this.referer, + ): ByteArray? { val transformedUrl = Uploader.transformUrl(url) val transformedReferer = Uploader.transformReferer(url, referer) - val result: ByteArray? = uploadClient.prepareGet(transformedUrl) { + val result: ByteArray? = client.prepareGet(transformedUrl) { if (transformedReferer.trim().isNotEmpty()) { header("Referer", transformedReferer) } @@ -127,8 +109,8 @@ data class MixShareInfo( fun contentType(): String = fileName.parseFileMimeType() - suspend fun fetchMixFile(): MixFile? { - val decryptedBytes = fetchFile(url) ?: return null + suspend fun fetchMixFile(client: HttpClient): MixFile? { + val decryptedBytes = fetchFile(url, client = client) ?: return null return MixFile.fromBytes(decryptedBytes) } @@ -136,15 +118,15 @@ data class MixShareInfo( data class MixFile( - @SerializedName("chunk_size") val chunkSize: Long, - @SerializedName("file_size") val fileSize: Long, - @SerializedName("version") val version: Long, - @SerializedName("file_list") val fileList: List, + @JSONField(name = "chunk_size") val chunkSize: Long, + @JSONField(name = "file_size") val fileSize: Long, + @JSONField(name = "version") val version: Long, + @JSONField(name = "file_list") val fileList: List, ) { companion object { fun fromBytes(data: ByteArray): MixFile = - GSON.fromJson(decompressGzip(data), MixFile::class.java) + decompressGzip(data).to() } fun getFileListByStartRange(startRange: Long): List> { @@ -161,6 +143,6 @@ data class MixFile( } - fun toBytes() = compressGzip(GSON.toJson(this)) + fun toBytes() = compressGzip(this.toJSONString()) } \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/utils/AnimatedGifEncoder.java b/app/src/main/java/com/donut/mixfile/server/image/AnimatedGifEncoder.java similarity index 99% rename from app/src/main/java/com/donut/mixfile/server/utils/AnimatedGifEncoder.java rename to app/src/main/java/com/donut/mixfile/server/image/AnimatedGifEncoder.java index 7e05e64..86ca3f7 100644 --- a/app/src/main/java/com/donut/mixfile/server/utils/AnimatedGifEncoder.java +++ b/app/src/main/java/com/donut/mixfile/server/image/AnimatedGifEncoder.java @@ -1,4 +1,4 @@ -package com.donut.mixfile.server.utils; +package com.donut.mixfile.server.image; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; diff --git a/app/src/main/java/com/donut/mixfile/server/image/GIFUtils.kt b/app/src/main/java/com/donut/mixfile/server/image/GIFUtils.kt new file mode 100644 index 0000000..39de3a8 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/image/GIFUtils.kt @@ -0,0 +1,42 @@ +package com.donut.mixfile.server.image + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import androidx.core.graphics.createBitmap +import java.io.ByteArrayOutputStream +import kotlin.random.Random + +fun createBlankBitmap( + width: Int = Random.nextInt(50, 100), + height: Int = Random.nextInt(50, 100), +): Bitmap { + val bitmap = createBitmap(width, height) + val canvas = Canvas(bitmap) + canvas.drawColor(Color.rgb(Random.nextInt(255), Random.nextInt(255), Random.nextInt(255))) + return bitmap +} + +fun Bitmap.compressToByteArray( + useWebp: Boolean = true, +): ByteArray { + val bitmap = this + val stream = ByteArrayOutputStream() + + if (useWebp) { + bitmap.compress(Bitmap.CompressFormat.WEBP, 0, stream) + } else { + bitmap.compress(Bitmap.CompressFormat.JPEG, 0, stream) + } + + return stream.toByteArray() +} + +fun Bitmap.toGif(): ByteArray { + val bos = ByteArrayOutputStream() + val encoder = AnimatedGifEncoder() + encoder.start(bos) + encoder.addFrame(this) + encoder.finish() + return bos.toByteArray() +} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/uploaders/A3Uploader.kt b/app/src/main/java/com/donut/mixfile/server/uploaders/A3Uploader.kt deleted file mode 100644 index 6a865e8..0000000 --- a/app/src/main/java/com/donut/mixfile/server/uploaders/A3Uploader.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.donut.mixfile.server.uploaders - -import com.donut.mixfile.server.Uploader -import com.donut.mixfile.server.uploadClient -import com.donut.mixfile.server.utils.fileFormHeaders -import com.donut.mixfile.util.add -import com.google.gson.JsonObject -import io.ktor.client.call.body -import io.ktor.client.request.forms.formData -import io.ktor.client.request.forms.submitFormWithBinaryData - -object A3Uploader : Uploader("线路A3") { - - override val referer: String - get() = "" - - override suspend fun doUpload(fileData: ByteArray): String { - val result = - uploadClient.submitFormWithBinaryData( - "https://chatbot.weixin.qq.com/weixinh5/webapp/pfnYYEumBeFN7Yb3TAxwrabYVOa4R9/cos/upload", - formData { - add("media", fileData, fileFormHeaders()) - }) { - }.body() - - return result.get("url").asString - } -} diff --git a/app/src/main/java/com/donut/mixfile/server/utils/Util.kt b/app/src/main/java/com/donut/mixfile/server/utils/Util.kt deleted file mode 100644 index 447bb3c..0000000 --- a/app/src/main/java/com/donut/mixfile/server/utils/Util.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.donut.mixfile.server.utils - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color -import androidx.core.graphics.createBitmap -import com.donut.mixfile.util.genRandomString -import com.donut.mixfile.util.generateRandomByteArray -import com.donut.mixfile.util.ignoreError -import io.ktor.http.Headers -import io.ktor.http.HttpHeaders -import io.ktor.server.application.ApplicationCall -import io.ktor.util.pipeline.PipelineContext -import kotlinx.coroutines.launch -import java.io.ByteArrayOutputStream -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.random.Random - - -fun fileFormHeaders( - suffix: String = ".gif", - mimeType: String = "image/gif", -): Headers { - return Headers.build { - append(HttpHeaders.ContentType, mimeType) - append( - HttpHeaders.ContentDisposition, - "filename=\"${genRandomString(5)}${suffix}\"" - ) - } -} - -fun createBlankBitmap( - width: Int = Random.nextInt(50, 100), - height: Int = Random.nextInt(50, 100), -): Bitmap { - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - canvas.drawColor(Color.rgb(Random.nextInt(255), Random.nextInt(255), Random.nextInt(255))) - return bitmap -} - -fun concurrencyLimit( - limit: Int, - route: suspend PipelineContext.(Unit) -> Unit, -): suspend PipelineContext.(Unit) -> Unit { - val tasks = CopyOnWriteArrayList<() -> Unit>() - return route@{ - while (tasks.size > limit) { - val remove = tasks.removeAt(0) - ignoreError { - remove() - } - } - val cancel: () -> Unit = { - launch { - throw Throwable("服务器达到并发限制") - } - } - tasks.add(cancel) - route(Unit) - tasks.remove(cancel) - } -} - -fun getRandomEncKey() = generateRandomByteArray(256) - -fun Bitmap.compressToByteArray( - useWebp: Boolean = true, -): ByteArray { - val bitmap = this - val stream = ByteArrayOutputStream() - - if (useWebp) { - bitmap.compress(Bitmap.CompressFormat.WEBP, 0, stream) - } else { - bitmap.compress(Bitmap.CompressFormat.JPEG, 0, stream) - } - - return stream.toByteArray() -} - -fun Bitmap.toGif(): ByteArray { - val bos = ByteArrayOutputStream() - val encoder = AnimatedGifEncoder() - encoder.start(bos) - encoder.addFrame(this) - encoder.finish() - return bos.toByteArray() -} diff --git a/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt b/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt index 6463f15..8cb449f 100644 --- a/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt +++ b/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt @@ -31,8 +31,8 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.donut.mixfile.util.genRandomString -import com.donut.mixfile.util.isNotNull +import com.donut.mixfile.server.core.utils.genRandomString +import com.donut.mixfile.server.core.utils.isNotNull import java.lang.ref.WeakReference diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/About.kt b/app/src/main/java/com/donut/mixfile/ui/routes/About.kt index eea93cb..8b041a4 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/About.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/About.kt @@ -1,7 +1,6 @@ package com.donut.mixfile.ui.routes import android.content.Intent -import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,7 +17,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.donut.mixfile.app +import androidx.core.net.toUri +import com.donut.mixfile.currentActivity import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.nav.MixNavPage import com.donut.mixfile.ui.theme.colorScheme @@ -115,11 +115,9 @@ val About = MixNavPage( val intent = Intent( Intent.ACTION_VIEW, - Uri.parse("https://github.com/InvertGeek/MixFile") - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - app.startActivity(intent) + "https://github.com/InvertGeek/MixFile".toUri() + ) + currentActivity.startActivity(intent) closeDialog() } setDefaultNegative() diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/Settings.kt b/app/src/main/java/com/donut/mixfile/ui/routes/Settings.kt index 68c0a69..89b37ea 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/Settings.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/Settings.kt @@ -29,17 +29,18 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.donut.mixfile.currentActivity +import com.donut.mixfile.server.CUSTOM_REFERER +import com.donut.mixfile.server.CUSTOM_UPLOAD_URL +import com.donut.mixfile.server.CustomUploader +import com.donut.mixfile.server.DOWNLOAD_TASK_COUNT import com.donut.mixfile.server.UPLOADERS import com.donut.mixfile.server.UPLOAD_RETRY_TIMES +import com.donut.mixfile.server.UPLOAD_TASK_COUNT +import com.donut.mixfile.server.core.uploaders.hidden.A1Uploader import com.donut.mixfile.server.currentUploader import com.donut.mixfile.server.enableAccessKey import com.donut.mixfile.server.getCurrentUploader -import com.donut.mixfile.server.routes.DOWNLOAD_TASK_COUNT -import com.donut.mixfile.server.routes.UPLOAD_TASK_COUNT -import com.donut.mixfile.server.uploaders.CUSTOM_REFERER -import com.donut.mixfile.server.uploaders.CUSTOM_UPLOAD_URL -import com.donut.mixfile.server.uploaders.CustomUploader -import com.donut.mixfile.server.uploaders.hidden.A1Uploader +import com.donut.mixfile.server.mixFileServer import com.donut.mixfile.ui.component.common.CommonSwitch import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.component.common.SingleSelectItemList @@ -102,7 +103,7 @@ val MixSettings = MixNavPage( Column { Text( modifier = Modifier.padding(10.dp, 0.dp), - text = "下载并发: ${DOWNLOAD_TASK_COUNT}", + text = "下载并发: $DOWNLOAD_TASK_COUNT", color = MaterialTheme.colorScheme.primary ) Slider( @@ -118,7 +119,7 @@ val MixSettings = MixNavPage( Column { Text( modifier = Modifier.padding(10.dp, 0.dp), - text = "上传并发: ${UPLOAD_TASK_COUNT}", + text = "上传并发: $UPLOAD_TASK_COUNT", color = MaterialTheme.colorScheme.primary ) Slider( @@ -148,7 +149,7 @@ val MixSettings = MixNavPage( Column { Text( modifier = Modifier.padding(10.dp, 0.dp), - text = "上传失败重试次数(单个分片): ${UPLOAD_RETRY_TIMES}", + text = "上传失败重试次数(单个分片): $UPLOAD_RETRY_TIMES", color = MaterialTheme.colorScheme.primary ) Slider( @@ -183,6 +184,7 @@ val MixSettings = MixNavPage( description = "开启后网页端将禁止访问" ) { enableAccessKey = it + mixFileServer.enableAccessKey = it } CommonSwitch( checked = useSystemPlayer, diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt index a153a82..9215f19 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt @@ -31,8 +31,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.donut.mixfile.server.core.utils.resolveMixShareInfo import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.component.common.SingleSelectItemList import com.donut.mixfile.ui.nav.MixNavPage @@ -46,14 +48,12 @@ import com.donut.mixfile.util.file.FileCardList import com.donut.mixfile.util.file.FileDataLog import com.donut.mixfile.util.file.downloadFile import com.donut.mixfile.util.file.favorites -import com.donut.mixfile.util.file.resolveMixShareInfo import com.donut.mixfile.util.file.selectAndUploadFile import com.donut.mixfile.util.file.showExportFileListDialog import com.donut.mixfile.util.formatFileSize import com.donut.mixfile.util.parseSortNum import com.donut.mixfile.util.showConfirmDialog import com.donut.mixfile.util.showToast -import com.donut.mixfile.util.truncate import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -167,7 +167,11 @@ val Favorites = MixNavPage( .weight(1.0f) .padding(10.dp, 0.dp) ) { - Text(text = "分类: ${currentCategory.ifEmpty { "全部" }.truncate(3)}") + Text( + text = "分类: ${currentCategory.ifEmpty { "全部" }}", + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) } Button( onClick = { diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt b/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt index 874aa1c..444ffb4 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt @@ -29,25 +29,25 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.donut.mixfile.server.serverPort +import com.donut.mixfile.server.core.utils.isFalse +import com.donut.mixfile.server.core.utils.resolveMixShareInfo +import com.donut.mixfile.server.mixFileServer import com.donut.mixfile.ui.nav.MixNavPage import com.donut.mixfile.ui.routes.UploadDialogCard import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.copyToClipboard import com.donut.mixfile.util.file.FileCardList import com.donut.mixfile.util.file.deleteUploadLog -import com.donut.mixfile.util.file.resolveMixShareInfo import com.donut.mixfile.util.file.selectAndUploadFile import com.donut.mixfile.util.file.showFileInfoDialog import com.donut.mixfile.util.file.showFileList import com.donut.mixfile.util.file.toDataLog import com.donut.mixfile.util.file.uploadLogs import com.donut.mixfile.util.getIpAddressInLocalNetwork -import com.donut.mixfile.util.isFalse import com.donut.mixfile.util.readClipBoardText import com.donut.mixfile.util.showToast -var serverAddress by mutableStateOf("http://${getIpAddressInLocalNetwork()}:${serverPort}") +var serverAddress by mutableStateOf("http://${getIpAddressInLocalNetwork()}:${mixFileServer.serverPort}") @OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) val Home = MixNavPage( @@ -183,7 +183,7 @@ fun tryResolveFile(text: String): Boolean { fun getLocalServerAddress(): String { - return "http://127.0.0.1:${serverPort}" + return "http://127.0.0.1:${mixFileServer.serverPort}" } diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt b/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt index 884d759..6f0f228 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/home/UploadTask.kt @@ -21,7 +21,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.donut.mixfile.appScope -import com.donut.mixfile.server.utils.bean.MixShareInfo +import com.donut.mixfile.server.core.utils.MixUploadTask +import com.donut.mixfile.server.core.utils.bean.MixShareInfo import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.file.InfoText @@ -94,15 +95,21 @@ class UploadTask( val fileName: String, val fileSize: Long, val add: Boolean = true, -) { +) : MixUploadTask { var progress = ProgressContent("上传中", 14.sp, colorScheme.secondary, false) - var onStop = {} + override var onStop = {} + + override suspend fun updateProgress(size: Long, total: Long) { + withContext(Dispatchers.Main) { + progress.increaseBytesWritten(size, total) + } + } - var stopped by mutableStateOf(false) + override var stopped by mutableStateOf(false) - var error: Throwable? by mutableStateOf(null) + override var error: Throwable? by mutableStateOf(null) var result by mutableStateOf("") private set @@ -129,7 +136,7 @@ class UploadTask( } - suspend fun complete(shareInfo: MixShareInfo) { + override suspend fun complete(shareInfo: MixShareInfo) { withContext(Dispatchers.Main) { result = shareInfo.toString() uploadTasks -= this@UploadTask diff --git a/app/src/main/java/com/donut/mixfile/util/CachedMutableValue.kt b/app/src/main/java/com/donut/mixfile/util/CachedMutableValue.kt index d381c2b..0b20136 100644 --- a/app/src/main/java/com/donut/mixfile/util/CachedMutableValue.kt +++ b/app/src/main/java/com/donut/mixfile/util/CachedMutableValue.kt @@ -4,8 +4,9 @@ import android.os.Parcelable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.alibaba.fastjson2.into +import com.alibaba.fastjson2.toJSONString import com.donut.mixfile.kv -import com.google.gson.reflect.TypeToken fun constructCachedMutableValue( value: T, @@ -58,15 +59,11 @@ inline fun > cachedMutableOf(value: C, key: S constructCachedMutableValue( value, key, - { kv.encode(key, it.toJsonString()) }, + { kv.encode(key, it.toJSONString()) }, getter@{ var result = value - val type = object : TypeToken() {}.type - catchError( - onError = { - kv.remove(key) - }) { - val json: C = GSON.fromJson(kv.decodeString(key), type) + catchError { + val json: C = kv.decodeString(key).into() result = json } return@getter result diff --git a/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt b/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt index ce28de7..dd19d96 100644 --- a/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/CommonUtil.kt @@ -10,27 +10,22 @@ import android.provider.OpenableColumns import android.util.Log import com.donut.mixfile.app import com.donut.mixfile.appScope -import com.github.amr.mimetypes.MimeTypes -import io.ktor.client.request.forms.FormBuilder -import io.ktor.http.Headers -import io.ktor.http.quote -import io.ktor.utils.io.InternalAPI +import com.donut.mixfile.server.core.utils.genRandomString +import com.donut.mixfile.server.core.utils.ignoreError +import com.donut.mixfile.server.core.utils.isFalse +import com.donut.mixfile.server.mixFileServer +import com.donut.mixfile.ui.routes.home.getLocalServerAddress import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.io.EOFException import java.math.BigInteger import java.net.NetworkInterface import java.net.URL import java.net.URLEncoder -import java.security.MessageDigest import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.log10 @@ -91,14 +86,6 @@ fun getAppVersion(context: Context): Pair { } } -fun String.decodeHex(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - - return chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() -} - class CachedDelegate(val getKeys: () -> Array, private val initializer: () -> T) { private var cache: T? = null @@ -119,46 +106,6 @@ class CachedDelegate(val getKeys: () -> Array, private val initializer: } -tailrec fun String.hashToMD5String(round: Int = 1): String { - val digest = hashMD5() - if (round > 1) { - return digest.toHex().hashToMD5String(round - 1) - } - return digest.toHex() -} - -fun ByteArray.toHex(): String { - val sb = StringBuilder() - for (b in this) { - sb.append(String.format("%02x", b)) - } - return sb.toString() -} - -fun String.hashMD5() = hashToHexString("MD5") - -fun String.hashSHA256() = hashToHexString("SHA-256") - -fun String.hashToHexString(algorithm: String): ByteArray { - val md = MessageDigest.getInstance(algorithm) - md.update(this.toByteArray()) - return md.digest() -} - -fun ByteArray.hashToHexString(algorithm: String): String { - return calcHash(algorithm).toHex() -} - -fun ByteArray.calcHash(algorithm: String): ByteArray { - val md = MessageDigest.getInstance(algorithm) - md.update(this) - return md.digest() -} - -fun ByteArray.hashSHA256() = calcHash("SHA-256") - -fun ByteArray.hashSHA256String() = hashToHexString("SHA-256") - inline fun String.isUrl(block: (URL) -> Unit = {}): Boolean { val urlPattern = Regex("^https?://(www\\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)\$") @@ -178,14 +125,6 @@ fun getUrlHost(url: String): String? { return null } -fun String.truncate(maxLength: Int): String { - return if (this.length > maxLength) { - this.substring(0, maxLength) + "..." - } else { - this - } -} - @OptIn(ExperimentalEncodingApi::class) fun ByteArray.encodeToBase64() = Base64.encode(this) @@ -246,25 +185,10 @@ infix fun List.elementEquals(other: List): Boolean { } -typealias UnitBlock = () -> Unit - - fun debug(text: String?, tag: String = "test") { Log.d(tag, text ?: "null") } -fun String.encodeURL(): String? { - return URLEncoder.encode(this, "UTF-8") -} - -fun String.getFileExtension(): String { - val index = this.lastIndexOf('.') - return if (index == -1) "" else this.substring(index + 1).lowercase() -} - -fun String.parseFileMimeType() = MimeTypes.getInstance() - .getByExtension(this.getFileExtension())?.mimeType ?: "application/octet-stream" - inline fun catchError(tag: String = "", onError: () -> Unit = {}, block: () -> Unit) { try { block() @@ -273,16 +197,6 @@ inline fun catchError(tag: String = "", onError: () -> Unit = {}, block: () -> U } } -inline fun ignoreError(block: () -> T): T? { - try { - return block() - } catch (_: Exception) { - - } - return null -} - - fun getCurrentDate(reverseDays: Long = 0): String { val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) return formatter.format(Date(System.currentTimeMillis() - (reverseDays * 86400 * 1000))) @@ -294,33 +208,8 @@ fun getCurrentTime(): String { return formatter.format(currentTime) } -fun genRandomString( - length: Int = 32, - charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') -): String { - return (1..length) - .map { kotlin.random.Random.nextInt(0, charPool.size) } - .map(charPool::get) - .joinToString("") -} - fun genRandomHexString(length: Int = 32) = genRandomString(length, ('0'..'9') + ('a'..'f')) -fun compressGzip(input: String): ByteArray { - val byteArrayOutputStream = ByteArrayOutputStream() - GZIPOutputStream(byteArrayOutputStream).use { gzip -> - gzip.write(input.toByteArray()) - } - return byteArrayOutputStream.toByteArray() -} - -fun decompressGzip(compressed: ByteArray): String { - val byteArrayInputStream = ByteArrayInputStream(compressed) - GZIPInputStream(byteArrayInputStream).use { gzip -> - return gzip.bufferedReader().use { it.readText() } - } -} - fun readRawFile(id: Int) = app.resources.openRawResource(id).readBytes() @@ -340,6 +229,19 @@ fun showError(e: Throwable, tag: String = "") { ) } +fun getFileAccessUrl( + host: String = getLocalServerAddress(), + shareInfo: String, + fileName: String +): String { + return "${host}/api/download?s=${ + URLEncoder.encode( + shareInfo, + "UTF-8" + ) + }&accessKey=${mixFileServer.accessKey}#${fileName}" +} + fun getIpAddressInLocalNetwork(): String { val networkInterfaces = NetworkInterface.getNetworkInterfaces().iterator().asSequence() val localAddresses = networkInterfaces.flatMap { @@ -369,11 +271,6 @@ inline fun errorDialog(title: String, block: () -> Unit) { } } -@OptIn(InternalAPI::class) -fun FormBuilder.add(key: String, value: Any?, headers: Headers = Headers.Empty) { - append(key.quote(), value ?: "", headers) -} - fun formatTime(date: Date, format: String = "yyyy-MM-dd HH:mm:ss"): String { val formatter = SimpleDateFormat(format, Locale.US) return formatter.format(date) diff --git a/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt b/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt index 5f7c747..393c3a9 100644 --- a/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt @@ -37,6 +37,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import com.donut.mixfile.appScope import com.donut.mixfile.currentActivity +import com.donut.mixfile.server.core.utils.isNotNull import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.theme.MainTheme import com.donut.mixfile.ui.theme.colorScheme diff --git a/app/src/main/java/com/donut/mixfile/util/Json.kt b/app/src/main/java/com/donut/mixfile/util/Json.kt deleted file mode 100644 index 32d459d..0000000 --- a/app/src/main/java/com/donut/mixfile/util/Json.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.donut.mixfile.util - -import com.google.gson.GsonBuilder -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonToken -import com.google.gson.stream.JsonWriter -import java.io.IOException -import java.util.Date - -class TimestampAdapter : TypeAdapter() { - @Throws(IOException::class) - override fun write(out: JsonWriter, value: Date?) { - if (value == null) { - out.nullValue() - return - } - out.value(value.time) - } - - @Throws(IOException::class) - override fun read(`in`: JsonReader): Date? { - if (`in`.peek() === JsonToken.NULL) { - `in`.nextNull() - return null - } - return Date(`in`.nextLong()) - } -} - -val GSON = GsonBuilder().registerTypeAdapter(Date::class.java, TimestampAdapter()).create() - diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt b/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt index bca5982..a23140c 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileCard.kt @@ -32,13 +32,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.donut.mixfile.server.core.utils.parseFileMimeType +import com.donut.mixfile.server.core.utils.resolveMixShareInfo +import com.donut.mixfile.server.downloadUrl import com.donut.mixfile.server.serverStarted import com.donut.mixfile.ui.routes.home.tryResolveFile import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.cachedMutableOf import com.donut.mixfile.util.formatFileSize import com.donut.mixfile.util.formatTime -import com.donut.mixfile.util.parseFileMimeType import com.donut.mixfile.util.reveiver.NetworkChangeReceiver var filePreview by cachedMutableOf("关闭", "mix_file_preview") diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt b/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt index 8c31aa7..f3a7d50 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileDataLog.kt @@ -7,15 +7,16 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import com.donut.mixfile.server.accessKey -import com.donut.mixfile.server.utils.bean.MixShareInfo +import com.alibaba.fastjson2.annotation.JSONField +import com.donut.mixfile.server.core.utils.bean.MixShareInfo +import com.donut.mixfile.server.core.utils.resolveMixShareInfo import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.routes.autoAddFavorite import com.donut.mixfile.ui.routes.favorites.currentCategory import com.donut.mixfile.ui.routes.home.getLocalServerAddress import com.donut.mixfile.util.cachedMutableOf +import com.donut.mixfile.util.getFileAccessUrl import com.donut.mixfile.util.showToast -import java.net.URLEncoder import java.util.Date @@ -35,15 +36,9 @@ data class FileDataLog( category = category.trim() } + @get:JSONField(serialize = false) val downloadUrl: String - get() { - return "${getLocalServerAddress()}/api/download?s=${ - URLEncoder.encode( - shareInfoData, - "UTF-8" - ) - }&accessKey=$accessKey#${name}" - } + get() = getFileAccessUrl(getLocalServerAddress(), shareInfoData, name) fun updateDataList(list: List, action: (FileDataLog) -> FileDataLog) = list.map { if (it.shareInfoData == this.shareInfoData) { diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt b/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt index 4379189..a4e2a44 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt @@ -20,12 +20,16 @@ import androidx.core.net.toUri import com.donut.mixfile.activity.video.VideoActivity import com.donut.mixfile.app import com.donut.mixfile.currentActivity -import com.donut.mixfile.server.utils.bean.MixShareInfo +import com.donut.mixfile.server.core.utils.bean.MixShareInfo +import com.donut.mixfile.server.core.utils.shareCode +import com.donut.mixfile.server.downloadUrl +import com.donut.mixfile.server.lanUrl import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.routes.favorites.importFileList import com.donut.mixfile.ui.routes.favorites.openCategorySelect import com.donut.mixfile.ui.routes.home.DownloadTask import com.donut.mixfile.ui.routes.home.showDownloadTaskWindow +import com.donut.mixfile.ui.routes.useShortCode import com.donut.mixfile.ui.routes.useSystemPlayer import com.donut.mixfile.ui.theme.colorScheme import com.donut.mixfile.util.copyToClipboard @@ -55,7 +59,7 @@ fun showFileInfoDialog( InfoText(key = "密钥: ", value = shareInfo.key) FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) { AssistChip(onClick = { - shareInfo.shareCode().copyToClipboard() + shareInfo.shareCode(useShortCode).copyToClipboard() }, label = { Text(text = "复制分享码", color = colorScheme.primary) }) diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt b/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt index 52e1dfa..bb709b1 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt @@ -11,23 +11,23 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.alibaba.fastjson2.into +import com.alibaba.fastjson2.toJSONString import com.donut.mixfile.activity.video.VideoActivity import com.donut.mixfile.app import com.donut.mixfile.currentActivity -import com.donut.mixfile.server.localClient +import com.donut.mixfile.server.core.localClient +import com.donut.mixfile.server.core.utils.compressGzip +import com.donut.mixfile.server.core.utils.decompressGzip +import com.donut.mixfile.server.core.utils.hashSHA256 +import com.donut.mixfile.server.core.utils.parseFileMimeType +import com.donut.mixfile.server.core.utils.toHex import com.donut.mixfile.ui.component.common.MixDialogBuilder -import com.donut.mixfile.util.GSON -import com.donut.mixfile.util.compressGzip -import com.donut.mixfile.util.decompressGzip import com.donut.mixfile.util.formatFileSize import com.donut.mixfile.util.getCurrentTime -import com.donut.mixfile.util.hashSHA256 import com.donut.mixfile.util.objects.ProgressContent -import com.donut.mixfile.util.parseFileMimeType import com.donut.mixfile.util.showErrorDialog import com.donut.mixfile.util.showToast -import com.donut.mixfile.util.toHex -import com.donut.mixfile.util.toJsonString import io.ktor.client.plugins.onDownload import io.ktor.client.request.prepareGet import io.ktor.client.request.url @@ -41,7 +41,7 @@ import kotlinx.coroutines.withContext import kotlinx.io.readByteArray fun exportFileList(fileList: Collection, name: String) { - val strData = fileList.toJsonString() + val strData = fileList.toJSONString() val compressedData = compressGzip(strData) doUploadFile( compressedData, @@ -75,7 +75,7 @@ fun showExportFileListDialog(fileList: Collection) { } } -fun List.hashSHA256() = joinToString { it.shareInfoData }.hashSHA256().toHex() +fun List.hashSHA256(): String = joinToString { it.shareInfoData }.hashSHA256().toHex() fun showFileList(fileList: List) { @@ -134,7 +134,7 @@ suspend fun loadFileList(url: String, progressContent: ProgressContent): Array::class.java) + return@execute extractedData.into() } } catch (e: Exception) { withContext(Dispatchers.Main) { diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt b/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt index 4545f45..226222a 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt @@ -21,10 +21,9 @@ import androidx.compose.ui.window.DialogProperties import com.donut.mixfile.MainActivity import com.donut.mixfile.app import com.donut.mixfile.appScope -import com.donut.mixfile.server.StreamContent -import com.donut.mixfile.server.accessKey -import com.donut.mixfile.server.localClient -import com.donut.mixfile.server.utils.bean.MixShareInfo +import com.donut.mixfile.server.core.StreamContent +import com.donut.mixfile.server.core.localClient +import com.donut.mixfile.server.mixFileServer import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.routes.home.getLocalServerAddress import com.donut.mixfile.ui.routes.home.tryResolveFile @@ -72,7 +71,7 @@ suspend fun putUploadFile( onUpload(progressContent.ktorListener) parameter("name", name) parameter("add", add) - parameter("accessKey", accessKey) + parameter("accessKey", mixFileServer.accessKey) setBody(data) } val message = response.bodyAsText() @@ -257,6 +256,3 @@ suspend fun saveFileToStorage( return fileUri } -fun resolveMixShareInfo(value: String): MixShareInfo? { - return parseShareCode(value) -} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt b/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt index eb8f84c..c875ff0 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.donut.mixfile.genImageLoader +import com.donut.mixfile.server.core.utils.isTrue import com.donut.mixfile.ui.component.common.MixDialogBuilder -import com.donut.mixfile.util.isTrue import com.donut.mixfile.util.objects.ProgressContent import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.zoomable diff --git a/app/src/main/java/com/donut/mixfile/util/file/ShareCode.kt b/app/src/main/java/com/donut/mixfile/util/file/ShareCode.kt deleted file mode 100644 index 8c8b07f..0000000 --- a/app/src/main/java/com/donut/mixfile/util/file/ShareCode.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.donut.mixfile.util.file - -import com.donut.mixfile.server.utils.bean.MixShareInfo -import com.donut.mixfile.ui.routes.useShortCode -import com.donut.mixfile.util.decodeHex -import com.donut.mixfile.util.hashMD5 -import com.donut.mixfile.util.toHex - -private val encodeMap = run { - val map = mutableMapOf() - for (value in 0xfe00..0xfe0f) { - val key = (value - 0xfe00).toString(16) - map[key] = "${value.toChar()}" - } - map -} - -fun MixShareInfo.shareCode(): String { - if (useShortCode) { - return "mf://${encodeHex(this.toString())}${ - MixShareInfo.ENCODER.encode( - this.url.hashMD5().copyOf(6) - ) - }" - } - return "mf://$this" -} - -fun parseShareCode(code: String): MixShareInfo? { - val mf = code.substringAfter("mf://") - val encoded = decodeHex(mf) - val parsed = MixShareInfo.tryFromString(encoded) ?: MixShareInfo.tryFromString(mf) - return parsed -} - -fun encodeHex(data: String): String { - val sb = StringBuilder() - for (element in data.toByteArray().toHex()) { - if (encodeMap.containsKey(element.toString())) { - sb.append(encodeMap[element.toString()]) - } - } - return sb.toString() -} - -fun decodeHex(data: String): String { - val sb = StringBuilder() - for (element in data) { - if (encodeMap.containsValue(element.toString())) { - sb.append(encodeMap.filterValues { it == element.toString() }.keys.first()) - } - } - return sb.toString().decodeHex().decodeToString() -} \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/util/reveiver/MetworkReceiver.kt b/app/src/main/java/com/donut/mixfile/util/reveiver/MetworkReceiver.kt index 51f0af4..930d26d 100644 --- a/app/src/main/java/com/donut/mixfile/util/reveiver/MetworkReceiver.kt +++ b/app/src/main/java/com/donut/mixfile/util/reveiver/MetworkReceiver.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.donut.mixfile.appScope import com.donut.mixfile.server.FileService -import com.donut.mixfile.server.serverPort +import com.donut.mixfile.server.mixFileServer import com.donut.mixfile.ui.routes.home.serverAddress import com.donut.mixfile.util.getIpAddressInLocalNetwork import kotlinx.coroutines.launch @@ -35,6 +35,6 @@ object NetworkChangeReceiver : BroadcastReceiver() { appScope.launch { FileService.instance?.updateNotification() } - serverAddress = "http://${getIpAddressInLocalNetwork()}:$serverPort" + serverAddress = "http://${getIpAddressInLocalNetwork()}:${mixFileServer.serverPort}" } } \ No newline at end of file diff --git a/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt b/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt index e0667a6..89bb995 100644 --- a/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt +++ b/app/src/test/java/com/donut/mixfile/ExampleUnitTest.kt @@ -1,8 +1,11 @@ package com.donut.mixfile -import com.donut.mixfile.util.file.encodeHex +import com.alibaba.fastjson2.to +import com.alibaba.fastjson2.toJSONString +import com.donut.mixfile.server.core.utils.registerJson import kotlinx.coroutines.runBlocking import org.junit.Test +import java.util.Date /** @@ -13,11 +16,18 @@ import org.junit.Test class ExampleUnitTest { + data class User( + val age: Int, + val date: Date = Date() + ) @Test fun main() { + println("程序继续执行") + registerJson() runBlocking { - + println(User(1).toJSONString()) + println("{\"age\":1,\"date\":1742876318663}".to()) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8d819b4..bb9c163 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,11 +7,12 @@ composeVideo = "1.2.0" dec = "0.1.2" dkplayerJava = "3.3.7" easywindow = "10.6" -firebaseAnalytics = "22.3.0" +fastjson2Kotlin = "2.0.56" +firebaseAnalytics = "22.4.0" gifencoder = "0.10.1" giffle = "1.2.0" jvmbrotli = "0.2.0" -kotlin = "2.1.0" +kotlin = "2.1.20" coreKtx = "1.15.0" junit = "4.13.2" junitVersion = "1.2.1" @@ -21,17 +22,20 @@ activityCompose = "1.10.1" composeBom = "2024.08.00" ktorClientCio = "3.1.1" lz4Java = "1.8.0" +materialIconsExtended = "1.7.8" media3Exoplayer = "1.5.1" media3Session = "1.5.1" mimetypes = "0.0.3" -mmkv = "1.3.5" -navigationCompose = "2.8.8" +mmkv = "2.1.0" +navigationCompose = "2.8.9" zoomable = "1.6.1" googleServices = "4.4.2" [libraries] android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawable" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" } +fastjson2-kotlin = { module = "com.alibaba.fastjson2:fastjson2-kotlin", version.ref = "fastjson2Kotlin" } google-services = { module = "com.google.gms:google-services", version.ref = "googleServices" } androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" } @@ -52,6 +56,8 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics", versio gifencoder = { module = "com.squareup:gifencoder", version.ref = "gifencoder" } giffle = { module = "com.madgag:giffle", version.ref = "giffle" } jvmbrotli = { module = "com.nixxcode.jvmbrotli:jvmbrotli", version.ref = "jvmbrotli" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientCio" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClientCio" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCio" }