diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e334a76..9ec8fbf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { applicationId = "com.donut.mixfile" minSdk = 26 targetSdk = 35 - versionCode = 137 - versionName = "1.17.7" + versionCode = 138 + versionName = "1.18.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt b/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt index 36f6eda..0fb348e 100644 --- a/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt +++ b/app/src/main/java/com/donut/mixfile/activity/video/player/PlayerView.kt @@ -1,7 +1,6 @@ package com.donut.mixfile.activity.video.player -import android.annotation.SuppressLint import android.net.Uri import android.widget.Toast import androidx.compose.foundation.background @@ -9,14 +8,12 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -34,6 +31,7 @@ import androidx.media3.ui.PlayerView import com.donut.mixfile.activity.video.VideoHistory import com.donut.mixfile.activity.video.playHistory import com.donut.mixfile.ui.theme.colorScheme +import com.donut.mixfile.util.ForceUpdateMutable import com.donut.mixfile.util.showToast import kotlinx.coroutines.delay import java.util.Locale @@ -58,20 +56,6 @@ val playerColorScheme ) -class ForceUpdateMutable(value: T) { - private var value by mutableStateOf(value) - var inc by mutableLongStateOf(0) - - val get get() = value - - fun set(value: T) { - this.value = value - inc++ - } -} - -@SuppressLint("PrivateResource") -@OptIn(ExperimentalMaterial3Api::class) @Composable fun VideoPlayerScreen( videoUris: List, @@ -99,7 +83,7 @@ fun VideoPlayerScreen( var currentMediaItem by remember { mutableIntStateOf(player.currentMediaItemIndex) } - var controlsVisible = remember { ForceUpdateMutable(true) } + val controlsVisible = remember { ForceUpdateMutable(true) } // 控制栏自动隐藏 diff --git a/app/src/main/java/com/donut/mixfile/server/FileService.kt b/app/src/main/java/com/donut/mixfile/server/FileService.kt index c520501..98d51d8 100644 --- a/app/src/main/java/com/donut/mixfile/server/FileService.kt +++ b/app/src/main/java/com/donut/mixfile/server/FileService.kt @@ -21,8 +21,8 @@ import com.donut.mixfile.server.core.MixFileServer import com.donut.mixfile.server.core.Uploader import com.donut.mixfile.server.core.routes.api.webdav.objects.WebDavManager import com.donut.mixfile.server.core.utils.MixUploadTask +import com.donut.mixfile.server.core.utils.extensions.kb import com.donut.mixfile.server.core.utils.ignoreError -import com.donut.mixfile.server.core.utils.kb import com.donut.mixfile.server.image.createBlankBitmap import com.donut.mixfile.server.image.toGif import com.donut.mixfile.ui.routes.favorites.result diff --git a/app/src/main/java/com/donut/mixfile/server/core/MixFileServer.kt b/app/src/main/java/com/donut/mixfile/server/core/MixFileServer.kt index 9fcc928..e4de520 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/MixFileServer.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/MixFileServer.kt @@ -4,8 +4,8 @@ package com.donut.mixfile.server.core import com.donut.mixfile.server.core.objects.MixShareInfo import com.donut.mixfile.server.core.routes.api.webdav.objects.WebDavManager import com.donut.mixfile.server.core.utils.MixUploadTask +import com.donut.mixfile.server.core.utils.extensions.mb import com.donut.mixfile.server.core.utils.findAvailablePort -import com.donut.mixfile.server.core.utils.mb import io.ktor.server.application.Application import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty diff --git a/app/src/main/java/com/donut/mixfile/server/core/Uploader.kt b/app/src/main/java/com/donut/mixfile/server/core/Uploader.kt index 75c6d00..96fd610 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/Uploader.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/Uploader.kt @@ -6,6 +6,9 @@ import com.donut.mixfile.server.core.utils.isValidURL import com.donut.mixfile.server.core.utils.retry import io.ktor.client.HttpClient import io.ktor.http.URLBuilder +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set abstract class Uploader(val name: String) { diff --git a/app/src/main/java/com/donut/mixfile/server/core/objects/FileDataLog.kt b/app/src/main/java/com/donut/mixfile/server/core/objects/FileDataLog.kt index 37e85ef..07e6cb1 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/objects/FileDataLog.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/objects/FileDataLog.kt @@ -11,9 +11,27 @@ data class FileDataLog( val name: String, val size: Long, val time: Long = System.currentTimeMillis(), - val category: String = "默认", + private var category: String = "默认", ) { + init { + sanitizeCategory() + } + + fun getCategory() = category + + fun setCategory(category: String) { + this.category = category + sanitizeCategory() + } + + fun sanitizeCategory() { + if (category.trim().isEmpty()) { + category = "默认" + } + category = category.take(20) + } + fun isSimilar(other: FileDataLog): Boolean { return other.shareInfoData.contentEquals(shareInfoData) } diff --git a/app/src/main/java/com/donut/mixfile/server/core/objects/MixShareInfo.kt b/app/src/main/java/com/donut/mixfile/server/core/objects/MixShareInfo.kt index bbc0c69..f3072c9 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/objects/MixShareInfo.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/objects/MixShareInfo.kt @@ -8,8 +8,8 @@ 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.extensions.mb import com.donut.mixfile.server.core.utils.hashMD5 -import com.donut.mixfile.server.core.utils.mb import com.donut.mixfile.server.core.utils.parseFileMimeType import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpRequestRetry diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt index 6f2b5f3..f10c1d5 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/Routes.kt @@ -2,7 +2,7 @@ package com.donut.mixfile.server.core.routes import com.donut.mixfile.server.core.MixFileServer import com.donut.mixfile.server.core.routes.api.getAPIRoute -import com.donut.mixfile.server.core.utils.paramPath +import com.donut.mixfile.server.core.utils.extensions.paramPath import com.donut.mixfile.server.core.utils.parseFileMimeType import io.ktor.http.HttpStatusCode import io.ktor.server.response.respond diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/api/DownloadRoute.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/api/DownloadRoute.kt index 448ab82..4ff27ed 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/api/DownloadRoute.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/api/DownloadRoute.kt @@ -5,8 +5,8 @@ import com.donut.mixfile.server.core.objects.MixFile import com.donut.mixfile.server.core.objects.MixShareInfo import com.donut.mixfile.server.core.utils.SortedTask import com.donut.mixfile.server.core.utils.encodeURL -import com.donut.mixfile.server.core.utils.ifNullOrBlank -import com.donut.mixfile.server.core.utils.mb +import com.donut.mixfile.server.core.utils.extensions.ifNullOrBlank +import com.donut.mixfile.server.core.utils.extensions.mb import com.donut.mixfile.server.core.utils.parseFileMimeType import com.donut.mixfile.server.core.utils.resolveMixShareInfo import io.ktor.http.HttpStatusCode diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/api/UploadRoute.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/api/UploadRoute.kt index 9020993..7cb6634 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/api/UploadRoute.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/api/UploadRoute.kt @@ -6,7 +6,7 @@ import com.donut.mixfile.server.core.aes.generateRandomByteArray import com.donut.mixfile.server.core.objects.MixFile import com.donut.mixfile.server.core.objects.MixShareInfo import com.donut.mixfile.server.core.utils.MixUploadTask -import com.donut.mixfile.server.core.utils.mb +import com.donut.mixfile.server.core.utils.extensions.mb import io.ktor.http.HttpStatusCode import io.ktor.server.request.contentLength import io.ktor.server.request.receiveChannel diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt index 962464f..fc20bd1 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/WebDavRoute.kt @@ -10,15 +10,15 @@ import com.donut.mixfile.server.core.routes.api.uploadFile import com.donut.mixfile.server.core.routes.api.webdav.objects.WebDavFile import com.donut.mixfile.server.core.routes.api.webdav.objects.WebDavManager import com.donut.mixfile.server.core.routes.api.webdav.objects.normalPath -import com.donut.mixfile.server.core.routes.api.webdav.objects.normalizePath -import com.donut.mixfile.server.core.utils.decodedPath +import com.donut.mixfile.server.core.routes.api.webdav.objects.parentPath +import com.donut.mixfile.server.core.routes.api.webdav.objects.pathFileName import com.donut.mixfile.server.core.utils.decompressGzip +import com.donut.mixfile.server.core.utils.extensions.decodedPath +import com.donut.mixfile.server.core.utils.extensions.mb +import com.donut.mixfile.server.core.utils.extensions.paramPath +import com.donut.mixfile.server.core.utils.extensions.routePrefix import com.donut.mixfile.server.core.utils.getHeader -import com.donut.mixfile.server.core.utils.mb -import com.donut.mixfile.server.core.utils.paramPath import com.donut.mixfile.server.core.utils.resolveMixShareInfo -import com.donut.mixfile.server.core.utils.routePrefix -import com.donut.mixfile.server.core.utils.sanitizeFileName import io.ktor.http.ContentType import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode @@ -49,10 +49,10 @@ val RoutingContext.davPath: String get() = paramPath val RoutingContext.davParentPath: String - get() = davPath.substringBeforeLast("/", "") + get() = davPath.parentPath() val RoutingContext.davFileName: String - get() = davPath.substringAfterLast("/").sanitizeFileName() + get() = davPath.pathFileName() val RoutingContext.davShareInfo: MixShareInfo? get() = resolveMixShareInfo( @@ -97,8 +97,12 @@ fun MixFileServer.getWebDAVRoute(): Route.() -> Unit { } webdav("GET") { if (davFileName.contentEquals("当前目录存档.mix_dav")) { - val fileList = webDav.listFilesRecursive(davParentPath) - val data = webDav.dataToBytes(fileList) + val file = webDav.getFile(davParentPath) + if (file == null) { + call.respond(HttpStatusCode.NotFound) + return@webdav + } + val data = webDav.dataToBytes(file.copy(name = "root")) val fileName = "${davParentPath.ifEmpty { "root" }}.mix_dav".encodeURLParameter() call.response.apply { header("Cache-Control", "no-cache, no-store, must-revalidate") @@ -134,14 +138,15 @@ fun MixFileServer.getWebDAVRoute(): Route.() -> Unit { val fileSize = call.request.contentLength() ?: 0 if (fileSize > 0 && fileSize < 50.mb) { if (davFileName.endsWith(".mix_dav")) { - val davFileList = + val davData = webDav.parseDataFromBytes(receiveBytes(50.mb)) - davFileList.forEach { (s, webDavFiles) -> - val path = normalizePath(s) - val newPath = normalizePath("${davParentPath}/${path}") - val fileList = webDav.WEBDAV_DATA.getOrDefault(newPath, HashSet()) - webDavFiles.addAll(fileList) - webDav.WEBDAV_DATA[newPath] = webDavFiles + val parentFile = webDav.getFile(davParentPath) + if (parentFile == null || !parentFile.isFolder) { + call.respond(HttpStatusCode.Conflict) + return@webdav + } + davData.files.forEach { + parentFile.addFile(it.value) } call.respond(HttpStatusCode.Created) webDav.saveData() @@ -151,16 +156,7 @@ fun MixFileServer.getWebDAVRoute(): Route.() -> Unit { val dataLogList = decompressGzip( receiveBytes(50.mb) ).into>() - dataLogList.forEach { - webDav.addFileNode( - davParentPath, - WebDavFile( - name = it.name, - shareInfoData = it.shareInfoData, - size = it.size - ) - ) - } + webDav.importMixList(dataLogList, davParentPath) call.respond(HttpStatusCode.Created) webDav.saveData() return@webdav diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/PathUtil.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/PathUtil.kt new file mode 100644 index 0000000..c112a03 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/PathUtil.kt @@ -0,0 +1,15 @@ +package com.donut.mixfile.server.core.routes.api.webdav.objects + +import com.donut.mixfile.server.core.utils.sanitizeFileName + +fun String?.normalPath() = normalizePath(this ?: "") + + +fun normalizePath(path: String): String { + if (path.isBlank()) return "" + return path.trim('/').replace(Regex("/+"), "/") +} + +fun String?.parentPath() = this.normalPath().substringBeforeLast("/", "") + +fun String?.pathFileName() = this.normalPath().substringAfterLast("/").sanitizeFileName() \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavFile.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavFile.kt index 7fed5f3..5be9651 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavFile.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavFile.kt @@ -12,30 +12,71 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set -fun WebDavFile.toDataLog() = FileDataLog(shareInfoData, name, size) +fun WebDavFile.toDataLog() = FileDataLog(shareInfoData, getName(), size) // WebDAV 文件类,包含额外属性 -class WebDavFile( - var name: String, +data class WebDavFile( + private var name: String, val size: Long = 0, val shareInfoData: String = "", val isFolder: Boolean = false, + val files: ConcurrentHashMap = ConcurrentHashMap(), var lastModified: Long = System.currentTimeMillis() ) { init { + sanitizeName() + } + + fun setName(name: String) { + this.name = name + sanitizeName() + } + + fun getName() = name + + fun sanitizeName() { name = name.trim().sanitizeFileName() } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is WebDavFile) return false + fun clone(): WebDavFile { - return name.contentEquals(other.name) + val newFiles = ConcurrentHashMap() + + files.forEach { (key, file) -> + newFiles[key] = file.clone() + } + + return copy(files = newFiles) } + fun addFile(file: WebDavFile) { + if (!this.isFolder) { + return + } + val existingFile = files[file.name] + if (existingFile != null && existingFile.isFolder && file.isFolder) { + file.files.forEach { (name, subFile) -> + if (subFile.isFolder) { + existingFile.addFile(subFile) + return@forEach + } + existingFile.files[name] = subFile.clone() + } + return + } + files[file.name] = file.clone() + } + + fun listFiles() = files.values.toList() + + @JSONField(serialize = false) fun getLastModifiedFormatted(): String { val sdf = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US) @@ -44,8 +85,6 @@ class WebDavFile( } - override fun hashCode(): Int = name.hashCode() - fun toXML(path: String, isRoot: Boolean = false): String { val pathName = name.takeIf { !isRoot } ?: "" if (isFolder) { diff --git a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavManager.kt b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavManager.kt index 1d16798..1b6bed3 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavManager.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/routes/api/webdav/objects/WebDavManager.kt @@ -2,6 +2,7 @@ package com.donut.mixfile.server.core.routes.api.webdav.objects import com.alibaba.fastjson2.into import com.alibaba.fastjson2.toJSONString +import com.donut.mixfile.server.core.objects.FileDataLog import com.donut.mixfile.server.core.utils.compressGzip import com.donut.mixfile.server.core.utils.decompressGzip import com.donut.mixfile.server.core.utils.resolveMixShareInfo @@ -10,18 +11,68 @@ import java.util.concurrent.ConcurrentHashMap open class WebDavManager { - var WEBDAV_DATA = ConcurrentHashMap>() + companion object { + const val VERSION_PREFIX = "V2_:\n" + } + + var WEBDAV_DATA = WebDavFile("root", isFolder = true) var loaded = true - fun dataToBytes(data: ConcurrentHashMap> = WEBDAV_DATA) = - compressGzip(data.toJSONString()) + fun dataToBytes(data: WebDavFile = WEBDAV_DATA) = + compressGzip(VERSION_PREFIX + data.toJSONString()) fun loadDataFromBytes(data: ByteArray) { WEBDAV_DATA = parseDataFromBytes(data) } - fun parseDataFromBytes(data: ByteArray): ConcurrentHashMap> = - decompressGzip(data).into() + fun parseDataFromBytes(data: ByteArray): WebDavFile { + val dataStr = decompressGzip(data) + if (dataStr.startsWith(VERSION_PREFIX)) { + return dataStr.substring(VERSION_PREFIX.length).into() + } + return loadLegacyData(dataStr) + } + + fun importMixList(list: List, path: String = "") { + list.forEach { + addFileNode(path, WebDavFile(it.getCategory(), isFolder = true)) + addFileNode( + "${path}/${it.getCategory()}".normalPath(), + WebDavFile( + name = it.name, + shareInfoData = it.shareInfoData, + size = it.size + ) + ) + } + } + + + private fun loadLegacyData(data: String): WebDavFile { + val davData: ConcurrentHashMap> = data.into() + val rootFile = WebDavFile("root", isFolder = true) + davData.forEach { (path, fileList) -> + if (path.isBlank()) return@forEach + val pathSegments = path.split("/").filter { it.isNotEmpty() } + var segmentFile = rootFile + + // 构建文件夹结构 + pathSegments.forEach { segment -> + segmentFile = segmentFile.files.getOrPut(segment) { + WebDavFile(name = segment, isFolder = true) + } + } + + fileList.forEach { + // 非文件夹则添加,添加文件夹可能覆盖已经有子files的文件夹 + if (!it.isFolder) { + segmentFile.files[it.getName()] = it + } + } + + } + return rootFile + } suspend fun saveData() { saveWebDavData(dataToBytes()) @@ -30,18 +81,13 @@ open class WebDavManager { open suspend fun saveWebDavData(data: ByteArray) {} // 添加文件或目录到指定路径 - open fun addFileNode(path: String, file: WebDavFile) { - val normalizedPath = normalizePath(path) - val fileList = WEBDAV_DATA.getOrPut(normalizedPath) { HashSet() } - synchronized(fileList) { - fileList.remove(file) - fileList.add(file) - WEBDAV_DATA[normalizedPath] = fileList - if (file.isFolder) { - val collectionPath = normalizePath("${normalizedPath}/${file.name}") - WEBDAV_DATA.getOrPut(collectionPath) { HashSet() } - } + open fun addFileNode(path: String, file: WebDavFile): Boolean { + val folder = getFile(path) ?: return false + if (!folder.isFolder) { + return false } + folder.addFile(file) + return true } open fun copyFile( @@ -55,123 +101,53 @@ open class WebDavManager { if (!overwrite && destFile != null) { return false } - getFile(dest.substringBeforeLast('/', "")) ?: return false - if (!keep) { - removeFileNode(path, false) - } - val destName = normalizePath(dest).substringAfterLast('/') + + val destName = dest.pathFileName() + addFileNode( - dest.substringBeforeLast('/', ""), - WebDavFile( - destName, - size = srcFile.size, + dest.parentPath(), + srcFile.copy( + name = destName, shareInfoData = srcFile.shareInfoData.let { val shareInfo = resolveMixShareInfo(srcFile.shareInfoData) shareInfo?.copy(fileName = destName)?.toString() ?: it - }, - isFolder = srcFile.isFolder, - lastModified = srcFile.lastModified - ) + }) ) - if (srcFile.isFolder) { - val fileList = listFiles(path) - fileList?.forEach { - val filePath = "${normalizePath(path)}/${it.name}" - copyFile(filePath, "${normalizePath(dest)}/${it.name}", overwrite, keep) - } + if (!keep) { + removeFileNode(path) } return true } // 删除指定路径的文件或目录 - open fun removeFileNode(path: String, removeFolder: Boolean = true) { + open fun removeFileNode(path: String): WebDavFile? { val normalizedPath = normalizePath(path) - val parentPath = normalizedPath.substringBeforeLast('/', "") - val name = normalizedPath.substringAfterLast('/') - val fileList = WEBDAV_DATA[parentPath] ?: return - synchronized(fileList) { - val node = fileList.firstOrNull { it.name.contentEquals(name) } - if (node != null) { - node.lastModified = System.currentTimeMillis() - fileList.remove(node) - // 如果是目录,递归删除其内容 - if (node.isFolder && removeFolder) { - val childPath = if (parentPath.isEmpty()) name else "$parentPath/$name" - removeCollectionContents(childPath) - } - WEBDAV_DATA[parentPath] = fileList - } - } + val parentPath = normalizedPath.parentPath() + val name = normalizedPath.pathFileName() + val parentFolder = getFile(parentPath) ?: return null + return parentFolder.files.remove(name) } + open fun getFile(path: String): WebDavFile? { val normalizedPath = normalizePath(path) - val parentPath = normalizedPath.substringBeforeLast('/', "") - val name = normalizedPath.substringAfterLast('/') - return getFile(parentPath, name) - } - - open fun getFile(path: String, name: String): WebDavFile? { - val normalizedPath = normalizePath(path) - val files = WEBDAV_DATA.getOrPut(normalizedPath) { HashSet() } - if (path.isEmpty() && name.isEmpty()) { - return WebDavFile(name = "root", isFolder = true) - } - synchronized(files) { - return files.firstOrNull { it.name.contentEquals(name) } + val pathSegments = normalizedPath.split("/").filter { it.isNotEmpty() } + var file = WEBDAV_DATA + for (segment in pathSegments) { + val pathFile = file.files[segment] ?: return null + file = pathFile } + return file } // 列出指定路径下的文件和目录 open fun listFiles(path: String): List? { - val normalizedPath = normalizePath(path) - val result = WEBDAV_DATA[normalizedPath] - if (result == null && path.isEmpty()) { - return emptyList() - } - return result?.toList() - } - - open fun listFilesRecursive(path: String): ConcurrentHashMap> { - val normalizedPath = normalizePath(path) - val result = ConcurrentHashMap>() - fun addFolder(path: String) { - val folderList = - WEBDAV_DATA.getOrDefault(normalizePath("$normalizedPath/$path"), mutableSetOf()) - result[path] = folderList - folderList.forEach { - if (it.isFolder) { - addFolder(normalizePath("$path/${it.name}")) - } - } - } - addFolder("") - return result + val folder = getFile(path) ?: return null + return folder.listFiles() } - // 辅助方法:递归删除目录及其内容 - private fun removeCollectionContents(collectionPath: String) { - val normalizedPath = normalizePath(collectionPath) - val fileList = WEBDAV_DATA[normalizedPath]?.toList() ?: return - fileList.forEach { node -> - node.lastModified = System.currentTimeMillis() - val childPath = normalizePath("$normalizedPath/${node.name}") - if (node.isFolder && node.name.isNotEmpty()) { - removeCollectionContents(childPath) // 递归删除子目录 - } - removeFileNode(childPath) // 删除子文件或目录 - } - WEBDAV_DATA.remove(normalizedPath) // 删除空目录 - } -} - -fun String?.normalPath() = normalizePath(this ?: "") - - -fun normalizePath(path: String): String { - if (path.isBlank()) return "" - return path.trim('/').replace(Regex("/+"), "/") } + diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt index 20aed05..895e1b6 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/Util.kt @@ -2,6 +2,7 @@ package com.donut.mixfile.server.core.utils import com.donut.mixfile.server.core.aes.generateRandomByteArray +import com.donut.mixfile.server.core.utils.extensions.mb import io.ktor.client.request.forms.FormBuilder import io.ktor.http.ContentType import io.ktor.http.Headers @@ -209,7 +210,7 @@ fun RoutingContext.getHeader(name: String) = call.request.header(name) fun findAvailablePort(startPort: Int = 9527, endPort: Int = 65535): Int? { for (port in startPort..endPort) { - ignoreError { + ignoreError { // 尝试绑定到指定端口 ServerSocket(port).use { serverSocket -> // 成功绑定,返回该端口 diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/extensions/Extension.kt similarity index 83% rename from app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt rename to app/src/main/java/com/donut/mixfile/server/core/utils/extensions/Extension.kt index 1293904..0c8b576 100644 --- a/app/src/main/java/com/donut/mixfile/server/core/utils/Extension.kt +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/extensions/Extension.kt @@ -1,10 +1,6 @@ -package com.donut.mixfile.server.core.utils +package com.donut.mixfile.server.core.utils.extensions + -import com.donut.mixfile.server.core.routes.api.webdav.objects.normalPath -import com.donut.mixfile.server.core.routes.api.webdav.objects.normalizePath -import io.ktor.http.decodeURLQueryComponent -import io.ktor.server.request.path -import io.ktor.server.routing.RoutingContext import java.nio.ByteBuffer import kotlin.streams.toList @@ -176,15 +172,4 @@ fun ByteArray.toInt(): Int = infix fun T?.default(value: T) = this ?: value -val RoutingContext.decodedPath: String get() = call.request.path().decodeURLQueryComponent() -val RoutingContext.paramPath: String - get() = normalizePath( - call.parameters.getAll("param")?.joinToString("/") ?: "" - ) - -val RoutingContext.routePrefix: String - get() { - val dPath = decodedPath.normalPath() - return dPath.take(dPath.length - paramPath.length).normalPath() - } diff --git a/app/src/main/java/com/donut/mixfile/server/core/utils/extensions/Ktor.kt b/app/src/main/java/com/donut/mixfile/server/core/utils/extensions/Ktor.kt new file mode 100644 index 0000000..1b9ec91 --- /dev/null +++ b/app/src/main/java/com/donut/mixfile/server/core/utils/extensions/Ktor.kt @@ -0,0 +1,20 @@ +package com.donut.mixfile.server.core.utils.extensions + +import com.donut.mixfile.server.core.routes.api.webdav.objects.normalPath +import com.donut.mixfile.server.core.routes.api.webdav.objects.normalizePath +import io.ktor.http.decodeURLQueryComponent +import io.ktor.server.request.path +import io.ktor.server.routing.RoutingContext + +val RoutingContext.decodedPath: String get() = call.request.path().decodeURLQueryComponent() + +val RoutingContext.paramPath: String + get() = normalizePath( + call.parameters.getAll("param")?.joinToString("/") ?: "" + ) + +val RoutingContext.routePrefix: String + get() { + val dPath = decodedPath.normalPath() + return dPath.take(dPath.length - paramPath.length).normalPath() + } \ No newline at end of file diff --git a/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt b/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt index 8cb449f..76f28b6 100644 --- a/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt +++ b/app/src/main/java/com/donut/mixfile/ui/nav/NavUtil.kt @@ -31,8 +31,8 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import com.donut.mixfile.server.core.utils.extensions.isNotNull import com.donut.mixfile.server.core.utils.genRandomString -import com.donut.mixfile.server.core.utils.isNotNull import java.lang.ref.WeakReference diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Dialogs.kt b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Dialogs.kt index 3e26fc7..9d5edfa 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Dialogs.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Dialogs.kt @@ -81,7 +81,7 @@ fun editCategory(name: String, callback: (String) -> Unit = {}) { currentCategory = newName showToast("修改分类名称成功") favorites = favorites.map { - if (it.category.contentEquals(name)) { + if (it.getCategory().contentEquals(name)) { it.copy(category = newName) } else { it @@ -104,7 +104,7 @@ fun deleteCategory(name: String, callback: (String) -> Unit = {}) { setPositiveButton("确定") { favCategories -= name favorites = favorites.filter { - it.category != name + it.getCategory() != name } showToast("删除分类成功") closeDialog() diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt index 85fdac2..ecaf9d0 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/favorites/Favorites.kt @@ -132,7 +132,7 @@ val Favorites = MixNavPage( favorites.asReversed() } result = result.filter { - currentCategory.isEmpty() || it.category == currentCategory + currentCategory.isEmpty() || it.getCategory() == currentCategory } when (favoriteSort) { "最新" -> result = result.sortedByDescending { it.time } @@ -261,7 +261,7 @@ val Favorites = MixNavPage( selected = emptySet() }, Pair("移动分类") { - openCategorySelect(selected.first().category) { category -> + openCategorySelect(selected.first().getCategory()) { category -> favorites = favorites.map { if (selected.contains(it)) it.copy(category = category) diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt b/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt index 91d4b05..3af50ba 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/home/Home.kt @@ -29,7 +29,7 @@ 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.core.utils.isFalse +import com.donut.mixfile.server.core.utils.extensions.isFalse import com.donut.mixfile.server.core.utils.resolveMixShareInfo import com.donut.mixfile.server.mixFileServer import com.donut.mixfile.ui.nav.MixNavPage diff --git a/app/src/main/java/com/donut/mixfile/ui/routes/webdav/Dialogs.kt b/app/src/main/java/com/donut/mixfile/ui/routes/webdav/Dialogs.kt index e3c8f39..8280f3c 100644 --- a/app/src/main/java/com/donut/mixfile/ui/routes/webdav/Dialogs.kt +++ b/app/src/main/java/com/donut/mixfile/ui/routes/webdav/Dialogs.kt @@ -34,7 +34,6 @@ import com.donut.mixfile.util.objects.ProgressContent import com.donut.mixfile.util.showToast import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.util.concurrent.ConcurrentHashMap fun clearWebDavData() { MixDialogBuilder("确定清空webdav数据?").apply { @@ -59,7 +58,7 @@ fun clearWebDavData() { showToast("请输入确认") return@setPositiveButton } - mixFileServer.webDav.WEBDAV_DATA.clear() + mixFileServer.webDav.WEBDAV_DATA.files.clear() kv.remove(WEB_DAV_KEY) showToast("数据已清空") closeDialog() @@ -112,17 +111,7 @@ fun importFileListToWebDav(url: String) { errorDialog("解析文件失败", onError = { closeDialog() }) { val dav = mixFileServer.webDav val fileList = loadFileList(url, progress) - fileList.forEach { - //创建分类文件夹 - dav.addFileNode( - "", - WebDavFile(it.category, isFolder = true) - ) - dav.addFileNode( - it.category, - WebDavFile(it.name, shareInfoData = it.shareInfoData, size = it.size) - ) - } + dav.importMixList(fileList) dav.saveData() showToast("导入完成") withContext(Dispatchers.Main) { @@ -192,14 +181,10 @@ fun tryImportWebDavData(code: String) { } } -suspend fun importWebDavData(data: ConcurrentHashMap>) { +suspend fun importWebDavData(data: WebDavFile) { val dav = mixFileServer.webDav - data.keys.forEach { key -> - val fileList = dav.WEBDAV_DATA.getOrDefault(key, mutableSetOf()) - val dataFileList = data.getOrDefault(key, mutableSetOf()) - fileList.removeAll(dataFileList) - fileList.addAll(dataFileList) - dav.WEBDAV_DATA[key] = fileList + data.files.forEach { + dav.WEBDAV_DATA.addFile(it.value) } dav.saveData() } diff --git a/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt b/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt index f5dd474..44ee3b0 100644 --- a/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/ComposeUtil.kt @@ -22,7 +22,11 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView @@ -37,7 +41,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.donut.mixfile.appScope import com.donut.mixfile.currentActivity -import com.donut.mixfile.server.core.utils.isNotNull +import com.donut.mixfile.server.core.utils.extensions.isNotNull import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.ui.theme.MainTheme import com.donut.mixfile.ui.theme.colorScheme @@ -78,6 +82,18 @@ fun OnDispose(block: () -> Unit) { } } +class ForceUpdateMutable(value: T) { + private var value by mutableStateOf(value) + var inc by mutableLongStateOf(0) + + val get get() = value + + fun set(value: T) { + this.value = value + inc++ + } +} + fun addComposeView( scheme: ColorScheme? = null, content: @Composable (removeView: () -> Unit) -> Unit diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt b/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt index 86c666a..52317bf 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileDialog.kt @@ -22,8 +22,8 @@ import com.donut.mixfile.currentActivity import com.donut.mixfile.server.core.objects.FileDataLog import com.donut.mixfile.server.core.objects.isImage import com.donut.mixfile.server.core.objects.isVideo +import com.donut.mixfile.server.core.utils.extensions.isTrue import com.donut.mixfile.server.core.utils.hashSHA256 -import com.donut.mixfile.server.core.utils.isTrue import com.donut.mixfile.server.core.utils.resolveMixShareInfo import com.donut.mixfile.server.core.utils.shareCode import com.donut.mixfile.server.core.utils.toHex @@ -107,14 +107,14 @@ fun showFileInfoDialog( Text(text = "重命名", color = colorScheme.primary) }) AssistChip(onClick = { - openCategorySelect(log.category) { category -> + openCategorySelect(log.getCategory()) { category -> favorites = log.updateDataList(favorites) { log.copy(category = category) } } }, label = { Text( - text = "分类: ${log.category}", + text = "分类: ${log.getCategory()}", color = colorScheme.primary ) }) diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt b/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt index 70e67c2..5e665b8 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileImport.kt @@ -56,7 +56,7 @@ fun showExportFileListDialog(fileList: Collection) { OutlinedTextField( value = listName, onValueChange = { - listName = it.sanitizeFileName() + listName = it }, modifier = Modifier.fillMaxWidth(), label = { @@ -69,7 +69,7 @@ fun showExportFileListDialog(fileList: Collection) { } setDefaultNegative() setPositiveButton("确定") { - exportFileList(fileList, listName) + exportFileList(fileList, listName.sanitizeFileName()) closeDialog() } show() @@ -138,7 +138,7 @@ fun showImportConfirmWindow(fileList: List) { val newFiles = mutableSetOf() val newCategories = mutableSetOf() fileList.forEach { - newCategories += it.category + newCategories += it.getCategory() if (!fileMap.contains(it.shareInfoData)) { newFiles += it } diff --git a/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt b/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt index eb51e8a..9d0ae81 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/FileUtil.kt @@ -21,8 +21,8 @@ import com.donut.mixfile.MainActivity import com.donut.mixfile.app import com.donut.mixfile.appScope import com.donut.mixfile.server.core.utils.StreamContent -import com.donut.mixfile.server.core.utils.kb -import com.donut.mixfile.server.core.utils.mb +import com.donut.mixfile.server.core.utils.extensions.kb +import com.donut.mixfile.server.core.utils.extensions.mb import com.donut.mixfile.server.core.utils.sanitizeFileName import com.donut.mixfile.server.mixFileServer import com.donut.mixfile.ui.component.common.MixDialogBuilder @@ -87,7 +87,7 @@ suspend fun putUploadFile( add: Boolean = true, progressContent: ProgressContent = ProgressContent(), ): String { - return errorDialog("上传失败") { + return errorDialog("上传失败") { val response = localClient.put { url("${getLocalServerAddress()}/api/upload") onUpload(progressContent.ktorListener) diff --git a/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt b/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt index c875ff0..8aee15d 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/ImageDialog.kt @@ -23,7 +23,7 @@ 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.server.core.utils.extensions.isTrue import com.donut.mixfile.ui.component.common.MixDialogBuilder import com.donut.mixfile.util.objects.ProgressContent import net.engawapg.lib.zoomable.rememberZoomState diff --git a/app/src/main/java/com/donut/mixfile/util/file/WebDavPreview.kt b/app/src/main/java/com/donut/mixfile/util/file/WebDavPreview.kt index fd7a0a0..7fd7656 100644 --- a/app/src/main/java/com/donut/mixfile/util/file/WebDavPreview.kt +++ b/app/src/main/java/com/donut/mixfile/util/file/WebDavPreview.kt @@ -66,7 +66,7 @@ fun DavFolder(file: WebDavFile, onClick: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Text( - text = file.name.trim(), + text = file.getName().trim(), color = colorScheme.primary, fontSize = 16.sp, maxLines = 2, @@ -117,7 +117,7 @@ fun showWebDavFileList(manager: WebDavManager) { if (!isActive) { throw Exception("排序取消") } - file1.name.compareByName(file2.name) + file1.getName().compareByName(file2.getName()) } withContext(Dispatchers.Main) { fileList = sorted @@ -165,7 +165,7 @@ fun showWebDavFileList(manager: WebDavManager) { } if (log.isFolder) { DavFolder(log) { - currentPath = normalizePath("${currentPath}/${log.name}") + currentPath = normalizePath("${currentPath}/${log.getName()}") } return@items } diff --git a/app/src/main/java/com/donut/mixfile/util/objects/UpdateChecker.kt b/app/src/main/java/com/donut/mixfile/util/objects/UpdateChecker.kt index aed23a3..3140883 100644 --- a/app/src/main/java/com/donut/mixfile/util/objects/UpdateChecker.kt +++ b/app/src/main/java/com/donut/mixfile/util/objects/UpdateChecker.kt @@ -1,6 +1,6 @@ package com.donut.mixfile.util.objects -import com.donut.mixfile.server.core.utils.isTrue +import com.donut.mixfile.server.core.utils.extensions.isTrue import org.jetbrains.annotations.Contract import java.io.IOException import java.net.HttpURLConnection diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7fcc2af..08095fd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Mar 11 12:26:58 CST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists