Compare commits
67 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0020fede1 | ||
|
|
6f1eaab456 | ||
|
|
6173eae772 | ||
|
|
0bb366b1f7 | ||
|
|
9a4d6f7f4d | ||
|
|
a4ae11e301 | ||
|
|
5af0acb619 | ||
|
|
042434b8f8 | ||
|
|
eddc7ef0c6 | ||
|
|
c96ca2d424 | ||
|
|
45be9008fd | ||
|
|
057da4e297 | ||
|
|
e4e41667ff | ||
|
|
95ca0a4af7 | ||
|
|
702dee7983 | ||
|
|
165d544448 | ||
|
|
ecf61bedc4 | ||
|
|
66a81a5da3 | ||
|
|
574c816ebb | ||
|
|
e7cafb74e4 | ||
|
|
5050aa37f5 | ||
|
|
53d3d96a06 | ||
|
|
d40b8a4c9c | ||
|
|
728671509c | ||
|
|
b7178a30fb | ||
|
|
939d6a1fd7 | ||
|
|
2986a9cc46 | ||
|
|
f36afaf5d3 | ||
|
|
8cec835583 | ||
|
|
a32838dad6 | ||
|
|
d54671757e | ||
|
|
d1dba56bcd | ||
|
|
919c06779d | ||
|
|
1c90fb4e18 | ||
|
|
c1f1d5185e | ||
|
|
aa4863712d | ||
|
|
247640f2e5 | ||
|
|
5b1f803fa8 | ||
|
|
accf590c17 | ||
|
|
19fbeab817 | ||
|
|
a785ab4680 | ||
|
|
5ee23cb379 | ||
|
|
145d2de001 | ||
|
|
8d3f5fe622 | ||
|
|
9ce4a88041 | ||
|
|
c0ecc9fa7d | ||
|
|
cb33a4468a | ||
|
|
168c4c5c64 | ||
|
|
9916edbd13 | ||
|
|
ab6b6a2127 | ||
|
|
c45f5f4c92 | ||
|
|
92ee2d72f2 | ||
|
|
a4364bcd6a | ||
|
|
d0827c3b0c | ||
|
|
036a04b0b3 | ||
|
|
eee016c643 | ||
|
|
472bf6e81f | ||
|
|
21229e352f | ||
|
|
1138f48a6e | ||
|
|
f044e0480e | ||
|
|
7047f17783 | ||
|
|
9308f15abb | ||
|
|
b2672f11fc | ||
|
|
f92c6586b2 | ||
|
|
69e07a9bd9 | ||
|
|
cdec60fd25 | ||
|
|
7c30933794 |
4
.github/workflows/osx-aarch64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
4
.github/workflows/osx-x86-64.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install the Apple certificate
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora'
|
||||
if: github.event_name == 'push' && github.repository == 'TermoraDev/termora' && env.BUILD_CERTIFICATE_BASE64 != ''
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
|
||||
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
- name: Setup the Notary information
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora'"
|
||||
if: "startsWith(github.event.head_commit.message, 'release: ') && github.repository == 'TermoraDev/termora' && env.APPLE_ID != ''"
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
127
README.md
@@ -1,53 +1,102 @@
|
||||
<div align="center">
|
||||
<a href="./README.zh_CN.md">🇨🇳 简体中文</a>
|
||||
<a href="./README.zh_CN.md">简体中文</a>
|
||||
</div>
|
||||
|
||||
# Termora
|
||||
|
||||
**Termora** is a terminal emulator and SSH client for Windows, macOS and Linux.
|
||||
**Termora** is a cross-platform terminal emulator and SSH client, available on **Windows, macOS, and Linux**.
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/readme.png" alt="termora" />
|
||||
<img src="docs/readme.png" alt="Readme" />
|
||||
</div>
|
||||
|
||||
**Termora** is developed using [Kotlin/JVM](https://kotlinlang.org) and partially implements the [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) protocol (with ongoing improvements). Its ultimate vision is to achieve full platform support (including Android, iOS, and iPadOS) through [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html).
|
||||
|
||||
## Features
|
||||
|
||||
- SSH and local terminal support
|
||||
- Serial port protocol support
|
||||
- [SFTP](./docs/sftp.png?raw=1) & [Command](./docs/sftp-command.png?raw=1) file transfer support
|
||||
- Compatible with Windows, macOS, and Linux
|
||||
- Zmodem protocol support
|
||||
- SSH port forwarding & Jump hosts
|
||||
- Support for X11 and SSH-Agent
|
||||
- Terminal log
|
||||
- Configuration synchronization via [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||
- Macro support (record and replay scripts)
|
||||
- Keyword highlighting
|
||||
- Key management
|
||||
- Broadcast commands to multiple sessions
|
||||
- [Find Everywhere](./docs/findeverywhere.png?raw=1) quick navigation
|
||||
- Data encryption
|
||||
- Support [plugins](https://www.termora.app/plugins)
|
||||
- ...
|
||||
|
||||
## Download
|
||||
|
||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||
|
||||
## Development
|
||||
|
||||
It is recommended to use the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) version of the JDK and run the program via `./gradlew :run` to run the program.
|
||||
|
||||
The program can be run via `./gradlew dist` to automatically build the local version. On macOS: `dmg`, on Windows: `zip`, on Linux: `tar.gz`.
|
||||
Termora is developed using [**Kotlin/JVM**](https://kotlinlang.org/) and partially implements the [**XTerm control sequence protocol**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html). Its long-term goal is to achieve **full platform support** (including Android, iOS, and iPadOS) via [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html).
|
||||
|
||||
|
||||
## LICENSE
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🧬 Cross-platform support
|
||||
- 🔐 Built-in key manager
|
||||
- 🖼️ X11 forwarding
|
||||
- 🧑💻 SSH-Agent integration
|
||||
- 💻 System information display
|
||||
- 📁 GUI-based SFTP file management
|
||||
- 📊 Nvidia GPU usage monitoring
|
||||
- ⚡ Quick command shortcuts
|
||||
|
||||
|
||||
## 🚀 File Transfer
|
||||
|
||||
- Direct transfers between server A ↔ B
|
||||
- Recursive folder support
|
||||
- Up to **6 concurrent transfer tasks**
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/transfer.png" alt="Transfer" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## 📝 File Editing
|
||||
|
||||
- Auto-upload after editing and saving
|
||||
- Rename files and folders
|
||||
- Quick deletion of large folders (`rm -rf` supported)
|
||||
- Visual permission editing
|
||||
- Create new files and folders
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/transfer-edit.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
## 💻 Hosts
|
||||
|
||||
- Tree-like hierarchical structure, similar to folders
|
||||
- Assign tags to individual hosts
|
||||
- Import hosts from other tools
|
||||
- Open with the transfer tool
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/host.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
## 🧩 Plugins
|
||||
|
||||
- 🌍 Geo: Display geolocation of hosts
|
||||
- 🔄 Sync: Sync settings to Gist or WebDAV
|
||||
- 🗂️ WebDAV: Connect to WebDAV storage
|
||||
- 📝 Editor: Built-in SFTP file editor
|
||||
- 📡 SMB: Connect to [SMB](https://en.wikipedia.org/wiki/Server_Message_Block)
|
||||
- ☁️ S3: Connect to S3 object storage
|
||||
- ☁️ Huawei OBS: Connect to Huawei Cloud OBS
|
||||
- ☁️ Tencent COS: Connect to Tencent Cloud COS
|
||||
- ☁️ Alibaba OSS: Connect to Alibaba Cloud OSS
|
||||
- 👉 [View all plugins...](https://www.termora.app/plugins)
|
||||
|
||||
|
||||
|
||||
|
||||
## 📦 Download
|
||||
|
||||
- 🧾 [Latest Release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- 🍺 **Homebrew**: `brew install --cask termora`
|
||||
- 🔨 **WinGet**: `winget install termora`
|
||||
|
||||
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
We recommend using the [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK for development.
|
||||
|
||||
- Run locally: `./gradlew :run`
|
||||
- Build for current OS: `./gradlew :dist`
|
||||
|
||||
|
||||
|
||||
## 📄 License
|
||||
|
||||
This software is distributed under a dual-license model. You may choose one of the following options:
|
||||
|
||||
- AGPL-3.0: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
||||
- Proprietary License: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||
- **AGPL-3.0**: Use, distribute, and modify the software under the terms of the [AGPL-3.0](https://opensource.org/license/agpl-v3).
|
||||
- **Proprietary License**: For closed-source or proprietary use, please contact the author to obtain a commercial license.
|
||||
|
||||
116
README.zh_CN.md
@@ -1,48 +1,100 @@
|
||||
# Termora
|
||||
|
||||
**Termora** 是一个终端模拟器和 SSH 客户端,支持 Windows,macOS 和 Linux。
|
||||
**Termora** 是一款跨平台终端模拟器和 SSH 客户端,支持 **Windows、macOS、Linux**。
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/readme-zh_CN.png" alt="termora" />
|
||||
<img src="docs/readme-zh_CN.png" alt="Readme" />
|
||||
</div>
|
||||
|
||||
**Termora** 采用 [Kotlin/JVM](https://kotlinlang.org/) 开发并实现了 [XTerm](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) 协议(尚未完全实现),它的最终目标是通过 [Kotlin Multiplatform](https://kotlinlang.org/docs/multiplatform.html) 实现全平台(含 Android、iOS、iPadOS 等)。
|
||||
Termora 使用 [**Kotlin/JVM**](https://kotlinlang.org/) 开发,支持(正在实现中) [**XTerm 控制序列协议**](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)。未来目标是借助 [**Kotlin Multiplatform**](https://kotlinlang.org/docs/multiplatform.html) 实现 **全平台支持**,包括 Android、iOS、iPadOS 等。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 支持 SSH 和本地终端
|
||||
- 支持串口协议
|
||||
- 支持 [SFTP](./docs/sftp-zh_CN.png?raw=1) & [命令行](./docs/sftp-command.png?raw=1) 文件传输
|
||||
- 支持 Windows、macOS、Linux 平台
|
||||
- 支持 Zmodem 协议
|
||||
- 支持 SSH 端口转发和跳板机
|
||||
- 支持 X11 和 SSH-Agent
|
||||
- 终端日志记录
|
||||
- 支持配置同步到 [Gist](https://gist.github.com) & [WebDAV](https://developer.mozilla.org/docs/Glossary/WebDAV)
|
||||
- 支持宏(录制脚本并回放)
|
||||
- 支持关键词高亮
|
||||
- 支持密钥管理器
|
||||
- 支持将命令发送到多个会话
|
||||
- 支持 [Find Everywhere](./docs/findeverywhere-zh_CN.png?raw=1) 快速跳转
|
||||
- 支持数据加密
|
||||
- 支持[插件](https://www.termora.app/plugins)
|
||||
- ...
|
||||
|
||||
## 下载
|
||||
## ✨ 功能特性
|
||||
|
||||
- [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- [Homebrew](https://formulae.brew.sh/cask/termora): `brew install --cask termora`
|
||||
- [WinGet](https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/TermoraDev/Termora): `winget install termora`
|
||||
- 🧬 跨平台运行
|
||||
- 🔐 内建密钥管理器
|
||||
- 🖼️ 支持 X11 转发
|
||||
- 🧑💻 SSH-Agent 集成
|
||||
- 💻 系统信息展示
|
||||
- 📁 图形化 SFTP 文件管理
|
||||
- 📊 Nvidia 显卡使用率查看
|
||||
- ⚡ 快捷指令支持
|
||||
|
||||
## 开发
|
||||
|
||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) 的 JDK 版本,通过 `./gradlew :run` 即可运行程序。
|
||||
## 🚀 文件传输
|
||||
|
||||
通过 `./gradlew dist` 可以自动构建适用于本机的版本。在 macOS 上是:`dmg`,在 Windows 上是:`zip`,在 Linux 上是:`tar.gz`。
|
||||
- 支持 A ↔ B 服务器间直接传输
|
||||
- 文件夹递归复制支持
|
||||
- 最多可同时运行 **6 个传输任务**
|
||||
|
||||
## 协议
|
||||
<div align="center">
|
||||
<img src="docs/transfer-zh_CN.png" alt="Transfer" />
|
||||
</div>
|
||||
|
||||
本软件采用双重许可模式,您可以选择以下任意一种许可方式:
|
||||
|
||||
- AGPL-3.0:根据 [AGPL-3.0](https://opensource.org/license/agpl-v3) 的条款,您可以自由使用、分发和修改本软件。
|
||||
- 专有许可:如果希望在闭源或专有环境中使用,请联系作者获取许可。
|
||||
## 📝 文件编辑功能
|
||||
|
||||
- 保存后自动上传修改内容
|
||||
- 文件 / 文件夹 重命名
|
||||
- 快速删除大文件夹:`rm -rf` 支持
|
||||
- 可视化更改权限
|
||||
- 支持新建文件 / 文件夹
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/transfer-edit-zh_CN.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
## 💻 主机
|
||||
|
||||
- 类似文件夹树形结构
|
||||
- 给主机添加标签
|
||||
- 从其它软件导入
|
||||
- 使用传输工具打开
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/host-zh_CN.png" alt="Transfer Edit" />
|
||||
</div>
|
||||
|
||||
|
||||
## 🧩 插件
|
||||
|
||||
- 🌍 Geo:显示主机位置信息
|
||||
- 🔄 Sync:将配置同步至 Gist 或 WebDAV
|
||||
- 🗂️ WebDAV:连接 WebDAV 对象存储
|
||||
- 📝 Editor:内置 SFTP 文件编辑器
|
||||
- 📡 SMB: 连接 [SMB](https://baike.baidu.com/item/smb/4750512) 文件共享协议
|
||||
- ☁️ S3:连接 S3 对象存储
|
||||
- ☁️ Huawei OBS:连接华为云对象存储
|
||||
- ☁️ Tencent COS:连接腾讯云 COS
|
||||
- ☁️ Alibaba OSS:连接阿里云 OSS
|
||||
- 👉 [查看所有插件...](https://www.termora.cn/plugins)
|
||||
|
||||
|
||||
|
||||
|
||||
## 📦 下载
|
||||
|
||||
- 🧾 [Latest release](https://github.com/TermoraDev/termora/releases/latest)
|
||||
- 🍺 **Homebrew**:`brew install --cask termora`
|
||||
- 🪟 **WinGet**:`winget install termora`
|
||||
|
||||
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
建议使用 [JetBrainsRuntime](https://github.com/JetBrains/JetBrainsRuntime) JDK 运行环境。
|
||||
|
||||
- 本地运行:`./gradlew :run`
|
||||
- 构建当前系统安装包:`./gradlew :dist`
|
||||
|
||||
|
||||
|
||||
## 📄 授权协议
|
||||
|
||||
Termora 采用双重许可方式,您可以选择:
|
||||
|
||||
- **AGPL-3.0**:自由使用、修改、分发(遵循 [AGPL 条款](https://opensource.org/license/agpl-v3))
|
||||
- **专有许可**:如需闭源或商业用途,请联系作者获取授权
|
||||
|
||||
@@ -62,9 +62,6 @@ dependencies {
|
||||
testImplementation(libs.h2)
|
||||
testImplementation(libs.exposed.migration)
|
||||
|
||||
// implementation(platform(libs.koin.bom))
|
||||
// implementation(libs.koin.core)
|
||||
|
||||
api(kotlin("reflect"))
|
||||
api(libs.slf4j.api)
|
||||
api(libs.pty4j)
|
||||
@@ -105,7 +102,6 @@ dependencies {
|
||||
|
||||
api(libs.colorpicker)
|
||||
api(libs.mixpanel)
|
||||
api(libs.jSerialComm)
|
||||
api(libs.ini4j)
|
||||
api(libs.restart4j)
|
||||
api(libs.exposed.core)
|
||||
@@ -137,10 +133,6 @@ application {
|
||||
|
||||
args.add("-DTERMORA_PLUGIN_DIRECTORY=${layout.buildDirectory.get().asFile.absolutePath}${File.separator}plugins")
|
||||
|
||||
if (os.isLinux) {
|
||||
args.add("-Dsun.java2d.opengl=true")
|
||||
}
|
||||
|
||||
applicationDefaultJvmArgs = args
|
||||
mainClass = "app.termora.MainKt"
|
||||
}
|
||||
@@ -441,6 +433,7 @@ tasks.register<Exec>("jpackage") {
|
||||
// NSWindow
|
||||
options.add("-Dapple.awt.application.appearance=system")
|
||||
options.add("--add-opens java.desktop/java.awt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.font=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
|
||||
options.add("--add-opens java.desktop/sun.lwawt.macosx.concurrent=ALL-UNNAMED")
|
||||
@@ -448,7 +441,6 @@ tasks.register<Exec>("jpackage") {
|
||||
}
|
||||
|
||||
if (os.isLinux) {
|
||||
options.add("-Dsun.java2d.opengl=true")
|
||||
if (isDeb) {
|
||||
options.add("-Djpackage.app-layout=deb")
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 61 KiB |
BIN
docs/host-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/host.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/plugins-zh_CN.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/plugins.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 166 KiB |
BIN
docs/readme.png
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB |
BIN
docs/sftp.png
|
Before Width: | Height: | Size: 49 KiB |
BIN
docs/tags-zh_CN.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/tags.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/transfer-edit-zh_CN.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/transfer-edit.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/transfer-zh_CN.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/transfer.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -4,8 +4,8 @@ slf4j = "2.0.17"
|
||||
pty4j = "0.13.6"
|
||||
tinylog = "2.7.0"
|
||||
kotlinx-coroutines = "1.10.2"
|
||||
flatlaf = "3.6"
|
||||
kotlinx-serialization-json = "1.8.1"
|
||||
flatlaf = "3.6.1-SNAPSHOT"
|
||||
kotlinx-serialization-json = "1.9.0"
|
||||
commons-codec = "1.18.0"
|
||||
commons-lang3 = "3.17.0"
|
||||
commons-csv = "1.14.0"
|
||||
@@ -22,9 +22,9 @@ jna = "5.17.0"
|
||||
jSystemThemeDetector = "3.9.1"
|
||||
commons-io = "2.19.0"
|
||||
jbr-api = "17.1.10.1"
|
||||
hutool = "5.8.37"
|
||||
jsch = "0.2.26"
|
||||
okhttp = "4.12.0"
|
||||
hutool = "5.8.39"
|
||||
jsch = "2.27.2"
|
||||
okhttp = "5.1.0"
|
||||
sshj = "0.39.0"
|
||||
sshd-core = "2.15.0"
|
||||
jgit = "7.2.0.202503040940-r"
|
||||
@@ -35,19 +35,19 @@ bip39 = "1.0.9"
|
||||
colorpicker = "2.0.1"
|
||||
rhino = "1.8.0"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.21.2"
|
||||
testcontainers = "1.21.3"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm = "2.11.0"
|
||||
jSerialComm = "2.11.2"
|
||||
ini4j = "0.5.5-2"
|
||||
restart4j = "0.0.1"
|
||||
eddsa = "0.3.0"
|
||||
exposed = "1.0.0-beta-2"
|
||||
exposed = "1.0.0-beta-3"
|
||||
h2 = "2.3.232"
|
||||
sqlite = "3.50.1.0"
|
||||
sqlite = "3.50.2.0"
|
||||
jug = "5.1.0"
|
||||
semver4j = "5.8.0"
|
||||
jsvg = "1.4.0"
|
||||
dom4j = "2.1.4"
|
||||
semver4j = "6.0.0"
|
||||
jsvg = "2.0.0"
|
||||
dom4j = "2.2.0"
|
||||
|
||||
[libraries]
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
|
||||
@@ -73,3 +73,7 @@ https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
GeoLite2 (https://www.maxmind.com)
|
||||
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
|
||||
https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
smbj
|
||||
Apache License, Version 2.0
|
||||
https://github.com/hierynomus/smbj/blob/master/LICENSE_HEADER
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.4"
|
||||
project.version = "0.0.5"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.termora.plugins.bg
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.database.DatabaseManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -96,9 +95,7 @@ internal class BackgroundManager private constructor() : Disposable, GlassPaneAw
|
||||
return
|
||||
}
|
||||
val body = response.body
|
||||
if (body != null) {
|
||||
tempFile.outputStream().use { IOUtils.copy(body.byteStream(), it) }
|
||||
}
|
||||
IOUtils.closeQuietly(body)
|
||||
return@use tempFile
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.qcloud:cos_api:5.6.245")
|
||||
implementation("com.qcloud:cos_api:5.6.247")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.cos
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
||||
return COSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return COSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("org.apache.commons:commons-pool2:2.12.1")
|
||||
testImplementation(project(":"))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import org.apache.commons.vfs2.Capability
|
||||
import org.apache.commons.vfs2.FileName
|
||||
import org.apache.commons.vfs2.FileSystem
|
||||
import org.apache.commons.vfs2.FileSystemOptions
|
||||
import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider
|
||||
|
||||
class FTPFileProvider private constructor() : AbstractOriginatingFileProvider() {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPFileProvider() }
|
||||
val capabilities = listOf(
|
||||
Capability.CREATE,
|
||||
Capability.DELETE,
|
||||
Capability.RENAME,
|
||||
Capability.GET_TYPE,
|
||||
Capability.LIST_CHILDREN,
|
||||
Capability.READ_CONTENT,
|
||||
Capability.URI,
|
||||
Capability.WRITE_CONTENT,
|
||||
Capability.GET_LAST_MODIFIED,
|
||||
Capability.SET_LAST_MODIFIED_FILE,
|
||||
Capability.RANDOM_ACCESS_READ,
|
||||
Capability.APPEND_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCapabilities(): Collection<Capability> {
|
||||
return FTPFileProvider.capabilities
|
||||
}
|
||||
|
||||
override fun doCreateFileSystem(
|
||||
rootFileName: FileName,
|
||||
fileSystemOptions: FileSystemOptions
|
||||
): FileSystem? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
|
||||
class FTPFileSystem(private val pool: GenericObjectPool<FTPClient>) : S3FileSystem(FTPSystemProvider(pool)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = FTPPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(pool)
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.plugin.internal.BasicProxyOption
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import java.nio.charset.Charset
|
||||
import javax.swing.*
|
||||
|
||||
class FTPHostOptionsPane : OptionsPane() {
|
||||
private val generalOption = GeneralOption()
|
||||
private val proxyOption = BasicProxyOption(authenticationTypes = listOf())
|
||||
private val sftpOption = SFTPOption()
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(proxyOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
}
|
||||
|
||||
fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = FTPProtocolProvider.PROTOCOL
|
||||
val port = generalOption.portTextField.value as Int
|
||||
var authentication = Authentication.Companion.No
|
||||
var proxy = Proxy.Companion.No
|
||||
val authenticationType = AuthenticationType.Password
|
||||
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
|
||||
|
||||
if (proxyOption.proxyTypeComboBox.selectedItem != ProxyType.No) {
|
||||
proxy = proxy.copy(
|
||||
type = proxyOption.proxyTypeComboBox.selectedItem as ProxyType,
|
||||
host = proxyOption.proxyHostTextField.text,
|
||||
username = proxyOption.proxyUsernameTextField.text,
|
||||
password = String(proxyOption.proxyPasswordTextField.password),
|
||||
port = proxyOption.proxyPortTextField.value as Int,
|
||||
authenticationType = proxyOption.proxyAuthenticationTypeComboBox.selectedItem as AuthenticationType,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val options = Options.Default.copy(
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
encoding = sftpOption.charsetComboBox.selectedItem as String,
|
||||
extras = mutableMapOf("passive" to (sftpOption.passiveComboBox.selectedItem as PassiveMode).name)
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
port = port,
|
||||
host = generalOption.hostTextField.text,
|
||||
username = generalOption.usernameTextField.text,
|
||||
authentication = authentication,
|
||||
proxy = proxy,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.text = host.username
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
generalOption.hostTextField.text = host.host
|
||||
generalOption.portTextField.value = host.port
|
||||
|
||||
proxyOption.proxyTypeComboBox.selectedItem = host.proxy.type
|
||||
proxyOption.proxyHostTextField.text = host.proxy.host
|
||||
proxyOption.proxyPasswordTextField.text = host.proxy.password
|
||||
proxyOption.proxyUsernameTextField.text = host.proxy.username
|
||||
proxyOption.proxyPortTextField.value = host.proxy.port
|
||||
proxyOption.proxyAuthenticationTypeComboBox.selectedItem = host.proxy.authenticationType
|
||||
|
||||
|
||||
val passive = host.options.extras["passive"] ?: PassiveMode.Local.name
|
||||
sftpOption.charsetComboBox.selectedItem = host.options.encoding
|
||||
sftpOption.passiveComboBox.selectedItem = runCatching { PassiveMode.valueOf(passive) }
|
||||
.getOrNull() ?: PassiveMode.Local
|
||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
val host = getHost()
|
||||
|
||||
// general
|
||||
if (validateField(generalOption.nameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if (validateField(generalOption.hostTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(generalOption.usernameTextField.text) || generalOption.passwordTextField.password.isNotEmpty()) {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validateField(generalOption.passwordTextField)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// proxy
|
||||
if (host.proxy.type != ProxyType.No) {
|
||||
if (validateField(proxyOption.proxyHostTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host.proxy.authenticationType != AuthenticationType.No) {
|
||||
if (validateField(proxyOption.proxyUsernameTextField)
|
||||
|| validateField(proxyOption.proxyPasswordTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && (if (textField is JPasswordField) textField.password.isEmpty() else textField.text.isBlank())) {
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(c: JComponent) {
|
||||
selectOptionJComponent(c)
|
||||
c.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
c.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner(21)
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val usernameTextField = OutlineTextField(128)
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val publicKeyComboBox = OutlineComboBox<String>()
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
val authenticationTypeComboBox = FlatComboBox<AuthenticationType>()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
publicKeyComboBox.isEditable = false
|
||||
|
||||
publicKeyComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = StringUtils.EMPTY
|
||||
if (value is String) {
|
||||
text = KeyManager.getInstance().getOhKeyPair(value)?.name ?: text
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value?.toString() ?: ""
|
||||
when (value) {
|
||||
AuthenticationType.Password -> {
|
||||
text = "Password"
|
||||
}
|
||||
|
||||
AuthenticationType.PublicKey -> {
|
||||
text = "Public Key"
|
||||
}
|
||||
|
||||
AuthenticationType.KeyboardInteractive -> {
|
||||
text = "Keyboard Interactive"
|
||||
}
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text,
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
|
||||
authenticationTypeComboBox.selectedItem = AuthenticationType.Password
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||
removeComponentListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.settings
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.general")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
remarkTextArea.rows = 8
|
||||
remarkTextArea.lineWrap = true
|
||||
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||
.add(hostTextField).xy(3, rows)
|
||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.authentication")}:").xy(1, rows)
|
||||
.add(authenticationTypeComboBox).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||
.xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
val defaultDirectoryField = OutlineTextField(255)
|
||||
val charsetComboBox = JComboBox<String>()
|
||||
val passiveComboBox = JComboBox<PassiveMode>()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
for (e in Charset.availableCharsets()) {
|
||||
charsetComboBox.addItem(e.key)
|
||||
}
|
||||
|
||||
charsetComboBox.selectedItem = "UTF-8"
|
||||
|
||||
passiveComboBox.addItem(PassiveMode.Local)
|
||||
passiveComboBox.addItem(PassiveMode.Remote)
|
||||
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.folder
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.transport.sftp")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.terminal.encoding")}:").xy(1, rows)
|
||||
.add(charsetComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${FTPI18n.getString("termora.plugins.ftp.passive")}:").xy(1, rows)
|
||||
.add(passiveComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
enum class PassiveMode {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
object FTPI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(FTPI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
|
||||
override fun getString(key: String): String {
|
||||
return try {
|
||||
substitutor.replace(getBundle().getString(key))
|
||||
} catch (_: MissingResourceException) {
|
||||
I18n.getString(key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class FTPPath(fileSystem: FTPFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.I18n
|
||||
import app.termora.Icons
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
@@ -27,6 +24,7 @@ class FTPPlugin : PaidPlugin {
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
|
||||
class FTPProtocolHostPanel : ProtocolHostPanel() {
|
||||
|
||||
private val pane = FTPHostOptionsPane()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
add(pane, BorderLayout.CENTER)
|
||||
Disposer.register(this, pane)
|
||||
}
|
||||
|
||||
private fun initEvents() {}
|
||||
|
||||
override fun getHost(): Host {
|
||||
return Host(
|
||||
name = StringUtils.EMPTY,
|
||||
protocol = FTPProtocolProvider.PROTOCOL
|
||||
)
|
||||
return pane.getHost()
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
|
||||
pane.setHost(host)
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return true
|
||||
return pane.validateFields()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class FTPProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolHostPanelExtension() }
|
||||
val instance = FTPProtocolHostPanelExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return FTPProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.AuthenticationType
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.FileObjectHandler
|
||||
import app.termora.protocol.FileObjectRequest
|
||||
import app.termora.ProxyType
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import org.apache.commons.vfs2.provider.FileProvider
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.pool2.BasePooledObjectFactory
|
||||
import org.apache.commons.pool2.PooledObject
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.nio.charset.Charset
|
||||
import java.time.Duration
|
||||
|
||||
|
||||
class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProvider() }
|
||||
private val log = LoggerFactory.getLogger(FTPProtocolProvider::class.java)
|
||||
|
||||
val instance = FTPProtocolProvider()
|
||||
const val PROTOCOL = "FTP"
|
||||
}
|
||||
|
||||
@@ -22,12 +39,82 @@ class FTPProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
return Icons.ftp
|
||||
}
|
||||
|
||||
override fun getFileProvider(): FileProvider {
|
||||
return FTPFileProvider.instance
|
||||
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||
val host = requester.host
|
||||
|
||||
val config = GenericObjectPoolConfig<FTPClient>().apply {
|
||||
maxTotal = 12
|
||||
// 与 transfer 最大传输量匹配
|
||||
maxIdle = 6
|
||||
minIdle = 1
|
||||
testOnBorrow = false
|
||||
testWhileIdle = true
|
||||
// 检测空闲对象线程每次运行时检测的空闲对象的数量
|
||||
timeBetweenEvictionRuns = Duration.ofSeconds(30)
|
||||
// 连接空闲的最小时间,达到此值后空闲链接将会被移除,且保留 minIdle 个空闲连接数
|
||||
softMinEvictableIdleDuration = Duration.ofSeconds(30)
|
||||
// 连接的最小空闲时间,达到此值后该空闲连接可能会被移除(还需看是否已达最大空闲连接数)
|
||||
minEvictableIdleDuration = Duration.ofMinutes(3)
|
||||
}
|
||||
|
||||
override fun getRootFileObject(requester: FileObjectRequest): FileObjectHandler {
|
||||
TODO("Not yet implemented")
|
||||
val ftpClientPool = GenericObjectPool(object : BasePooledObjectFactory<FTPClient>() {
|
||||
override fun create(): FTPClient {
|
||||
val client = FTPClient()
|
||||
client.charset = Charset.forName(host.options.encoding)
|
||||
client.controlEncoding = client.charset.name()
|
||||
client.connect(host.host, host.port)
|
||||
if (client.isConnected.not()) {
|
||||
throw IllegalStateException("FTP client is not connected")
|
||||
}
|
||||
|
||||
if (host.proxy.type == ProxyType.HTTP) {
|
||||
client.proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
} else if (host.proxy.type == ProxyType.SOCKS5) {
|
||||
client.proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(host.proxy.host, host.proxy.port))
|
||||
}
|
||||
|
||||
val password = if (host.authentication.type == AuthenticationType.Password)
|
||||
host.authentication.password else StringUtils.EMPTY
|
||||
if (client.login(host.username, password).not()) {
|
||||
throw IllegalStateException("Incorrect account or password")
|
||||
}
|
||||
|
||||
if (host.options.extras["passive"] == FTPHostOptionsPane.PassiveMode.Remote.name) {
|
||||
client.enterRemotePassiveMode()
|
||||
} else {
|
||||
client.enterLocalPassiveMode()
|
||||
}
|
||||
|
||||
client.listHiddenFiles = true
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
override fun wrap(obj: FTPClient): PooledObject<FTPClient> {
|
||||
return DefaultPooledObject(obj)
|
||||
}
|
||||
|
||||
override fun validateObject(p: PooledObject<FTPClient>): Boolean {
|
||||
val ftp = p.`object`
|
||||
return ftp.isConnected.not() && ftp.sendNoOp()
|
||||
}
|
||||
|
||||
override fun destroyObject(p: PooledObject<FTPClient>) {
|
||||
try {
|
||||
p.`object`.disconnect()
|
||||
} catch (e: Exception) {
|
||||
if (log.isWarnEnabled) {
|
||||
log.warn(e.message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, config)
|
||||
|
||||
val defaultPath = host.options.sftpDefaultDirectory
|
||||
val fs = FTPFileSystem(ftpClientPool)
|
||||
return PathHandler(fs, fs.getPath(defaultPath))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class FTPProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { FTPProtocolProviderExtension() }
|
||||
val instance = FTPProtocolProviderExtension()
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return FTPProtocolProvider.Companion.instance
|
||||
return FTPProtocolProvider.instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package app.termora.plugins.ftp
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystemProvider
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.net.ftp.FTPClient
|
||||
import org.apache.commons.net.ftp.FTPFile
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.exists
|
||||
|
||||
class FTPSystemProvider(private val pool: GenericObjectPool<FTPClient>) : S3FileSystemProvider() {
|
||||
|
||||
|
||||
override fun getScheme(): String? {
|
||||
return "ftp"
|
||||
}
|
||||
|
||||
override fun getOutputStream(path: S3Path): OutputStream {
|
||||
return createStreamer(path)
|
||||
}
|
||||
|
||||
override fun getInputStream(path: S3Path): InputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val fs = ftp.retrieveFileStream(path.absolutePathString())
|
||||
return object : InputStream() {
|
||||
override fun read(): Int {
|
||||
return fs.read()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(fs)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createStreamer(path: S3Path): OutputStream {
|
||||
val ftp = pool.borrowObject()
|
||||
val os = ftp.storeFileStream(path.absolutePathString())
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
os.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(os)
|
||||
ftp.completePendingCommand()
|
||||
pool.returnObject(ftp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||
val paths = mutableListOf<S3Path>()
|
||||
if (path.exists().not()) {
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
withFtpClient {
|
||||
val files = it.listFiles(path.absolutePathString())
|
||||
for (file in files) {
|
||||
val p = path.resolve(file.name)
|
||||
p.attributes = p.attributes.copy(
|
||||
directory = file.isDirectory,
|
||||
regularFile = file.isFile,
|
||||
size = file.size,
|
||||
lastModifiedTime = file.timestamp.timeInMillis,
|
||||
)
|
||||
p.attributes.permissions = ftpPermissionsToPosix(file)
|
||||
paths.add(p)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun ftpPermissionsToPosix(file: FTPFile): Set<PosixFilePermission> {
|
||||
val perms = mutableSetOf<PosixFilePermission>()
|
||||
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_READ)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_WRITE)
|
||||
if (file.hasPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_READ)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_WRITE)
|
||||
if (file.hasPermission(FTPFile.GROUP_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.GROUP_EXECUTE)
|
||||
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.READ_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_READ)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.WRITE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_WRITE)
|
||||
if (file.hasPermission(FTPFile.WORLD_ACCESS, FTPFile.EXECUTE_PERMISSION))
|
||||
perms.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||
|
||||
return perms
|
||||
}
|
||||
|
||||
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||
withFtpClient { it.mkd(dir.absolutePathString()) }
|
||||
}
|
||||
|
||||
override fun move(source: Path?, target: Path?, vararg options: CopyOption?) {
|
||||
if (source != null && target != null) {
|
||||
withFtpClient {
|
||||
it.rename(source.absolutePathString(), target.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||
withFtpClient {
|
||||
if (isDirectory) {
|
||||
it.rmd(path.absolutePathString())
|
||||
} else {
|
||||
it.deleteFile(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||
withFtpClient {
|
||||
if (it.cwd(path.absolutePathString()) == 250) {
|
||||
return
|
||||
}
|
||||
if (it.listFiles(path.absolutePathString()).isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
private inline fun <T> withFtpClient(block: (FTPClient) -> T): T {
|
||||
val client = pool.borrowObject()
|
||||
return try {
|
||||
block(client)
|
||||
} finally {
|
||||
pool.returnObject(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to FTP</description>
|
||||
<description language="zh_CN">支持连接到到 FTP</description>
|
||||
<description language="zh_CN">支持连接到 FTP</description>
|
||||
<description language="zh_TW">支援連接到 FTP</description>
|
||||
</descriptions>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#6C707E"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#6C707E"></path></svg>
|
||||
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#6C707E"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#6C707E"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
||||
<svg t="1747213953443" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1523" width="16" height="16"><path d="M851.4776 101.12H170.72239999A80.1984 80.1984 0 0 0 90.61999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.75520001c44.16 0 80.1024-35.9424 80.10239999-80.1216V181.2224c0-44.16-35.9424-80.1024-80.10239999-80.1024zM877.81999999 679.5776c0 14.5344-11.8272 26.3424-26.34239999 26.3424H170.72239999A26.3808 26.3808 0 0 1 144.38 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.34239999-26.3424h680.75520001c14.5152 0 26.3424 11.8272 26.34239999 26.3424v498.3552zM731.9 840.32h-441.60000001a26.88 26.88 0 0 0 0 53.76h441.60000001a26.88 26.88 0 0 0 0-53.76z" p-id="1524" fill="#CED0D6"></path><path d="M242.3576 554.72h46.90559999v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H242.3576zM408.51439999 359.1296h65.9328v195.5904h46.92480001V359.1296h66.56639999v-38.9952h-179.424zM703.06159999 320.1344h-77.03039999v234.5664h46.90559999v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44000001 36.4416 0.0192 26.9568-15.5136 40.5888-47.84640001 40.5888z" p-id="1525" fill="#CED0D6"></path></svg>
|
||||
<svg t="1751945257078" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1612" width="16" height="16"><path d="M853.97759999 101.12H173.22239999A80.1984 80.1984 0 0 0 93.11999999 181.2224v498.3552a80.2176 80.2176 0 0 0 80.1024 80.1216h680.7552c44.16 0 80.1024-35.9424 80.1024-80.1216V181.2224c0-44.16-35.9424-80.1024-80.1024-80.1024zM880.31999999 679.5776c0 14.5344-11.8272 26.3424-26.3424 26.3424H173.22239999A26.3808 26.3808 0 0 1 146.87999999 679.5776V181.2224c0-14.5152 11.8272-26.3424 26.3424-26.3424h680.7552c14.5152 0 26.3424 11.8272 26.3424 26.3424v498.3552zM734.39999999 840.32h-441.6a26.88 26.88 0 0 0 0 53.76h441.6a26.88 26.88 0 0 0 0-53.76z" p-id="1613" fill="#CED0D6"></path><path d="M244.85759999 554.72h46.9056v-95.1168h83.3664v-39.2832h-83.3664v-61.1904h97.632v-38.9952H244.85759999zM411.01439999 359.1296h65.9328v195.5904h46.9248V359.1296h66.5664v-38.9952h-179.424zM705.56159999 320.1344h-77.0304v234.5664h46.9056v-83.3664h31.392c50.4 0 90.6624-24.0768 90.6624-77.664 0-55.4688-39.936-73.536-91.9296-73.536z m-1.9008 114.1248h-28.224v-77.0304h26.6304c32.3328 0 49.44 9.1968 49.44 36.4416 0.0192 26.9568-15.5136 40.5888-47.8464 40.5888z" p-id="1614" fill="#CED0D6"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
1
plugins/ftp/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=Passive Mode
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被动模式
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.ftp.passive=被動模式
|
||||
@@ -2,14 +2,14 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.5"
|
||||
project.version = "0.0.7"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.maxmind.geoip2:geoip2:4.3.1")
|
||||
// https://github.com/hstyi/geolite2
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202506280327")
|
||||
implementation("com.github.hstyi:geolite2:v1.0-202507040118")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.tree.HostTreeNode
|
||||
import app.termora.tree.MarkerSimpleTreeCellAnnotation
|
||||
import app.termora.tree.SimpleTreeCellAnnotation
|
||||
import app.termora.tree.SimpleTreeCellRendererExtension
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import java.awt.Color
|
||||
import javax.swing.JTree
|
||||
|
||||
@@ -33,7 +34,7 @@ class GeoSimpleTreeCellRendererExtension private constructor() : SimpleTreeCellR
|
||||
if (GeoHostTreeShowMoreEnableExtension.instance.isShowMore().not()) return emptyList()
|
||||
val country = geo.country(node.data.host) ?: return emptyList()
|
||||
|
||||
val text = "${countryCodeToFlagEmoji(country.isoCode)}${country.name}"
|
||||
val text = if (SystemInfo.isMacOS) "${countryCodeToFlagEmoji(country.isoCode)}${country.name}" else country.name
|
||||
return listOf(
|
||||
MarkerSimpleTreeCellAnnotation(
|
||||
text,
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -46,7 +46,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
controlsVisible = false
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
title = I18n.getString("termora.doorman.safe")
|
||||
title = MigrationI18n.getString("termora.doorman.safe")
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_TITLE, false)
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
label.text = I18n.getString("termora.doorman.safe")
|
||||
tip.text = I18n.getString("termora.doorman.unlock-data")
|
||||
label.text = MigrationI18n.getString("termora.doorman.safe")
|
||||
tip.text = MigrationI18n.getString("termora.doorman.unlock-data")
|
||||
icon.icon = FlatSVGIcon(Icons.role.name, 80, 80)
|
||||
safeBtn.icon = Icons.unlocked
|
||||
|
||||
@@ -95,24 +95,24 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
.add(passwordTextField).xy(2, rows)
|
||||
.add(safeBtn).xy(4, rows).apply { rows += step }
|
||||
.add(tip).xyw(2, rows, 4, "center, fill").apply { rows += step }
|
||||
.add(JXHyperlink(object : AnAction(I18n.getString("termora.doorman.forget-password")) {
|
||||
.add(JXHyperlink(object : AnAction(MigrationI18n.getString("termora.doorman.forget-password")) {
|
||||
override fun actionPerformed(evt: AnActionEvent) {
|
||||
val option = OptionPane.showConfirmDialog(
|
||||
this@DoormanDialog, I18n.getString("termora.doorman.forget-password-message"),
|
||||
this@DoormanDialog, MigrationI18n.getString("termora.doorman.forget-password-message"),
|
||||
options = arrayOf(
|
||||
I18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
I18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
MigrationI18n.getString("termora.doorman.have-a-mnemonic"),
|
||||
MigrationI18n.getString("termora.doorman.dont-have-a-mnemonic"),
|
||||
),
|
||||
optionType = JOptionPane.YES_NO_OPTION,
|
||||
messageType = JOptionPane.INFORMATION_MESSAGE,
|
||||
initialValue = I18n.getString("termora.doorman.have-a-mnemonic")
|
||||
initialValue = MigrationI18n.getString("termora.doorman.have-a-mnemonic")
|
||||
)
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
showMnemonicsDialog()
|
||||
} else if (option == JOptionPane.NO_OPTION) {
|
||||
OptionPane.showMessageDialog(
|
||||
this@DoormanDialog,
|
||||
I18n.getString("termora.doorman.delete-data"),
|
||||
MigrationI18n.getString("termora.doorman.delete-data"),
|
||||
messageType = JOptionPane.WARNING_MESSAGE
|
||||
)
|
||||
Application.browse(MigrationApplicationRunnerExtension.instance.getDatabaseFile().toURI())
|
||||
@@ -141,7 +141,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
log.error(e.message, e)
|
||||
}
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
this, MigrationI18n.getString("termora.doorman.mnemonic-data-corrupted"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
passwordTextField.outline = "error"
|
||||
@@ -166,7 +166,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
} catch (e: Exception) {
|
||||
if (e is PasswordWrongException) {
|
||||
OptionPane.showMessageDialog(
|
||||
this, I18n.getString("termora.doorman.password-wrong"),
|
||||
this, MigrationI18n.getString("termora.doorman.password-wrong"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
isModal = true
|
||||
isResizable = true
|
||||
controlsVisible = false
|
||||
title = I18n.getString("termora.doorman.mnemonic.title")
|
||||
title = MigrationI18n.getString("termora.doorman.mnemonic.title")
|
||||
init()
|
||||
pack()
|
||||
size = Dimension(max(size.width, UIManager.getInt("Dialog.width") - 250), size.height)
|
||||
@@ -251,7 +251,7 @@ class DoormanDialog(owner: Window?) : DialogWrapper(owner) {
|
||||
} catch (e: Exception) {
|
||||
OptionPane.showMessageDialog(
|
||||
this,
|
||||
I18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
MigrationI18n.getString("termora.doorman.mnemonic.incorrect"),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
return
|
||||
|
||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 For more information, please see: <a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=Migrate
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=Data is encrypted
|
||||
termora.doorman.unlock-data=Enter password to unlock data
|
||||
termora.doorman.password-wrong=Wrong password
|
||||
termora.doorman.forget-password=Forgot password?
|
||||
termora.doorman.delete-data=Delete the data catalog and restart, This will lose all data
|
||||
termora.doorman.forget-password-message=Unlock data with a mnemonic. Without it, data cannot be accessed
|
||||
termora.doorman.have-a-mnemonic=I have a mnemonic
|
||||
termora.doorman.dont-have-a-mnemonic=I don't have a mnemonic
|
||||
termora.doorman.mnemonic-data-corrupted=Unable to decrypt data with the mnemonic, the data maybe corrupted
|
||||
|
||||
termora.doorman.mnemonic.title=Enter 12 mnemonic words
|
||||
termora.doorman.mnemonic.incorrect=Incorrect mnemonic
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=Данные защифрованы
|
||||
termora.doorman.unlock-data=Введите пароль для разблокировки данных
|
||||
termora.doorman.verify-password=Введите пароль для проверки
|
||||
termora.doorman.password-wrong=Неверный пароль
|
||||
termora.doorman.password-correct=Пароль верный
|
||||
termora.doorman.unsafe=Данные не зашифрованы
|
||||
termora.doorman.lock-data=Спрашивать пароль при запуске
|
||||
termora.doorman.forget-password=Забыли пароль?
|
||||
termora.doorman.delete-data=Удалить данные и перезапустить, это приведет к потере всех данных
|
||||
termora.doorman.forget-password-message=Разблокировать данные с помощью мнемоники. Без него доступ к данным невозможен.
|
||||
termora.doorman.have-a-mnemonic=У меня есть мнемоники
|
||||
termora.doorman.dont-have-a-mnemonic=У меня нет мнемоники
|
||||
termora.doorman.mnemonic-data-corrupted=Невозможно расшифровать данные с помощью мнемоники, возможно, данные повреждены.
|
||||
|
||||
termora.doorman.mnemonic.title=Введите 12 слов мнемоники
|
||||
termora.doorman.mnemonic.incorrect=Неверные мнемоники
|
||||
@@ -7,3 +7,17 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 更多信息请查看:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=迁移
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=数据已加密
|
||||
termora.doorman.unlock-data=输入密码解锁数据
|
||||
termora.doorman.password-wrong=密码错误
|
||||
termora.doorman.forget-password=忘记密码?
|
||||
termora.doorman.delete-data=删除数据目录后重新启动程序,这样会丢失所有数据
|
||||
termora.doorman.forget-password-message=通过助记词解锁数据,没有助记词则无法解锁
|
||||
termora.doorman.have-a-mnemonic=我有助记词
|
||||
termora.doorman.dont-have-a-mnemonic=我没有助记词
|
||||
termora.doorman.mnemonic-data-corrupted=无法从助记词解密数据,数据可能已经损坏
|
||||
|
||||
termora.doorman.mnemonic.title=输入 12 个助记词
|
||||
termora.doorman.mnemonic.incorrect=助记词错误
|
||||
|
||||
@@ -7,3 +7,18 @@ termora.plugins.migration.message=<html> \
|
||||
<h3 align="center">📎 更多資訊請參見:<a href="https://github.com/TermoraDev/termora/issues/645">TermoraDev/termora/issues/645</a></h3> \
|
||||
</html>
|
||||
termora.plugins.migration.migrate=遷移
|
||||
|
||||
|
||||
# Doorman
|
||||
termora.doorman.safe=資料已加密
|
||||
termora.doorman.unlock-data=輸入密碼解鎖資料
|
||||
termora.doorman.password-wrong=密碼錯誤
|
||||
termora.doorman.forget-password=忘記密碼?
|
||||
termora.doorman.delete-data=刪除資料目錄後重新啟動程序,這樣會遺失所有數據
|
||||
termora.doorman.forget-password-message=透過助記詞解鎖數據,沒有助記詞則無法解鎖
|
||||
termora.doorman.have-a-mnemonic=我有助記詞
|
||||
termora.doorman.dont-have-a-mnemonic=我沒有助記詞
|
||||
termora.doorman.mnemonic-data-corrupted=無法從助記詞解密數據,資料可能已損壞
|
||||
termora.doorman.mnemonic.title=輸入 12 個助記詞
|
||||
termora.doorman.mnemonic.incorrect=助記詞錯誤
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
project.version = "0.0.2"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.4")
|
||||
implementation("com.huaweicloud:esdk-obs-java-bundle:3.25.5")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.obs
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
||||
return OBSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return OBSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.2")
|
||||
implementation("com.aliyun.oss:aliyun-sdk-oss:3.18.3")
|
||||
implementation("javax.xml.bind:jaxb-api:2.3.1")
|
||||
implementation("javax.activation:activation:1.1.1")
|
||||
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.oss
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
|
||||
return OSSProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return OSSProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.5"
|
||||
project.version = "0.0.6"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.s3
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
|
||||
return S3ProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return S3ProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
17
plugins/serial/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
|
||||
|
||||
project.version = "0.0.1"
|
||||
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
compileOnly(project(":"))
|
||||
implementation("com.fazecast:jSerialComm:2.11.2")
|
||||
}
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.plugin.internal.BasicGeneralOption
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.Plugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
internal class SerialPlugin : Plugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { SerialProtocolProviderExtension.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SerialProtocolHostPanelExtension.instance }
|
||||
}
|
||||
|
||||
|
||||
override fun getName(): String {
|
||||
return "Serial Comm"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -14,8 +15,11 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
|
||||
return SerialProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return SerialProtocolHostPanel()
|
||||
}
|
||||
|
||||
override fun ordered(): Long {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.actions.DataProvider
|
||||
@@ -1,4 +1,4 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
@@ -1,6 +1,9 @@
|
||||
package app.termora.plugin.internal.serial
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.*
|
||||
import app.termora.Host
|
||||
import app.termora.Icons
|
||||
import app.termora.PtyHostTerminalTab
|
||||
import app.termora.WindowScope
|
||||
import app.termora.terminal.PtyConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -1,5 +1,8 @@
|
||||
package app.termora
|
||||
package app.termora.plugins.serial
|
||||
|
||||
import app.termora.Host
|
||||
import app.termora.SerialCommFlowControl
|
||||
import app.termora.SerialCommParity
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
|
||||
object Serials {
|
||||
22
plugins/serial/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>serial</id>
|
||||
|
||||
<name>Serial Comm</name>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.serial.SerialPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Supports access to serial ports</description>
|
||||
<description language="zh_CN">支持访问串口</description>
|
||||
<description language="zh_TW">支援訪問串口</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
@@ -0,0 +1 @@
|
||||
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169" width="16" height="16"><path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z" fill="#6C707E" p-id="1170"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg t="1747210120200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1169"
|
||||
width="16" height="16">
|
||||
<path d="M806.11718723 531.44140652l-78.2578125 78.50390571L410.22265625 291.21874973l82.08984402-78.53906223a231.46874973 231.46874973 0 0 1 162.49218723-68.66015625 206.54296902 206.54296902 0 0 1 131.02734402 44.296875L856.98828125 117.125a35.15625027 35.15625027 0 0 1 49.67578125-0.03515652h0.03515652a35.15625027 35.15625027 0 0 1 0 49.74609429l-74.35546875 74.35546875a225.21093777 225.21093777 0 0 1-26.22656304 290.25z m-191.63671875 27.07031223a24.57421875 24.57421875 0 0 1-1.51171821 33.08203098l-57.72656277 57.76171929 63.98437473 63.73828071-83.42578125 83.46093777a230.62499973 230.62499973 0 0 1-161.71874946 68.66015625 204.92578125 204.92578125 0 0 1-136.75781304-49.21874973l-95.83593723 95.80078071a24.18750027 24.18750027 0 0 1-34.24218723-0.07031223 24.890625 24.890625 0 0 1-0.35156277-34.76953098l95.94140598-100.12500027a225.98437473 225.98437473 0 0 1 21.09375-299.28515598L305.98437473 394.0859375l68.66015625 68.66015625L427.13281277 411.10156277a24.39843777 24.39843777 0 0 1 34.34765598 34.59375L409.94140652 497.234375l117.9140625 117.63281277 56.53125-57.5859375a20.28515652 20.28515652 0 0 1 30.09374946 1.23046848z m-112.32421875 199.19531223l44.05078179-44.05078098-240.22265679-240.46875-44.296875 44.57812473a169.91015625 169.91015625 0 0 0-4.67578071 240.18750027l4.640625 4.92187473a151.875 151.875 0 0 0 117.9140625 48.97265652 175.35937473 175.35937473 0 0 0 122.58984321-54.70312473v0.56249946zM933.875 512c0 232.98046875-188.89453125 421.875-421.875 421.875-7.87499973 0-15.71484375-0.2109375-23.48437473-0.6328125l73.12499946-73.16015598a351.77343777 351.77343777 0 0 0 298.44140679-298.40625027l73.12499946-73.16015598c0.45703152 7.76953098 0.66796902 15.609375 0.66796902 23.48437473zM535.48437473 90.7578125L462.35937527 163.953125a351.77343777 351.77343777 0 0 0-298.44140679 298.40625027l-73.12499946 73.16015598A428.625 428.625 0 0 1 90.125 512C90.125 279.01953125 279.01953125 90.125 512 90.125c7.87499973 0 15.71484375 0.2109375 23.48437473 0.6328125z"
|
||||
fill="#CED0D6" p-id="1170"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
14
plugins/smb/build.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.3"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
implementation("com.hierynomus:smbj:0.14.0")
|
||||
compileOnly(project(":"))
|
||||
}
|
||||
|
||||
|
||||
apply(from = "$rootDir/plugins/common.gradle.kts")
|
||||
@@ -0,0 +1,25 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystem
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import com.hierynomus.smbj.session.Session
|
||||
import com.hierynomus.smbj.share.DiskShare
|
||||
|
||||
class SMBFileSystem(private val share: DiskShare, session: Session) :
|
||||
S3FileSystem(SMBFileSystemProvider(share, session)) {
|
||||
|
||||
override fun create(root: String?, names: List<String>): S3Path {
|
||||
val path = SMBPath(this, root, names)
|
||||
if (names.isEmpty()) {
|
||||
path.attributes = path.attributes.copy(directory = true)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
|
||||
override fun close() {
|
||||
share.close()
|
||||
super.close()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3FileSystemProvider
|
||||
import app.termora.transfer.s3.S3Path
|
||||
import com.hierynomus.msdtyp.AccessMask
|
||||
import com.hierynomus.msfscc.FileAttributes
|
||||
import com.hierynomus.mssmb2.SMB2CreateDisposition
|
||||
import com.hierynomus.mssmb2.SMB2CreateOptions
|
||||
import com.hierynomus.mssmb2.SMB2ShareAccess
|
||||
import com.hierynomus.smbj.session.Session
|
||||
import com.hierynomus.smbj.share.DiskShare
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import kotlin.io.path.absolutePathString
|
||||
|
||||
|
||||
class SMBFileSystemProvider(private val share: DiskShare, private val session: Session) : S3FileSystemProvider() {
|
||||
|
||||
override fun getScheme(): String? {
|
||||
return "smb"
|
||||
}
|
||||
|
||||
override fun getOutputStream(path: S3Path): OutputStream {
|
||||
val file = share.openFile(
|
||||
path.absolutePathString(),
|
||||
setOf(AccessMask.GENERIC_WRITE),
|
||||
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
||||
setOf(SMB2ShareAccess.FILE_SHARE_READ),
|
||||
SMB2CreateDisposition.FILE_OVERWRITE_IF,
|
||||
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
|
||||
)
|
||||
val os = file.outputStream
|
||||
|
||||
return object : OutputStream() {
|
||||
override fun write(b: Int) {
|
||||
os.write(b)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(os)
|
||||
file.closeNoWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInputStream(path: S3Path): InputStream {
|
||||
val file = share.openFile(
|
||||
path.absolutePathString(),
|
||||
setOf(AccessMask.GENERIC_READ),
|
||||
setOf(FileAttributes.FILE_ATTRIBUTE_NORMAL),
|
||||
setOf(SMB2ShareAccess.FILE_SHARE_READ),
|
||||
SMB2CreateDisposition.FILE_OPEN,
|
||||
setOf(SMB2CreateOptions.FILE_NON_DIRECTORY_FILE)
|
||||
)
|
||||
val input = file.inputStream
|
||||
return object : InputStream() {
|
||||
override fun read(): Int = input.read()
|
||||
override fun close() {
|
||||
IOUtils.closeQuietly(input)
|
||||
file.closeNoWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun fetchChildren(path: S3Path): MutableList<S3Path> {
|
||||
val paths = mutableListOf<S3Path>()
|
||||
val absolutePath = FilenameUtils.separatorsToUnix(path.absolutePathString())
|
||||
for (information in share.list(if (absolutePath == path.fileSystem.separator) StringUtils.EMPTY else absolutePath)) {
|
||||
if (information.fileName == "." || information.fileName == "..") continue
|
||||
val isDir = information.fileAttributes and FileAttributes.FILE_ATTRIBUTE_DIRECTORY.value != 0L
|
||||
val path = path.resolve(information.fileName)
|
||||
path.attributes = path.attributes.copy(
|
||||
directory = isDir, regularFile = isDir.not(),
|
||||
size = information.endOfFile,
|
||||
lastModifiedTime = information.lastWriteTime.toDate().time,
|
||||
lastAccessTime = information.lastAccessTime.toDate().time,
|
||||
)
|
||||
paths.add(path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||
share.mkdir(dir.absolutePathString())
|
||||
}
|
||||
|
||||
override fun delete(path: S3Path, isDirectory: Boolean) {
|
||||
if (isDirectory) {
|
||||
share.rmdir(path.absolutePathString(), false)
|
||||
} else {
|
||||
share.rm(path.absolutePathString())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun checkAccess(path: S3Path, vararg modes: AccessMode) {
|
||||
if (share.fileExists(path.absolutePathString()) || share.folderExists(path.absolutePathString())) {
|
||||
return
|
||||
}
|
||||
throw NoSuchFileException(path.absolutePathString())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.*
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.hierynomus.smbj.SMBClient
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.event.ComponentAdapter
|
||||
import java.awt.event.ComponentEvent
|
||||
import javax.swing.*
|
||||
|
||||
class SMBHostOptionsPane : OptionsPane() {
|
||||
private val generalOption = GeneralOption()
|
||||
private val sftpOption = SFTPOption()
|
||||
|
||||
init {
|
||||
addOption(generalOption)
|
||||
addOption(sftpOption)
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun getHost(): Host {
|
||||
val name = generalOption.nameTextField.text
|
||||
val protocol = SMBProtocolProvider.PROTOCOL
|
||||
val host = generalOption.hostTextField.text
|
||||
val port = generalOption.portTextField.value as Int
|
||||
var authentication = Authentication.Companion.No
|
||||
val authenticationType = AuthenticationType.Password
|
||||
|
||||
authentication = authentication.copy(
|
||||
type = authenticationType,
|
||||
password = String(generalOption.passwordTextField.password)
|
||||
)
|
||||
|
||||
|
||||
val options = Options.Default.copy(
|
||||
sftpDefaultDirectory = sftpOption.defaultDirectoryField.text,
|
||||
extras = mutableMapOf(
|
||||
"smb.share" to generalOption.shareTextField.text,
|
||||
)
|
||||
)
|
||||
|
||||
return Host(
|
||||
name = name,
|
||||
protocol = protocol,
|
||||
host = host,
|
||||
port = port,
|
||||
username = generalOption.usernameTextField.selectedItem as String,
|
||||
authentication = authentication,
|
||||
sort = System.currentTimeMillis(),
|
||||
remark = generalOption.remarkTextArea.text,
|
||||
options = options,
|
||||
)
|
||||
}
|
||||
|
||||
fun setHost(host: Host) {
|
||||
generalOption.nameTextField.text = host.name
|
||||
generalOption.usernameTextField.selectedItem = host.username
|
||||
generalOption.hostTextField.text = host.host
|
||||
generalOption.portTextField.value = host.port
|
||||
generalOption.remarkTextArea.text = host.remark
|
||||
generalOption.passwordTextField.text = host.authentication.password
|
||||
generalOption.shareTextField.text = host.options.extras["smb.share"] ?: StringUtils.EMPTY
|
||||
|
||||
sftpOption.defaultDirectoryField.text = host.options.sftpDefaultDirectory
|
||||
}
|
||||
|
||||
fun validateFields(): Boolean {
|
||||
|
||||
// general
|
||||
if (validateField(generalOption.nameTextField)
|
||||
|| validateField(generalOption.hostTextField)
|
||||
|| validateField(generalOption.shareTextField)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
val username = generalOption.usernameTextField.selectedItem as String?
|
||||
if (username.isNullOrBlank()) {
|
||||
setOutlineError(generalOption.usernameTextField)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(textField: JTextField): Boolean {
|
||||
if (textField.isEnabled && textField.text.isBlank()) {
|
||||
setOutlineError(textField)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun setOutlineError(textField: JComponent) {
|
||||
selectOptionJComponent(textField)
|
||||
textField.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
textField.requestFocusInWindow()
|
||||
}
|
||||
|
||||
|
||||
private inner class GeneralOption : JPanel(BorderLayout()), Option {
|
||||
val portTextField = PortSpinner(SMBClient.DEFAULT_PORT)
|
||||
val nameTextField = OutlineTextField(128)
|
||||
val shareTextField = OutlineTextField(256)
|
||||
val usernameTextField = OutlineComboBox<String>()
|
||||
val hostTextField = OutlineTextField(255)
|
||||
val passwordTextField = OutlinePasswordField(255)
|
||||
val remarkTextArea = FixedLengthTextArea(512)
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
usernameTextField.isEditable = true
|
||||
usernameTextField.addItem("Guest")
|
||||
usernameTextField.addItem("Anonymous")
|
||||
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentResized(e: ComponentEvent) {
|
||||
SwingUtilities.invokeLater { nameTextField.requestFocusInWindow() }
|
||||
removeComponentListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.settings
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.general")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, pref, $FORM_MARGIN, default",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
remarkTextArea.setFocusTraversalKeys(
|
||||
KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
|
||||
KeyboardFocusManager.getCurrentKeyboardFocusManager()
|
||||
.getDefaultFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
|
||||
)
|
||||
|
||||
remarkTextArea.rows = 8
|
||||
remarkTextArea.lineWrap = true
|
||||
remarkTextArea.border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.general.name")}:").xy(1, rows)
|
||||
.add(nameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.host")}:").xy(1, rows)
|
||||
.add(hostTextField).xy(3, rows)
|
||||
.add("${I18n.getString("termora.new-host.general.port")}:").xy(5, rows)
|
||||
.add(portTextField).xy(7, rows).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.username")}:").xy(1, rows)
|
||||
.add(usernameTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.password")}:").xy(1, rows)
|
||||
.add(passwordTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${SMBI18n.getString("termora.plugins.smb.share")}:").xy(1, rows)
|
||||
.add(shareTextField).xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.add("${I18n.getString("termora.new-host.general.remark")}:").xy(1, rows)
|
||||
.add(JScrollPane(remarkTextArea).apply { border = FlatTextBorder() })
|
||||
.xyw(3, rows, 5).apply { rows += step }
|
||||
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private inner class SFTPOption : JPanel(BorderLayout()), Option {
|
||||
val defaultDirectoryField = OutlineTextField(255)
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.folder
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.transport.sftp")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.settings.sftp.default-directory")}:").xy(1, rows)
|
||||
.add(defaultDirectoryField).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
|
||||
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.I18n
|
||||
import app.termora.NamedI18n
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.*
|
||||
|
||||
object SMBI18n : NamedI18n("i18n/messages") {
|
||||
private val log = LoggerFactory.getLogger(SMBI18n::class.java)
|
||||
|
||||
override fun getLogger(): Logger {
|
||||
return log
|
||||
}
|
||||
|
||||
override fun getString(key: String): String {
|
||||
return try {
|
||||
substitutor.replace(getBundle().getString(key))
|
||||
} catch (_: MissingResourceException) {
|
||||
I18n.getString(key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.transfer.s3.S3Path
|
||||
|
||||
class SMBPath(fileSystem: SMBFileSystem, root: String?, names: List<String>) : S3Path(fileSystem, root, names) {
|
||||
override val isBucket: Boolean
|
||||
get() = false
|
||||
|
||||
override val bucketName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override val objectName: String
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun getCustomType(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.protocol.PathHandler
|
||||
import com.hierynomus.smbj.SMBClient
|
||||
import com.hierynomus.smbj.session.Session
|
||||
import org.apache.commons.io.IOUtils
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Path
|
||||
|
||||
class SMBPathHandler(
|
||||
private val client: SMBClient,
|
||||
private val session: Session,
|
||||
fileSystem: FileSystem, path: Path
|
||||
) : PathHandler(fileSystem, path) {
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
session.close()
|
||||
IOUtils.closeQuietly(client)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.plugin.Extension
|
||||
import app.termora.plugin.ExtensionSupport
|
||||
import app.termora.plugin.PaidPlugin
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class SMBPlugin : PaidPlugin {
|
||||
private val support = ExtensionSupport()
|
||||
|
||||
init {
|
||||
support.addExtension(ProtocolProviderExtension::class.java) { SMBProtocolProviderExtension.instance }
|
||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SMBProtocolHostPanelExtension.instance }
|
||||
}
|
||||
|
||||
override fun getAuthor(): String {
|
||||
return "TermoraDev"
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return "SMB"
|
||||
}
|
||||
|
||||
|
||||
override fun <T : Extension> getExtensions(clazz: Class<T>): List<T> {
|
||||
return support.getExtensions(clazz)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.Disposer
|
||||
import app.termora.Host
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import java.awt.BorderLayout
|
||||
|
||||
class SMBProtocolHostPanel : ProtocolHostPanel() {
|
||||
|
||||
private val pane = SMBHostOptionsPane()
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
add(pane, BorderLayout.CENTER)
|
||||
Disposer.register(this, pane)
|
||||
}
|
||||
|
||||
private fun initEvents() {}
|
||||
|
||||
override fun getHost(): Host {
|
||||
return pane.getHost()
|
||||
}
|
||||
|
||||
override fun setHost(host: Host) {
|
||||
pane.setHost(host)
|
||||
}
|
||||
|
||||
override fun validateFields(): Boolean {
|
||||
return pane.validateFields()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
|
||||
class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExtension {
|
||||
companion object {
|
||||
val instance by lazy { SMBProtocolHostPanelExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return SMBProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return SMBProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.DynamicIcon
|
||||
import app.termora.Icons
|
||||
import app.termora.protocol.PathHandler
|
||||
import app.termora.protocol.PathHandlerRequest
|
||||
import app.termora.protocol.TransferProtocolProvider
|
||||
import com.hierynomus.smbj.SMBClient
|
||||
import com.hierynomus.smbj.auth.AuthenticationContext
|
||||
import com.hierynomus.smbj.share.DiskShare
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
class SMBProtocolProvider private constructor() : TransferProtocolProvider {
|
||||
|
||||
companion object {
|
||||
val instance by lazy { SMBProtocolProvider() }
|
||||
const val PROTOCOL = "SMB"
|
||||
}
|
||||
|
||||
override fun getProtocol(): String {
|
||||
return PROTOCOL
|
||||
}
|
||||
|
||||
override fun getIcon(width: Int, height: Int): DynamicIcon {
|
||||
return Icons.windows7
|
||||
}
|
||||
|
||||
override fun createPathHandler(requester: PathHandlerRequest): PathHandler {
|
||||
val client = SMBClient()
|
||||
val host = requester.host
|
||||
val connection = client.connect(host.host, host.port)
|
||||
val session = when (host.username) {
|
||||
"Guest" -> connection.authenticate(AuthenticationContext.guest())
|
||||
"Anonymous" -> connection.authenticate(AuthenticationContext.anonymous())
|
||||
else -> connection.authenticate(
|
||||
AuthenticationContext(
|
||||
host.username,
|
||||
host.authentication.password.toCharArray(),
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
val share = session.connectShare(host.options.extras["smb.share"] ?: StringUtils.EMPTY) as DiskShare
|
||||
var sftpDefaultDirectory = StringUtils.defaultString(host.options.sftpDefaultDirectory)
|
||||
sftpDefaultDirectory = if (sftpDefaultDirectory.isNotBlank()) {
|
||||
FilenameUtils.separatorsToUnix(sftpDefaultDirectory)
|
||||
} else {
|
||||
"/"
|
||||
}
|
||||
|
||||
val fs = SMBFileSystem(share, session)
|
||||
return SMBPathHandler(client, session, fs, fs.getPath(sftpDefaultDirectory))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.termora.plugins.smb
|
||||
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
import app.termora.protocol.ProtocolProviderExtension
|
||||
|
||||
class SMBProtocolProviderExtension private constructor() : ProtocolProviderExtension {
|
||||
companion object {
|
||||
val instance by lazy { SMBProtocolProviderExtension() }
|
||||
}
|
||||
|
||||
override fun getProtocolProvider(): ProtocolProvider {
|
||||
return SMBProtocolProvider.instance
|
||||
}
|
||||
}
|
||||
24
plugins/smb/src/main/resources/META-INF/plugin.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<termora-plugin>
|
||||
|
||||
<id>smb</id>
|
||||
|
||||
<name>SMB</name>
|
||||
|
||||
<paid/>
|
||||
|
||||
<version>${projectVersion}</version>
|
||||
|
||||
<termora-version since=">=${rootProjectVersion}" until=""/>
|
||||
|
||||
<entry>app.termora.plugins.smb.SMBPlugin</entry>
|
||||
|
||||
<descriptions>
|
||||
<description>Connecting to SMB</description>
|
||||
<description language="zh_CN">支持连接到 SMB</description>
|
||||
<description language="zh_TW">支援連接到 SMB</description>
|
||||
</descriptions>
|
||||
|
||||
<vendor url="https://github.com/TermoraDev">TermoraDev</vendor>
|
||||
|
||||
|
||||
</termora-plugin>
|
||||
1
plugins/smb/src/main/resources/META-INF/pluginIcon.svg
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
1
plugins/smb/src/main/resources/i18n/messages.properties
Normal file
@@ -0,0 +1 @@
|
||||
termora.plugins.smb.share=Share name
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.smb.share=共享名称
|
||||
@@ -0,0 +1 @@
|
||||
termora.plugins.smb.share=共享名稱
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
|
||||
project.version = "0.0.2"
|
||||
project.version = "0.0.3"
|
||||
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -2,7 +2,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
project.version = "0.0.1"
|
||||
project.version = "0.0.2"
|
||||
|
||||
dependencies {
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.termora.plugins.webdav
|
||||
|
||||
import app.termora.account.AccountOwner
|
||||
import app.termora.protocol.ProtocolHostPanel
|
||||
import app.termora.protocol.ProtocolHostPanelExtension
|
||||
import app.termora.protocol.ProtocolProvider
|
||||
@@ -13,7 +14,7 @@ class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanel
|
||||
return WebDAVProtocolProvider.instance
|
||||
}
|
||||
|
||||
override fun createProtocolHostPanel(): ProtocolHostPanel {
|
||||
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
|
||||
return WebDAVProtocolHostPanel()
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@ include("plugins:s3")
|
||||
include("plugins:oss")
|
||||
include("plugins:cos")
|
||||
include("plugins:obs")
|
||||
//include("plugins:ftp")
|
||||
include("plugins:ftp")
|
||||
include("plugins:bg")
|
||||
include("plugins:sync")
|
||||
include("plugins:migration")
|
||||
include("plugins:editor")
|
||||
include("plugins:geo")
|
||||
include("plugins:webdav")
|
||||
include("plugins:smb")
|
||||
include("plugins:serial")
|
||||
|
||||
@@ -37,7 +37,7 @@ record ExtensionProxy(Plugin plugin, Extension extension) implements InvocationH
|
||||
}
|
||||
throw new IllegalCallerException(target.getMessage(), target);
|
||||
}
|
||||
throw e;
|
||||
throw target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.actions.ActionManager
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.keymap.KeymapManager
|
||||
import app.termora.plugin.ExtensionManager
|
||||
import app.termora.plugin.PluginManager
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
@@ -22,10 +20,7 @@ import org.apache.commons.lang3.LocaleUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.awt.MenuItem
|
||||
import java.awt.PopupMenu
|
||||
import java.awt.SystemTray
|
||||
import java.awt.TrayIcon
|
||||
import java.awt.*
|
||||
import java.awt.desktop.AppReopenedEvent
|
||||
import java.awt.desktop.AppReopenedListener
|
||||
import java.awt.desktop.SystemEventListener
|
||||
@@ -57,12 +52,6 @@ class ApplicationRunner {
|
||||
// 统计
|
||||
enableAnalytics()
|
||||
|
||||
// init ActionManager、KeymapManager、VFS
|
||||
swingCoroutineScope.launch(Dispatchers.IO) {
|
||||
ActionManager.getInstance()
|
||||
KeymapManager.getInstance()
|
||||
}
|
||||
|
||||
// 设置 LAF
|
||||
setupLaf()
|
||||
|
||||
@@ -173,7 +162,6 @@ class ApplicationRunner {
|
||||
private fun setupLaf() {
|
||||
|
||||
System.setProperty(FlatSystemProperties.USE_WINDOW_DECORATIONS, "${SystemInfo.isLinux || SystemInfo.isWindows}")
|
||||
System.setProperty(FlatSystemProperties.USE_ROUNDED_POPUP_BORDER, "false")
|
||||
|
||||
if (SystemInfo.isLinux) {
|
||||
JFrame.setDefaultLookAndFeelDecorated(true)
|
||||
@@ -197,12 +185,13 @@ class ApplicationRunner {
|
||||
|
||||
themeManager.change(theme, true)
|
||||
|
||||
|
||||
if (Application.isBetaVersion()) {
|
||||
FlatInspector.install("ctrl shift X")
|
||||
}
|
||||
|
||||
UIManager.put(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
UIManager.put(FlatClientProperties.USE_WINDOW_DECORATIONS, false)
|
||||
UIManager.put("TitlePane.useWindowDecorations", false)
|
||||
UIManager.put(FlatClientProperties.POPUP_FORCE_HEAVY_WEIGHT, true)
|
||||
|
||||
UIManager.put("Component.arc", 5)
|
||||
UIManager.put("TextComponent.arc", UIManager.getInt("Component.arc"))
|
||||
@@ -213,7 +202,6 @@ class ApplicationRunner {
|
||||
UIManager.put("Dialog.width", 650)
|
||||
UIManager.put("Dialog.height", 550)
|
||||
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
UIManager.put("TabbedPane.tabHeight", UIManager.getInt("TitleBar.height"))
|
||||
} else if (SystemInfo.isLinux) {
|
||||
@@ -231,15 +219,33 @@ class ApplicationRunner {
|
||||
UIManager.put("Table.rowHeight", 24)
|
||||
UIManager.put("Table.focusCellHighlightBorder", FlatTableCellBorder.Default())
|
||||
UIManager.put("Table.focusSelectedCellHighlightBorder", FlatTableCellBorder.Default())
|
||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||
|
||||
UIManager.put("Tree.rowHeight", 24)
|
||||
UIManager.put("Tree.background", DynamicColor("window"))
|
||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Tree.showCellFocusIndicator", false)
|
||||
UIManager.put("Tree.repaintWholeRow", true)
|
||||
|
||||
// Linux 更多的是尖锐风格
|
||||
if (SystemInfo.isMacOS || SystemInfo.isWindows) {
|
||||
val selectionInsets = Insets(0, 2, 0, 2)
|
||||
UIManager.put("Tree.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Tree.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("List.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("List.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("ComboBox.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("ComboBox.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("Table.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("Table.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("MenuBar.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("MenuBar.selectionInsets", selectionInsets)
|
||||
|
||||
UIManager.put("MenuItem.selectionArc", UIManager.getInt("Component.arc"))
|
||||
UIManager.put("MenuItem.selectionInsets", selectionInsets)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
|
||||
size = preferredSize
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
public override fun paintComponent(g: Graphics) {
|
||||
if (g is Graphics2D) {
|
||||
g.setRenderingHints(
|
||||
RenderingHints(
|
||||
@@ -33,7 +33,7 @@ class BannerPanel(fontSize: Int = 11, val beautiful: Boolean = false) : JCompone
|
||||
}
|
||||
|
||||
g.font = font
|
||||
g.color = UIManager.getColor("TextField.placeholderForeground")
|
||||
g.color = foreground ?: UIManager.getColor("TextField.placeholderForeground")
|
||||
|
||||
val height = g.fontMetrics.height
|
||||
val descent = g.fontMetrics.descent
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.Application.ohMyJson
|
||||
import app.termora.actions.MultipleAction
|
||||
import app.termora.database.DatabaseManager
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -18,10 +15,10 @@ import javax.swing.event.ListDataListener
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class CustomizeToolBarDialog(
|
||||
internal class CustomizeToolBarDialog(
|
||||
owner: Window,
|
||||
private val windowScope: WindowScope,
|
||||
private val toolbar: TermoraToolBar
|
||||
private val model: TermoraToolbarModel,
|
||||
) : DialogWrapper(owner) {
|
||||
|
||||
private val moveTopBtn = JButton(Icons.moveUp)
|
||||
@@ -40,6 +37,7 @@ class CustomizeToolBarDialog(
|
||||
private val actionManager get() = ActionManager.getInstance()
|
||||
|
||||
private var isOk = false
|
||||
private val actions = mutableListOf<ToolBarAction>()
|
||||
|
||||
init {
|
||||
size = Dimension(UIManager.getInt("Dialog.width") - 150, UIManager.getInt("Dialog.height") - 100)
|
||||
@@ -147,7 +145,7 @@ class CustomizeToolBarDialog(
|
||||
resetBtn.addActionListener {
|
||||
leftList.model.removeAllElements()
|
||||
rightList.model.removeAllElements()
|
||||
for (action in toolbar.getAllActions()) {
|
||||
for (action in model.getAllActions()) {
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
}
|
||||
}
|
||||
@@ -258,7 +256,7 @@ class CustomizeToolBarDialog(
|
||||
override fun windowOpened(e: WindowEvent) {
|
||||
removeWindowListener(this)
|
||||
|
||||
for (action in toolbar.getActions()) {
|
||||
for (action in model.getActions()) {
|
||||
if (action.visible) {
|
||||
getActionHolder(action.id)?.let { rightList.model.addElement(it) }
|
||||
} else {
|
||||
@@ -271,12 +269,7 @@ class CustomizeToolBarDialog(
|
||||
}
|
||||
|
||||
private fun getActionHolder(actionId: String): ActionHolder? {
|
||||
var action = actionManager.getAction(actionId)
|
||||
if (action == null) {
|
||||
if (actionId == MultipleAction.MULTIPLE) {
|
||||
action = MultipleAction.getInstance(windowScope)
|
||||
}
|
||||
}
|
||||
val action = actionManager.getAction(actionId)
|
||||
if (action == null) return null
|
||||
return ActionHolder(actionId, action)
|
||||
}
|
||||
@@ -365,12 +358,14 @@ class CustomizeToolBarDialog(
|
||||
actions.add(ToolBarAction(leftList.model.getElementAt(i).id, false))
|
||||
}
|
||||
|
||||
DatabaseManager.getInstance()
|
||||
.properties.putString("Termora.ToolBar.Actions", ohMyJson.encodeToString(actions))
|
||||
this.actions.clear()
|
||||
this.actions.addAll(actions)
|
||||
|
||||
super.doOKAction()
|
||||
}
|
||||
|
||||
fun getActions()=actions
|
||||
|
||||
fun open(): Boolean {
|
||||
isModal = true
|
||||
isVisible = true
|
||||
|
||||