mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: support SMB
This commit is contained in:
@@ -73,3 +73,7 @@ https://www.apache.org/licenses/LICENSE-2.0.html
|
|||||||
GeoLite2 (https://www.maxmind.com)
|
GeoLite2 (https://www.maxmind.com)
|
||||||
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||||
https://creativecommons.org/licenses/by-sa/4.0/
|
https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
|
||||||
|
smbj
|
||||||
|
Apache License, Version 2.0
|
||||||
|
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER
|
||||||
14
plugins/smb/build.gradle.kts
Normal file
14
plugins/smb/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.hierynomus:smbj:0.14.0")
|
||||||
|
compileOnly(project(":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystem
|
||||||
|
import com.hierynomus.smbj.session.Session
|
||||||
|
import com.hierynomus.smbj.share.DiskShare
|
||||||
|
|
||||||
|
class SMBFileSystem(private val share: DiskShare, session: Session) :
|
||||||
|
S3FileSystem(SMBFileSystemProvider(share, session)) {
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
share.close()
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.transfer.s3.S3FileSystemProvider
|
||||||
|
import app.termora.transfer.s3.S3Path
|
||||||
|
import com.hierynomus.msdtyp.AccessMask
|
||||||
|
import com.hierynomus.msfscc.FileAttributes
|
||||||
|
import com.hierynomus.mssmb2.SMB2CreateDisposition
|
||||||
|
import com.hierynomus.mssmb2.SMB2CreateOptions
|
||||||
|
import com.hierynomus.mssmb2.SMB2ShareAccess
|
||||||
|
import com.hierynomus.smbj.session.Session
|
||||||
|
import com.hierynomus.smbj.share.DiskShare
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.AccessMode
|
||||||
|
import java.nio.file.NoSuchFileException
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.attribute.FileAttribute
|
||||||
|
import kotlin.io.path.absolutePathString
|
||||||
|
|
||||||
|
|
||||||
|
class SMBFileSystemProvider(private val share: DiskShare, private val session: Session) : S3FileSystemProvider() {
|
||||||
|
|
||||||
|
override fun getScheme(): String? {
|
||||||
|
return "smb"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOutputStream(path: S3Path): OutputStream {
|
||||||
|
val file = share.openFile(
|
||||||
|
path.absolutePathString(),
|
||||||
|
setOf(AccessMask.GENERIC_WRITE),
|
||||||
|
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
||||||
|
setOf(SMB2ShareAccess.FILE_SHARE_READ),
|
||||||
|
SMB2CreateDisposition.FILE_OVERWRITE_IF,
|
||||||
|
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
|
||||||
|
)
|
||||||
|
val os = file.outputStream
|
||||||
|
|
||||||
|
return object : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
os.write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(os)
|
||||||
|
file.closeNoWait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInputStream(path: S3Path): InputStream {
|
||||||
|
val file = share.openFile(
|
||||||
|
path.absolutePathString(),
|
||||||
|
setOf(AccessMask.GENERIC_READ),
|
||||||
|
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
||||||
|
setOf(SMB2ShareAccess.FILE_SHARE_READ),
|
||||||
|
SMB2CreateDisposition.FILE_OPEN,
|
||||||
|
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
|
||||||
|
)
|
||||||
|
val input = file.inputStream
|
||||||
|
return object : InputStream() {
|
||||||
|
override fun read(): Int = input.read()
|
||||||
|
override fun close() {
|
||||||
|
IOUtils.closeQuietly(input)
|
||||||
|
file.closeNoWait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||||
|
val paths = mutableListOf<S3Path>()
|
||||||
|
val absolutePath = FilenameUtils.separatorsToUnix(path.absolutePathString())
|
||||||
|
for (information in share.list(if (absolutePath == path.fileSystem.separator) StringUtils.EMPTY else absolutePath)) {
|
||||||
|
if (information.fileName == "." || information.fileName == "..") continue
|
||||||
|
val isDir = information.fileAttributes and FileAttributes.FILE_ATTRIBUTE_DIRECTORY.value != 0L
|
||||||
|
val path = path.resolve(information.fileName)
|
||||||
|
path.attributes = path.attributes.copy(
|
||||||
|
directory = isDir, regularFile = isDir.not(),
|
||||||
|
size = information.endOfFile,
|
||||||
|
lastModifiedTime = information.lastWriteTime.toDate().time,
|
||||||
|
lastAccessTime = information.lastAccessTime.toDate().time,
|
||||||
|
)
|
||||||
|
paths.add(path)
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||||
|
share.mkdir(dir.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||||
|
if (isDirectory) {
|
||||||
|
share.rmdir(path.absolutePathString(), false)
|
||||||
|
} else {
|
||||||
|
share.rm(path.absolutePathString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||||
|
if (share.fileExists(path.absolutePathString()) || share.folderExists(path.absolutePathString())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw NoSuchFileException(path.absolutePathString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties
|
||||||
|
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||||
|
import com.hierynomus.smbj.SMBClient
|
||||||
|
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 SMBHostOptionsPane : OptionsPane() {
|
||||||
|
private val generalOption = GeneralOption()
|
||||||
|
private val sftpOption = SFTPOption()
|
||||||
|
|
||||||
|
init {
|
||||||
|
addOption(generalOption)
|
||||||
|
addOption(sftpOption)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getHost(): Host {
|
||||||
|
val name = generalOption.nameTextField.text
|
||||||
|
val protocol = SMBProtocolProvider.PROTOCOL
|
||||||
|
val host = generalOption.hostTextField.text
|
||||||
|
val port = generalOption.portTextField.value as Int
|
||||||
|
var authentication = Authentication.Companion.No
|
||||||
|
val authenticationType = AuthenticationType.Password
|
||||||
|
|
||||||
|
authentication = authentication.copy(
|
||||||
|
type = authenticationType,
|
||||||
|
password = String(generalOption.passwordTextField.password)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val options = Options.Default.copy(
|
||||||
|
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||||
|
extras = mutableMapOf(
|
||||||
|
"smb.share" to generalOption.shareTextField.text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Host(
|
||||||
|
name = name,
|
||||||
|
protocol = protocol,
|
||||||
|
host = host,
|
||||||
|
port = port,
|
||||||
|
username = generalOption.usernameTextField.selectedItem as String,
|
||||||
|
authentication = authentication,
|
||||||
|
sort = System.currentTimeMillis(),
|
||||||
|
remark = generalOption.remarkTextArea.text,
|
||||||
|
options = options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHost(host: Host) {
|
||||||
|
generalOption.nameTextField.text = host.name
|
||||||
|
generalOption.usernameTextField.selectedItem = host.username
|
||||||
|
generalOption.hostTextField.text = host.host
|
||||||
|
generalOption.portTextField.value = host.port
|
||||||
|
generalOption.remarkTextArea.text = host.remark
|
||||||
|
generalOption.passwordTextField.text = host.authentication.password
|
||||||
|
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
|
||||||
|
|
||||||
|
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateFields(): Boolean {
|
||||||
|
|
||||||
|
// general
|
||||||
|
if (validateField(generalOption.nameTextField)
|
||||||
|
|| validateField(generalOption.hostTextField)
|
||||||
|
|| validateField(generalOption.shareTextField)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val username = generalOption.usernameTextField.selectedItem as String?
|
||||||
|
if (username.isNullOrBlank()) {
|
||||||
|
setOutlineError(generalOption.usernameTextField)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 true 表示有错误
|
||||||
|
*/
|
||||||
|
private fun validateField(textField: JTextField): Boolean {
|
||||||
|
if (textField.isEnabled && textField.text.isBlank()) {
|
||||||
|
setOutlineError(textField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOutlineError(textField: JComponent) {
|
||||||
|
selectOptionJComponent(textField)
|
||||||
|
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||||
|
textField.requestFocusInWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||||
|
val portTextField = PortSpinner(SMBClient.DEFAULT_PORT)
|
||||||
|
val nameTextField = OutlineTextField(128)
|
||||||
|
val shareTextField = OutlineTextField(256)
|
||||||
|
val usernameTextField = OutlineComboBox<String>()
|
||||||
|
val hostTextField = OutlineTextField(255)
|
||||||
|
val passwordTextField = OutlinePasswordField(255)
|
||||||
|
val remarkTextArea = FixedLengthTextArea(512)
|
||||||
|
|
||||||
|
init {
|
||||||
|
initView()
|
||||||
|
initEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() {
|
||||||
|
usernameTextField.isEditable = true
|
||||||
|
usernameTextField.addItem("Guest")
|
||||||
|
usernameTextField.addItem("Anonymous")
|
||||||
|
|
||||||
|
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("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||||
|
.add(hostTextField).xy(3, rows)
|
||||||
|
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||||
|
.add(portTextField).xy(7, rows).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("${SMBI18n.getString("termora.plugins.smb.share")}:").xy(1, rows)
|
||||||
|
.add(shareTextField).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,24 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.I18n
|
||||||
|
import app.termora.NamedI18n
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object SMBI18n : NamedI18n("i18n/messages") {
|
||||||
|
private val log = LoggerFactory.getLogger(SMBI18n::class.java)
|
||||||
|
|
||||||
|
override fun getLogger(): Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getString(key: String): String {
|
||||||
|
return try {
|
||||||
|
substitutor.replace(getBundle().getString(key))
|
||||||
|
} catch (_: MissingResourceException) {
|
||||||
|
I18n.getString(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import com.hierynomus.smbj.SMBClient
|
||||||
|
import com.hierynomus.smbj.session.Session
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import java.nio.file.FileSystem
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class SMBPathHandler(
|
||||||
|
private val client: SMBClient,
|
||||||
|
private val session: Session,
|
||||||
|
fileSystem: FileSystem, path: Path
|
||||||
|
) : PathHandler(fileSystem, path) {
|
||||||
|
override fun dispose() {
|
||||||
|
super.dispose()
|
||||||
|
session.close()
|
||||||
|
IOUtils.closeQuietly(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
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 SMBPlugin : PaidPlugin {
|
||||||
|
private val support = ExtensionSupport()
|
||||||
|
|
||||||
|
init {
|
||||||
|
support.addExtension(ProtocolProviderExtension::class.java) { SMBProtocolProviderExtension.instance }
|
||||||
|
support.addExtension(ProtocolHostPanelExtension::class.java) { SMBProtocolHostPanelExtension.instance }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthor(): String {
|
||||||
|
return "TermoraDev"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getName(): String {
|
||||||
|
return "SMB"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||||
|
return support.getExtensions(clazz)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.Disposer
|
||||||
|
import app.termora.Host
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import java.awt.BorderLayout
|
||||||
|
|
||||||
|
class SMBProtocolHostPanel : ProtocolHostPanel() {
|
||||||
|
|
||||||
|
private val pane = SMBHostOptionsPane()
|
||||||
|
|
||||||
|
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.smb
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolHostPanel
|
||||||
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
|
||||||
|
class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { SMBProtocolHostPanelExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return SMBProtocolProvider.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||||
|
return SMBProtocolHostPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.DynamicIcon
|
||||||
|
import app.termora.Icons
|
||||||
|
import app.termora.protocol.PathHandler
|
||||||
|
import app.termora.protocol.PathHandlerRequest
|
||||||
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
import com.hierynomus.smbj.SMBClient
|
||||||
|
import com.hierynomus.smbj.auth.AuthenticationContext
|
||||||
|
import com.hierynomus.smbj.share.DiskShare
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
|
||||||
|
class SMBProtocolProvider private constructor() : TransferProtocolProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { SMBProtocolProvider() }
|
||||||
|
const val PROTOCOL = "SMB"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocol(): String {
|
||||||
|
return PROTOCOL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||||
|
return Icons.windows7
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||||
|
val client = SMBClient()
|
||||||
|
val host = requester.host
|
||||||
|
val connection = client.connect(host.host, host.port)
|
||||||
|
val session = when (host.username) {
|
||||||
|
"Guest" -> connection.authenticate(AuthenticationContext.guest())
|
||||||
|
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
|
||||||
|
else -> connection.authenticate(
|
||||||
|
AuthenticationContext(
|
||||||
|
host.username,
|
||||||
|
host.authentication.password.toCharArray(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val share = session.connectShare(host.options.extras["smb.share"] ?: StringUtils.EMPTY) as DiskShare
|
||||||
|
var sftpDefaultDirectory = StringUtils.defaultString(host.options.sftpDefaultDirectory)
|
||||||
|
sftpDefaultDirectory = if (sftpDefaultDirectory.isNotBlank()) {
|
||||||
|
FilenameUtils.separatorsToUnix(sftpDefaultDirectory)
|
||||||
|
} else {
|
||||||
|
"/"
|
||||||
|
}
|
||||||
|
|
||||||
|
val fs = SMBFileSystem(share, session)
|
||||||
|
return SMBPathHandler(client, session, fs, fs.getPath(sftpDefaultDirectory))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.termora.plugins.smb
|
||||||
|
|
||||||
|
import app.termora.protocol.ProtocolProvider
|
||||||
|
import app.termora.protocol.ProtocolProviderExtension
|
||||||
|
|
||||||
|
class SMBProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||||
|
companion object {
|
||||||
|
val instance by lazy { SMBProtocolProviderExtension() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getProtocolProvider(): ProtocolProvider {
|
||||||
|
return SMBProtocolProvider.instance
|
||||||
|
}
|
||||||
|
}
|
||||||
24
plugins/smb/src/main/resources/META-INF/plugin.xml
Normal file
24
plugins/smb/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<termora-plugin>
|
||||||
|
|
||||||
|
<id>smb</id>
|
||||||
|
|
||||||
|
<name>SMB</name>
|
||||||
|
|
||||||
|
<paid/>
|
||||||
|
|
||||||
|
<version>${projectVersion}</version>
|
||||||
|
|
||||||
|
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||||
|
|
||||||
|
<entry>app.termora.plugins.smb.SMBPlugin</entry>
|
||||||
|
|
||||||
|
<descriptions>
|
||||||
|
<description>Connecting to SMB</description>
|
||||||
|
<description language="zh_CN">支持连接到 SMB</description>
|
||||||
|
<description language="zh_TW">支援連接到 SMB</description>
|
||||||
|
</descriptions>
|
||||||
|
|
||||||
|
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||||
|
|
||||||
|
|
||||||
|
</termora-plugin>
|
||||||
1
plugins/smb/src/main/resources/META-INF/pluginIcon.svg
Normal file
1
plugins/smb/src/main/resources/META-INF/pluginIcon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.8 KiB |
1
plugins/smb/src/main/resources/i18n/messages.properties
Normal file
1
plugins/smb/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.smb.share=Share name
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.smb.share=共享名称
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
termora.plugins.smb.share=共享名稱
|
||||||
@@ -14,3 +14,4 @@ include("plugins:migration")
|
|||||||
include("plugins:editor")
|
include("plugins:editor")
|
||||||
include("plugins:geo")
|
include("plugins:geo")
|
||||||
include("plugins:webdav")
|
include("plugins:webdav")
|
||||||
|
include("plugins:smb")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package app.termora
|
|||||||
|
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
|
|
||||||
open class DynamicIcon(name: String, private val darkName: String, val allowColorFilter: Boolean = true) :
|
open class DynamicIcon(name: String, private val darkName: String = name, val allowColorFilter: Boolean = true) :
|
||||||
FlatSVGIcon(name) {
|
FlatSVGIcon(name) {
|
||||||
constructor(name: String) : this(name, name)
|
constructor(name: String) : this(name, name)
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ object Icons {
|
|||||||
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
val ssh by lazy { DynamicIcon("icons/ssh.svg", "icons/ssh_dark.svg") }
|
||||||
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
val ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_dark.svg") }
|
||||||
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
val minio by lazy { DynamicIcon("icons/minio.svg", "icons/minio_dark.svg") }
|
||||||
|
val windows7 by lazy { DynamicIcon("icons/windows7.svg", allowColorFilter = false) }
|
||||||
val powershell by lazy { DynamicIcon("icons/powershell.svg", "icons/powershell_dark.svg") }
|
val powershell by lazy { DynamicIcon("icons/powershell.svg", "icons/powershell_dark.svg") }
|
||||||
val serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") }
|
val serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_dark.svg") }
|
||||||
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }
|
val fileFormat by lazy { DynamicIcon("icons/fileFormat.svg", "icons/fileFormat_dark.svg") }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.formdev.flatlaf.extras.components.FlatTextField
|
|||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.ui.FlatLineBorder
|
import com.formdev.flatlaf.ui.FlatLineBorder
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import org.apache.commons.lang3.SystemUtils
|
import org.apache.commons.lang3.SystemUtils
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||||
@@ -205,7 +206,7 @@ class TransportNavigationPanel(
|
|||||||
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
||||||
textField.text = StringUtils.EMPTY
|
textField.text = StringUtils.EMPTY
|
||||||
} else {
|
} else {
|
||||||
textField.text = path.absolutePathString()
|
textField.text = FilenameUtils.separatorsToUnix(path.absolutePathString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/main/resources/icons/windows7.svg
Normal file
1
src/main/resources/icons/windows7.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.8 KiB |
Reference in New Issue
Block a user