mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: transfer support compress and extract
This commit is contained in:
@@ -11,14 +11,14 @@ class CommandTransfer(
|
||||
isDirectory: Boolean,
|
||||
private val size: Long,
|
||||
val command: String,
|
||||
) : AbstractTransfer(parentId, path, path, isDirectory) {
|
||||
) : AbstractTransfer(parentId, path, path, isDirectory), TransferIndeterminate {
|
||||
|
||||
private var executed = false
|
||||
|
||||
override suspend fun transfer(bufferSize: Int): Long {
|
||||
if (executed) return 0
|
||||
val fs = source().fileSystem as SftpFileSystem
|
||||
fs.session.executeRemoteCommand(command)
|
||||
fs.clientSession.executeRemoteCommand(command)
|
||||
executed = true
|
||||
return this.size()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import kotlin.io.path.name
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.math.max
|
||||
|
||||
class DefaultInternalTransferManager(
|
||||
internal class DefaultInternalTransferManager(
|
||||
private val owner: Supplier<Window>,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val transferManager: TransferManager,
|
||||
|
||||
@@ -4,7 +4,7 @@ import app.termora.Disposable
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
interface InternalTransferManager {
|
||||
internal interface InternalTransferManager {
|
||||
enum class TransferMode {
|
||||
Delete,
|
||||
Transfer,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package app.termora.transfer
|
||||
|
||||
interface TransferIndeterminate
|
||||
@@ -30,4 +30,8 @@ interface TransferManager {
|
||||
*/
|
||||
fun addTransferListener(listener: TransferListener): Disposable
|
||||
|
||||
/**
|
||||
* 移除传输监听器
|
||||
*/
|
||||
fun removeTransferListener(listener: TransferListener)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.Disposable
|
||||
import app.termora.I18n
|
||||
import app.termora.NativeIcons
|
||||
import app.termora.OptionPane
|
||||
import app.termora.*
|
||||
import app.termora.transfer.TransferTreeTableNode.State
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.util.SoftCache
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -15,6 +14,7 @@ import java.awt.Component
|
||||
import java.awt.Graphics
|
||||
import java.awt.Insets
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -23,6 +23,7 @@ import javax.swing.table.DefaultTableCellRenderer
|
||||
import javax.swing.tree.DefaultTreeCellRenderer
|
||||
import kotlin.io.path.name
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
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_ESTIMATED_TIME).cellRenderer = centerTableCellRenderer
|
||||
columnModel.getColumn(TransferTableModel.COLUMN_PROGRESS).cellRenderer = ProgressTableCellRenderer()
|
||||
.apply { Disposer.register(table, this) }
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
@@ -169,10 +171,16 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
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 progressInt = 0
|
||||
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 {
|
||||
horizontalAlignment = CENTER
|
||||
@@ -189,9 +197,19 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
|
||||
this.progress = 0.0
|
||||
this.progressInt = 0
|
||||
this.value = value
|
||||
|
||||
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.progressInt = floor(progress * 100.0).toInt()
|
||||
// 因为有一些 0B 大小的文件,所以如果在进行中,那么最大就是99
|
||||
@@ -200,6 +218,7 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
this.progressInt = floor(progress * 100.0).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return super.getTableCellRendererComponent(
|
||||
@@ -213,6 +232,9 @@ class TransferTable(private val coroutineScope: CoroutineScope, private val tabl
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
val width = width
|
||||
val height = height
|
||||
|
||||
// 原始背景
|
||||
g.color = background
|
||||
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.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.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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,11 +87,15 @@ class TransferTableModel(private val coroutineScope: CoroutineScope) :
|
||||
eventListener.add(TransferListener::class.java, listener)
|
||||
return object : Disposable {
|
||||
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 {
|
||||
val node = TransferTreeTableNode(transfer)
|
||||
val parent = if (transfer.parentId().isBlank()) getRoot() else map[transfer.parentId()] ?: return false
|
||||
|
||||
@@ -61,15 +61,18 @@ class TransferTreeTableNode(transfer: Transfer) : DefaultMutableTreeTableNode(tr
|
||||
(transfer is DeleteTransfer && transfer.isDirectory() && (state() == State.Processing || state() == State.Ready))
|
||||
val speed = counter.getLastSecondBytes()
|
||||
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) {
|
||||
TransferTableModel.COLUMN_NAME -> transfer.source().name
|
||||
TransferTableModel.COLUMN_STATUS -> formatStatus(state)
|
||||
TransferTableModel.COLUMN_SOURCE_PATH -> formatPath(transfer.source(), false)
|
||||
TransferTableModel.COLUMN_TARGET_PATH -> formatPath(transfer.target(), true)
|
||||
TransferTableModel.COLUMN_SIZE -> "${formatBytes(totalBytesTransferred)} / ${formatBytes(filesize)}"
|
||||
TransferTableModel.COLUMN_SPEED -> if (isProcessing) "${formatBytes(speed)}/s" else "-"
|
||||
TransferTableModel.COLUMN_ESTIMATED_TIME -> if (isProcessing) formatSeconds(estimatedTime) else "-"
|
||||
TransferTableModel.COLUMN_SIZE -> if (indeterminate) "-" else formatSize
|
||||
TransferTableModel.COLUMN_SPEED -> if (indeterminate) "-" else if (isProcessing) "${formatBytes(speed)}/s" else "-"
|
||||
TransferTableModel.COLUMN_ESTIMATED_TIME -> formatEstimatedTime
|
||||
TransferTableModel.COLUMN_PROGRESS -> this
|
||||
else -> StringUtils.EMPTY
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package app.termora.transfer
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.internal.wsl.WSLHostTerminalTab
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.transfer.TransportTableModel.Attributes
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
@@ -60,11 +62,13 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal class TransportPanel(
|
||||
private val transferManager: InternalTransferManager,
|
||||
private val internalTransferManager: InternalTransferManager,
|
||||
val host: Host,
|
||||
val loader: TransportSupportLoader,
|
||||
) : JPanel(BorderLayout()), DataProvider, Disposable, TransportNavigator {
|
||||
companion object {
|
||||
val MyTransportPanel = DataKey(TransportPanel::class)
|
||||
|
||||
private val log = LoggerFactory.getLogger(TransportPanel::class.java)
|
||||
private val folderIcon = FlatTreeClosedIcon()
|
||||
private val fileIcon = FlatTreeLeafIcon()
|
||||
@@ -117,7 +121,7 @@ internal class TransportPanel(
|
||||
|
||||
private val disposed = AtomicBoolean(false)
|
||||
private val futures = Collections.synchronizedSet(mutableSetOf<Future<*>>())
|
||||
|
||||
private val support = DataProviderSupport()
|
||||
|
||||
/**
|
||||
* 工作目录
|
||||
@@ -212,6 +216,8 @@ internal class TransportPanel(
|
||||
|
||||
add(toolbar, BorderLayout.NORTH)
|
||||
add(layeredPane, BorderLayout.CENTER)
|
||||
|
||||
support.addData(MyTransportPanel, this)
|
||||
}
|
||||
|
||||
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) {
|
||||
if (state != TransferTreeTableNode.State.Done && state != TransferTreeTableNode.State.Failed) return
|
||||
val target = transfer.target()
|
||||
@@ -294,13 +300,17 @@ internal class TransportPanel(
|
||||
if (target.fileSystem != loader.getSyncTransportSupport().getFileSystem()) return
|
||||
}
|
||||
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) }
|
||||
|
||||
// High 专门用于编辑目的,下载完成之后立即去编辑
|
||||
transferManager.addTransferListener(editTransferListener).let { Disposer.register(this, it) }
|
||||
internalTransferManager.addTransferListener(editTransferListener).let { Disposer.register(this, it) }
|
||||
|
||||
// parent button
|
||||
addPropertyChangeListener("loading") { evt ->
|
||||
@@ -427,8 +437,8 @@ internal class TransportPanel(
|
||||
enterSelectionFolder()
|
||||
} else {
|
||||
val paths = listOf(model.getPath(row) to attributes)
|
||||
if (loader.isOpened() && transferManager.canTransfer(paths.map { it.first })) {
|
||||
transferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
|
||||
if (loader.isOpened() && internalTransferManager.canTransfer(paths.map { it.first })) {
|
||||
internalTransferManager.addTransfer(paths, InternalTransferManager.TransferMode.Transfer)
|
||||
}
|
||||
}
|
||||
} else if (SwingUtilities.isRightMouseButton(e)) {
|
||||
@@ -517,7 +527,7 @@ internal class TransportPanel(
|
||||
override fun importData(support: TransferSupport): Boolean {
|
||||
val data = getTransferData(support, true) ?: return false
|
||||
|
||||
val future = transferManager
|
||||
val future = internalTransferManager
|
||||
.addTransfer(data.files, data.workdir, InternalTransferManager.TransferMode.Transfer)
|
||||
|
||||
mountFuture(future)
|
||||
@@ -609,7 +619,7 @@ internal class TransportPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerSelectRow(name: String) {
|
||||
fun registerSelectRow(name: String) {
|
||||
nextReloadCallbacks.add {
|
||||
for (i in 0 until model.rowCount) {
|
||||
if (model.getAttributes(i).name == name) {
|
||||
@@ -796,11 +806,14 @@ internal class TransportPanel(
|
||||
|
||||
private fun showContextmenu(rows: Array<Int>, e: MouseEvent) {
|
||||
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.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 {
|
||||
assertEventDispatchThread()
|
||||
@@ -927,7 +940,7 @@ internal class TransportPanel(
|
||||
if (fs.isOpen.not()) continue
|
||||
|
||||
// 发送到服务器
|
||||
transferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString()))
|
||||
internalTransferManager.addHighTransfer(localPath, fs.getPath(target.absolutePathString()))
|
||||
oldMillis = millis
|
||||
}
|
||||
}
|
||||
@@ -1036,7 +1049,7 @@ internal class TransportPanel(
|
||||
val target = source.parent.resolve(e.source.toString())
|
||||
processPath(e.source.toString()) { source.moveTo(target) }
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Rmrf) {
|
||||
transferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
||||
internalTransferManager.addTransfer(files, InternalTransferManager.TransferMode.Rmrf)
|
||||
} else if (actionCommand == TransportPopupMenu.ActionCommand.Reconnect) {
|
||||
// reload now
|
||||
reload()
|
||||
@@ -1046,7 +1059,7 @@ internal class TransportPanel(
|
||||
processPath(path.name) {
|
||||
if (c.includeSubFolder) {
|
||||
val future = withContext(Dispatchers.Swing) {
|
||||
transferManager.addTransfer(
|
||||
internalTransferManager.addTransfer(
|
||||
listOf(path to files.first().second.copy(permissions = c.permissions)),
|
||||
InternalTransferManager.TransferMode.ChangePermission
|
||||
)
|
||||
@@ -1061,14 +1074,14 @@ internal class TransportPanel(
|
||||
}
|
||||
|
||||
private fun transfer(mode: InternalTransferManager.TransferMode) {
|
||||
val future = transferManager.addTransfer(files, mode)
|
||||
val future = internalTransferManager.addTransfer(files, mode)
|
||||
mountFuture(future)
|
||||
}
|
||||
|
||||
private fun edit() {
|
||||
for (path in files.map { it.first }) {
|
||||
val target = Application.createSubTemporaryDir().resolve(path.name)
|
||||
val transferId = transferManager.addHighTransfer(path, target)
|
||||
val transferId = internalTransferManager.addHighTransfer(path, target)
|
||||
editTransferListener.addListenTransfer(transferId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package app.termora.transfer
|
||||
|
||||
import app.termora.Application
|
||||
import app.termora.ApplicationScope
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.OptionPane
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.transfer.TransportPanel.Companion.isLocallyFileSystem
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import org.apache.commons.io.IOUtils
|
||||
@@ -20,7 +21,6 @@ import java.util.*
|
||||
import javax.swing.JMenu
|
||||
import javax.swing.JMenuItem
|
||||
import javax.swing.JOptionPane
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.event.EventListenerList
|
||||
import kotlin.io.path.absolutePathString
|
||||
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 renameMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.rename"))
|
||||
private val deleteMenu = JMenuItem(I18n.getString("termora.transport.table.contextmenu.delete"))
|
||||
private val rmrfMenu = JMenuItem("rm -rf", Icons.warningIntroduction)
|
||||
|
||||
// @formatter:off
|
||||
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 newFileMenu = newMenu.add(I18n.getString("termora.transport.table.contextmenu.new.file"))
|
||||
|
||||
private val extensionManager get() = ExtensionManager.getInstance()
|
||||
private val eventListeners = EventListenerList()
|
||||
private val mnemonics = mapOf(
|
||||
refreshMenu to KeyEvent.VK_R,
|
||||
@@ -89,13 +89,32 @@ internal class TransportPopupMenu(
|
||||
addSeparator()
|
||||
add(renameMenu)
|
||||
add(deleteMenu)
|
||||
if (fileSystem is SftpFileSystem) {
|
||||
add(rmrfMenu)
|
||||
}
|
||||
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()
|
||||
add(refreshMenu)
|
||||
addSeparator()
|
||||
|
||||
add(newMenu)
|
||||
|
||||
// 开发环境提供断线
|
||||
@@ -113,7 +132,6 @@ internal class TransportPopupMenu(
|
||||
&& files.all { it.second.isFile && it.second.isSymbolicLink.not() }
|
||||
renameMenu.isEnabled = hasParent.not() && files.size == 1
|
||||
deleteMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||
rmrfMenu.isEnabled = hasParent.not() && files.isNotEmpty()
|
||||
changePermissionsMenu.isVisible = hasParent.not() && fileSystem is SftpFileSystem && files.size == 1
|
||||
|
||||
for ((item, mnemonic) in mnemonics) {
|
||||
@@ -134,16 +152,7 @@ internal class TransportPopupMenu(
|
||||
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) }
|
||||
editMenu.addActionListener { fireActionPerformed(it, ActionCommand.Edit) }
|
||||
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)) {
|
||||
listener.actionPerformed(ActionEvent(evt.source, evt.id, command.name))
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.nio.file.Path
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import javax.swing.table.DefaultTableModel
|
||||
|
||||
class TransportTableModel() : DefaultTableModel() {
|
||||
internal class TransportTableModel() : DefaultTableModel() {
|
||||
companion object {
|
||||
const val COLUMN_NAME = 0
|
||||
const val COLUMN_TYPE = 1
|
||||
@@ -60,7 +60,7 @@ class TransportTableModel() : DefaultTableModel() {
|
||||
}
|
||||
|
||||
|
||||
data class Attributes(
|
||||
internal data class Attributes(
|
||||
val name: String,
|
||||
val type: String,
|
||||
val isDirectory: Boolean,
|
||||
|
||||
@@ -4,6 +4,8 @@ import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.DynamicColor
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.terminal.DataKey
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import java.awt.*
|
||||
@@ -15,6 +17,9 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposable {
|
||||
companion object {
|
||||
val MyTransferManager = DataKey(TransferManager::class)
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val splitPane = JSplitPane()
|
||||
@@ -26,6 +31,7 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
private val rightTransferManager = createInternalTransferManager(rightTabbed, leftTabbed)
|
||||
private val rootSplitPane = JSplitPane(JSplitPane.VERTICAL_SPLIT)
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val support = DataProviderSupport()
|
||||
|
||||
init {
|
||||
initView()
|
||||
@@ -56,6 +62,8 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
rootSplitPane.topComponent = splitPane
|
||||
rootSplitPane.bottomComponent = scrollPane
|
||||
|
||||
support.addData(MyTransferManager, transferManager)
|
||||
|
||||
add(rootSplitPane, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
@@ -147,6 +155,10 @@ internal class TransportViewer : JPanel(BorderLayout()), DataProvider, Disposabl
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
return support.getData(dataKey)
|
||||
}
|
||||
|
||||
internal class MyIcon(private val color: Color) : Icon {
|
||||
private val size = 10
|
||||
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,15 @@ import app.termora.FrameExtension
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.InternalPlugin
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
import app.termora.transfer.TransportContextMenuExtension
|
||||
|
||||
internal class SFTPPlugin : InternalPlugin() {
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { SFTPProtocolProviderExtension.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 {
|
||||
|
||||
@@ -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.change-permissions=Change Permissions...
|
||||
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.folder=${termora.welcome.contextmenu.new.folder.name}
|
||||
termora.transport.table.contextmenu.new.file=New 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.change-permissions=更改权限...
|
||||
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.delete-warning=如果文件夹太大,删除可能需要耗费一定时间
|
||||
termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令删除文件存在很大风险
|
||||
|
||||
@@ -323,6 +323,11 @@ termora.transport.table.contextmenu.edit-command=你必須在 “設定 - SFTP
|
||||
termora.transport.table.contextmenu.open-in-folder=在${termora.finder}中打開
|
||||
termora.transport.table.contextmenu.change-permissions=更改權限...
|
||||
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.delete-warning=如果資料夾太大,刪除可能需要耗費一定時間
|
||||
termora.transport.table.contextmenu.rm-warning=使用 rm -rf 命令刪除資料存在很大風險
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM linuxserver/openssh-server
|
||||
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 \
|
||||
&& 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
|
||||
|
||||
Reference in New Issue
Block a user