chore!: migrate to version 2.x

This commit is contained in:
hstyi
2025-06-13 15:16:56 +08:00
committed by GitHub
parent ca484618c7
commit 6177bbdc68
444 changed files with 18594 additions and 3832 deletions

View File

@@ -0,0 +1,21 @@
plugins {
alias(libs.plugins.kotlin.jvm)
}
project.version = "0.0.1"
dependencies {
testImplementation(kotlin("test"))
testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers)
testImplementation(libs.testcontainers.junit.jupiter)
testImplementation(project(":"))
implementation("io.minio:minio:8.5.17")
compileOnly(project(":"))
}
apply(from = "$rootDir/plugins/common.gradle.kts")

View File

@@ -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
)
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View 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
}
}

View 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>

View 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

View File

@@ -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

View File

@@ -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
}
})
}
}