From 54116a4bf56402bd1a5d94d3c967db0e304be075 Mon Sep 17 00:00:00 2001 From: hstyi Date: Fri, 27 Jun 2025 15:42:00 +0800 Subject: [PATCH] feat: support Huawei OBS --- plugins/cos/build.gradle.kts | 2 +- .../plugins/cos/COSProtocolProvider.kt | 3 - .../termora/plugins/obs/OBSClientHandler.kt | 81 +++++ .../termora/plugins/obs/OBSFileProvider.kt | 41 --- .../app/termora/plugins/obs/OBSFileSystem.kt | 16 + .../plugins/obs/OBSFileSystemProvider.kt | 140 +++++++ .../termora/plugins/obs/OBSHostOptionsPane.kt | 344 ++++++++++++++++++ .../app/termora/plugins/obs/OBSPlugin.kt | 3 - .../plugins/obs/OBSProtocolHostPanel.kt | 28 +- .../plugins/obs/OBSProtocolProvider.kt | 26 +- .../obs/OBSProtocolProviderExtension.kt | 2 +- plugins/oss/build.gradle.kts | 2 +- .../plugins/oss/OSSProtocolProvider.kt | 3 - settings.gradle.kts | 2 +- .../app/termora/actions/NewHostAction.kt | 2 +- 15 files changed, 625 insertions(+), 70 deletions(-) create mode 100644 plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSClientHandler.kt delete mode 100644 plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileProvider.kt create mode 100644 plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystem.kt create mode 100644 plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystemProvider.kt create mode 100644 plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSHostOptionsPane.kt diff --git a/plugins/cos/build.gradle.kts b/plugins/cos/build.gradle.kts index ce50d6e..99d8198 100644 --- a/plugins/cos/build.gradle.kts +++ b/plugins/cos/build.gradle.kts @@ -2,7 +2,7 @@ plugins { alias(libs.plugins.kotlin.jvm) } -project.version = "0.0.1" +project.version = "0.0.2" diff --git a/plugins/cos/src/main/kotlin/app/termora/plugins/cos/COSProtocolProvider.kt b/plugins/cos/src/main/kotlin/app/termora/plugins/cos/COSProtocolProvider.kt index 5c2d8cf..e306cbe 100644 --- a/plugins/cos/src/main/kotlin/app/termora/plugins/cos/COSProtocolProvider.kt +++ b/plugins/cos/src/main/kotlin/app/termora/plugins/cos/COSProtocolProvider.kt @@ -39,9 +39,6 @@ class COSProtocolProvider private constructor() : TransferProtocolProvider { try { buckets = cosClient.listBuckets() - if (buckets.isEmpty()) { - throw IllegalStateException("没有获取到桶信息") - } } finally { cosClient.shutdown() } diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSClientHandler.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSClientHandler.kt new file mode 100644 index 0000000..98b89a6 --- /dev/null +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSClientHandler.kt @@ -0,0 +1,81 @@ +package app.termora.plugins.obs + +import app.termora.AuthenticationType +import app.termora.Proxy +import app.termora.ProxyType +import com.obs.services.IObsCredentialsProvider +import com.obs.services.ObsClient +import com.obs.services.ObsConfiguration +import com.obs.services.model.ObsBucket +import org.apache.commons.io.IOUtils +import java.io.Closeable +import java.util.concurrent.atomic.AtomicBoolean + +class OBSClientHandler( + private val cred: IObsCredentialsProvider, + private val proxy: Proxy, + val buckets: List +) : Closeable { + + companion object { + fun createOBSClient( + cred: IObsCredentialsProvider, + endpoint: String, + proxy: Proxy + ): ObsClient { + val configuration = ObsConfiguration() + + if (proxy.type == ProxyType.HTTP) { + if (proxy.authenticationType == AuthenticationType.Password) { + configuration.setHttpProxy(proxy.host, proxy.port, proxy.username, proxy.password) + } else { + configuration.setHttpProxy(proxy.host, proxy.port, null, null) + } + } + + + var newEndpoint = endpoint + if ((newEndpoint.startsWith("http://") || newEndpoint.startsWith("https://")).not()) { + newEndpoint = "https://$endpoint" + } + + configuration.endPoint = newEndpoint + + val obsClient = ObsClient(cred, configuration) + + + return obsClient + } + } + + /** + * key: Region + * value: Client + */ + private val clients = mutableMapOf() + private val closed = AtomicBoolean(false) + + fun getClientForBucket(bucket: String): ObsClient { + if (closed.get()) throw IllegalStateException("Client already closed") + + synchronized(this) { + val bucket = buckets.first { it.bucketName == bucket } + if (clients.containsKey(bucket.location)) { + return clients.getValue(bucket.location) + } + clients[bucket.location] = createOBSClient(cred, "https://obs.${bucket.location}.myhuaweicloud.com", proxy) + return clients.getValue(bucket.location) + } + } + + override fun close() { + if (closed.compareAndSet(false, true)) { + synchronized(this) { + clients.forEach { IOUtils.closeQuietly(it.value) } + clients.clear() + } + } + } + + +} \ No newline at end of file diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileProvider.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileProvider.kt deleted file mode 100644 index ecb5031..0000000 --- a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileProvider.kt +++ /dev/null @@ -1,41 +0,0 @@ -package app.termora.plugins.obs - -import org.apache.commons.vfs2.Capability -import org.apache.commons.vfs2.FileName -import org.apache.commons.vfs2.FileSystem -import org.apache.commons.vfs2.FileSystemOptions -import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider - -class OBSFileProvider private constructor() : AbstractOriginatingFileProvider() { - - companion object { - val instance by lazy { OBSFileProvider() } - val capabilities = listOf( - Capability.CREATE, - Capability.DELETE, - Capability.RENAME, - Capability.GET_TYPE, - Capability.LIST_CHILDREN, - Capability.READ_CONTENT, - Capability.URI, - Capability.WRITE_CONTENT, - Capability.GET_LAST_MODIFIED, - Capability.SET_LAST_MODIFIED_FILE, - Capability.RANDOM_ACCESS_READ, - Capability.APPEND_CONTENT - ) - } - - override fun getCapabilities(): Collection { - return OBSFileProvider.capabilities - } - - override fun doCreateFileSystem( - rootFileName: FileName, - fileSystemOptions: FileSystemOptions - ): FileSystem? { - TODO("Not yet implemented") - } - - -} \ No newline at end of file diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystem.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystem.kt new file mode 100644 index 0000000..3c7f6e5 --- /dev/null +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystem.kt @@ -0,0 +1,16 @@ +package app.termora.plugins.obs + +import app.termora.transfer.s3.S3FileSystem +import org.apache.commons.io.IOUtils + +/** + * key: region + */ +class OBSFileSystem(private val clientHandler: OBSClientHandler) : + S3FileSystem(OBSFileSystemProvider(clientHandler)) { + + override fun close() { + IOUtils.closeQuietly(clientHandler) + super.close() + } +} diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystemProvider.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystemProvider.kt new file mode 100644 index 0000000..2cb68d2 --- /dev/null +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSFileSystemProvider.kt @@ -0,0 +1,140 @@ +package app.termora.plugins.obs + +import app.termora.transfer.s3.S3FileAttributes +import app.termora.transfer.s3.S3FileSystemProvider +import app.termora.transfer.s3.S3Path +import com.obs.services.model.ListObjectsRequest +import com.obs.services.model.PutObjectRequest +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.StringUtils +import java.io.InputStream +import java.io.OutputStream +import java.io.PipedInputStream +import java.io.PipedOutputStream +import java.nio.file.AccessMode +import java.nio.file.NoSuchFileException +import java.util.concurrent.atomic.AtomicReference +import kotlin.io.path.absolutePathString + +class OBSFileSystemProvider(private val clientHandler: OBSClientHandler) : S3FileSystemProvider() { + + + override fun getScheme(): String? { + return "oss" + } + + override fun getOutputStream(path: S3Path): OutputStream { + return createStreamer(path) + } + + override fun getInputStream(path: S3Path): InputStream { + val client = clientHandler.getClientForBucket(path.bucketName) + return client.getObject(path.bucketName, path.objectName).objectContent + } + + private fun createStreamer(path: S3Path): OutputStream { + val pis = PipedInputStream() + val pos = PipedOutputStream(pis) + val exception = AtomicReference() + + val thread = Thread.ofVirtual().start { + try { + val client = clientHandler.getClientForBucket(path.bucketName) + client.putObject(PutObjectRequest(path.bucketName, path.objectName, pis)) + } catch (e: Exception) { + 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() + + // root + if (path.isRoot) { + for (bucket in clientHandler.buckets) { + val p = path.resolve(bucket.bucketName) + p.attributes = S3FileAttributes( + directory = true, + lastModifiedTime = bucket.creationDate.toInstant().toEpochMilli() + ) + paths.add(p) + } + return paths + } + + var nextMarker = StringUtils.EMPTY + val maxKeys = 100 + val bucketName = path.bucketName + while (true) { + val request = ListObjectsRequest() + request.bucketName = bucketName + request.maxKeys = maxKeys + request.delimiter = path.fileSystem.separator + + if (path.objectName.isNotBlank()) request.prefix = path.objectName + path.fileSystem.separator + if (nextMarker.isNotBlank()) request.marker = nextMarker + + val objectListing = clientHandler.getClientForBucket(bucketName).listObjects(request) + for (e in objectListing.commonPrefixes) { + val p = path.bucket.resolve(e) + p.attributes = p.attributes.copy(directory = true) + delete(p) + paths.add(p) + } + + for (e in objectListing.objects) { + val p = path.bucket.resolve(e.objectKey) + p.attributes = p.attributes.copy( + regularFile = true, size = e.metadata.contentLength, + lastModifiedTime = e.metadata.lastModified.time + ) + paths.add(p) + } + + if (objectListing.isTruncated.not()) { + break + } + + nextMarker = objectListing.nextMarker + + } + + paths.addAll(directories[path.absolutePathString()] ?: emptyList()) + + return paths + } + + override fun delete(path: S3Path, isDirectory: Boolean) { + if (isDirectory.not()) + clientHandler.getClientForBucket(path.bucketName).deleteObject(path.bucketName, path.objectName) + } + + override fun checkAccess(path: S3Path, vararg modes: AccessMode) { + try { + val client = clientHandler.getClientForBucket(path.bucketName) + if (client.doesObjectExist(path.bucketName, path.objectName).not()) { + throw NoSuchFileException(path.objectName) + } + } catch (e: Exception) { + if (e is NoSuchFileException) throw e + throw NoSuchFileException(e.message) + } + } + +} \ No newline at end of file diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSHostOptionsPane.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSHostOptionsPane.kt new file mode 100644 index 0000000..6cf7ed8 --- /dev/null +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSHostOptionsPane.kt @@ -0,0 +1,344 @@ +package app.termora.plugins.obs + +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 OBSHostOptionsPane : 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 = OBSProtocolProvider.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, + extras = mutableMapOf( + "oss.delimiter" to StringUtils.defaultIfBlank(generalOption.delimiterTextField.text, "/"), +// "oss.region" to generalOption.regionComboBox.selectedItem as String, + ) + ) + + return Host( + name = name, + protocol = protocol, + port = port, + 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.delimiterTextField.text = host.options.extras["oss.delimiter"] ?: "/" +// generalOption.regionComboBox.selectedItem = host.options.extras["oss.region"] ?: StringUtils.EMPTY + + 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.usernameTextField)) { + return false + } + + if (host.authentication.type == AuthenticationType.Password) { + 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 && 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(255) + val remarkTextArea = FixedLengthTextArea(512) + + val delimiterTextField = OutlineTextField(128) + + init { + initView() + initEvents() + } + + private fun initView() { + + /*regionComboBox.isEditable = true + + + // 亚太-中国 + regionComboBox.addItem("oss-cn-hangzhou") + regionComboBox.addItem("oss-cn-shanghai") + regionComboBox.addItem("oss-cn-nanjing") + regionComboBox.addItem("oss-cn-qingdao") + regionComboBox.addItem("oss-cn-beijing") + regionComboBox.addItem("oss-cn-zhangjiakou") + regionComboBox.addItem("oss-cn-huhehaote") + regionComboBox.addItem("oss-cn-wulanchabu") + regionComboBox.addItem("oss-cn-shenzhen") + regionComboBox.addItem("oss-cn-heyuan") + regionComboBox.addItem("oss-cn-guangzhou") + regionComboBox.addItem("oss-cn-chengdu") + regionComboBox.addItem("oss-cn-hongkong") + + // 亚太-其他 + regionComboBox.addItem("oss-ap-northeast-1") + regionComboBox.addItem("oss-ap-northeast-2") + regionComboBox.addItem("oss-ap-southeast-1") + regionComboBox.addItem("oss-ap-southeast-3") + regionComboBox.addItem("oss-ap-southeast-5") + regionComboBox.addItem("oss-ap-southeast-6") + regionComboBox.addItem("oss-ap-southeast-7") + + // 欧洲与美洲 + regionComboBox.addItem("oss-eu-central-1") + regionComboBox.addItem("oss-eu-west-1") + regionComboBox.addItem("oss-us-west-1") + regionComboBox.addItem("oss-us-east-1") + regionComboBox.addItem("oss-na-south-1") + + // 中东 + regionComboBox.addItem("oss-me-east-1") + regionComboBox.addItem("oss-me-central-1") + + endpointTextField.isEditable = false*/ + + + + delimiterTextField.text = "/" + delimiterTextField.isEditable = false + add(getCenterComponent(), BorderLayout.CENTER) + } + + private fun initEvents() { + + addComponentListener(object : ComponentAdapter() { + override fun componentResized(e: ComponentEvent) { + SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() } + removeComponentListener(this) + } + }) + + /*regionComboBox.addItemListener { + if (it.stateChange == ItemEvent.SELECTED) { + endpointTextField.text = "${regionComboBox.selectedItem}.aliyuncs.com" + } + }*/ + } + + + 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("Region:").xy(1, rows) +// .add(regionComboBox).xyw(3, rows, 5).apply { rows += step } + +// .add("Endpoint:").xy(1, rows) +// .add(endpointTextField).xyw(3, rows, 5).apply { rows += step } + + .add("SecretId:").xy(1, rows) + .add(usernameTextField).xyw(3, rows, 5).apply { rows += step } + + .add("SecretKey:").xy(1, rows) + .add(passwordTextField).xyw(3, rows, 5).apply { rows += step } + + .add("Delimiter:").xy(1, rows) + .add(delimiterTextField).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/obs/src/main/kotlin/app/termora/plugins/obs/OBSPlugin.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSPlugin.kt index 5c444d2..e6cbf2b 100644 --- a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSPlugin.kt +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSPlugin.kt @@ -1,8 +1,5 @@ package app.termora.plugins.obs -import app.termora.DynamicIcon -import app.termora.I18n -import app.termora.Icons import app.termora.plugin.Extension import app.termora.plugin.ExtensionSupport import app.termora.plugin.PaidPlugin diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolHostPanel.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolHostPanel.kt index 340ae17..b60893e 100644 --- a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolHostPanel.kt +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolHostPanel.kt @@ -1,22 +1,36 @@ package app.termora.plugins.obs +import app.termora.Disposer import app.termora.Host import app.termora.protocol.ProtocolHostPanel -import org.apache.commons.lang3.StringUtils +import java.awt.BorderLayout class OBSProtocolHostPanel : ProtocolHostPanel() { + + private val pane = OBSHostOptionsPane() + + init { + initView() + initEvents() + } + + + private fun initView() { + add(pane, BorderLayout.CENTER) + Disposer.register(this, pane) + } + + private fun initEvents() {} + override fun getHost(): Host { - return Host( - name = StringUtils.EMPTY, - protocol = OBSProtocolProvider.PROTOCOL - ) + return pane.getHost() } override fun setHost(host: Host) { - + pane.setHost(host) } override fun validateFields(): Boolean { - return true + return pane.validateFields() } } \ No newline at end of file diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProvider.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProvider.kt index fe1f709..6bfb16e 100644 --- a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProvider.kt +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProvider.kt @@ -2,10 +2,13 @@ package app.termora.plugins.obs import app.termora.DynamicIcon import app.termora.Icons -import app.termora.protocol.FileObjectHandler -import app.termora.protocol.FileObjectRequest +import app.termora.protocol.PathHandler +import app.termora.protocol.PathHandlerRequest import app.termora.protocol.TransferProtocolProvider -import org.apache.commons.vfs2.provider.FileProvider +import com.obs.services.BasicObsCredentialsProvider +import com.obs.services.ObsClient +import com.obs.services.model.ListBucketsRequest + class OBSProtocolProvider private constructor() : TransferProtocolProvider { @@ -22,12 +25,19 @@ class OBSProtocolProvider private constructor() : TransferProtocolProvider { return Icons.huawei } - override fun getFileProvider(): FileProvider { - return OBSFileProvider.instance + override fun createPathHandler(requester: PathHandlerRequest): PathHandler { + val host = requester.host + val accessKeyId = host.username + val secretAccessKey = host.authentication.password + + val cred = BasicObsCredentialsProvider(accessKeyId, secretAccessKey) + val obsClient = ObsClient(cred, "https://obs.cn-north-4.myhuaweicloud.com") + val buckets = obsClient.listBuckets(ListBucketsRequest()) + val defaultPath = host.options.sftpDefaultDirectory + val fs = OBSFileSystem(OBSClientHandler(cred, host.proxy, buckets)) + return PathHandler(fs, fs.getPath(defaultPath)) + } - override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler { - TODO("Not yet implemented") - } } \ No newline at end of file diff --git a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProviderExtension.kt b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProviderExtension.kt index 33a9c96..c3d3583 100644 --- a/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProviderExtension.kt +++ b/plugins/obs/src/main/kotlin/app/termora/plugins/obs/OBSProtocolProviderExtension.kt @@ -9,6 +9,6 @@ class OBSProtocolProviderExtension private constructor() : ProtocolProviderExten } override fun getProtocolProvider(): ProtocolProvider { - return OBSProtocolProvider.Companion.instance + return OBSProtocolProvider.instance } } \ No newline at end of file diff --git a/plugins/oss/build.gradle.kts b/plugins/oss/build.gradle.kts index 872d151..1879796 100644 --- a/plugins/oss/build.gradle.kts +++ b/plugins/oss/build.gradle.kts @@ -2,7 +2,7 @@ plugins { alias(libs.plugins.kotlin.jvm) } -project.version = "0.0.1" +project.version = "0.0.2" dependencies { testImplementation(kotlin("test")) diff --git a/plugins/oss/src/main/kotlin/app/termora/plugins/oss/OSSProtocolProvider.kt b/plugins/oss/src/main/kotlin/app/termora/plugins/oss/OSSProtocolProvider.kt index 1b9cfa6..fad689b 100644 --- a/plugins/oss/src/main/kotlin/app/termora/plugins/oss/OSSProtocolProvider.kt +++ b/plugins/oss/src/main/kotlin/app/termora/plugins/oss/OSSProtocolProvider.kt @@ -43,9 +43,6 @@ class OSSProtocolProvider private constructor() : TransferProtocolProvider { try { buckets = oss.listBuckets() - if (buckets.isEmpty()) { - throw IllegalStateException("没有获取到桶信息") - } } finally { oss.shutdown() } diff --git a/settings.gradle.kts b/settings.gradle.kts index 551013c..5e4af04 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,7 +6,7 @@ rootProject.name = "termora" include("plugins:s3") include("plugins:oss") include("plugins:cos") -//include("plugins:obs") +include("plugins:obs") //include("plugins:ftp") include("plugins:bg") include("plugins:sync") diff --git a/src/main/kotlin/app/termora/actions/NewHostAction.kt b/src/main/kotlin/app/termora/actions/NewHostAction.kt index 33ef5ab..dbb277c 100644 --- a/src/main/kotlin/app/termora/actions/NewHostAction.kt +++ b/src/main/kotlin/app/termora/actions/NewHostAction.kt @@ -18,7 +18,7 @@ class NewHostAction : AnAction() { val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return if (lastNode.host.isFolder.not()) { - lastNode = lastNode.parent ?: return + lastNode = lastNode.parent ?: tree.simpleTreeModel.root } // Root 不可以添加,如果是 Root 那么加到用户下