refine project structure

This commit is contained in:
Evan You
2025-03-26 11:35:09 +08:00
parent 64d43eb38d
commit 6a5576d1b0
55 changed files with 872 additions and 696 deletions

2
app/.gitignore vendored
View File

@@ -1,2 +1,2 @@
/build
/src/main/java/com/donut/mixfile/server/uploaders/hidden
/src/main/java/com/donut/mixfile/server/core/uploaders/hidden

View File

@@ -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)

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)
)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View 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)

View 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
}

View File

@@ -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)
}
}

View File

@@ -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))
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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())
}
}
}

View File

@@ -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
}
}

View File

@@ -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")
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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()
}

View 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)
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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())
}

View File

@@ -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;

View 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()
}

View File

@@ -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
}
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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,

View File

@@ -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 = {

View File

@@ -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}"
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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")

View File

@@ -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) {

View File

@@ -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)
})

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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}"
}
}

View File

@@ -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>())
}
}

View File

@@ -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" }