update webdav data structure
This commit is contained in:
1
2025-06-08 11:23:30 +08:00
parent 7d6789426b
commit 4254da04a7
30 changed files with 272 additions and 234 deletions

View File

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

View File

@@ -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<T>(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<Uri>,
@@ -99,7 +83,7 @@ fun VideoPlayerScreen(
var currentMediaItem by remember { mutableIntStateOf(player.currentMediaItemIndex) }
var controlsVisible = remember { ForceUpdateMutable(true) }
val controlsVisible = remember { ForceUpdateMutable(true) }
// 控制栏自动隐藏

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String, WebDavFile> = 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<String, WebDavFile>()
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) {

View File

@@ -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<String, MutableSet<WebDavFile>>()
companion object {
const val VERSION_PREFIX = "V2_:\n"
}
var WEBDAV_DATA = WebDavFile("root", isFolder = true)
var loaded = true
fun dataToBytes(data: ConcurrentHashMap<String, MutableSet<WebDavFile>> = 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<String, MutableSet<WebDavFile>> =
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<FileDataLog>, 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<String, MutableSet<WebDavFile>> = 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<WebDavFile>? {
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<String, MutableSet<WebDavFile>> {
val normalizedPath = normalizePath(path)
val result = ConcurrentHashMap<String, MutableSet<WebDavFile>>()
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("/+"), "/")
}

View File

@@ -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<Int> {
// 尝试绑定到指定端口
ServerSocket(port).use { serverSocket ->
// 成功绑定,返回该端口

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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<String, MutableSet<WebDavFile>>) {
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()
}

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ fun showExportFileListDialog(fileList: Collection<FileDataLog>) {
OutlinedTextField(
value = listName,
onValueChange = {
listName = it.sanitizeFileName()
listName = it
},
modifier = Modifier.fillMaxWidth(),
label = {
@@ -69,7 +69,7 @@ fun showExportFileListDialog(fileList: Collection<FileDataLog>) {
}
setDefaultNegative()
setPositiveButton("确定") {
exportFileList(fileList, listName)
exportFileList(fileList, listName.sanitizeFileName())
closeDialog()
}
show()
@@ -138,7 +138,7 @@ fun showImportConfirmWindow(fileList: List<FileDataLog>) {
val newFiles = mutableSetOf<FileDataLog>()
val newCategories = mutableSetOf<String>()
fileList.forEach {
newCategories += it.category
newCategories += it.getCategory()
if (!fileMap.contains(it.shareInfoData)) {
newFiles += it
}

View File

@@ -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<String>("上传失败") {
val response = localClient.put {
url("${getLocalServerAddress()}/api/upload")
onUpload(progressContent.ktorListener)

View File

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

View File

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

View File

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

View File

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