mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
chore!: migrate to version 2.x
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.vfs2.FileObjectDescriptor
|
||||
import io.minio.*
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import org.apache.commons.vfs2.FileType
|
||||
import org.apache.commons.vfs2.provider.AbstractFileName
|
||||
import org.apache.commons.vfs2.provider.AbstractFileObject
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
|
||||
class S3FileObject(
|
||||
private val minio: MinioClient,
|
||||
fileName: AbstractFileName,
|
||||
fileSystem: S3FileSystem
|
||||
) : AbstractFileObject<S3FileSystem>(fileName, fileSystem), FileObjectDescriptor {
|
||||
private var attributes = Attributes()
|
||||
|
||||
init {
|
||||
attributes = attributes.copy(isRoot = name.path == fileSystem.getDelimiter())
|
||||
}
|
||||
|
||||
override fun doGetContentSize(): Long {
|
||||
return attributes.size
|
||||
}
|
||||
|
||||
override fun doGetType(): FileType {
|
||||
return if (attributes.isRoot || attributes.isBucket) FileType.FOLDER
|
||||
else if (attributes.isDirectory && attributes.isFile) FileType.FILE_OR_FOLDER
|
||||
else if (attributes.isFile) FileType.FILE
|
||||
else if (attributes.isDirectory) FileType.FOLDER
|
||||
else FileType.IMAGINARY
|
||||
}
|
||||
|
||||
override fun doListChildren(): Array<out String?>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun doCreateFolder() {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
private fun getBucketName(): String {
|
||||
if (StringUtils.isNotBlank(attributes.bucket)) {
|
||||
return attributes.bucket
|
||||
}
|
||||
if (parent is S3FileObject) {
|
||||
return (parent as S3FileObject).getBucketName()
|
||||
}
|
||||
throw IllegalArgumentException("Bucket must be a S3 file object")
|
||||
}
|
||||
|
||||
override fun doListChildrenResolved(): Array<FileObject>? {
|
||||
if (isFile) return null
|
||||
|
||||
val children = mutableListOf<FileObject>()
|
||||
|
||||
if (attributes.isRoot) {
|
||||
val buckets = minio.listBuckets()
|
||||
for (bucket in buckets) {
|
||||
val file = resolveFile(bucket.name())
|
||||
if (file is S3FileObject) {
|
||||
file.attributes = file.attributes.copy(
|
||||
isBucket = true,
|
||||
bucket = bucket.name(),
|
||||
isDirectory = false,
|
||||
isFile = false,
|
||||
lastModified = bucket.creationDate().toInstant().toEpochMilli()
|
||||
)
|
||||
children.add(file)
|
||||
}
|
||||
}
|
||||
} else if (attributes.isBucket || attributes.isDirectory) {
|
||||
val builder = ListObjectsArgs.builder().bucket(getBucketName())
|
||||
.delimiter(fileSystem.getDelimiter())
|
||||
var prefix = StringUtils.EMPTY
|
||||
if (attributes.isDirectory) {
|
||||
// remove first delimiter
|
||||
prefix = StringUtils.removeStart(name.path, fileSystem.getDelimiter())
|
||||
// remove bucket
|
||||
prefix = StringUtils.removeStart(prefix, getBucketName())
|
||||
// remove first delimiter
|
||||
prefix = StringUtils.removeStart(prefix, fileSystem.getDelimiter())
|
||||
// remove last delimiter
|
||||
prefix = StringUtils.removeEnd(prefix, fileSystem.getDelimiter())
|
||||
prefix = prefix + fileSystem.getDelimiter()
|
||||
}
|
||||
builder.prefix(prefix)
|
||||
|
||||
for (e in minio.listObjects(builder.build())) {
|
||||
val item = e.get()
|
||||
val objectName = StringUtils.removeStart(item.objectName(), prefix)
|
||||
val file = resolveFile(objectName)
|
||||
if (file is S3FileObject) {
|
||||
val lastModified = if (item.lastModified() != null) item.lastModified()
|
||||
.toInstant().toEpochMilli() else 0
|
||||
val owner = if (item.owner() != null) item.owner().displayName() else StringUtils.EMPTY
|
||||
file.attributes = file.attributes.copy(
|
||||
bucket = attributes.bucket,
|
||||
isDirectory = item.isDir,
|
||||
isFile = item.isDir.not(),
|
||||
lastModified = lastModified,
|
||||
size = if (item.isDir.not()) item.size() else 0,
|
||||
owner = owner
|
||||
)
|
||||
children.add(file)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return children.toTypedArray()
|
||||
}
|
||||
|
||||
override fun getFileSystem(): S3FileSystem {
|
||||
return super.getFileSystem() as S3FileSystem
|
||||
}
|
||||
|
||||
override fun doGetLastModifiedTime(): Long {
|
||||
return attributes.lastModified
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon? {
|
||||
if (attributes.isBucket) {
|
||||
return Icons.dbms
|
||||
}
|
||||
return super.getIcon(width, height)
|
||||
}
|
||||
|
||||
override fun getTypeDescription(): String? {
|
||||
if (attributes.isBucket) {
|
||||
return "Bucket"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getLastModified(): Long? {
|
||||
return attributes.lastModified
|
||||
}
|
||||
|
||||
override fun getOwner(): String? {
|
||||
return attributes.owner
|
||||
}
|
||||
|
||||
override fun doDelete() {
|
||||
if (isFile) {
|
||||
minio.removeObject(
|
||||
RemoveObjectArgs.builder()
|
||||
.bucket(getBucketName()).`object`(getObjectName()).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun doGetOutputStream(bAppend: Boolean): OutputStream? {
|
||||
return createStreamer()
|
||||
}
|
||||
|
||||
private fun createStreamer(): OutputStream {
|
||||
val pis = PipedInputStream()
|
||||
val pos = PipedOutputStream(pis)
|
||||
|
||||
val thread = Thread.ofVirtual().start {
|
||||
minio.putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(getBucketName())
|
||||
.stream(pis, -1, 32 * 1024 * 1024)
|
||||
.`object`(getObjectName()).build()
|
||||
)
|
||||
IOUtils.closeQuietly(pis)
|
||||
}
|
||||
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
pos.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
pos.close()
|
||||
thread.join()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun doGetInputStream(bufferSize: Int): InputStream? {
|
||||
return minio.getObject(GetObjectArgs.builder().bucket(getBucketName()).`object`(getObjectName()).build())
|
||||
}
|
||||
|
||||
private fun getObjectName(): String {
|
||||
var objectName = StringUtils.removeStart(name.path, fileSystem.getDelimiter())
|
||||
objectName = StringUtils.removeStart(objectName, getBucketName())
|
||||
objectName = StringUtils.removeStart(objectName, fileSystem.getDelimiter())
|
||||
return objectName
|
||||
}
|
||||
|
||||
private data class Attributes(
|
||||
val isRoot: Boolean = false,
|
||||
val isBucket: Boolean = false,
|
||||
val isDirectory: Boolean = false,
|
||||
val isFile: Boolean = false,
|
||||
/**
|
||||
* 只要不是 root 那么一定存在 bucket
|
||||
*/
|
||||
val bucket: String = StringUtils.EMPTY,
|
||||
/**
|
||||
* 最后修改时间
|
||||
*/
|
||||
val lastModified: Long = 0,
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
val size: Long = 0,
|
||||
/**
|
||||
* 所有者
|
||||
*/
|
||||
val owner: String = StringUtils.EMPTY
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import io.minio.MinioClient
|
||||
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 S3FileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { S3FileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return S3FileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
options: FileSystemOptions
|
||||
): FileSystem {
|
||||
val region = S3FileSystemConfigBuilder.instance.getRegion(options)
|
||||
val endpoint = S3FileSystemConfigBuilder.instance.getEndpoint(options)
|
||||
val accessKey = S3FileSystemConfigBuilder.instance.getAccessKey(options)
|
||||
val secretKey = S3FileSystemConfigBuilder.instance.getSecretKey(options)
|
||||
val builder = MinioClient.builder()
|
||||
builder.endpoint(endpoint)
|
||||
builder.credentials(accessKey, secretKey)
|
||||
if (region.isNotBlank()) builder.region(region)
|
||||
return S3FileSystem(builder.build(), rootFileName, options)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import io.minio.MinioClient
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractFileName
|
||||
import org.apache.commons.vfs2.provider.AbstractFileSystem
|
||||
|
||||
class S3FileSystem(
|
||||
private val minio: MinioClient,
|
||||
rootName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
) : AbstractFileSystem(rootName, null, fileSystemOptions) {
|
||||
|
||||
override fun addCapabilities(caps: MutableCollection<Capability>) {
|
||||
caps.addAll(S3FileProvider.capabilities)
|
||||
}
|
||||
|
||||
override fun createFile(name: AbstractFileName): FileObject? {
|
||||
return S3FileObject(minio, name, this)
|
||||
}
|
||||
|
||||
fun getDelimiter(): String {
|
||||
return S3FileSystemConfigBuilder.instance.getDelimiter(fileSystemOptions)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
minio.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.vfs2.s3.AbstractS3FileSystemConfigBuilder
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
|
||||
class S3FileSystemConfigBuilder private constructor() : AbstractS3FileSystemConfigBuilder() {
|
||||
companion object {
|
||||
val instance by lazy { S3FileSystemConfigBuilder() }
|
||||
}
|
||||
|
||||
override fun getConfigClass(): Class<out FileSystem> {
|
||||
return S3FileSystem::class.java
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
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 S3HostOptionsPane : OptionsPane() {
|
||||
private val generalOption = GeneralOption()
|
||||
private val proxyOption = BasicProxyOption()
|
||||
private val sftpOption = SFTPOption()
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(proxyOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = S3ProtocolProvider.PROTOCOL
|
||||
val host = generalOption.hostTextField.text
|
||||
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(
|
||||
"s3.region" to generalOption.regionTextField.text,
|
||||
"s3.delimiter" to generalOption.delimiterTextField.text,
|
||||
)
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
host = host,
|
||||
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.hostTextField.text = host.host
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
generalOption.regionTextField.text = host.options.extras["s3.region"] ?: StringUtils.EMPTY
|
||||
generalOption.delimiterTextField.text = host.options.extras["s3.delimiter"] ?: 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)
|
||||
|| validateField(generalOption.hostTextField)
|
||||
) {
|
||||
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(textField: JTextField) {
|
||||
selectOptionJComponent(textField)
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
private inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val regionTextField = OutlineTextField(128)
|
||||
val delimiterTextField = OutlineTextField(128)
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
delimiterTextField.text = "/"
|
||||
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(hostTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("AccessKey:").xy(1, rows)
|
||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("SecureKey:").xy(1, rows)
|
||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("Region:").xy(1, rows)
|
||||
.add(regionTextField).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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
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
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class S3Plugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { S3ProtocolProviderExtension.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { S3ProtocolHostPanelExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "S3"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import java.awt.BorderLayout
|
||||
|
||||
class S3ProtocolHostPanel : ProtocolHostPanel() {
|
||||
|
||||
private val pane = S3HostOptionsPane()
|
||||
|
||||
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.s3
|
||||
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { S3ProtocolHostPanelExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return S3ProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
return S3ProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import io.minio.MinioClient
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.VFS
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
|
||||
class S3ProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { S3ProtocolProvider() }
|
||||
const val PROTOCOL = "S3"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.minio
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return S3FileProvider.instance
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
val host = requester.host
|
||||
val builder = MinioClient.builder()
|
||||
.endpoint(host.host)
|
||||
.credentials(host.username, host.authentication.password)
|
||||
val region = host.options.extras["s3.region"]
|
||||
if (StringUtils.isNotBlank(region)) {
|
||||
builder.region(region)
|
||||
}
|
||||
val delimiter = host.options.extras["s3.delimiter"] ?: "/"
|
||||
val options = FileSystemOptions()
|
||||
val defaultPath = host.options.sftpDefaultDirectory
|
||||
|
||||
S3FileSystemConfigBuilder.instance.setRegion(options, StringUtils.defaultString(region))
|
||||
S3FileSystemConfigBuilder.instance.setEndpoint(options, host.host)
|
||||
S3FileSystemConfigBuilder.instance.setAccessKey(options, host.username)
|
||||
S3FileSystemConfigBuilder.instance.setSecretKey(options, host.authentication.password)
|
||||
S3FileSystemConfigBuilder.instance.setDelimiter(options, delimiter)
|
||||
|
||||
val file = VFS.getManager().resolveFile(
|
||||
"s3://${StringUtils.defaultIfBlank(defaultPath, "/")}",
|
||||
options
|
||||
)
|
||||
return FileObjectHandler(file)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class S3ProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { S3ProtocolProviderExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return S3ProtocolProvider.Companion.instance
|
||||
}
|
||||
}
|
||||
24
plugins/s3/src/main/resources/META-INF/plugin.xml
Normal file
24
plugins/s3/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>s3</id>
|
||||
|
||||
<name>S3</name>
|
||||
|
||||
<paid/>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.s3.S3Plugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to MinIO or AWS or other S3-compliant object storage services</description>
|
||||
<description language="zh_CN">支持连接到 MinIO 或 AWS 或其他兼容 S3 协议的对象存储</description>
|
||||
<description language="zh_TW">支援連接到 MinIO 或 AWS 或其他相容 S3 協定的物件存儲</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
1
plugins/s3/src/main/resources/META-INF/pluginIcon.svg
Normal file
1
plugins/s3/src/main/resources/META-INF/pluginIcon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1747210577463" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1516" width="16" height="16"><path d="M411.4 627.4l107.4-67.1 0.3-75.8C439 511.3 411.4 627.4 411.4 627.4z" fill="#6C707E" p-id="1517"></path><path d="M95.2 145.5l-2.7 730c-0.1 29.3 23.6 53.2 52.9 53.3l730 2.7c29.3 0.1 53.2-23.6 53.3-52.9l2.7-730c0.1-29.3-23.6-53.2-52.9-53.3l-730-2.7c-29.3-0.2-53.2 23.5-53.3 52.9zM540 264.6c1.1 29 97.4 112.9 122.8 134.5 4.5 3.9 8.4 8.4 11.3 13.6 10.2 17.9 26 58.9 5.5 87.8-26.6 37.5-49.6 58.3-101.9 92l-2.3 240.1-49.9-50.3 0.6-167-198 106.4S342.9 485.2 563 422l-0.4 105.8s18.2 0.2 58.6-45.7c36.7-41.6-32.9-84.8-60.6-108.6-27.7-23.8-72.8-74.3-78.6-115.5-5.1-35.8 30.4-78.3 81.9-62.8 51.4 15.5 83.2 78.2 83.2 78.2l48.3 114.3-131.6-142.5s-25-14-23.8 19.4z" fill="#6C707E" p-id="1518"></path></svg>
|
||||
|
After Width: | Height: | Size: 844 B |
@@ -0,0 +1,6 @@
|
||||
<svg t="1747210577463" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1516"
|
||||
width="16" height="16">
|
||||
<path d="M411.4 627.4l107.4-67.1 0.3-75.8C439 511.3 411.4 627.4 411.4 627.4z" fill="#CED0D6" p-id="1517"></path>
|
||||
<path d="M95.2 145.5l-2.7 730c-0.1 29.3 23.6 53.2 52.9 53.3l730 2.7c29.3 0.1 53.2-23.6 53.3-52.9l2.7-730c0.1-29.3-23.6-53.2-52.9-53.3l-730-2.7c-29.3-0.2-53.2 23.5-53.3 52.9zM540 264.6c1.1 29 97.4 112.9 122.8 134.5 4.5 3.9 8.4 8.4 11.3 13.6 10.2 17.9 26 58.9 5.5 87.8-26.6 37.5-49.6 58.3-101.9 92l-2.3 240.1-49.9-50.3 0.6-167-198 106.4S342.9 485.2 563 422l-0.4 105.8s18.2 0.2 58.6-45.7c36.7-41.6-32.9-84.8-60.6-108.6-27.7-23.8-72.8-74.3-78.6-115.5-5.1-35.8 30.4-78.3 81.9-62.8 51.4 15.5 83.2 78.2 83.2 78.2l48.3 114.3-131.6-142.5s-25-14-23.8 19.4z"
|
||||
fill="#CED0D6" p-id="1518"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 870 B |
@@ -0,0 +1,112 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.Authentication
|
||||
import app.termora.AuthenticationType
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.vfs2.VFSWalker
|
||||
import io.minio.MakeBucketArgs
|
||||
import io.minio.MinioClient
|
||||
import io.minio.PutObjectArgs
|
||||
import org.apache.commons.vfs2.FileObject
|
||||
import org.apache.commons.vfs2.VFS
|
||||
import org.apache.commons.vfs2.cache.WeakRefFilesCache
|
||||
import org.apache.commons.vfs2.impl.DefaultFileSystemManager
|
||||
import org.testcontainers.containers.GenericContainer
|
||||
import org.testcontainers.junit.jupiter.Container
|
||||
import org.testcontainers.junit.jupiter.Testcontainers
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.file.FileVisitResult
|
||||
import java.nio.file.FileVisitor
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.*
|
||||
import kotlin.test.Test
|
||||
|
||||
@Testcontainers
|
||||
class S3FileProviderTest {
|
||||
|
||||
private val ak = UUID.randomUUID().toString()
|
||||
private val sk = UUID.randomUUID().toString()
|
||||
|
||||
@Container
|
||||
private val monio: GenericContainer<*> = GenericContainer("minio/minio")
|
||||
.withEnv("MINIO_ACCESS_KEY", ak)
|
||||
.withEnv("MINIO_SECRET_KEY", sk)
|
||||
.withExposedPorts(9000, 9090)
|
||||
.withCommand("server", "/data", "--console-address", ":9090", "-address", ":9000")
|
||||
|
||||
companion object {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val endpoint = "http://127.0.0.1:${monio.getMappedPort(9000)}"
|
||||
val minioClient = MinioClient.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentials(ak, sk)
|
||||
.build()
|
||||
|
||||
val fileSystemManager = DefaultFileSystemManager()
|
||||
fileSystemManager.addProvider("s3", S3ProtocolProvider.instance.getFileProvider())
|
||||
fileSystemManager.filesCache = WeakRefFilesCache()
|
||||
fileSystemManager.init()
|
||||
VFS.setManager(fileSystemManager)
|
||||
|
||||
for (i in 0 until 5) {
|
||||
val bucket = "bucket-$i"
|
||||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build())
|
||||
|
||||
minioClient.putObject(
|
||||
PutObjectArgs.builder().bucket(bucket)
|
||||
.`object`("test-1/test-2/test-3/file-$i")
|
||||
.stream(ByteArrayInputStream("hello".toByteArray()), -1, 5 * 1024 * 1024)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
val requester = FileObjectRequest(
|
||||
host = Host(
|
||||
name = "test",
|
||||
protocol = S3ProtocolProvider.PROTOCOL,
|
||||
host = endpoint,
|
||||
username = ak,
|
||||
authentication = Authentication.No.copy(type = AuthenticationType.Password, password = sk),
|
||||
),
|
||||
)
|
||||
val file = S3ProtocolProvider.instance.getRootFileObject(requester).file
|
||||
VFSWalker.walk(file, object : FileVisitor<FileObject> {
|
||||
override fun preVisitDirectory(
|
||||
dir: FileObject,
|
||||
attrs: BasicFileAttributes
|
||||
): FileVisitResult {
|
||||
println("preVisitDirectory: ${dir.name}")
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFile(
|
||||
file: FileObject,
|
||||
attrs: BasicFileAttributes
|
||||
): FileVisitResult {
|
||||
println("visitFile: ${file.name}")
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
override fun visitFileFailed(
|
||||
file: FileObject,
|
||||
exc: IOException
|
||||
): FileVisitResult {
|
||||
return FileVisitResult.TERMINATE
|
||||
}
|
||||
|
||||
override fun postVisitDirectory(
|
||||
dir: FileObject,
|
||||
exc: IOException?
|
||||
): FileVisitResult {
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user