feat: transfer support compress and extract

This commit is contained in:
hstyi
2025-07-07 15:38:45 +08:00
committed by hstyi
parent 574c816ebb
commit 66a81a5da3
22 changed files with 603 additions and 50 deletions

View File

@@ -11,14 +11,14 @@ class CommandTransfer(
isDirectory: Boolean, isDirectory: Boolean,
private val size: Long, private val size: Long,
val command: String, val command: String,
) : AbstractTransfer(parentId, path, path, isDirectory) { ) : AbstractTransfer(parentId, path, path, isDirectory), TransferIndeterminate {
private var executed = false private var executed = false
override suspend fun transfer(bufferSize: Int): Long { override suspend fun transfer(bufferSize: Int): Long {
if (executed) return 0 if (executed) return 0
val fs = source().fileSystem as SftpFileSystem val fs = source().fileSystem as SftpFileSystem
fs.session.executeRemoteCommand(command) fs.clientSession.executeRemoteCommand(command)
executed = true executed = true
return this.size() return this.size()
} }

View File

@@ -38,7 +38,7 @@ import kotlin.io.path.name
import kotlin.io.path.pathString import kotlin.io.path.pathString
import kotlin.math.max import kotlin.math.max
class DefaultInternalTransferManager( internal class DefaultInternalTransferManager(
private val owner: Supplier<Window>, private val owner: Supplier<Window>,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val transferManager: TransferManager, private val transferManager: TransferManager,

View File

@@ -4,7 +4,7 @@ import app.termora.Disposable
import java.nio.file.Path import java.nio.file.Path
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
interface InternalTransferManager { internal interface InternalTransferManager {
enum class TransferMode { enum class TransferMode {
Delete, Delete,
Transfer, Transfer,

View File

@@ -0,0 +1,3 @@
package app.termora.transfer
interface TransferIndeterminate

View File

@@ -30,4 +30,8 @@ interface TransferManager {
*/ */
fun addTransferListener(listener: TransferListener): Disposable fun addTransferListener(listener: TransferListener): Disposable
/**
* 移除传输监听器
*/
fun removeTransferListener(listener: TransferListener)
} }

View File

@@ -1,11 +1,10 @@
package app.termora.transfer package app.termora.transfer
import app.termora.Disposable import app.termora.*
import app.termora.I18n import app.termora.transfer.TransferTreeTableNode.State
import app.termora.NativeIcons
import app.termora.OptionPane
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import com.formdev.flatlaf.util.SoftCache
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
@@ -15,6 +14,7 @@ import java.awt.Component
import java.awt.Graphics import java.awt.Graphics
import java.awt.Insets import java.awt.Insets
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -23,6 +23,7 @@ import javax.swing.table.DefaultTableCellRenderer
import javax.swing.tree.DefaultTreeCellRenderer import javax.swing.tree.DefaultTreeCellRenderer
import kotlin.io.path.name import kotlin.io.path.name
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.min
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -85,6 +86,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
columnModel.getColumn(TransferTableModel.COLUMN_SPEED).cellRenderer = centerTableCellRenderer columnModel.getColumn(TransferTableModel.COLUMN_SPEED).cellRenderer = centerTableCellRenderer
columnModel.getColumn(TransferTableModel.COLUMN_ESTIMATED_TIME).cellRenderer = centerTableCellRenderer columnModel.getColumn(TransferTableModel.COLUMN_ESTIMATED_TIME).cellRenderer = centerTableCellRenderer
columnModel.getColumn(TransferTableModel.COLUMN_PROGRESS).cellRenderer = ProgressTableCellRenderer() columnModel.getColumn(TransferTableModel.COLUMN_PROGRESS).cellRenderer = ProgressTableCellRenderer()
.apply { Disposer.register(table, this) }
} }
private fun initEvents() { private fun initEvents() {
@@ -169,10 +171,16 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
disposed.set(true) disposed.set(true)
} }
private inner class ProgressTableCellRenderer : DefaultTableCellRenderer() { data class Indeterminate(val progress: Int = 0)
private inner class ProgressTableCellRenderer : DefaultTableCellRenderer(), Disposable, ActionListener {
private var progress = 0.0 private var progress = 0.0
private var progressInt = 0 private var progressInt = 0
private val padding = 4 private val padding = 4
private val map = SoftCache<Any, Indeterminate>()
private val timer = Timer(1000 / 40, this).apply { start() }
private var value: Any? = null
private val block = 36
init { init {
horizontalAlignment = CENTER horizontalAlignment = CENTER
@@ -189,9 +197,19 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
this.progress = 0.0 this.progress = 0.0
this.progressInt = 0 this.progressInt = 0
this.value = value
if (value is TransferTreeTableNode) { if (value is TransferTreeTableNode) {
if (value.state() == TransferTreeTableNode.State.Processing || value.waitingChildrenCompleted() || value.transfer is DeleteTransfer) {
if (value.transfer is TransferIndeterminate) {
if (map.containsKey(value).not()) {
map[value] = Indeterminate()
}
} else if (map.containsKey(value)) {
map.remove(value)
}
if (value.state() == State.Processing || value.waitingChildrenCompleted() || value.transfer is DeleteTransfer) {
this.progress = value.transferred.get() * 1.0 / value.filesize.get() this.progress = value.transferred.get() * 1.0 / value.filesize.get()
this.progressInt = floor(progress * 100.0).toInt() this.progressInt = floor(progress * 100.0).toInt()
// 因为有一些 0B 大小的文件所以如果在进行中那么最大就是99 // 因为有一些 0B 大小的文件所以如果在进行中那么最大就是99
@@ -200,6 +218,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
this.progressInt = floor(progress * 100.0).toInt() this.progressInt = floor(progress * 100.0).toInt()
} }
} }
} }
return super.getTableCellRendererComponent( return super.getTableCellRendererComponent(
@@ -213,6 +232,9 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
} }
override fun paintComponent(g: Graphics) { override fun paintComponent(g: Graphics) {
val width = width
val height = height
// 原始背景 // 原始背景
g.color = background g.color = background
g.fillRect(0, 0, width, height) g.fillRect(0, 0, width, height)
@@ -221,6 +243,25 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
g.color = UIManager.getColor("Table.selectionInactiveBackground") g.color = UIManager.getColor("Table.selectionInactiveBackground")
g.fillRect(0, padding, width, height - padding * 2) g.fillRect(0, padding, width, height - padding * 2)
if (map.containsKey(value)) {
val state = getState(value)
if (state == State.Processing || state == State.Failed) {
val indeterminate = map.getValue(value)
g.color = if (state == State.Processing) UIManager.getColor("ProgressBar.foreground")
else UIManager.getColor("Component.error.focusedBorderColor")
g.fillRect(indeterminate.progress, padding, block, height - padding * 2)
if (indeterminate.progress + block > width) {
val c = width - indeterminate.progress - block
val x = -block - c
g.fillRect(x, padding, block, height - padding * 2)
}
return
}
}
// 进度条颜色 // 进度条颜色
g.color = UIManager.getColor("ProgressBar.foreground") g.color = UIManager.getColor("ProgressBar.foreground")
g.fillRect(0, padding, (width * progress).toInt(), height - padding * 2) g.fillRect(0, padding, (width * progress).toInt(), height - padding * 2)
@@ -233,6 +274,31 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
// 绘制文字 // 绘制文字
ui.paint(g, this) ui.paint(g, this)
} }
override fun dispose() {
timer.stop()
}
override fun actionPerformed(e: ActionEvent) {
for (i in 0 until table.rowCount) {
val row = table.getPathForRow(i).lastPathComponent ?: continue
val node = tableModel.getValueAt(row, TransferTableModel.COLUMN_PROGRESS)
if (node !is TransferTreeTableNode) continue
if (node.state() != State.Processing) continue
val c = map[node] ?: continue
val rect = table.getCellRect(i, TransferTableModel.COLUMN_PROGRESS, false)
val indeterminate = c.copy(progress = min(c.progress + block / 10, rect.width))
map[node] = if (indeterminate.progress == rect.width) Indeterminate() else indeterminate
table.repaint(rect)
}
}
private fun getState(value: Any?): State? {
if (value == null) return null
val c = tableModel.getValueAt(value, TransferTableModel.COLUMN_PROGRESS)
if (c !is TransferTreeTableNode) return null
return c.state()
}
} }
} }

View File

@@ -87,11 +87,15 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
eventListener.add(TransferListener::class.java, listener) eventListener.add(TransferListener::class.java, listener)
return object : Disposable { return object : Disposable {
override fun dispose() { override fun dispose() {
eventListener.remove(TransferListener::class.java, listener) removeTransferListener(listener)
} }
} }
} }
override fun removeTransferListener(listener: TransferListener) {
eventListener.remove(TransferListener::class.java, listener)
}
override fun addTransfer(transfer: Transfer): Boolean { override fun addTransfer(transfer: Transfer): Boolean {
val node = TransferTreeTableNode(transfer) val node = TransferTreeTableNode(transfer)
val parent = if (transfer.parentId().isBlank()) getRoot() else map[transfer.parentId()] ?: return false val parent = if (transfer.parentId().isBlank()) getRoot() else map[transfer.parentId()] ?: return false

View File

@@ -61,15 +61,18 @@ class TransferTreeTableNode(transfer: Transfer) : DefaultMutableTreeTableNode(tr
(transfer is DeleteTransfer && transfer.isDirectory() && (state() == State.Processing || state() == State.Ready)) (transfer is DeleteTransfer && transfer.isDirectory() && (state() == State.Processing || state() == State.Ready))
val speed = counter.getLastSecondBytes() val speed = counter.getLastSecondBytes()
val estimatedTime = max(if (isProcessing && speed > 0) (filesize - totalBytesTransferred) / speed else 0, 0) val estimatedTime = max(if (isProcessing && speed > 0) (filesize - totalBytesTransferred) / speed else 0, 0)
val indeterminate = transfer is TransferIndeterminate
val formatSize = "${formatBytes(totalBytesTransferred)} / ${formatBytes(filesize)}"
val formatEstimatedTime = if (indeterminate) "-" else if (isProcessing) formatSeconds(estimatedTime) else "-"
return when (column) { return when (column) {
TransferTableModel.COLUMN_NAME -> transfer.source().name TransferTableModel.COLUMN_NAME -> transfer.source().name
TransferTableModel.COLUMN_STATUS -> formatStatus(state) TransferTableModel.COLUMN_STATUS -> formatStatus(state)
TransferTableModel.COLUMN_SOURCE_PATH -> formatPath(transfer.source(), false) TransferTableModel.COLUMN_SOURCE_PATH -> formatPath(transfer.source(), false)
TransferTableModel.COLUMN_TARGET_PATH -> formatPath(transfer.target(), true) TransferTableModel.COLUMN_TARGET_PATH -> formatPath(transfer.target(), true)
TransferTableModel.COLUMN_SIZE -> "${formatBytes(totalBytesTransferred)} / ${formatBytes(filesize)}" TransferTableModel.COLUMN_SIZE -> if (indeterminate) "-" else formatSize
TransferTableModel.COLUMN_SPEED -> if (isProcessing) "${formatBytes(speed)}/s" else "-" TransferTableModel.COLUMN_SPEED -> if (indeterminate) "-" else if (isProcessing) "${formatBytes(speed)}/s" else "-"
TransferTableModel.COLUMN_ESTIMATED_TIME -> if (isProcessing) formatSeconds(estimatedTime) else "-" TransferTableModel.COLUMN_ESTIMATED_TIME -> formatEstimatedTime
TransferTableModel.COLUMN_PROGRESS -> this TransferTableModel.COLUMN_PROGRESS -> this
else -> StringUtils.EMPTY else -> StringUtils.EMPTY
} }

View File

@@ -0,0 +1,22 @@
package app.termora.transfer
import app.termora.WindowScope
import app.termora.plugin.Extension
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenuItem
internal interface TransportContextMenuExtension : Extension {
/**
* 抛出 [UnsupportedOperationException] 表示不支持
*
* @param fileSystem 为 null 表示可能已经断线,处于不可用状态
*/
fun createJMenuItem(
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>
): JMenuItem
}

View File

@@ -3,9 +3,11 @@ package app.termora.transfer
import app.termora.* import app.termora.*
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.database.DatabaseManager import app.termora.database.DatabaseManager
import app.termora.plugin.ExtensionManager import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.wsl.WSLHostTerminalTab import app.termora.plugin.internal.wsl.WSLHostTerminalTab
import app.termora.terminal.DataKey
import app.termora.transfer.TransportTableModel.Attributes import app.termora.transfer.TransportTableModel.Attributes
import com.formdev.flatlaf.FlatClientProperties import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatToolBar import com.formdev.flatlaf.extras.components.FlatToolBar
@@ -60,11 +62,13 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
internal class TransportPanel( internal class TransportPanel(
private val transferManager: InternalTransferManager, private val internalTransferManager: InternalTransferManager,
val host: Host, val host: Host,
val loader: TransportSupportLoader, val loader: TransportSupportLoader,
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator { ) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
companion object { companion object {
val MyTransportPanel = DataKey(TransportPanel::class)
private val log = LoggerFactory.getLogger(TransportPanel::class.java) private val log = LoggerFactory.getLogger(TransportPanel::class.java)
private val folderIcon = FlatTreeClosedIcon() private val folderIcon = FlatTreeClosedIcon()
private val fileIcon = FlatTreeLeafIcon() private val fileIcon = FlatTreeLeafIcon()
@@ -117,7 +121,7 @@ internal class TransportPanel(
private val disposed = AtomicBoolean(false) private val disposed = AtomicBoolean(false)
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>()) private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
private val support = DataProviderSupport()
/** /**
* 工作目录 * 工作目录
@@ -212,6 +216,8 @@ internal class TransportPanel(
add(toolbar, BorderLayout.NORTH) add(toolbar, BorderLayout.NORTH)
add(layeredPane, BorderLayout.CENTER) add(layeredPane, BorderLayout.CENTER)
support.addData(MyTransportPanel, this)
} }
private fun compare(o1: Attributes, o2: Attributes): Int? { private fun compare(o1: Attributes, o2: Attributes): Int? {
@@ -286,7 +292,7 @@ internal class TransportPanel(
}) })
// 传输完成之后刷新 // 传输完成之后刷新
transferManager.addTransferListener(object : TransferListener { internalTransferManager.addTransferListener(object : TransferListener {
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) { override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
val target = transfer.target() val target = transfer.target()
@@ -294,13 +300,17 @@ internal class TransportPanel(
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
} }
if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) { if (target.pathString == workdir?.pathString || target.parent.pathString == workdir?.pathString) {
reload(requestFocus = false) if (loading) {
nextReloadCallbacks.add { reload(requestFocus = false) }
} else {
reload(requestFocus = false)
}
} }
} }
}).let { Disposer.register(this, it) } }).let { Disposer.register(this, it) }
// High 专门用于编辑目的,下载完成之后立即去编辑 // High 专门用于编辑目的,下载完成之后立即去编辑
transferManager.addTransferListener(editTransferListener).let { Disposer.register(this, it) } internalTransferManager.addTransferListener(editTransferListener).let { Disposer.register(this, it) }
// parent button // parent button
addPropertyChangeListener("loading") { evt -> addPropertyChangeListener("loading") { evt ->
@@ -427,8 +437,8 @@ internal class TransportPanel(
enterSelectionFolder() enterSelectionFolder()
} else { } else {
val paths = listOf(model.getPath(row) to attributes) val paths = listOf(model.getPath(row) to attributes)
if (loader.isOpened() && transferManager.canTransfer(paths.map { it.first })) { if (loader.isOpened() && internalTransferManager.canTransfer(paths.map { it.first })) {
transferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer) internalTransferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
} }
} }
} else if (SwingUtilities.isRightMouseButton(e)) { } else if (SwingUtilities.isRightMouseButton(e)) {
@@ -517,7 +527,7 @@ internal class TransportPanel(
override fun importData(support: TransferSupport): Boolean { override fun importData(support: TransferSupport): Boolean {
val data = getTransferData(support, true) ?: return false val data = getTransferData(support, true) ?: return false
val future = transferManager val future = internalTransferManager
.addTransfer(data.files, data.workdir, InternalTransferManager.TransferMode.Transfer) .addTransfer(data.files, data.workdir, InternalTransferManager.TransferMode.Transfer)
mountFuture(future) mountFuture(future)
@@ -609,7 +619,7 @@ internal class TransportPanel(
} }
} }
private fun registerSelectRow(name: String) { fun registerSelectRow(name: String) {
nextReloadCallbacks.add { nextReloadCallbacks.add {
for (i in 0 until model.rowCount) { for (i in 0 until model.rowCount) {
if (model.getAttributes(i).name == name) { if (model.getAttributes(i).name == name) {
@@ -796,11 +806,14 @@ internal class TransportPanel(
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) { private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
val files = rows.map { model.getPath(it) to model.getAttributes(it) } val files = rows.map { model.getPath(it) to model.getAttributes(it) }
val popupMenu = TransportPopupMenu(owner, model, transferManager, loader, files) val popupMenu = TransportPopupMenu(owner, model, internalTransferManager, loader, files)
popupMenu.addActionListener(PopupMenuActionListener(files)) popupMenu.addActionListener(PopupMenuActionListener(files))
popupMenu.show(table, e.x, e.y) popupMenu.show(table, e.x, e.y)
} }
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey)
}
override fun navigateTo(destination: String): Boolean { override fun navigateTo(destination: String): Boolean {
assertEventDispatchThread() assertEventDispatchThread()
@@ -927,7 +940,7 @@ internal class TransportPanel(
if (fs.isOpen.not()) continue if (fs.isOpen.not()) continue
// 发送到服务器 // 发送到服务器
transferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString())) internalTransferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString()))
oldMillis = millis oldMillis = millis
} }
} }
@@ -1036,7 +1049,7 @@ internal class TransportPanel(
val target = source.parent.resolve(e.source.toString()) val target = source.parent.resolve(e.source.toString())
processPath(e.source.toString()) { source.moveTo(target) } processPath(e.source.toString()) { source.moveTo(target) }
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf) internalTransferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
} else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) { } else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) {
// reload now // reload now
reload() reload()
@@ -1046,7 +1059,7 @@ internal class TransportPanel(
processPath(path.name) { processPath(path.name) {
if (c.includeSubFolder) { if (c.includeSubFolder) {
val future = withContext(Dispatchers.Swing) { val future = withContext(Dispatchers.Swing) {
transferManager.addTransfer( internalTransferManager.addTransfer(
listOf(path to files.first().second.copy(permissions = c.permissions)), listOf(path to files.first().second.copy(permissions = c.permissions)),
InternalTransferManager.TransferMode.ChangePermission InternalTransferManager.TransferMode.ChangePermission
) )
@@ -1061,14 +1074,14 @@ internal class TransportPanel(
} }
private fun transfer(mode: InternalTransferManager.TransferMode) { private fun transfer(mode: InternalTransferManager.TransferMode) {
val future = transferManager.addTransfer(files, mode) val future = internalTransferManager.addTransfer(files, mode)
mountFuture(future) mountFuture(future)
} }
private fun edit() { private fun edit() {
for (path in files.map { it.first }) { for (path in files.map { it.first }) {
val target = Application.createSubTemporaryDir().resolve(path.name) val target = Application.createSubTemporaryDir().resolve(path.name)
val transferId = transferManager.addHighTransfer(path, target) val transferId = internalTransferManager.addHighTransfer(path, target)
editTransferListener.addListenTransfer(transferId) editTransferListener.addListenTransfer(transferId)
} }
} }

View File

@@ -1,9 +1,10 @@
package app.termora.transfer package app.termora.transfer
import app.termora.Application import app.termora.Application
import app.termora.ApplicationScope
import app.termora.I18n import app.termora.I18n
import app.termora.Icons
import app.termora.OptionPane import app.termora.OptionPane
import app.termora.plugin.ExtensionManager
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
import com.formdev.flatlaf.extras.components.FlatPopupMenu import com.formdev.flatlaf.extras.components.FlatPopupMenu
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
@@ -20,7 +21,6 @@ import java.util.*
import javax.swing.JMenu import javax.swing.JMenu
import javax.swing.JMenuItem import javax.swing.JMenuItem
import javax.swing.JOptionPane import javax.swing.JOptionPane
import javax.swing.SwingUtilities
import javax.swing.event.EventListenerList import javax.swing.event.EventListenerList
import kotlin.io.path.absolutePathString import kotlin.io.path.absolutePathString
import kotlin.io.path.name import kotlin.io.path.name
@@ -42,7 +42,6 @@ internal class TransportPopupMenu(
private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder")) private val openInFinderMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.open-in-folder"))
private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename")) private val renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete")) private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
private val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
// @formatter:off // @formatter:off
private val changePermissionsMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.change-permissions")) private val changePermissionsMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.change-permissions"))
@@ -52,6 +51,7 @@ internal class TransportPopupMenu(
private val newFolderMenu = newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.folder")) private val newFolderMenu = newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.folder"))
private val newFileMenu = newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.file")) private val newFileMenu = newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.file"))
private val extensionManager get() = ExtensionManager.getInstance()
private val eventListeners = EventListenerList() private val eventListeners = EventListenerList()
private val mnemonics = mapOf( private val mnemonics = mapOf(
refreshMenu to KeyEvent.VK_R, refreshMenu to KeyEvent.VK_R,
@@ -89,13 +89,32 @@ internal class TransportPopupMenu(
addSeparator() addSeparator()
add(renameMenu) add(renameMenu)
add(deleteMenu) add(deleteMenu)
if (fileSystem is SftpFileSystem) {
add(rmrfMenu)
}
add(changePermissionsMenu) add(changePermissionsMenu)
val menus = mutableListOf<JMenuItem>()
for (extension in extensionManager.getExtensions(TransportContextMenuExtension::class.java)) {
try {
val menu = extension.createJMenuItem(
ApplicationScope.forWindowScope(owner),
fileSystem,
this,
files
)
menus.add(menu)
} catch (_: UnsupportedOperationException) {
continue
}
}
if (menus.isNotEmpty()) {
addSeparator()
menus.forEach { add(it) }
}
addSeparator() addSeparator()
add(refreshMenu) add(refreshMenu)
addSeparator() addSeparator()
add(newMenu) add(newMenu)
// 开发环境提供断线 // 开发环境提供断线
@@ -113,7 +132,6 @@ internal class TransportPopupMenu(
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() } && files.all { it.second.isFile && it.second.isSymbolicLink.not() }
renameMenu.isEnabled = hasParent.not() && files.size == 1 renameMenu.isEnabled = hasParent.not() && files.size == 1
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty() deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
rmrfMenu.isEnabled = hasParent.not() && files.isNotEmpty()
changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1 changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1
for ((item, mnemonic) in mnemonics) { for ((item, mnemonic) in mnemonics) {
@@ -134,16 +152,7 @@ internal class TransportPopupMenu(
fireActionPerformed(it, ActionCommand.Delete) fireActionPerformed(it, ActionCommand.Delete)
} }
} }
rmrfMenu.addActionListener {
if (OptionPane.showConfirmDialog(
SwingUtilities.getWindowAncestor(this),
I18n.getString("termora.transport.table.contextmenu.rm-warning"),
messageType = JOptionPane.ERROR_MESSAGE
) == JOptionPane.YES_OPTION
) {
fireActionPerformed(it, ActionCommand.Rmrf)
}
}
renameMenu.addActionListener { newFolderOrNewFile(it, ActionCommand.Rename) } renameMenu.addActionListener { newFolderOrNewFile(it, ActionCommand.Rename) }
editMenu.addActionListener { fireActionPerformed(it, ActionCommand.Edit) } editMenu.addActionListener { fireActionPerformed(it, ActionCommand.Edit) }
newFolderMenu.addActionListener { newFolderOrNewFile(it, ActionCommand.NewFolder) } newFolderMenu.addActionListener { newFolderOrNewFile(it, ActionCommand.NewFolder) }
@@ -159,7 +168,7 @@ internal class TransportPopupMenu(
} }
} }
private fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) { fun fireActionPerformed(evt: ActionEvent, command: ActionCommand) {
for (listener in eventListeners.getListeners(ActionListener::class.java)) { for (listener in eventListeners.getListeners(ActionListener::class.java)) {
listener.actionPerformed(ActionEvent(evt.source, evt.id, command.name)) listener.actionPerformed(ActionEvent(evt.source, evt.id, command.name))
} }

View File

@@ -7,7 +7,7 @@ import java.nio.file.Path
import java.nio.file.attribute.PosixFilePermission import java.nio.file.attribute.PosixFilePermission
import javax.swing.table.DefaultTableModel import javax.swing.table.DefaultTableModel
class TransportTableModel() : DefaultTableModel() { internal class TransportTableModel() : DefaultTableModel() {
companion object { companion object {
const val COLUMN_NAME = 0 const val COLUMN_NAME = 0
const val COLUMN_TYPE = 1 const val COLUMN_TYPE = 1
@@ -60,7 +60,7 @@ class TransportTableModel() : DefaultTableModel() {
} }
data class Attributes( internal data class Attributes(
val name: String, val name: String,
val type: String, val type: String,
val isDirectory: Boolean, val isDirectory: Boolean,

View File

@@ -4,6 +4,8 @@ import app.termora.Disposable
import app.termora.Disposer import app.termora.Disposer
import app.termora.DynamicColor import app.termora.DynamicColor
import app.termora.actions.DataProvider import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.terminal.DataKey
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import java.awt.* import java.awt.*
@@ -15,6 +17,9 @@ import kotlin.time.Duration.Companion.milliseconds
internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable { internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
companion object {
val MyTransferManager = DataKey(TransferManager::class)
}
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val splitPane = JSplitPane() private val splitPane = JSplitPane()
@@ -26,6 +31,7 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
private val rightTransferManager = createInternalTransferManager(rightTabbed, leftTabbed) private val rightTransferManager = createInternalTransferManager(rightTabbed, leftTabbed)
private val rootSplitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT) private val rootSplitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT)
private val owner get() = SwingUtilities.getWindowAncestor(this) private val owner get() = SwingUtilities.getWindowAncestor(this)
private val support = DataProviderSupport()
init { init {
initView() initView()
@@ -56,6 +62,8 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
rootSplitPane.topComponent = splitPane rootSplitPane.topComponent = splitPane
rootSplitPane.bottomComponent = scrollPane rootSplitPane.bottomComponent = scrollPane
support.addData(MyTransferManager, transferManager)
add(rootSplitPane, BorderLayout.CENTER) add(rootSplitPane, BorderLayout.CENTER)
} }
@@ -147,6 +155,10 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
coroutineScope.cancel() coroutineScope.cancel()
} }
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
return support.getData(dataKey)
}
internal class MyIcon(private val color: Color) : Icon { internal class MyIcon(private val color: Color) : Icon {
private val size = 10 private val size = 10

View File

@@ -0,0 +1,8 @@
package app.termora.transfer.internal.sftp
internal enum class CompressMode(val extension: String) {
TarGz("tar.gz"),
Tar("tar"),
Zip("zip"),
SevenZ("7z"),
}

View File

@@ -0,0 +1,153 @@
package app.termora.transfer.internal.sftp
import app.termora.I18n
import app.termora.WindowScope
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.randomUUID
import app.termora.transfer.*
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.common.file.util.MockPath
import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenu
import javax.swing.JMenuItem
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
import kotlin.io.path.pathString
internal class CompressTransportContextMenuExtension private constructor() : TransportContextMenuExtension {
companion object {
val instance = CompressTransportContextMenuExtension()
}
override fun createJMenuItem(
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>
): JMenuItem {
if (files.isEmpty() || fileSystem !is SftpFileSystem) throw UnsupportedOperationException()
val hasParent = files.any { it.second.isParent }
if (hasParent) throw UnsupportedOperationException()
val compressMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.compress"))
for (mode in CompressMode.entries) {
compressMenu.add(mode.extension).addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
compress(evt, fileSystem, mode, files)
}
})
}
return compressMenu
}
private fun compress(
event: AnActionEvent,
fileSystem: SftpFileSystem,
mode: CompressMode,
files: List<Pair<Path, TransportTableModel.Attributes>>,
) {
val transferManager = event.getData(TransportViewer.MyTransferManager) ?: return
val file = files.first().first
val workdir = file.parent ?: file.fileSystem.getPath(file.fileSystem.separator)
val name = StringUtils.defaultIfBlank(if (files.size > 1) workdir.name else file.name, "compress")
val target = workdir.resolve(name + ".${mode.extension}")
val myTransfer = CompressTransfer(fileSystem, mode, files, workdir, target)
if (transferManager.addTransfer(myTransfer).not()) return
val panel = event.getData(TransportPanel.MyTransportPanel) ?: return
transferManager.addTransferListener(object : TransferListener {
override fun onTransferChanged(transfer: Transfer, state: TransferTreeTableNode.State) {
if (transfer.id() != myTransfer.id()) return
if (state == TransferTreeTableNode.State.Done || state == TransferTreeTableNode.State.Failed) {
transferManager.removeTransferListener(this)
if (state == TransferTreeTableNode.State.Done) {
panel.registerSelectRow(target.name)
}
}
}
})
}
private class CompressTransfer(
private val fileSystem: SftpFileSystem,
private val mode: CompressMode,
private val files: List<Pair<Path, TransportTableModel.Attributes>>,
private val workdir: Path,
private val target: Path,
) : Transfer, TransferIndeterminate {
private val myID = randomUUID()
private var end = false
private val mySource = if (files.size == 1) files.first().first
else MockPath(files.joinToString(",") { it.first.pathString })
@Suppress("CascadeIf")
override suspend fun transfer(bufferSize: Int): Long {
if (end) return 0
val paths = files.joinToString(StringUtils.SPACE) { "'${it.second.name}'" }
val command = StringBuilder()
command.append("cd '${workdir.absolutePathString()}'")
command.append(" && ")
if (mode == CompressMode.TarGz) {
command.append("tar -czf")
} else if (mode == CompressMode.Tar) {
command.append("tar -cf")
} else if (mode == CompressMode.Zip) {
command.append("zip -r")
} else if (mode == CompressMode.SevenZ) {
command.append("7z a")
}
command.append(" '${target.name}' ")
command.append(paths)
fileSystem.clientSession.executeRemoteCommand(command.toString(), System.out, Charsets.UTF_8)
end = true
return size()
}
override fun source(): Path {
return mySource
}
override fun target(): Path {
return target
}
override fun size(): Long {
return files.size.toLong()
}
override fun isDirectory(): Boolean {
return false
}
override fun priority(): Transfer.Priority {
return Transfer.Priority.High
}
override fun scanning(): Boolean {
return false
}
override fun id(): String {
return myID
}
override fun parentId(): String {
return StringUtils.EMPTY
}
}
override fun ordered(): Long {
return 1
}
}

View File

@@ -0,0 +1,189 @@
package app.termora.transfer.internal.sftp
import app.termora.I18n
import app.termora.WindowScope
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.randomUUID
import app.termora.transfer.*
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenu
import javax.swing.JMenuItem
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
internal class ExtractTransportContextMenuExtension private constructor() : TransportContextMenuExtension {
companion object {
val instance = ExtractTransportContextMenuExtension()
}
override fun createJMenuItem(
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>
): JMenuItem {
if (files.isEmpty() || fileSystem !is SftpFileSystem) throw UnsupportedOperationException()
val hasParent = files.any { it.second.isParent || it.second.isDirectory }
if (hasParent) throw UnsupportedOperationException()
val map = mutableMapOf<Path, CompressMode>()
for ((path, attr) in files) {
val mode = CompressMode.entries.firstOrNull { attr.name.endsWith(".${it.extension}", true) }
if (mode == null) {
throw UnsupportedOperationException()
}
map[path] = mode
}
val extractMenu = JMenu(I18n.getString("termora.transport.table.contextmenu.extract"))
extractMenu.add(I18n.getString("termora.transport.table.contextmenu.extract.here"))
.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
extract(evt, fileSystem, ExtractLocation.Here, files.map { it.first to map.getValue(it.first) })
}
})
if (files.size == 1) {
val first = files.first()
val name = StringUtils.removeEndIgnoreCase(first.second.name, ".${map.getValue(first.first).extension}")
val text = I18n.getString("termora.transport.table.contextmenu.extract.single", name)
extractMenu.add(text).addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
extract(evt, fileSystem, ExtractLocation.Self, files.map { it.first to map.getValue(it.first) })
}
})
} else {
extractMenu.add(I18n.getString("termora.transport.table.contextmenu.extract.multi"))
.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
extract(evt, fileSystem, ExtractLocation.Self, files.map { it.first to map.getValue(it.first) })
}
})
}
return extractMenu
}
private fun extract(
event: AnActionEvent,
fileSystem: SftpFileSystem,
location: ExtractLocation,
files: List<Pair<Path, CompressMode>>,
) {
for (pair in files) {
extract(event, fileSystem, location, pair.second, pair.first)
}
}
private fun extract(
event: AnActionEvent,
fileSystem: SftpFileSystem,
location: ExtractLocation,
mode: CompressMode,
file: Path,
) {
val transferManager = event.getData(TransportViewer.MyTransferManager) ?: return
val workdir = file.parent ?: file.fileSystem.getPath(file.fileSystem.separator)
val target = if (location == ExtractLocation.Self)
workdir.resolve(StringUtils.removeEndIgnoreCase(file.name, ".${mode.extension}"))
else workdir
transferManager.addTransfer(ExtractTransfer(fileSystem, mode, file, workdir, target))
}
private class ExtractTransfer(
private val fileSystem: SftpFileSystem,
private val mode: CompressMode,
private val file: Path,
private val workdir: Path,
private val target: Path,
) : Transfer, TransferIndeterminate {
private val myID = randomUUID()
private var end = false
@Suppress("CascadeIf")
override suspend fun transfer(bufferSize: Int): Long {
if (end) return 0
val command = StringBuilder()
command.append("cd '${workdir.absolutePathString()}'")
command.append(" && ")
if (mode == CompressMode.TarGz || mode == CompressMode.Tar) {
command.append(" mkdir -p '${target.absolutePathString()}' ")
command.append(" && ")
command.append(" tar ")
if (mode == CompressMode.Tar) {
command.append(" -xf ")
} else {
command.append(" -zxf ")
}
command.append(" '${source().absolutePathString()}' ")
command.append(" -C '${target.absolutePathString()}' ")
} else if (mode == CompressMode.Zip) {
command.append(" unzip -o -q ")
command.append(" '${source().absolutePathString()}' ")
command.append(" -d '${target.absolutePathString()}' ")
} else if (mode == CompressMode.SevenZ) {
command.append(" 7z x ")
command.append(" '${source().absolutePathString()}' ")
command.append(" -o'${target.absolutePathString()}' -y > /dev/null")
}
fileSystem.clientSession.executeRemoteCommand(command.toString(), System.out, Charsets.UTF_8)
end = true
return size()
}
override fun source(): Path {
return file
}
override fun target(): Path {
return target
}
override fun size(): Long {
return 1
}
override fun isDirectory(): Boolean {
return false
}
override fun priority(): Transfer.Priority {
return Transfer.Priority.High
}
override fun scanning(): Boolean {
return false
}
override fun id(): String {
return myID
}
override fun parentId(): String {
return StringUtils.EMPTY
}
}
private enum class ExtractLocation {
Self,
Here,
}
override fun ordered(): Long {
return 2
}
}

View File

@@ -0,0 +1,48 @@
package app.termora.transfer.internal.sftp
import app.termora.I18n
import app.termora.Icons
import app.termora.OptionPane
import app.termora.WindowScope
import app.termora.transfer.TransportContextMenuExtension
import app.termora.transfer.TransportPopupMenu
import app.termora.transfer.TransportTableModel
import org.apache.sshd.sftp.client.fs.SftpFileSystem
import java.nio.file.FileSystem
import java.nio.file.Path
import javax.swing.JMenuItem
import javax.swing.JOptionPane
internal class RmrfTransportContextMenuExtension private constructor() : TransportContextMenuExtension {
companion object {
val instance = RmrfTransportContextMenuExtension()
}
override fun createJMenuItem(
windowScope: WindowScope,
fileSystem: FileSystem?,
popupMenu: TransportPopupMenu,
files: List<Pair<Path, TransportTableModel.Attributes>>
): JMenuItem {
if (files.isEmpty() || fileSystem !is SftpFileSystem) throw UnsupportedOperationException()
val hasParent = files.any { it.second.isParent }
if (hasParent) throw UnsupportedOperationException()
val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
rmrfMenu.addActionListener {
if (OptionPane.showConfirmDialog(
windowScope.window,
I18n.getString("termora.transport.table.contextmenu.rm-warning"),
messageType = JOptionPane.ERROR_MESSAGE
) == JOptionPane.YES_OPTION
) {
popupMenu.fireActionPerformed(it, TransportPopupMenu.ActionCommand.Rmrf)
}
}
return rmrfMenu
}
override fun ordered(): Long {
return 0
}
}

View File

@@ -4,11 +4,15 @@ import app.termora.FrameExtension
import app.termora.plugin.Extension import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin import app.termora.plugin.InternalPlugin
import app.termora.protocol.ProtocolProviderExtension import app.termora.protocol.ProtocolProviderExtension
import app.termora.transfer.TransportContextMenuExtension
internal class SFTPPlugin : InternalPlugin() { internal class SFTPPlugin : InternalPlugin() {
init { init {
support.addExtension(ProtocolProviderExtension::class.java) { SFTPProtocolProviderExtension.instance } support.addExtension(ProtocolProviderExtension::class.java) { SFTPProtocolProviderExtension.instance }
support.addExtension(FrameExtension::class.java) { SFTPFrameExtension.instance } support.addExtension(FrameExtension::class.java) { SFTPFrameExtension.instance }
support.addExtension(TransportContextMenuExtension::class.java) { RmrfTransportContextMenuExtension.instance }
support.addExtension(TransportContextMenuExtension::class.java) { CompressTransportContextMenuExtension.instance }
support.addExtension(TransportContextMenuExtension::class.java) { ExtractTransportContextMenuExtension.instance }
} }
override fun getName(): String { override fun getName(): String {

View File

@@ -337,6 +337,11 @@ termora.transport.table.contextmenu.delete-warning=If the folder is too large, d
termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous termora.transport.table.contextmenu.rm-warning=Using the rm -rf command to delete a file is very dangerous
termora.transport.table.contextmenu.change-permissions=Change Permissions... termora.transport.table.contextmenu.change-permissions=Change Permissions...
termora.transport.table.contextmenu.refresh=Refresh termora.transport.table.contextmenu.refresh=Refresh
termora.transport.table.contextmenu.compress=Compress
termora.transport.table.contextmenu.extract=Extract
termora.transport.table.contextmenu.extract.here=Extract here
termora.transport.table.contextmenu.extract.single=Extract to {0}\\
termora.transport.table.contextmenu.extract.multi=Extract to *\\*
termora.transport.table.contextmenu.new=${termora.welcome.contextmenu.new} termora.transport.table.contextmenu.new=${termora.welcome.contextmenu.new}
termora.transport.table.contextmenu.new.folder=${termora.welcome.contextmenu.new.folder.name} termora.transport.table.contextmenu.new.folder=${termora.welcome.contextmenu.new.folder.name}
termora.transport.table.contextmenu.new.file=New File termora.transport.table.contextmenu.new.file=New File

View File

@@ -328,6 +328,11 @@ termora.transport.table.contextmenu.edit-command=你必须在 “设置 - SFTP
termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打开 termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打开
termora.transport.table.contextmenu.change-permissions=更改权限... termora.transport.table.contextmenu.change-permissions=更改权限...
termora.transport.table.contextmenu.refresh=刷新 termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=压缩
termora.transport.table.contextmenu.extract=解压
termora.transport.table.contextmenu.extract.here=解压到当前目录
termora.transport.table.contextmenu.extract.single=解压到 {0}\\
termora.transport.table.contextmenu.extract.multi=解压到 *\\*
termora.transport.table.contextmenu.new.file=${termora.transport.table.contextmenu.new}文件 termora.transport.table.contextmenu.new.file=${termora.transport.table.contextmenu.new}文件
termora.transport.table.contextmenu.delete-warning=如果文件夹太大,删除可能需要耗费一定时间 termora.transport.table.contextmenu.delete-warning=如果文件夹太大,删除可能需要耗费一定时间
termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令删除文件存在很大风险 termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令删除文件存在很大风险

View File

@@ -323,6 +323,11 @@ termora.transport.table.contextmenu.edit-command=你必須在 “設定 - SFTP
termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打開 termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打開
termora.transport.table.contextmenu.change-permissions=更改權限... termora.transport.table.contextmenu.change-permissions=更改權限...
termora.transport.table.contextmenu.refresh=刷新 termora.transport.table.contextmenu.refresh=刷新
termora.transport.table.contextmenu.compress=壓縮
termora.transport.table.contextmenu.extract=解壓縮
termora.transport.table.contextmenu.extract.here=解壓縮到目前目錄
termora.transport.table.contextmenu.extract.single=解壓縮到 {0}\\
termora.transport.table.contextmenu.extract.multi=解壓縮到 *\\*
termora.transport.table.contextmenu.new.file=${termora.transport.table.contextmenu.new}文件 termora.transport.table.contextmenu.new.file=${termora.transport.table.contextmenu.new}文件
termora.transport.table.contextmenu.delete-warning=如果資料夾太大,刪除可能需要耗費一定時間 termora.transport.table.contextmenu.delete-warning=如果資料夾太大,刪除可能需要耗費一定時間
termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令刪除資料存在很大風險 termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令刪除資料存在很大風險

View File

@@ -1,6 +1,6 @@
FROM linuxserver/openssh-server FROM linuxserver/openssh-server
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update && apk add wget tmux gcc g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \ && apk update && apk add wget tmux gcc zip p7zip g++ git make zsh htop stress-ng inetutils-telnet xclock xcalc xorg-server xinit && wget https://ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz \
&& tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \ && tar -xf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20 && ./configure && make && make install \
&& ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz && ln -s /usr/local/bin/lrz /usr/local/bin/rz && ln -s /usr/local/bin/lsz /usr/local/bin/sz
RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config RUN sed -i 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config