From f28e785301bd1296c455de6d523c2c8b12763f8e Mon Sep 17 00:00:00 2001 From: hstyi Date: Fri, 27 Jun 2025 12:10:56 +0800 Subject: [PATCH] feat: support WebDAV --- plugins/webdav/build.gradle.kts | 14 + .../plugins/webdav/WebDAVFileSystem.kt | 25 ++ .../webdav/WebDAVFileSystemProvider.kt | 144 +++++++++ .../plugins/webdav/WebDAVHostOptionsPane.kt | 285 ++++++++++++++++++ .../app/termora/plugins/webdav/WebDAVPath.kt | 19 ++ .../termora/plugins/webdav/WebDAVPlugin.kt | 33 ++ .../plugins/webdav/WebDAVProtocolHostPanel.kt | 36 +++ .../WebDAVProtocolHostPanelExtension.kt | 19 ++ .../plugins/webdav/WebDAVProtocolProvider.kt | 69 +++++ .../webdav/WebDAVProtocolProviderExtension.kt | 14 + .../src/main/resources/META-INF/plugin.xml | 24 ++ .../main/resources/META-INF/pluginIcon.svg | 1 + .../resources/META-INF/pluginIcon_dark.svg | 1 + settings.gradle.kts | 1 + src/main/kotlin/app/termora/Icons.kt | 2 + .../termora/transfer/TransferTableModel.kt | 2 +- src/main/resources/icons/dav.svg | 1 + src/main/resources/icons/dav_dark.svg | 1 + .../resources/icons/springCloudFileSet.svg | 7 + .../icons/springCloudFileSet_dark.svg | 7 + 20 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 plugins/webdav/build.gradle.kts create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystem.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystemProvider.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVHostOptionsPane.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPath.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPlugin.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanel.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanelExtension.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProvider.kt create mode 100644 plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProviderExtension.kt create mode 100644 plugins/webdav/src/main/resources/META-INF/plugin.xml create mode 100644 plugins/webdav/src/main/resources/META-INF/pluginIcon.svg create mode 100644 plugins/webdav/src/main/resources/META-INF/pluginIcon_dark.svg create mode 100644 src/main/resources/icons/dav.svg create mode 100644 src/main/resources/icons/dav_dark.svg create mode 100644 src/main/resources/icons/springCloudFileSet.svg create mode 100644 src/main/resources/icons/springCloudFileSet_dark.svg diff --git a/plugins/webdav/build.gradle.kts b/plugins/webdav/build.gradle.kts new file mode 100644 index 0000000..01cc273 --- /dev/null +++ b/plugins/webdav/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} + +project.version = "0.0.1" + +dependencies { + testImplementation(kotlin("test")) + implementation("com.github.lookfirst:sardine:5.13") + compileOnly(project(":")) +} + + +apply(from = "$rootDir/plugins/common.gradle.kts") diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystem.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystem.kt new file mode 100644 index 0000000..efb94c5 --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystem.kt @@ -0,0 +1,25 @@ +package app.termora.plugins.webdav + +import app.termora.transfer.s3.S3FileSystem +import app.termora.transfer.s3.S3Path +import com.github.sardine.Sardine + +class WebDAVFileSystem( + private val sardine: Sardine, endpoint: String, + authorization: String, +) : + S3FileSystem(WebDAVFileSystemProvider(sardine, endpoint, authorization)) { + + override fun create(root: String?, names: List): S3Path { + val path = WebDAVPath(this, root, names) + if (names.isEmpty()) { + path.attributes = path.attributes.copy(directory = true) + } + return path + } + + override fun close() { + sardine.shutdown() + super.close() + } +} diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystemProvider.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystemProvider.kt new file mode 100644 index 0000000..006921f --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVFileSystemProvider.kt @@ -0,0 +1,144 @@ +package app.termora.plugins.webdav + +import app.termora.Application +import app.termora.ResponseException +import app.termora.transfer.s3.S3FileSystemProvider +import app.termora.transfer.s3.S3Path +import com.github.sardine.Sardine +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody +import okio.BufferedSink +import okio.source +import org.apache.commons.io.IOUtils +import java.io.InputStream +import java.io.OutputStream +import java.io.PipedInputStream +import java.io.PipedOutputStream +import java.net.URI +import java.nio.file.AccessMode +import java.nio.file.NoSuchFileException +import java.nio.file.Path +import java.nio.file.attribute.FileAttribute +import java.util.concurrent.atomic.AtomicReference +import kotlin.io.path.absolutePathString +import kotlin.io.path.name + +class WebDAVFileSystemProvider( + private val sardine: Sardine, + private val endpoint: String, + private val authorization: String, +) : S3FileSystemProvider() { + + + override fun getScheme(): String? { + return "webdav" + } + + override fun getOutputStream(path: S3Path): OutputStream { + return createStreamer(path) + } + + override fun getInputStream(path: S3Path): InputStream { + return sardine.get(getFullUrl(path)) + } + + private fun createStreamer(path: S3Path): OutputStream { + val pis = PipedInputStream() + val pos = PipedOutputStream(pis) + val exception = AtomicReference() + + val thread = Thread.ofVirtual().start { + try { + val builder = Request.Builder() + .url("${endpoint}${path.absolutePathString()}") + .put(object : RequestBody() { + override fun contentType(): MediaType? { + return null + } + + override fun contentLength(): Long { + return -1 + } + + override fun writeTo(sink: BufferedSink) { + pis.source().use { sink.writeAll(it) } + } + + }) + + if (authorization.isNotBlank()) + builder.header("Authorization", authorization) + + // sardine 会重试,这里使用 okhttp + val response = Application.httpClient.newCall(builder.build()).execute() + IOUtils.closeQuietly(response) + if (response.isSuccessful.not()) { + throw ResponseException(response.code, response) + } + } catch (e: Exception) { + e.printStackTrace() + exception.set(e) + } finally { + IOUtils.closeQuietly(pis) + } + } + + return object : OutputStream() { + override fun write(b: Int) { + val exception = exception.get() + if (exception != null) throw exception + pos.write(b) + } + + override fun close() { + pos.close() + if (thread.isAlive) thread.join() + } + } + + } + + override fun fetchChildren(path: S3Path): MutableList { + val paths = mutableListOf() + val resources = sardine.list(getFullUrl(path)) + for (i in 1 until resources.size) { + val resource = resources[i] + val p = path.resolve(resource.name) + p.attributes = p.attributes.copy( + directory = resource.isDirectory, + regularFile = resource.isDirectory.not(), + size = resource.contentLength, + lastModifiedTime = resource.modified.time, + ) + paths.add(p) + } + return paths + + } + + override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) { + sardine.createDirectory(getFullUrl(dir)) + } + + override fun delete(path: S3Path, isDirectory: Boolean) { + sardine.delete(getFullUrl(path)) + } + + override fun checkAccess(path: S3Path, vararg modes: AccessMode) { + try { + if (sardine.exists(getFullUrl(path)).not()) { + throw NoSuchFileException(path.name) + } + } catch (e: Exception) { + if (e is NoSuchFileException) throw e + throw NoSuchFileException(e.message) + } + } + + private fun getFullUrl(path: Path): String { + val pathname = URI(null, null, path.absolutePathString(), null).toString() + return "${endpoint}${pathname}" + } + +} \ No newline at end of file diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVHostOptionsPane.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVHostOptionsPane.kt new file mode 100644 index 0000000..4d8fc6c --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVHostOptionsPane.kt @@ -0,0 +1,285 @@ +package app.termora.plugins.webdav + +import app.termora.* +import app.termora.plugin.internal.BasicProxyOption +import com.formdev.flatlaf.FlatClientProperties +import com.formdev.flatlaf.ui.FlatTextBorder +import com.jgoodies.forms.builder.FormBuilder +import com.jgoodies.forms.layout.FormLayout +import org.apache.commons.lang3.StringUtils +import java.awt.BorderLayout +import java.awt.KeyboardFocusManager +import java.awt.event.ComponentAdapter +import java.awt.event.ComponentEvent +import javax.swing.* + +class WebDAVHostOptionsPane : OptionsPane() { + private val generalOption = GeneralOption() + private val proxyOption = BasicProxyOption(listOf(ProxyType.HTTP)) + private val sftpOption = SFTPOption() + + init { + addOption(generalOption) + addOption(proxyOption) + addOption(sftpOption) + + } + + fun getHost(): Host { + val name = generalOption.nameTextField.text + val protocol = WebDAVProtocolProvider.PROTOCOL + val port = 0 + var authentication = Authentication.Companion.No + var proxy = Proxy.Companion.No + val authenticationType = AuthenticationType.Password + + authentication = authentication.copy( + type = authenticationType, + password = String(generalOption.passwordTextField.password) + ) + + + if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) { + proxy = proxy.copy( + type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType, + host = proxyOption.proxyHostTextField.text, + username = proxyOption.proxyUsernameTextField.text, + password = String(proxyOption.proxyPasswordTextField.password), + port = proxyOption.proxyPortTextField.value as Int, + authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType, + ) + } + + + val options = Options.Default.copy(sftpDefaultDirectory = sftpOption.defaultDirectoryField.text) + + return Host( + name = name, + protocol = protocol, + port = port, + host = generalOption.endpointTextField.text, + username = generalOption.usernameTextField.text, + authentication = authentication, + proxy = proxy, + sort = System.currentTimeMillis(), + remark = generalOption.remarkTextArea.text, + options = options, + ) + } + + fun setHost(host: Host) { + generalOption.nameTextField.text = host.name + generalOption.usernameTextField.text = host.username + generalOption.remarkTextArea.text = host.remark + generalOption.passwordTextField.text = host.authentication.password + generalOption.endpointTextField.text = host.host + + proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type + proxyOption.proxyHostTextField.text = host.proxy.host + proxyOption.proxyPasswordTextField.text = host.proxy.password + proxyOption.proxyUsernameTextField.text = host.proxy.username + proxyOption.proxyPortTextField.value = host.proxy.port + proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType + + + sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory + } + + fun validateFields(): Boolean { + val host = getHost() + + // general + if (validateField(generalOption.nameTextField)) { + return false + } + + + if (validateField(generalOption.endpointTextField)) { + return false + } + + if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) { + if (validateField(generalOption.usernameTextField)) { + return false + } + + if (validateField(generalOption.passwordTextField)) { + return false + } + } + + // proxy + if (host.proxy.type != ProxyType.No) { + if (validateField(proxyOption.proxyHostTextField) + ) { + return false + } + + if (host.proxy.authenticationType != AuthenticationType.No) { + if (validateField(proxyOption.proxyUsernameTextField) + || validateField(proxyOption.proxyPasswordTextField) + ) { + return false + } + } + } + + return true + } + + /** + * 返回 true 表示有错误 + */ + private fun validateField(textField: JTextField): Boolean { + if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) { + setOutlineError(textField) + return true + } + return false + } + + private fun setOutlineError(c: JComponent) { + selectOptionJComponent(c) + c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR) + c.requestFocusInWindow() + } + + + private inner class GeneralOption : JPanel(BorderLayout()), Option { + val nameTextField = OutlineTextField(128) + val usernameTextField = OutlineTextField(128) + val passwordTextField = OutlinePasswordField(256) + val endpointTextField = OutlineTextField(256) + val remarkTextArea = FixedLengthTextArea(512) + + init { + initView() + initEvents() + } + + private fun initView() { + add(getCenterComponent(), BorderLayout.CENTER) + } + + private fun initEvents() { + + addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() } + removeComponentListener(this) + } + }) + } + + + override fun getIcon(isSelected: Boolean): Icon { + return Icons.settings + } + + override fun getTitle(): String { + return I18n.getString("termora.new-host.general") + } + + override fun getJComponent(): JComponent { + return this + } + + private fun getCenterComponent(): JComponent { + val layout = FormLayout( + "left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default", + "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref" + ) + remarkTextArea.setFocusTraversalKeys( + KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS) + ) + remarkTextArea.setFocusTraversalKeys( + KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS) + ) + + remarkTextArea.rows = 8 + remarkTextArea.lineWrap = true + remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4) + + + var rows = 1 + val step = 2 + val panel = FormBuilder.create().layout(layout) + .add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows) + .add(nameTextField).xyw(3, rows, 5).apply { rows += step } + + .add("Endpoint:").xy(1, rows) + .add(endpointTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows) + .add(usernameTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows) + .add(passwordTextField).xyw(3, rows, 5).apply { rows += step } + + .add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows) + .add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() }) + .xyw(3, rows, 5).apply { rows += step } + + .build() + + + return panel + } + + } + + + private inner class SFTPOption : JPanel(BorderLayout()), Option { + val defaultDirectoryField = OutlineTextField(255) + + + init { + initView() + initEvents() + } + + private fun initView() { + add(getCenterComponent(), BorderLayout.CENTER) + } + + private fun initEvents() { + + } + + + override fun getIcon(isSelected: Boolean): Icon { + return Icons.folder + } + + override fun getTitle(): String { + return I18n.getString("termora.transport.sftp") + } + + override fun getJComponent(): JComponent { + return this + } + + private fun getCenterComponent(): JComponent { + val layout = FormLayout( + "left:pref, $FORM_MARGIN, default:grow", + "pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref" + ) + + var rows = 1 + val step = 2 + val panel = FormBuilder.create().layout(layout) + .add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows) + .add(defaultDirectoryField).xy(3, rows).apply { rows += step } + .build() + + + return panel + } + } + + +} \ No newline at end of file diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPath.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPath.kt new file mode 100644 index 0000000..cf49a96 --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPath.kt @@ -0,0 +1,19 @@ +package app.termora.plugins.webdav + +import app.termora.transfer.s3.S3FileSystem +import app.termora.transfer.s3.S3Path + +class WebDAVPath(fileSystem: S3FileSystem, root: String?, names: List) : S3Path(fileSystem, root, names) { + override val isBucket: Boolean + get() = false + + override val bucketName: String + get() = throw UnsupportedOperationException() + + override val objectName: String + get() = throw UnsupportedOperationException() + + override fun getCustomType(): String? { + return null + } +} \ No newline at end of file diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPlugin.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPlugin.kt new file mode 100644 index 0000000..8b146e2 --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVPlugin.kt @@ -0,0 +1,33 @@ +package app.termora.plugins.webdav + +import app.termora.plugin.Extension +import app.termora.plugin.ExtensionSupport +import app.termora.plugin.PaidPlugin +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProviderExtension + +class WebDAVPlugin : PaidPlugin { + private val support = ExtensionSupport() + + init { + support.addExtension(ProtocolProviderExtension::class.java) { WebDAVProtocolProviderExtension.Companion.instance } + support.addExtension(ProtocolHostPanelExtension::class.java) { WebDAVProtocolHostPanelExtension.Companion.instance } + } + + override fun getAuthor(): String { + return "TermoraDev" + } + + + override fun getName(): String { + return "WebDAV" + } + + + + override fun getExtensions(clazz: Class): List { + return support.getExtensions(clazz) + } + + +} \ No newline at end of file diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanel.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanel.kt new file mode 100644 index 0000000..cfa5c69 --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanel.kt @@ -0,0 +1,36 @@ +package app.termora.plugins.webdav + +import app.termora.Disposer +import app.termora.Host +import app.termora.protocol.ProtocolHostPanel +import java.awt.BorderLayout + +class WebDAVProtocolHostPanel : ProtocolHostPanel() { + + private val pane = WebDAVHostOptionsPane() + + init { + initView() + initEvents() + } + + + private fun initView() { + add(pane, BorderLayout.CENTER) + Disposer.register(this, pane) + } + + private fun initEvents() {} + + override fun getHost(): Host { + return pane.getHost() + } + + override fun setHost(host: Host) { + pane.setHost(host) + } + + override fun validateFields(): Boolean { + return pane.validateFields() + } +} \ No newline at end of file diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanelExtension.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanelExtension.kt new file mode 100644 index 0000000..0123481 --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolHostPanelExtension.kt @@ -0,0 +1,19 @@ +package app.termora.plugins.webdav + +import app.termora.protocol.ProtocolHostPanel +import app.termora.protocol.ProtocolHostPanelExtension +import app.termora.protocol.ProtocolProvider + +class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension { + companion object { + val instance by lazy { WebDAVProtocolHostPanelExtension() } + } + + override fun getProtocolProvider(): ProtocolProvider { + return WebDAVProtocolProvider.instance + } + + override fun createProtocolHostPanel(): ProtocolHostPanel { + return WebDAVProtocolHostPanel() + } +} \ No newline at end of file diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProvider.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProvider.kt new file mode 100644 index 0000000..5de2dc3 --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProvider.kt @@ -0,0 +1,69 @@ +package app.termora.plugins.webdav + +import app.termora.AuthenticationType +import app.termora.DynamicIcon +import app.termora.Icons +import app.termora.ProxyType +import app.termora.protocol.PathHandler +import app.termora.protocol.PathHandlerRequest +import app.termora.protocol.TransferProtocolProvider +import com.github.sardine.SardineFactory +import okhttp3.Credentials +import org.apache.commons.lang3.StringUtils +import java.io.IOException +import java.net.* + + +class WebDAVProtocolProvider private constructor() : TransferProtocolProvider { + + companion object { + val instance by lazy { WebDAVProtocolProvider() } + const val PROTOCOL = "WebDAV" + } + + override fun getProtocol(): String { + return PROTOCOL + } + + override fun getIcon(width: Int, height: Int): DynamicIcon { + return Icons.dav + } + + override fun createPathHandler(requester: PathHandlerRequest): PathHandler { + val host = requester.host + + val sardine = if (host.authentication.type != AuthenticationType.No) { + if (host.proxy.type != ProxyType.No) { + SardineFactory.begin(host.username, host.authentication.password, object : ProxySelector() { + override fun select(uri: URI): List { + if (host.proxy.type == ProxyType.HTTP) { + return listOf(Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))) + } + return listOf(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))) + } + + override fun connectFailed( + uri: URI, + sa: SocketAddress, + ioe: IOException + ) { + throw ioe + } + }) + } else { + SardineFactory.begin(host.username, host.authentication.password) + } + } else { + SardineFactory.begin() + } + + val authorization = if (host.authentication.type != AuthenticationType.No) + Credentials.basic(host.username, host.authentication.password) else StringUtils.EMPTY + val defaultPath = host.options.sftpDefaultDirectory + val fs = WebDAVFileSystem(sardine, StringUtils.removeEnd(host.host, "/"), authorization) + return PathHandler(fs, fs.getPath(defaultPath)) + + } + + +} \ No newline at end of file diff --git a/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProviderExtension.kt b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProviderExtension.kt new file mode 100644 index 0000000..c245996 --- /dev/null +++ b/plugins/webdav/src/main/kotlin/app/termora/plugins/webdav/WebDAVProtocolProviderExtension.kt @@ -0,0 +1,14 @@ +package app.termora.plugins.webdav + +import app.termora.protocol.ProtocolProvider +import app.termora.protocol.ProtocolProviderExtension + +class WebDAVProtocolProviderExtension private constructor() : ProtocolProviderExtension { + companion object { + val instance by lazy { WebDAVProtocolProviderExtension() } + } + + override fun getProtocolProvider(): ProtocolProvider { + return WebDAVProtocolProvider.instance + } +} \ No newline at end of file diff --git a/plugins/webdav/src/main/resources/META-INF/plugin.xml b/plugins/webdav/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..315105b --- /dev/null +++ b/plugins/webdav/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,24 @@ + + + webdav + + WebDAV + + + + ${projectVersion} + + + + app.termora.plugins.webdav.WebDAVPlugin + + + Connecting to WebDAV + 支持连接到 WebDAV + 支援連接到 WebDAV + + + TermoraDev + + + diff --git a/plugins/webdav/src/main/resources/META-INF/pluginIcon.svg b/plugins/webdav/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 0000000..ea69227 --- /dev/null +++ b/plugins/webdav/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/webdav/src/main/resources/META-INF/pluginIcon_dark.svg b/plugins/webdav/src/main/resources/META-INF/pluginIcon_dark.svg new file mode 100644 index 0000000..f0f53d1 --- /dev/null +++ b/plugins/webdav/src/main/resources/META-INF/pluginIcon_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ecb23dc..551013c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,3 +13,4 @@ include("plugins:sync") include("plugins:migration") include("plugins:editor") include("plugins:geo") +include("plugins:webdav") diff --git a/src/main/kotlin/app/termora/Icons.kt b/src/main/kotlin/app/termora/Icons.kt index 1dfc370..a441861 100644 --- a/src/main/kotlin/app/termora/Icons.kt +++ b/src/main/kotlin/app/termora/Icons.kt @@ -159,4 +159,6 @@ object Icons { val desktop_mac by lazy { DynamicIcon("icons/desktop_mac.svg", "icons/desktop_mac_dark.svg") } val desktop by lazy { DynamicIcon("icons/desktop.svg", "icons/desktop_dark.svg") } val moreHorizontal by lazy { DynamicIcon("icons/moreHorizontal.svg", "icons/moreHorizontal_dark.svg") } + val springCloudFileSet by lazy { DynamicIcon("icons/springCloudFileSet.svg", "icons/springCloudFileSet_dark.svg") } + val dav by lazy { DynamicIcon("icons/dav.svg", "icons/dav_dark.svg") } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/transfer/TransferTableModel.kt b/src/main/kotlin/app/termora/transfer/TransferTableModel.kt index 1890fad..d6277dc 100644 --- a/src/main/kotlin/app/termora/transfer/TransferTableModel.kt +++ b/src/main/kotlin/app/termora/transfer/TransferTableModel.kt @@ -436,7 +436,7 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) : } } } catch (e: Exception) { - tryChangeState(node, State.Failed) + withContext(Dispatchers.Swing) { tryChangeState(node, State.Failed) } if (e !is UserCanceledException) { node.setException(e) throw e diff --git a/src/main/resources/icons/dav.svg b/src/main/resources/icons/dav.svg new file mode 100644 index 0000000..ea69227 --- /dev/null +++ b/src/main/resources/icons/dav.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/icons/dav_dark.svg b/src/main/resources/icons/dav_dark.svg new file mode 100644 index 0000000..f0f53d1 --- /dev/null +++ b/src/main/resources/icons/dav_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/icons/springCloudFileSet.svg b/src/main/resources/icons/springCloudFileSet.svg new file mode 100644 index 0000000..86ce899 --- /dev/null +++ b/src/main/resources/icons/springCloudFileSet.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/icons/springCloudFileSet_dark.svg b/src/main/resources/icons/springCloudFileSet_dark.svg new file mode 100644 index 0000000..cc994a4 --- /dev/null +++ b/src/main/resources/icons/springCloudFileSet_dark.svg @@ -0,0 +1,7 @@ + + + + + + +