mirror of
https://github.com/InvertGeek/MixFile.git
synced 2026-06-04 02:21:31 +08:00
refine project structure
This commit is contained in:
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
/build
|
||||
/src/main/java/com/donut/mixfile/server/uploaders/hidden
|
||||
/src/main/java/com/donut/mixfile/server/core/uploaders/hidden
|
||||
@@ -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)
|
||||
|
||||
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
13
app/src/main/java/com/donut/mixfile/server/Utils.kt
Normal file
13
app/src/main/java/com/donut/mixfile/server/Utils.kt
Normal file
@@ -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)
|
||||
63
app/src/main/java/com/donut/mixfile/server/core/Client.kt
Normal file
63
app/src/main/java/com/donut/mixfile/server/core/Client.kt
Normal file
@@ -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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, (String) -> 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))
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<Pair<String, Int>>,
|
||||
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<Deferred<Unit>>()
|
||||
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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<String>().to<JSONObject>()
|
||||
|
||||
return result.getString("url")
|
||||
}
|
||||
}
|
||||
@@ -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> 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> T.toJsonString(): String = GSON.toJson(this)
|
||||
|
||||
|
||||
infix fun <T> T?.default(value: T) = this ?: value
|
||||
@@ -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<Date?> {
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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<String, String>()
|
||||
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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
111
app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt
Normal file
111
app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt
Normal file
@@ -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<Char> = ('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, ApplicationCall>.(Unit) -> Unit,
|
||||
): suspend PipelineContext<Unit, ApplicationCall>.(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 <T> 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)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
@@ -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)
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<String>,
|
||||
@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<String>,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun fromBytes(data: ByteArray): MixFile =
|
||||
GSON.fromJson(decompressGzip(data), MixFile::class.java)
|
||||
decompressGzip(data).to()
|
||||
}
|
||||
|
||||
fun getFileListByStartRange(startRange: Long): List<Pair<String, Int>> {
|
||||
@@ -161,6 +143,6 @@ data class MixFile(
|
||||
}
|
||||
|
||||
|
||||
fun toBytes() = compressGzip(GSON.toJson(this))
|
||||
fun toBytes() = compressGzip(this.toJSONString())
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
42
app/src/main/java/com/donut/mixfile/server/image/GIFUtils.kt
Normal file
42
app/src/main/java/com/donut/mixfile/server/image/GIFUtils.kt
Normal file
@@ -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()
|
||||
}
|
||||
@@ -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<JsonObject>()
|
||||
|
||||
return result.get("url").asString
|
||||
}
|
||||
}
|
||||
@@ -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, ApplicationCall>.(Unit) -> Unit,
|
||||
): suspend PipelineContext<Unit, ApplicationCall>.(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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <T> constructCachedMutableValue(
|
||||
value: T,
|
||||
@@ -58,15 +59,11 @@ inline fun <reified T, reified C : Iterable<T>> 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<C>() {}.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
|
||||
|
||||
@@ -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<String, Long> {
|
||||
}
|
||||
}
|
||||
|
||||
fun String.decodeHex(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
|
||||
class CachedDelegate<T>(val getKeys: () -> Array<Any?>, private val initializer: () -> T) {
|
||||
private var cache: T? = null
|
||||
@@ -119,46 +106,6 @@ class CachedDelegate<T>(val getKeys: () -> Array<Any?>, 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 <T> List<T>.elementEquals(other: List<T>): 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 <T> 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<Char> = ('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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Date?>() {
|
||||
@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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<FileDataLog>, action: (FileDataLog) -> FileDataLog) = list.map {
|
||||
if (it.shareInfoData == this.shareInfoData) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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<FileDataLog>, name: String) {
|
||||
val strData = fileList.toJsonString()
|
||||
val strData = fileList.toJSONString()
|
||||
val compressedData = compressGzip(strData)
|
||||
doUploadFile(
|
||||
compressedData,
|
||||
@@ -75,7 +75,7 @@ fun showExportFileListDialog(fileList: Collection<FileDataLog>) {
|
||||
}
|
||||
}
|
||||
|
||||
fun List<FileDataLog>.hashSHA256() = joinToString { it.shareInfoData }.hashSHA256().toHex()
|
||||
fun List<FileDataLog>.hashSHA256(): String = joinToString { it.shareInfoData }.hashSHA256().toHex()
|
||||
|
||||
|
||||
fun showFileList(fileList: List<FileDataLog>) {
|
||||
@@ -134,7 +134,7 @@ suspend fun loadFileList(url: String, progressContent: ProgressContent): Array<F
|
||||
}
|
||||
val data = it.bodyAsChannel().readRemaining(1024 * 1024 * 50).readByteArray()
|
||||
val extractedData = decompressGzip(data)
|
||||
return@execute GSON.fromJson(extractedData, Array<FileDataLog>::class.java)
|
||||
return@execute extractedData.into()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<String, String>()
|
||||
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()
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
@@ -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<User>())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user