mirror of
https://github.com/InvertGeek/MixFile.git
synced 2026-09-03 07:18:45 +08:00
1.18.0
update webdav data structure
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
// 控制栏自动隐藏
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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) {
|
||||
|
||||
@@ -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("/+"), "/")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
// 成功绑定,返回该端口
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user