mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: support WebDAV
This commit is contained in:
14
plugins/webdav/build.gradle.kts
Normal file
14
plugins/webdav/build.gradle.kts
Normal file
@@ -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")
|
||||
@@ -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<String>): 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()
|
||||
}
|
||||
}
|
||||
@@ -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<Throwable>()
|
||||
|
||||
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<S3Path> {
|
||||
val paths = mutableListOf<S3Path>()
|
||||
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}"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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<String>) : 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
|
||||
}
|
||||
}
|
||||
@@ -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 <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<Proxy> {
|
||||
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))
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
24
plugins/webdav/src/main/resources/META-INF/plugin.xml
Normal file
24
plugins/webdav/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>webdav</id>
|
||||
|
||||
<name>WebDAV</name>
|
||||
|
||||
<paid/>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.webdav.WebDAVPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to WebDAV</description>
|
||||
<description language="zh_CN">支持连接到 WebDAV</description>
|
||||
<description language="zh_TW">支援連接到 WebDAV</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
@@ -0,0 +1 @@
|
||||
<svg t="1750996462119" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1582" width="16" height="16"><path d="M933.875 828.40625H591.1015625v-52.734375h290.0390625V142.859375H628.38476563l-105.46875001 105.46875H142.859375v527.34375h290.0390625v52.734375H90.125V195.59375h410.95898438l105.46874999-105.46875H933.875z m-158.203125 52.734375h52.734375v52.734375h-52.734375z m105.46875 0h52.734375v52.734375h-52.734375zM90.125 881.140625h52.734375v52.734375H90.125z m105.46875 0h52.734375v52.734375h-52.734375zM301.0625 881.140625h421.875v52.734375H301.0625z" p-id="1583" fill="#6C707E"></path><path d="M485.6328125 775.671875h52.734375v158.203125h-52.734375zM116.4921875 670.203125h791.015625v52.734375H116.4921875z" p-id="1584" fill="#6C707E"></path></svg>
|
||||
|
After Width: | Height: | Size: 798 B |
@@ -0,0 +1 @@
|
||||
<svg t="1750996462119" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1582" width="16" height="16"><path d="M933.875 828.40625H591.1015625v-52.734375h290.0390625V142.859375H628.38476563l-105.46875001 105.46875H142.859375v527.34375h290.0390625v52.734375H90.125V195.59375h410.95898438l105.46874999-105.46875H933.875z m-158.203125 52.734375h52.734375v52.734375h-52.734375z m105.46875 0h52.734375v52.734375h-52.734375zM90.125 881.140625h52.734375v52.734375H90.125z m105.46875 0h52.734375v52.734375h-52.734375zM301.0625 881.140625h421.875v52.734375H301.0625z" p-id="1583" fill="#CED0D6"></path><path d="M485.6328125 775.671875h52.734375v158.203125h-52.734375zM116.4921875 670.203125h791.015625v52.734375H116.4921875z" p-id="1584" fill="#CED0D6"></path></svg>
|
||||
|
After Width: | Height: | Size: 798 B |
@@ -13,3 +13,4 @@ include("plugins:sync")
|
||||
include("plugins:migration")
|
||||
include("plugins:editor")
|
||||
include("plugins:geo")
|
||||
include("plugins:webdav")
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
1
src/main/resources/icons/dav.svg
Normal file
1
src/main/resources/icons/dav.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1750996462119" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1582" width="16" height="16"><path d="M933.875 828.40625H591.1015625v-52.734375h290.0390625V142.859375H628.38476563l-105.46875001 105.46875H142.859375v527.34375h290.0390625v52.734375H90.125V195.59375h410.95898438l105.46874999-105.46875H933.875z m-158.203125 52.734375h52.734375v52.734375h-52.734375z m105.46875 0h52.734375v52.734375h-52.734375zM90.125 881.140625h52.734375v52.734375H90.125z m105.46875 0h52.734375v52.734375h-52.734375zM301.0625 881.140625h421.875v52.734375H301.0625z" p-id="1583" fill="#6C707E"></path><path d="M485.6328125 775.671875h52.734375v158.203125h-52.734375zM116.4921875 670.203125h791.015625v52.734375H116.4921875z" p-id="1584" fill="#6C707E"></path></svg>
|
||||
|
After Width: | Height: | Size: 798 B |
1
src/main/resources/icons/dav_dark.svg
Normal file
1
src/main/resources/icons/dav_dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1750996462119" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1582" width="16" height="16"><path d="M933.875 828.40625H591.1015625v-52.734375h290.0390625V142.859375H628.38476563l-105.46875001 105.46875H142.859375v527.34375h290.0390625v52.734375H90.125V195.59375h410.95898438l105.46874999-105.46875H933.875z m-158.203125 52.734375h52.734375v52.734375h-52.734375z m105.46875 0h52.734375v52.734375h-52.734375zM90.125 881.140625h52.734375v52.734375H90.125z m105.46875 0h52.734375v52.734375h-52.734375zM301.0625 881.140625h421.875v52.734375H301.0625z" p-id="1583" fill="#CED0D6"></path><path d="M485.6328125 775.671875h52.734375v158.203125h-52.734375zM116.4921875 670.203125h791.015625v52.734375H116.4921875z" p-id="1584" fill="#CED0D6"></path></svg>
|
||||
|
After Width: | Height: | Size: 798 B |
7
src/main/resources/icons/springCloudFileSet.svg
Normal file
7
src/main/resources/icons/springCloudFileSet.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 11V5.82843C3 5.29799 3.21071 4.78929 3.58579 4.41421L6.41421 1.58579C6.78929 1.21071 7.29799 1 7.82843 1H10C11.1046 1 12 1.89543 12 3V9.03245C11.8364 9.01104 11.6695 9 11.5 9C10.211 9 9.07285 9.64131 8.38421 10.6183C7.24585 10.9313 6.35944 11.8476 6.08704 13H5C3.89543 13 3 12.1046 3 11Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 5.82843V11C3 12.1046 3.89543 13 5 13H6.08704C6.17216 12.6399 6.31725 12.3028 6.51125 12H5C4.44772 12 4 11.5523 4 11V6H6.5C7.32843 6 8 5.32843 8 4.5V2H10C10.5523 2 11 2.44772 11 3V9.03257C11.1636 9.01109 11.3305 9 11.5 9C11.6695 9 11.8364 9.01104 12 9.03245V3C12 1.89543 11.1046 1 10 1H7.82843C7.29799 1 6.78929 1.21071 6.41421 1.58579L3.58579 4.41421C3.21071 4.78929 3 5.29799 3 5.82843ZM7 2.41421L4.41421 5H6.5C6.77614 5 7 4.77614 7 4.5V2.41421Z" fill="#6C707E"/>
|
||||
<path d="M14 9.93486C13.7035 9.67653 13.3666 9.46348 13 9.30634V4.5C13 4.22386 13.2239 4 13.5 4C13.7761 4 14 4.22386 14 4.5V9.93486Z" fill="#6C707E"/>
|
||||
<path d="M13.2751 12.4584L13.4232 13.2097L14.1872 13.2626C14.6481 13.2945 15 13.6706 15 14.125C15 14.6077 14.6077 15 14.125 15H9.25C8.56104 15 8 14.439 8 13.75C8 13.1069 8.48746 12.5758 9.11197 12.5094L9.64538 12.4527L9.89316 11.9769C10.1968 11.394 10.8042 11 11.5 11C12.3778 11 13.1107 11.624 13.2751 12.4584Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4232 13.2097L13.2751 12.4584C13.1107 11.624 12.3778 11 11.5 11C10.8042 11 10.1968 11.394 9.89316 11.9769L9.64538 12.4527L9.11197 12.5094C8.48746 12.5758 8 13.1069 8 13.75C8 14.439 8.56103 15 9.25 15H14.125C14.6077 15 15 14.6077 15 14.125C15 13.6706 14.6481 13.2945 14.1872 13.2626L13.4232 13.2097ZM11.5 10C12.865 10 14.0012 10.9713 14.2562 12.265C15.2312 12.3325 16 13.135 16 14.125C16 15.16 15.16 16 14.125 16H9.25C8.00875 16 7 14.9913 7 13.75C7 12.5912 7.8775 11.635 9.00625 11.515C9.475 10.615 10.4162 10 11.5 10Z" fill="#3574F0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
7
src/main/resources/icons/springCloudFileSet_dark.svg
Normal file
7
src/main/resources/icons/springCloudFileSet_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 11V5.82843C3 5.29799 3.21071 4.78929 3.58579 4.41421L6.41421 1.58579C6.78929 1.21071 7.29799 1 7.82843 1H10C11.1046 1 12 1.89543 12 3V9.03245C11.8364 9.01104 11.6695 9 11.5 9C10.211 9 9.07285 9.64131 8.38421 10.6183C7.24585 10.9313 6.35944 11.8476 6.08704 13H5C3.89543 13 3 12.1046 3 11Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 5.82843V11C3 12.1046 3.89543 13 5 13H6.08704C6.17216 12.6399 6.31725 12.3028 6.51125 12H5C4.44772 12 4 11.5523 4 11V6H6.5C7.32843 6 8 5.32843 8 4.5V2H10C10.5523 2 11 2.44772 11 3V9.03257C11.1636 9.01109 11.3305 9 11.5 9C11.6695 9 11.8364 9.01104 12 9.03245V3C12 1.89543 11.1046 1 10 1H7.82843C7.29799 1 6.78929 1.21071 6.41421 1.58579L3.58579 4.41421C3.21071 4.78929 3 5.29799 3 5.82843ZM7 2.41421L4.41421 5H6.5C6.77614 5 7 4.77614 7 4.5V2.41421Z" fill="#CED0D6"/>
|
||||
<path d="M14 9.93486C13.7035 9.67653 13.3666 9.46348 13 9.30634V4.5C13 4.22386 13.2239 4 13.5 4C13.7761 4 14 4.22386 14 4.5V9.93486Z" fill="#CED0D6"/>
|
||||
<path d="M13.2751 12.4584L13.4232 13.2097L14.1872 13.2626C14.6481 13.2945 15 13.6706 15 14.125C15 14.6077 14.6077 15 14.125 15H9.25C8.56104 15 8 14.439 8 13.75C8 13.1069 8.48746 12.5758 9.11197 12.5094L9.64538 12.4527L9.89316 11.9769C10.1968 11.394 10.8042 11 11.5 11C12.3778 11 13.1107 11.624 13.2751 12.4584Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4232 13.2097L13.2751 12.4584C13.1107 11.624 12.3778 11 11.5 11C10.8042 11 10.1968 11.394 9.89316 11.9769L9.64538 12.4527L9.11197 12.5094C8.48746 12.5758 8 13.1069 8 13.75C8 14.439 8.56103 15 9.25 15H14.125C14.6077 15 15 14.6077 15 14.125C15 13.6706 14.6481 13.2945 14.1872 13.2626L13.4232 13.2097ZM11.5 10C12.865 10 14.0012 10.9713 14.2562 12.265C15.2312 12.3325 16 13.135 16 14.125C16 15.16 15.16 16 14.125 16H9.25C8.00875 16 7 14.9913 7 13.75C7 12.5912 7.8775 11.635 9.00625 11.515C9.475 10.615 10.4162 10 11.5 10Z" fill="#548AF7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
Reference in New Issue
Block a user