From ea25b5b46f8f1e62d7e7ad322633a924b0721519 Mon Sep 17 00:00:00 2001 From: hstyi Date: Mon, 12 May 2025 15:26:29 +0800 Subject: [PATCH] feat: modify permissions to support recursion (#571) --- .../app/termora/sftp/FileSystemViewTable.kt | 193 +++++++++--------- .../termora/sftp/FileSystemViewTableModel.kt | 6 +- .../termora/sftp/PosixFilePermissionDialog.kt | 10 +- src/main/kotlin/app/termora/vfs2/VFSWalker.kt | 88 ++++++++ src/main/resources/i18n/messages.properties | 1 + .../resources/i18n/messages_zh_CN.properties | 1 + .../resources/i18n/messages_zh_TW.properties | 11 + 7 files changed, 207 insertions(+), 103 deletions(-) create mode 100644 src/main/kotlin/app/termora/vfs2/VFSWalker.kt diff --git a/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt b/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt index 0d6397c..239f7a0 100644 --- a/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt +++ b/src/main/kotlin/app/termora/sftp/FileSystemViewTable.kt @@ -4,6 +4,7 @@ import app.termora.* import app.termora.actions.AnActionEvent import app.termora.actions.SettingsAction import app.termora.sftp.FileSystemViewTable.AskTransfer.Action +import app.termora.vfs2.VFSWalker import app.termora.vfs2.sftp.MySftpFileObject import app.termora.vfs2.sftp.MySftpFileSystem import com.formdev.flatlaf.FlatClientProperties @@ -37,7 +38,6 @@ import java.nio.file.FileVisitor import java.nio.file.Paths import java.nio.file.StandardOpenOption import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.attribute.FileTime import java.text.MessageFormat import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -45,6 +45,21 @@ import java.util.regex.Pattern import javax.swing.* import javax.swing.table.DefaultTableCellRenderer import kotlin.collections.ArrayDeque +import kotlin.collections.List +import kotlin.collections.all +import kotlin.collections.contains +import kotlin.collections.filter +import kotlin.collections.filterIsInstance +import kotlin.collections.find +import kotlin.collections.forEach +import kotlin.collections.isEmpty +import kotlin.collections.isNotEmpty +import kotlin.collections.last +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.mapOf +import kotlin.collections.mutableListOf +import kotlin.collections.sortedArray import kotlin.io.path.absolutePathString import kotlin.math.max import kotlin.time.Duration.Companion.milliseconds @@ -360,34 +375,7 @@ class FileSystemViewTable( override fun actionPerformed(e: ActionEvent) { val last = files.last() if (last !is MySftpFileObject) return - - val dialog = PosixFilePermissionDialog( - SwingUtilities.getWindowAncestor(table), - model.getFilePermissions(last) - ) - val permissions = dialog.open() ?: return - - if (fileSystemViewPanel.requestLoading()) { - coroutineScope.launch(Dispatchers.IO) { - val c = runCatching { last.setPosixFilePermissions(permissions) }.onFailure { - withContext(Dispatchers.Swing) { - OptionPane.showMessageDialog( - owner, - ExceptionUtils.getMessage(it), - messageType = JOptionPane.ERROR_MESSAGE - ) - } - } - - // stop loading - fileSystemViewPanel.stopLoading() - - // reload - if (c.isSuccess) { - fileSystemViewPanel.reload(true) - } - } - } + changePermission(last) } }) refresh.addActionListener { fileSystemViewPanel.reload() } @@ -409,6 +397,80 @@ class FileSystemViewTable( popupMenu.show(table, e.x, e.y) } + private fun changePermission(file: MySftpFileObject) { + + val dialog = PosixFilePermissionDialog( + SwingUtilities.getWindowAncestor(table), + model.getFilePermissions(file) + ) + val permissions = dialog.open() ?: return + val isIncludeSubdirectories = dialog.isIncludeSubdirectories() + + if (fileSystemViewPanel.requestLoading()) { + coroutineScope.launch(Dispatchers.IO) { + val c = runCatching { + file.setPosixFilePermissions(permissions) + if (isIncludeSubdirectories && file.isFolder) { + file.refresh() + VFSWalker.walk(file, object : FileVisitor { + override fun preVisitDirectory( + dir: FileObject, + attrs: BasicFileAttributes + ): FileVisitResult { + dir.refresh() + if (dir is MySftpFileObject) { + dir.setPosixFilePermissions(permissions) + } + return FileVisitResult.CONTINUE + } + + override fun visitFile( + file: FileObject, + attrs: BasicFileAttributes + ): FileVisitResult { + if (file is MySftpFileObject) { + file.setPosixFilePermissions(permissions) + } + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed( + file: FileObject, + exc: IOException + ): FileVisitResult { + return FileVisitResult.TERMINATE + } + + override fun postVisitDirectory( + dir: FileObject, + exc: IOException? + ): FileVisitResult { + return FileVisitResult.CONTINUE + } + + }) + } + }.onFailure { + withContext(Dispatchers.Swing) { + OptionPane.showMessageDialog( + owner, + ExceptionUtils.getMessage(it), + messageType = JOptionPane.ERROR_MESSAGE + ) + } + } + + // stop loading + fileSystemViewPanel.stopLoading() + + // reload + if (c.isSuccess) { + fileSystemViewPanel.reload(true) + } + } + } + } + private fun renameSelection() { val index = selectedRow if (index < 0) return @@ -886,36 +948,7 @@ class FileSystemViewTable( dir: FileObject, visitor: FileVisitor, ): FileVisitResult { - - // clear cache - if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) { - return FileVisitResult.TERMINATE - } - - for (e in dir.children) { - if (e.name.baseName == ".." || e.name.baseName == ".") continue - if (e.isFolder) { - if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) { - return FileVisitResult.TERMINATE - } - } else { - val result = visitor.visitFile( - dir.resolveFile(e.name.baseName), - EmptyBasicFileAttributes.INSTANCE - ) - if (result == FileVisitResult.TERMINATE) { - return FileVisitResult.TERMINATE - } else if (result == FileVisitResult.SKIP_SUBTREE) { - break - } - } - } - - if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) { - return FileVisitResult.TERMINATE - } - - return FileVisitResult.CONTINUE + return VFSWalker.walk(dir, visitor) } private fun addTransport( @@ -974,47 +1007,5 @@ class FileSystemViewTable( } - private class EmptyBasicFileAttributes : BasicFileAttributes { - companion object { - val INSTANCE = EmptyBasicFileAttributes() - } - - override fun lastModifiedTime(): FileTime { - TODO("Not yet implemented") - } - - override fun lastAccessTime(): FileTime { - TODO("Not yet implemented") - } - - override fun creationTime(): FileTime { - TODO("Not yet implemented") - } - - override fun isRegularFile(): Boolean { - TODO("Not yet implemented") - } - - override fun isDirectory(): Boolean { - TODO("Not yet implemented") - } - - override fun isSymbolicLink(): Boolean { - TODO("Not yet implemented") - } - - override fun isOther(): Boolean { - TODO("Not yet implemented") - } - - override fun size(): Long { - TODO("Not yet implemented") - } - - override fun fileKey(): Any { - TODO("Not yet implemented") - } - - } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/sftp/FileSystemViewTableModel.kt b/src/main/kotlin/app/termora/sftp/FileSystemViewTableModel.kt index 492641c..1a6670b 100644 --- a/src/main/kotlin/app/termora/sftp/FileSystemViewTableModel.kt +++ b/src/main/kotlin/app/termora/sftp/FileSystemViewTableModel.kt @@ -157,7 +157,11 @@ class FileSystemViewTableModel : DefaultTableModel() { fun getPathNames(): Set { val names = linkedSetOf() for (i in 0 until rowCount) { - names.add(getFileObject(i).name.baseName) + if (hasParent && i == 0) { + names.add("..") + } else { + names.add(getFileObject(i).name.baseName) + } } return names } diff --git a/src/main/kotlin/app/termora/sftp/PosixFilePermissionDialog.kt b/src/main/kotlin/app/termora/sftp/PosixFilePermissionDialog.kt index 0f8cf30..bec028b 100644 --- a/src/main/kotlin/app/termora/sftp/PosixFilePermissionDialog.kt +++ b/src/main/kotlin/app/termora/sftp/PosixFilePermissionDialog.kt @@ -25,6 +25,7 @@ class PosixFilePermissionDialog( private val otherRead = JCheckBox(I18n.getString("termora.transport.permissions.read")) private val otherWrite = JCheckBox(I18n.getString("termora.transport.permissions.write")) private val otherExecute = JCheckBox(I18n.getString("termora.transport.permissions.execute")) + private val includeSubFolder = JCheckBox(I18n.getString("termora.transport.permissions.include-subfolder")) private var isCancelled = false @@ -60,13 +61,14 @@ class PosixFilePermissionDialog( otherRead.isFocusable = false otherWrite.isFocusable = false otherExecute.isFocusable = false + includeSubFolder.isFocusable = false } override fun createCenterPanel(): JComponent { val formMargin = "7dlu" val layout = FormLayout( "default:grow, $formMargin, default:grow, $formMargin, default:grow", - "pref, $formMargin, pref, $formMargin, pref" + "pref, $formMargin, pref, $formMargin, pref, $formMargin, pref" ) val builder = FormBuilder.create().padding("0, $formMargin, $formMargin, $formMargin") @@ -95,6 +97,8 @@ class PosixFilePermissionDialog( otherBox.border = BorderFactory.createTitledBorder(I18n.getString("termora.transport.permissions.others")) builder.add(otherBox).xy(5, 3) + builder.add(includeSubFolder).xyw(1, 5, 5) + return builder.build() } @@ -103,6 +107,10 @@ class PosixFilePermissionDialog( super.doCancelAction() } + fun isIncludeSubdirectories(): Boolean { + return includeSubFolder.isSelected + } + /** * @return 返回空表示取消了 */ diff --git a/src/main/kotlin/app/termora/vfs2/VFSWalker.kt b/src/main/kotlin/app/termora/vfs2/VFSWalker.kt new file mode 100644 index 0000000..0366b96 --- /dev/null +++ b/src/main/kotlin/app/termora/vfs2/VFSWalker.kt @@ -0,0 +1,88 @@ +package app.termora.vfs2 + +import org.apache.commons.vfs2.FileObject +import java.nio.file.FileVisitResult +import java.nio.file.FileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime + +object VFSWalker { + fun walk( + dir: FileObject, + visitor: FileVisitor, + ): FileVisitResult { + + // clear cache + if (visitor.preVisitDirectory(dir, EmptyBasicFileAttributes.INSTANCE) == FileVisitResult.TERMINATE) { + return FileVisitResult.TERMINATE + } + + for (e in dir.children) { + if (e.name.baseName == ".." || e.name.baseName == ".") continue + if (e.isFolder) { + if (walk(dir.resolveFile(e.name.baseName), visitor) == FileVisitResult.TERMINATE) { + return FileVisitResult.TERMINATE + } + } else { + val result = visitor.visitFile( + dir.resolveFile(e.name.baseName), + EmptyBasicFileAttributes.INSTANCE + ) + if (result == FileVisitResult.TERMINATE) { + return FileVisitResult.TERMINATE + } else if (result == FileVisitResult.SKIP_SUBTREE) { + break + } + } + } + + if (visitor.postVisitDirectory(dir, null) == FileVisitResult.TERMINATE) { + return FileVisitResult.TERMINATE + } + + return FileVisitResult.CONTINUE + } + + private class EmptyBasicFileAttributes : BasicFileAttributes { + companion object { + val INSTANCE = EmptyBasicFileAttributes() + } + + override fun lastModifiedTime(): FileTime { + TODO("Not yet implemented") + } + + override fun lastAccessTime(): FileTime { + TODO("Not yet implemented") + } + + override fun creationTime(): FileTime { + TODO("Not yet implemented") + } + + override fun isRegularFile(): Boolean { + TODO("Not yet implemented") + } + + override fun isDirectory(): Boolean { + TODO("Not yet implemented") + } + + override fun isSymbolicLink(): Boolean { + TODO("Not yet implemented") + } + + override fun isOther(): Boolean { + TODO("Not yet implemented") + } + + override fun size(): Long { + TODO("Not yet implemented") + } + + override fun fileKey(): Any { + TODO("Not yet implemented") + } + + } +} \ No newline at end of file diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 75066d6..86de93e 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -310,6 +310,7 @@ termora.transport.permissions.execute=Execute termora.transport.permissions.owner=Owner termora.transport.permissions.group=Group termora.transport.permissions.others=Others +termora.transport.permissions.include-subfolder=Include subdirectories termora.transport.sftp.retry=Retry termora.transport.sftp.select-another-host=Select another host diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 47057a6..2580351 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -323,6 +323,7 @@ termora.transport.permissions.execute=执行 termora.transport.permissions.owner=所有者 termora.transport.permissions.group=组 termora.transport.permissions.others=其他 +termora.transport.permissions.include-subfolder=包含子目录 # transport job termora.transport.jobs.table.name=名称 diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index bd42246..c4a5cf6 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -306,6 +306,17 @@ termora.transport.sftp.already-exists.destination=目標文件 termora.transport.sftp.already-exists.source=原始檔 termora.transport.sftp.already-exists.actions=操作 +# permissions +termora.transport.permissions=更改權限 +termora.transport.permissions.file-folder-permissions=檔案/資料夾權限 +termora.transport.permissions.read=讀取 +termora.transport.permissions.write=寫入 +termora.transport.permissions.execute=執行 +termora.transport.permissions.owner=所有者 +termora.transport.permissions.group=群組 +termora.transport.permissions.others=其他 +termora.transport.permissions.include-subfolder=包含子目錄 + # transport job termora.transport.jobs.table.name=名稱 termora.transport.jobs.table.status=狀態