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)
|
||||
Creative Commons Attribution-ShareAlike 4.0 International (CC 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:geo")
|
||||
include("plugins:webdav")
|
||||
include("plugins:smb")
|
||||
|
||||
@@ -2,7 +2,7 @@ package app.termora
|
||||
|
||||
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) {
|
||||
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 ftp by lazy { DynamicIcon("icons/ftp.svg", "icons/ftp_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 serial by lazy { DynamicIcon("icons/serial.svg", "icons/serial_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.ui.FlatLineBorder
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
@@ -205,7 +206,7 @@ class TransportNavigationPanel(
|
||||
if (path.fileSystem.isWindowsFileSystem() && path.pathString == path.fileSystem.separator) {
|
||||
textField.text = StringUtils.EMPTY
|
||||
} 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