tabby-tftp 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/README.zh-CN.md +50 -0
- package/SECURITY.md +11 -0
- package/dist/buttonProvider.d.ts +7 -0
- package/dist/components/tftpModal.component.d.ts +30 -0
- package/dist/components/tftpServer.component.d.ts +24 -0
- package/dist/config.d.ts +37 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/services/tftp.service.d.ts +19 -0
- package/dist/services/tftpServer.service.d.ts +32 -0
- package/dist/tftp/client.d.ts +43 -0
- package/dist/tftp/protocol.d.ts +30 -0
- package/dist/tftp/server.d.ts +41 -0
- package/docs/PUBLISHING.md +32 -0
- package/package.json +77 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.2.1] - 2026-08-11
|
|
6
|
+
|
|
7
|
+
- Keep the firmware TFTP server alive when its management window is closed.
|
|
8
|
+
- Persist server settings, including the selected root directory.
|
|
9
|
+
- Make automatic server shutdown opt-in.
|
|
10
|
+
- Add a shared server state and clearer serial-console workflow.
|
|
11
|
+
|
|
12
|
+
## [0.2.0]
|
|
13
|
+
|
|
14
|
+
- Add a local read-only TFTP server with MobaXterm-style controls.
|
|
15
|
+
- Add bind-address selection, local IPv4 discovery, logs, notifications, and auto-stop.
|
|
16
|
+
|
|
17
|
+
## [0.1.0]
|
|
18
|
+
|
|
19
|
+
- Initial Tabby TFTP client with upload/download support.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve Tabby TFTP.
|
|
4
|
+
|
|
5
|
+
## Local setup
|
|
6
|
+
|
|
7
|
+
Use Node.js 18 or newer, then install dependencies and run the complete verification suite:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
npm ci
|
|
11
|
+
npm run verify
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The source is TypeScript. Protocol behavior is covered by the tests in `tests/`; UI changes should also be checked in a local Tabby installation.
|
|
15
|
+
|
|
16
|
+
## Pull requests
|
|
17
|
+
|
|
18
|
+
- Keep changes focused and explain the user-visible behavior.
|
|
19
|
+
- Add or update tests for protocol and server behavior.
|
|
20
|
+
- Update `README.md`, `README.zh-CN.md`, and `CHANGELOG.md` when behavior or configuration changes.
|
|
21
|
+
- Do not commit generated `dist/`, `.tgz` packages, local settings, credentials, or firmware binaries.
|
|
22
|
+
- Make sure `npm run verify` passes before opening a pull request.
|
|
23
|
+
|
|
24
|
+
## Commit messages
|
|
25
|
+
|
|
26
|
+
Short, imperative messages are preferred, for example `Fix server lifetime after modal close` or `Add retry test for duplicate ACK`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 linzi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Tabby TFTP
|
|
2
|
+
|
|
3
|
+
`tabby-tftp` adds firmware-friendly TFTP tools to [Tabby](https://github.com/Eugeny/tabby):
|
|
4
|
+
|
|
5
|
+
- TFTP client for uploads and downloads.
|
|
6
|
+
- A local, read-only TFTP server for bootloaders and serial-console firmware updates.
|
|
7
|
+
- RFC 2347/2348/2349 option negotiation (`blksize`, `timeout`, and `tsize`).
|
|
8
|
+
- Transfer progress, retry/cancel controls, server logs, and local IPv4 discovery.
|
|
9
|
+
- Server settings that survive closing and reopening the management window.
|
|
10
|
+
|
|
11
|
+
> TFTP has no authentication or encryption. Use it only on a trusted, isolated network.
|
|
12
|
+
|
|
13
|
+
## Serial-console firmware workflow
|
|
14
|
+
|
|
15
|
+
1. Open **TFTP File Transfer** from Tabby's toolbar.
|
|
16
|
+
2. Select **Firmware TFTP Server**, choose the firmware root directory, and start the server.
|
|
17
|
+
3. Copy the device-facing local IPv4 address shown by the plugin.
|
|
18
|
+
4. In the serial bootloader, configure the server address and request the firmware file. For U-Boot this is commonly `serverip`, `ipaddr`, and `tftpboot`.
|
|
19
|
+
5. Close the dialog when finished. Closing the dialog only hides the UI; the server keeps running until you stop it, its optional auto-stop timer expires, or Tabby exits.
|
|
20
|
+
|
|
21
|
+
The server is intentionally read-only: devices can download files from the selected root, but cannot upload or overwrite files on the computer.
|
|
22
|
+
|
|
23
|
+
## TFTP client
|
|
24
|
+
|
|
25
|
+
Use the **TFTP Client** tab to upload or download a file. The default port is `69`. A download requires a remote filename; for an upload, leaving the remote filename empty uses the local filename.
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
Requirements: Node.js 18 or newer and a Tabby-compatible Angular/toolchain environment.
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
npm ci
|
|
33
|
+
npm run verify
|
|
34
|
+
npm run pack:plugin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`npm run verify` runs type checking, protocol/server tests, and the production bundle. The installable package is generated by `npm pack`.
|
|
38
|
+
|
|
39
|
+
## Local installation
|
|
40
|
+
|
|
41
|
+
Close Tabby, then install the generated `.tgz` into Tabby's user plugin directory and restart Tabby:
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
cd "$env:APPDATA\tabby\plugins"
|
|
45
|
+
npm install C:\path\to\tabby-tftp-0.2.1.tgz --legacy-peer-deps
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Security notes
|
|
49
|
+
|
|
50
|
+
The server protects against path traversal and only serves regular files below the configured root directory. UDP port 69 and the negotiated transfer port must be allowed by the local firewall. See [SECURITY.md](SECURITY.md) for reporting security issues.
|
|
51
|
+
|
|
52
|
+
## Contributing and publishing
|
|
53
|
+
|
|
54
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [CHANGELOG.md](CHANGELOG.md), and [docs/PUBLISHING.md](docs/PUBLISHING.md).
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT. See [LICENSE](LICENSE).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Tabby TFTP
|
|
2
|
+
|
|
3
|
+
`tabby-tftp` 为 [Tabby](https://github.com/Eugeny/tabby) 提供适合固件下载的 TFTP 工具:
|
|
4
|
+
|
|
5
|
+
- TFTP 文件上传和下载客户端。
|
|
6
|
+
- 本地只读 TFTP 服务端,适合串口 Bootloader 烧录固件。
|
|
7
|
+
- 支持 `blksize`、`timeout`、`tsize` 选项协商。
|
|
8
|
+
- 显示传输进度、速度、重试/取消状态、服务端日志和本机 IPv4 地址。
|
|
9
|
+
- 关闭并重新打开管理窗口后,服务端状态和配置仍然保留。
|
|
10
|
+
|
|
11
|
+
> TFTP 没有身份认证和加密功能,只应在可信、隔离的局域网中使用。
|
|
12
|
+
|
|
13
|
+
## 串口烧录流程
|
|
14
|
+
|
|
15
|
+
1. 从 Tabby 工具栏打开 **TFTP 文件传输**。
|
|
16
|
+
2. 选择 **固件 TFTP 服务端**,选择固件根目录并启动服务端。
|
|
17
|
+
3. 记下插件显示的设备可访问本机 IPv4 地址。
|
|
18
|
+
4. 在串口 Bootloader 中配置服务端地址并请求固件。U-Boot 常见命令包括 `serverip`、`ipaddr` 和 `tftpboot`。
|
|
19
|
+
5. 关闭窗口不会停止服务端;只有手动停止、启用的自动停止计时器到期或退出 Tabby 才会停止。
|
|
20
|
+
|
|
21
|
+
服务端是只读的:设备可以下载根目录中的文件,但不能向电脑上传或覆盖文件。
|
|
22
|
+
|
|
23
|
+
## 本地构建与测试
|
|
24
|
+
|
|
25
|
+
需要 Node.js 18 或更高版本。
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
npm ci
|
|
29
|
+
npm run verify
|
|
30
|
+
npm run pack:plugin
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`npm run verify` 会执行类型检查、协议/服务端测试和生产构建。
|
|
34
|
+
|
|
35
|
+
## 本地安装
|
|
36
|
+
|
|
37
|
+
关闭 Tabby,在 Tabby 用户插件目录中安装生成的 `.tgz` 包,然后重新启动 Tabby:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
cd "$env:APPDATA\tabby\plugins"
|
|
41
|
+
npm install C:\path\to\tabby-tftp-0.2.1.tgz --legacy-peer-deps
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 参与贡献与发布
|
|
45
|
+
|
|
46
|
+
参见 [CONTRIBUTING.md](CONTRIBUTING.md)、[CHANGELOG.md](CHANGELOG.md) 和 [docs/PUBLISHING.md](docs/PUBLISHING.md)。
|
|
47
|
+
|
|
48
|
+
## 许可证
|
|
49
|
+
|
|
50
|
+
MIT,详见 [LICENSE](LICENSE)。
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This plugin implements unauthenticated TFTP. TFTP traffic is unencrypted and the local server is intended for trusted, isolated networks only.
|
|
6
|
+
|
|
7
|
+
The server is read-only and restricts requests to regular files below its configured root directory. This is not a replacement for network access controls or firewall rules.
|
|
8
|
+
|
|
9
|
+
## Reporting a vulnerability
|
|
10
|
+
|
|
11
|
+
Please do not disclose a suspected vulnerability in a public issue. Once the GitHub repository is created, use its private security advisory flow or contact the maintainer privately with reproduction steps, affected version, and environment details.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|
2
|
+
import { IToolbarButton, ToolbarButtonProvider } from 'tabby-core';
|
|
3
|
+
export declare class TftpButtonProvider extends ToolbarButtonProvider {
|
|
4
|
+
private readonly modal;
|
|
5
|
+
constructor(modal: NgbModal);
|
|
6
|
+
provide(): IToolbarButton[];
|
|
7
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ChangeDetectorRef, NgZone, OnDestroy, OnInit } from '@angular/core';
|
|
2
|
+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
|
3
|
+
import { ConfigService } from 'tabby-core';
|
|
4
|
+
import { TftpPluginSettings } from '../config';
|
|
5
|
+
import { TftpService } from '../services/tftp.service';
|
|
6
|
+
import { TftpProgress } from '../tftp/client';
|
|
7
|
+
export declare class TftpModalComponent implements OnInit, OnDestroy {
|
|
8
|
+
readonly activeModal: NgbActiveModal;
|
|
9
|
+
private readonly config;
|
|
10
|
+
readonly tftp: TftpService;
|
|
11
|
+
private readonly zone;
|
|
12
|
+
private readonly changeDetector;
|
|
13
|
+
mode: 'server' | 'client';
|
|
14
|
+
settings: TftpPluginSettings;
|
|
15
|
+
status: TftpProgress | null;
|
|
16
|
+
validationError: string;
|
|
17
|
+
private subscription?;
|
|
18
|
+
constructor(activeModal: NgbActiveModal, config: ConfigService, tftp: TftpService, zone: NgZone, changeDetector: ChangeDetectorRef);
|
|
19
|
+
get busy(): boolean;
|
|
20
|
+
get percent(): number;
|
|
21
|
+
get progressType(): string;
|
|
22
|
+
ngOnInit(): void;
|
|
23
|
+
ngOnDestroy(): void;
|
|
24
|
+
upload(): Promise<void>;
|
|
25
|
+
download(): Promise<void>;
|
|
26
|
+
cancel(): void;
|
|
27
|
+
formatBytes(value: number): string;
|
|
28
|
+
private execute;
|
|
29
|
+
private validate;
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChangeDetectorRef, NgZone, OnDestroy, OnInit } from '@angular/core';
|
|
2
|
+
import { ConfigService } from 'tabby-core';
|
|
3
|
+
import { TftpServerSettings } from '../config';
|
|
4
|
+
import { TftpServerService, TftpServerState } from '../services/tftpServer.service';
|
|
5
|
+
export declare class TftpServerComponent implements OnInit, OnDestroy {
|
|
6
|
+
private readonly config;
|
|
7
|
+
readonly server: TftpServerService;
|
|
8
|
+
private readonly zone;
|
|
9
|
+
private readonly changeDetector;
|
|
10
|
+
settings: TftpServerSettings;
|
|
11
|
+
state: TftpServerState;
|
|
12
|
+
localAddresses: string[];
|
|
13
|
+
starting: boolean;
|
|
14
|
+
error: string;
|
|
15
|
+
private subscription?;
|
|
16
|
+
constructor(config: ConfigService, server: TftpServerService, zone: NgZone, changeDetector: ChangeDetectorRef);
|
|
17
|
+
ngOnInit(): void;
|
|
18
|
+
ngOnDestroy(): void;
|
|
19
|
+
chooseDirectory(): Promise<void>;
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
formatTime(timestamp: number): string;
|
|
22
|
+
private validate;
|
|
23
|
+
private saveSettings;
|
|
24
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ConfigProvider } from 'tabby-core';
|
|
2
|
+
export interface TftpPluginSettings {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
remoteFilename: string;
|
|
6
|
+
blockSize: number;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
retries: number;
|
|
9
|
+
server: TftpServerSettings;
|
|
10
|
+
}
|
|
11
|
+
export interface TftpServerSettings {
|
|
12
|
+
rootDirectory: string;
|
|
13
|
+
bindAddress: string;
|
|
14
|
+
port: number;
|
|
15
|
+
blockSize: number;
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
retries: number;
|
|
18
|
+
autoStop: boolean;
|
|
19
|
+
autoStopSeconds: number;
|
|
20
|
+
notifyDownloads: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare const defaultTftpServerSettings: TftpServerSettings;
|
|
23
|
+
export declare const defaultTftpSettings: TftpPluginSettings;
|
|
24
|
+
export declare class TftpConfigProvider extends ConfigProvider {
|
|
25
|
+
defaults: {
|
|
26
|
+
tftp: {
|
|
27
|
+
host: string;
|
|
28
|
+
port: number;
|
|
29
|
+
remoteFilename: string;
|
|
30
|
+
blockSize: number;
|
|
31
|
+
timeoutMs: number;
|
|
32
|
+
retries: number;
|
|
33
|
+
server: TftpServerSettings;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
platformDefaults: {};
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(()=>{"use strict";var t={983(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,n=arguments.length,o=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(o=(n<3?i(o):n>3?i(e,s,o):i(e,s))||o);return n>3&&o&&Object.defineProperty(e,s,o),o},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpButtonProvider=void 0;const n=s(976),o=s(385),a=s(414),l=s(263);let c=class extends a.ToolbarButtonProvider{constructor(t){super(),this.modal=t}provide(){return[{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M7 7h11l-3.5-3.5L16 2l6 6-6 6-1.5-1.5L18 9H7V7zm10 10H6l3.5 3.5L8 22l-6-6 6-6 1.5 1.5L6 15h11v2z"/></svg>',title:"TFTP 文件传输",weight:6,click:()=>this.modal.open(l.TftpModalComponent,{size:"lg"})}]}};e.TftpButtonProvider=c,e.TftpButtonProvider=c=r([(0,n.Injectable)(),i("design:paramtypes",[o.NgbModal])],c)},263(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,n=arguments.length,o=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(o=(n<3?i(o):n>3?i(e,s,o):i(e,s))||o);return n>3&&o&&Object.defineProperty(e,s,o),o},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpModalComponent=void 0;const n=s(976),o=s(385),a=s(414),l=s(28),c=s(306);let d=class{constructor(t,e,s,r,i){this.activeModal=t,this.config=e,this.tftp=s,this.zone=r,this.changeDetector=i,this.mode="server",this.settings={...l.defaultTftpSettings,server:{...l.defaultTftpServerSettings}},this.status=null,this.validationError=""}get busy(){return this.tftp.busy}get percent(){return this.status?"completed"===this.status.state?100:void 0===this.status.total?this.busy?100:0:0===this.status.total?100:Math.min(100,Math.round(100*this.status.transferred/this.status.total)):0}get progressType(){var t,e,s;return"error"===(null===(t=this.status)||void 0===t?void 0:t.state)||"cancelled"===(null===(e=this.status)||void 0===e?void 0:e.state)?"danger":"completed"===(null===(s=this.status)||void 0===s?void 0:s.state)?"success":"info"}ngOnInit(){var t,e;const s=null!==(t=this.config.store.tftp)&&void 0!==t?t:{};this.settings={...l.defaultTftpSettings,...s,server:{...l.defaultTftpServerSettings,...null!==(e=s.server)&&void 0!==e?e:{}}},this.subscription=this.tftp.progress$.subscribe(t=>{this.zone.run(()=>{this.status=t,this.changeDetector.markForCheck()})})}ngOnDestroy(){var t;null===(t=this.subscription)||void 0===t||t.unsubscribe()}async upload(){this.validate(!1)&&await this.execute(()=>this.tftp.upload(this.settings))}async download(){this.validate(!0)&&await this.execute(()=>this.tftp.download(this.settings))}cancel(){this.tftp.cancel()}formatBytes(t){if(!Number.isFinite(t)||t<=0)return"0 B";const e=["B","KB","MB","GB","TB"],s=Math.min(e.length-1,Math.floor(Math.log(t)/Math.log(1024))),r=t/Math.pow(1024,s);return`${r>=100||0===s?r.toFixed(0):r.toFixed(1)} ${e[s]}`}async execute(t){this.validationError="",this.config.store.tftp||(this.config.store.tftp={}),Object.assign(this.config.store.tftp,{host:this.settings.host,port:this.settings.port,remoteFilename:this.settings.remoteFilename,blockSize:this.settings.blockSize,timeoutMs:this.settings.timeoutMs,retries:this.settings.retries}),await this.config.save();try{await t()}catch(t){this.validationError=t instanceof Error?t.message:String(t)}}validate(t){return this.validationError="",this.settings.host.trim()?t&&!this.settings.remoteFilename.trim()?this.validationError="下载时必须填写远端文件名":!Number.isInteger(Number(this.settings.port))||Number(this.settings.port)<1||Number(this.settings.port)>65535?this.validationError="端口必须在 1 到 65535 之间":!Number.isInteger(Number(this.settings.blockSize))||Number(this.settings.blockSize)<8||Number(this.settings.blockSize)>65464?this.validationError="块大小必须在 8 到 65464 之间":!Number.isInteger(Number(this.settings.timeoutMs))||Number(this.settings.timeoutMs)<250||Number(this.settings.timeoutMs)>6e4?this.validationError="超时时间必须在 250 到 60000 毫秒之间":(!Number.isInteger(Number(this.settings.retries))||Number(this.settings.retries)<0||Number(this.settings.retries)>20)&&(this.validationError="重试次数必须在 0 到 20 之间"):this.validationError="请填写 TFTP 主机地址",!this.validationError}};e.TftpModalComponent=d,e.TftpModalComponent=d=r([(0,n.Component)({selector:"tabby-tftp-modal",template:'\n <div class="modal-header">\n <h5 class="modal-title"><i class="fa fa-exchange-alt mr-2"></i>TFTP 文件传输与固件服务</h5>\n <button type="button" class="close" aria-label="关闭" (click)="activeModal.dismiss()">\n <span aria-hidden="true">×</span>\n </button>\n </div>\n\n <div class="mode-tabs px-3 pt-3">\n <div class="btn-group w-100">\n <button class="btn" [class.btn-primary]="mode === \'server\'" [class.btn-outline-secondary]="mode !== \'server\'" (click)="mode = \'server\'">\n <i class="fa fa-server mr-1"></i>固件 TFTP 服务端\n </button>\n <button class="btn" [class.btn-primary]="mode === \'client\'" [class.btn-outline-secondary]="mode !== \'client\'" (click)="mode = \'client\'">\n <i class="fa fa-exchange-alt mr-1"></i>TFTP 客户端\n </button>\n </div>\n </div>\n\n <div class="modal-body" *ngIf="mode === \'server\'">\n <tabby-tftp-server></tabby-tftp-server>\n </div>\n\n <div class="modal-body" *ngIf="mode === \'client\'">\n <div class="alert alert-warning py-2 small">\n TFTP 没有身份认证和加密。请仅在可信局域网中使用。\n </div>\n\n <div class="form-row">\n <div class="form-group col-md-8">\n <label>主机或 IP 地址</label>\n <input class="form-control" [(ngModel)]="settings.host" placeholder="例如 192.168.1.10" [disabled]="busy">\n </div>\n <div class="form-group col-md-4">\n <label>端口</label>\n <input class="form-control" type="number" min="1" max="65535" [(ngModel)]="settings.port" [disabled]="busy">\n </div>\n </div>\n\n <div class="form-group">\n <label>远端文件名</label>\n <input class="form-control" [(ngModel)]="settings.remoteFilename"\n placeholder="下载时必填;上传留空则使用本地文件名" [disabled]="busy">\n <small class="form-text text-muted">TFTP 不支持浏览远端目录,路径格式由服务端决定。</small>\n </div>\n\n <details class="mb-3">\n <summary class="text-muted">高级选项</summary>\n <div class="form-row mt-2">\n <div class="form-group col-md-4">\n <label>块大小</label>\n <input class="form-control" type="number" min="8" max="65464" [(ngModel)]="settings.blockSize" [disabled]="busy">\n </div>\n <div class="form-group col-md-4">\n <label>超时(毫秒)</label>\n <input class="form-control" type="number" min="250" max="60000" [(ngModel)]="settings.timeoutMs" [disabled]="busy">\n </div>\n <div class="form-group col-md-4">\n <label>重试次数</label>\n <input class="form-control" type="number" min="0" max="20" [(ngModel)]="settings.retries" [disabled]="busy">\n </div>\n </div>\n </details>\n\n <div class="transfer-status" *ngIf="status">\n <div class="d-flex align-items-center mb-1">\n <strong>{{ status.direction === \'upload\' ? \'上传\' : \'下载\' }}</strong>\n <span class="ml-auto text-muted">{{ formatBytes(status.transferred) }}<span *ngIf="status.total !== undefined"> / {{ formatBytes(status.total) }}</span></span>\n </div>\n <ngb-progressbar [type]="progressType" [value]="percent" [striped]="busy" [animated]="busy"></ngb-progressbar>\n <div class="d-flex small mt-1">\n <span [class.text-danger]="status.state === \'error\'">{{ status.message }}</span>\n <span class="ml-auto" *ngIf="status.speed > 0">{{ formatBytes(status.speed) }}/s</span>\n </div>\n </div>\n <div class="text-danger mt-2" *ngIf="validationError">{{ validationError }}</div>\n </div>\n\n <div class="modal-footer">\n <button class="btn btn-outline-secondary" (click)="activeModal.close()">关闭</button>\n <ng-container *ngIf="mode === \'client\'">\n <button class="btn btn-outline-danger" *ngIf="busy" (click)="cancel()"><i class="fa fa-stop mr-1"></i>取消传输</button>\n <button class="btn btn-primary" *ngIf="!busy" (click)="upload()"><i class="fa fa-upload mr-1"></i>上传</button>\n <button class="btn btn-success" *ngIf="!busy" (click)="download()"><i class="fa fa-download mr-1"></i>下载</button>\n </ng-container>\n </div>\n ',styles:["\n :host { display: block; }\n summary { cursor: pointer; user-select: none; }\n .transfer-status { border: 1px solid rgba(127, 127, 127, .25); border-radius: .25rem; padding: .75rem; }\n .modal-body label { margin-bottom: .25rem; }\n .mode-tabs .btn { flex: 1; }\n "]}),i("design:paramtypes",[o.NgbActiveModal,a.ConfigService,c.TftpService,n.NgZone,n.ChangeDetectorRef])],d)},153(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,n=arguments.length,o=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(o=(n<3?i(o):n>3?i(e,s,o):i(e,s))||o);return n>3&&o&&Object.defineProperty(e,s,o),o},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServerComponent=void 0;const n=s(976),o=s(414),a=s(28),l=s(21),c="tabby-tftp-server-settings-v1";let d=class{constructor(t,e,s,r){this.config=t,this.server=e,this.zone=s,this.changeDetector=r,this.settings={...a.defaultTftpServerSettings},this.state={running:!1,activeTransfers:0,logs:[]},this.localAddresses=[],this.starting=!1,this.error=""}ngOnInit(){var t,e,s,r;let i={};try{i=JSON.parse(null!==(t=localStorage.getItem(c))&&void 0!==t?t:"{}")}catch{}this.settings={...a.defaultTftpServerSettings,...null!==(s=null===(e=this.config.store.tftp)||void 0===e?void 0:e.server)&&void 0!==s?s:{},...i,...null!==(r=this.server.activeSettings)&&void 0!==r?r:{}},this.localAddresses=this.server.getLocalIPv4Addresses(),this.subscription=this.server.state$.subscribe(t=>{this.zone.run(()=>{this.state=t,this.changeDetector.markForCheck()})})}ngOnDestroy(){var t;null===(t=this.subscription)||void 0===t||t.unsubscribe()}async chooseDirectory(){const t=await this.server.chooseRootDirectory();t&&(this.settings.rootDirectory=t,await this.saveSettings())}async start(){if(this.error="",this.validate()){this.starting=!0,await this.saveSettings();try{await this.server.start(this.settings)}catch(t){this.error=t instanceof Error?t.message:String(t)}finally{this.starting=!1}}}formatTime(t){return new Date(t).toLocaleTimeString(void 0,{hour12:!1})}validate(){return this.settings.rootDirectory.trim()?!Number.isInteger(Number(this.settings.port))||Number(this.settings.port)<1||Number(this.settings.port)>65535?this.error="监听端口必须在 1 到 65535 之间":!Number.isInteger(Number(this.settings.blockSize))||Number(this.settings.blockSize)<8||Number(this.settings.blockSize)>65464?this.error="TFTP 缓冲区必须在 8 到 65464 字节之间":this.settings.autoStop&&(!Number.isInteger(Number(this.settings.autoStopSeconds))||Number(this.settings.autoStopSeconds)<1||Number(this.settings.autoStopSeconds)>86400)&&(this.error="自动停止时间必须在 1 到 86400 秒之间"):this.error="请选择固件所在的 TFTP 根目录",!this.error}async saveSettings(){localStorage.setItem(c,JSON.stringify(this.settings)),this.config.store.tftp||(this.config.store.tftp={}),this.config.store.tftp.server||(this.config.store.tftp.server={}),Object.assign(this.config.store.tftp.server,this.settings),await this.config.save()}};e.TftpServerComponent=d,e.TftpServerComponent=d=r([(0,n.Component)({selector:"tabby-tftp-server",template:'\n <div class="alert alert-info py-2 small">\n 串口负责给 Bootloader 下命令;本服务端通过网口向设备提供固件。服务端为只读模式,设备不能覆盖电脑文件。\n </div>\n\n <div class="form-group">\n <label>TFTP 根目录</label>\n <div class="input-group">\n <input class="form-control" [(ngModel)]="settings.rootDirectory" placeholder="固件所在目录" [disabled]="state.running">\n <div class="input-group-append">\n <button class="btn btn-outline-secondary" (click)="chooseDirectory()" [disabled]="state.running" title="选择目录">\n <i class="fa fa-folder-open"></i>\n </button>\n <button class="btn btn-outline-secondary" (click)="server.openRootDirectory(settings.rootDirectory)" [disabled]="!settings.rootDirectory" title="打开目录">\n <i class="fa fa-external-link-alt"></i>\n </button>\n </div>\n </div>\n </div>\n\n <div class="form-row">\n <div class="form-group col-md-5">\n <label>监听地址</label>\n <select class="form-control" [(ngModel)]="settings.bindAddress" [disabled]="state.running">\n <option value="0.0.0.0">0.0.0.0(所有网卡)</option>\n <option *ngFor="let address of localAddresses" [value]="address">{{ address }}</option>\n </select>\n </div>\n <div class="form-group col-md-3">\n <label>监听端口</label>\n <input class="form-control" type="number" min="1" max="65535" [(ngModel)]="settings.port" [disabled]="state.running">\n </div>\n <div class="form-group col-md-4">\n <label>TFTP 缓冲区</label>\n <div class="input-group">\n <input class="form-control" type="number" min="8" max="65464" [(ngModel)]="settings.blockSize" [disabled]="state.running">\n <div class="input-group-append"><span class="input-group-text">Bytes</span></div>\n </div>\n </div>\n </div>\n\n <div class="local-addresses mb-3">\n <span class="text-muted mr-2">设备可访问的本机 IP:</span>\n <button class="btn btn-sm btn-outline-info mr-1" *ngFor="let address of localAddresses" (click)="server.copyAddress(address)" title="点击复制">\n {{ address }}\n </button>\n <span class="text-warning small" *ngIf="!localAddresses.length">未检测到可用 IPv4 地址</span>\n </div>\n\n <div class="form-row align-items-center mb-3">\n <div class="col-md-5">\n <div class="custom-control custom-checkbox">\n <input type="checkbox" class="custom-control-input" id="tftp-auto-stop" [(ngModel)]="settings.autoStop" [disabled]="state.running">\n <label class="custom-control-label" for="tftp-auto-stop">自动停止服务端</label>\n </div>\n </div>\n <div class="col-md-3">\n <div class="input-group input-group-sm">\n <input class="form-control" type="number" min="1" max="86400" [(ngModel)]="settings.autoStopSeconds" [disabled]="state.running || !settings.autoStop">\n <div class="input-group-append"><span class="input-group-text">秒</span></div>\n </div>\n </div>\n <div class="col-md-4">\n <div class="custom-control custom-checkbox">\n <input type="checkbox" class="custom-control-input" id="tftp-notify" [(ngModel)]="settings.notifyDownloads">\n <label class="custom-control-label" for="tftp-notify">下载完成后通知</label>\n </div>\n </div>\n </div>\n\n <details class="mb-3">\n <summary class="text-muted">可靠性选项</summary>\n <div class="form-row mt-2">\n <div class="form-group col-md-6">\n <label>响应超时(毫秒)</label>\n <input class="form-control" type="number" min="250" max="60000" [(ngModel)]="settings.timeoutMs" [disabled]="state.running">\n </div>\n <div class="form-group col-md-6">\n <label>重试次数</label>\n <input class="form-control" type="number" min="0" max="20" [(ngModel)]="settings.retries" [disabled]="state.running">\n </div>\n </div>\n </details>\n\n <div class="d-flex align-items-center mb-2">\n <span class="status-dot mr-2" [class.running]="state.running"></span>\n <strong>{{ state.running ? \'TFTP 服务端运行中\' : \'TFTP 服务端已停止\' }}</strong>\n <span class="badge badge-info ml-2" *ngIf="state.activeTransfers">{{ state.activeTransfers }} 个传输</span>\n <div class="ml-auto">\n <button class="btn btn-sm btn-link text-muted" (click)="server.clearLogs()" [disabled]="!state.logs.length">清空日志</button>\n <button class="btn btn-success ml-1" *ngIf="!state.running" (click)="start()" [disabled]="starting">\n <i class="fa fa-play mr-1"></i>{{ starting ? \'启动中…\' : \'启动服务端\' }}\n </button>\n <button class="btn btn-danger ml-1" *ngIf="state.running" (click)="server.stop()">\n <i class="fa fa-stop mr-1"></i>停止服务端\n </button>\n </div>\n </div>\n\n <div class="server-log">\n <div *ngIf="!state.logs.length" class="text-muted">服务端输出将显示在这里</div>\n <div *ngFor="let log of state.logs" [class.text-success]="log.level === \'success\'" [class.text-danger]="log.level === \'error\'">\n [{{ formatTime(log.timestamp) }}] {{ log.message }}\n </div>\n </div>\n <div class="text-danger mt-2" *ngIf="error">{{ error }}</div>\n ',styles:["\n :host { display: block; }\n label { margin-bottom: .25rem; }\n summary { cursor: pointer; user-select: none; }\n .local-addresses { min-height: 31px; display: flex; align-items: center; flex-wrap: wrap; }\n .status-dot { width: 10px; height: 10px; border-radius: 50%; background: #6c757d; display: inline-block; }\n .status-dot.running { background: #28a745; box-shadow: 0 0 7px rgba(40, 167, 69, .8); }\n .server-log { height: 175px; overflow: auto; padding: .65rem; border: 1px solid rgba(127, 127, 127, .3); border-radius: .25rem; background: rgba(0, 0, 0, .18); font-family: Consolas, monospace; font-size: 12px; white-space: pre-wrap; }\n "]}),i("design:paramtypes",[o.ConfigService,l.TftpServerService,n.NgZone,n.ChangeDetectorRef])],d)},28(t,e,s){e.TftpConfigProvider=e.defaultTftpSettings=e.defaultTftpServerSettings=void 0;const r=s(414);e.defaultTftpServerSettings={rootDirectory:"",bindAddress:"0.0.0.0",port:69,blockSize:8192,timeoutMs:3e3,retries:5,autoStop:!1,autoStopSeconds:360,notifyDownloads:!0},e.defaultTftpSettings={host:"",port:69,remoteFilename:"",blockSize:1468,timeoutMs:3e3,retries:5,server:{...e.defaultTftpServerSettings}};class i extends r.ConfigProvider{constructor(){super(...arguments),this.defaults={tftp:{...e.defaultTftpSettings}},this.platformDefaults={}}}e.TftpConfigProvider=i},156(t,e,s){var r,i=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),n=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__decorate||function(t,e,s,r){var i,n=arguments.length,o=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(o=(n<3?i(o):n>3?i(e,s,o):i(e,s))||o);return n>3&&o&&Object.defineProperty(e,s,o),o},a=this&&this.__importStar||(r=function(t){return r=Object.getOwnPropertyNames||function(t){var e=[];for(var s in t)Object.prototype.hasOwnProperty.call(t,s)&&(e[e.length]=s);return e},r(t)},function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s=r(t),o=0;o<s.length;o++)"default"!==s[o]&&i(e,t,s[o]);return n(e,t),e}),l=this&&this.__exportStar||function(t,e){for(var s in t)"default"===s||Object.prototype.hasOwnProperty.call(e,s)||i(e,t,s)};Object.defineProperty(e,"__esModule",{value:!0});const c=s(282),d=s(976),u=s(98),h=s(385),f=a(s(414)),p=s(983),m=s(263),g=s(153),b=s(28),v=s(306),y=s(21);let w=class{};w=o([(0,d.NgModule)({imports:[c.CommonModule,u.FormsModule,h.NgbModule,f.default],declarations:[m.TftpModalComponent,g.TftpServerComponent],providers:[v.TftpService,y.TftpServerService,{provide:f.ConfigProvider,useClass:b.TftpConfigProvider,multi:!0},{provide:f.ToolbarButtonProvider,useClass:p.TftpButtonProvider,multi:!0}]})],w),e.default=w,l(s(328),e),l(s(211),e),l(s(108),e)},306(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,n=arguments.length,o=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(o=(n<3?i(o):n>3?i(e,s,o):i(e,s))||o);return n>3&&o&&Object.defineProperty(e,s,o),o},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpService=void 0;const n=s(976),o=s(573),a=s(414),l=s(328),c={direction:"download",state:"idle",transferred:0,speed:0,message:"准备就绪"};class d{constructor(t){this.upload=t,this.pending=Buffer.alloc(0),this.ended=!1,this.size=t.getSize()}async read(t){for(;!this.ended&&this.pending.length<t;){const t=await this.upload.read();if(!t.length){this.ended=!0;break}this.pending=this.pending.length?Buffer.concat([this.pending,Buffer.from(t)]):Buffer.from(t)}const e=Buffer.from(this.pending.subarray(0,t));return this.pending=this.pending.subarray(e.length),e}close(){this.upload.close()}abort(){this.upload.cancel()}isCancelled(){return this.upload.isCancelled()}}class u{constructor(t,e){this.download=t,this.completed=!1,e||(t.getSize=()=>this.completed?t.getCompletedBytes():Math.max(1,t.getCompletedBytes()+1))}write(t){return this.download.write(t)}close(){this.completed=!0,this.download.close()}abort(){this.download.cancel()}isCancelled(){return this.download.isCancelled()}}let h=class{constructor(t,e){this.platform=t,this.notifications=e,this.progress$=new o.BehaviorSubject(c),this.controller=null,this.client=new l.TftpClient}get busy(){return null!==this.controller}async upload(t){this.ensureIdle();const e=(await this.platform.startUpload({multiple:!1}))[0];if(!e)return null;const s=t.remoteFilename.trim()||e.getName(),r=new d(e);return this.run("upload",s,e=>this.client.upload(this.toClientOptions(t,s),r,e.signal,t=>this.progress$.next(t)))}async download(t){this.ensureIdle();const e=t.remoteFilename.trim();if(!e)throw new Error("下载时必须填写远端文件名");const s=e.split(/[\\/]/).filter(Boolean).pop()||"download.bin";return this.run("download",e,r=>this.client.download(this.toClientOptions(t,e),async t=>{const e=await this.platform.startDownload(s,420,null!=t?t:1);return e?new u(e,void 0!==t):null},r.signal,t=>this.progress$.next(t)))}cancel(){var t;null===(t=this.controller)||void 0===t||t.abort()}async run(t,e,s){this.ensureIdle();const r=new AbortController;this.controller=r;try{const i=await s(r);return this.notifications.notice(`TFTP ${"upload"===t?"上传":"下载"}完成:${e}`),i}catch(t){const e=t instanceof Error?t:new Error(String(t));throw"AbortError"===e.name?this.notifications.info("TFTP 传输已取消"):this.notifications.error("TFTP 传输失败",e.message),e}finally{this.controller===r&&(this.controller=null)}}ensureIdle(){if(this.busy)throw new Error("已有一个 TFTP 传输正在进行")}toClientOptions(t,e){return{host:t.host,port:Number(t.port),remoteFilename:e,blockSize:Number(t.blockSize),timeoutMs:Number(t.timeoutMs),retries:Number(t.retries)}}};e.TftpService=h,e.TftpService=h=r([(0,n.Injectable)(),i("design:paramtypes",[a.PlatformService,a.NotificationsService])],h)},21(t,e,s){var r,i=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),n=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__decorate||function(t,e,s,r){var i,n=arguments.length,o=n<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(o=(n<3?i(o):n>3?i(e,s,o):i(e,s))||o);return n>3&&o&&Object.defineProperty(e,s,o),o},a=this&&this.__importStar||(r=function(t){return r=Object.getOwnPropertyNames||function(t){var e=[];for(var s in t)Object.prototype.hasOwnProperty.call(t,s)&&(e[e.length]=s);return e},r(t)},function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s=r(t),o=0;o<s.length;o++)"default"!==s[o]&&i(e,t,s[o]);return n(e,t),e}),l=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServerService=void 0;const c=s(976),d=s(573),u=s(414),h=a(s(366)),f=new(s(108).TftpServer),p=new d.BehaviorSubject({running:!1,activeTransfers:0,logs:[]});let m=null,g=class{constructor(t,e){this.notifications=t,this.platform=e,this.state$=p,this.server=f}get running(){return this.server.running}get activeSettings(){return m?{...m}:null}getLocalIPv4Addresses(){const t=new Set,e=h.networkInterfaces();for(const s of Object.values(e))for(const e of null!=s?s:[])"IPv4"!==e.family||e.internal||t.add(e.address);return Array.from(t).sort()}async chooseRootDirectory(){var t;const e=s(402),r=await e.dialog.showOpenDialog({title:"选择 TFTP 根目录",properties:["openDirectory","createDirectory"]});return r.canceled?null:null!==(t=r.filePaths[0])&&void 0!==t?t:null}openRootDirectory(t){t&&this.platform.openPath(t)}copyAddress(t){this.platform.setClipboard({text:t}),this.notifications.notice(`已复制本机 IP:${t}`)}async start(t){m={...t};try{await this.server.start({rootDirectory:t.rootDirectory,bindAddress:t.bindAddress,port:Number(t.port),blockSize:Number(t.blockSize),timeoutMs:Number(t.timeoutMs),retries:Number(t.retries),autoStopSeconds:t.autoStop?Number(t.autoStopSeconds):0},t=>this.handleEvent(t))}catch(t){const e=t instanceof Error?t.message:String(t);throw this.appendLog("error",`启动失败:${e}`),this.notifications.error("TFTP 服务端启动失败",e),t}}stop(){this.server.stop()}clearLogs(){this.state$.next({...this.state$.value,logs:[]})}handleEvent(t){var e;const s="completed"===t.type?"success":"error"===t.type?"error":"info",r=this.state$.value,i={running:"started"===t.type||"stopped"!==t.type&&this.server.running,activeTransfers:t.activeTransfers,startedAt:"started"===t.type?t.timestamp:r.startedAt,logs:[...r.logs,{timestamp:t.timestamp,level:s,message:t.message}].slice(-300)};this.state$.next(i),"completed"===t.type&&(null==m?void 0:m.notifyDownloads)?this.notifications.notice(`TFTP 下载完成:${null!==(e=t.filename)&&void 0!==e?e:""}`):"stopped"===t.type&&this.notifications.info("TFTP 服务端已停止")}appendLog(t,e){const s=this.state$.value;this.state$.next({...s,logs:[...s.logs,{timestamp:Date.now(),level:t,message:e}].slice(-300)})}};e.TftpServerService=g,e.TftpServerService=g=o([(0,c.Injectable)(),l("design:paramtypes",[u.NotificationsService,u.PlatformService])],g)},328(t,e,s){var r,i=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),n=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__importStar||(r=function(t){return r=Object.getOwnPropertyNames||function(t){var e=[];for(var s in t)Object.prototype.hasOwnProperty.call(t,s)&&(e[e.length]=s);return e},r(t)},function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s=r(t),o=0;o<s.length;o++)"default"!==s[o]&&i(e,t,s[o]);return n(e,t),e});Object.defineProperty(e,"__esModule",{value:!0}),e.TftpClient=void 0;const a=o(s(935)),l=s(211);function c(t){var e,s,r,i;const n=t.host.trim(),o=t.remoteFilename.trim(),a=null!==(e=t.port)&&void 0!==e?e:69,l=null!==(s=t.blockSize)&&void 0!==s?s:1468,c=null!==(r=t.timeoutMs)&&void 0!==r?r:3e3,d=null!==(i=t.retries)&&void 0!==i?i:5;if(!n)throw new Error("TFTP 主机不能为空");if(!o)throw new Error("远端文件名不能为空");if(!Number.isInteger(a)||a<1||a>65535)throw new Error("TFTP 端口必须在 1 到 65535 之间");if(!Number.isInteger(l)||l<8||l>65464)throw new Error("TFTP 块大小必须在 8 到 65464 之间");if(!Number.isInteger(c)||c<250||c>6e4)throw new Error("超时时间必须在 250 到 60000 毫秒之间");if(!Number.isInteger(d)||d<0||d>20)throw new Error("重试次数必须在 0 到 20 之间");return{host:n,remoteFilename:o,port:a,blockSize:l,timeoutMs:c,retries:d}}function d(){const t=new Error("传输已取消");return t.name="AbortError",t}class u{constructor(t,e,s,r){this.direction=t,this.options=e,this.signal=s,this.onProgress=r,this.startedAt=Date.now(),this.transferred=0,this.negotiatedBlockSize=512,this.endpoint=null,this.lastPacket=null,this.lastTarget=null,this.retryCount=0,this.retryTimer=null,this.cancelPoll=null,this.settled=!1,this.queue=Promise.resolve(),this.socket=a.createSocket("udp4")}runSession(t){var e;const s=new Promise((t,e)=>{this.resolvePromise=t,this.rejectPromise=e});return this.socket.on("error",t=>this.fail(t)),this.socket.on("message",(t,e)=>{this.queue=this.queue.then(()=>this.handleMessage(t,e)).catch(t=>this.fail(t instanceof Error?t:new Error(String(t))))}),null===(e=this.signal)||void 0===e||e.addEventListener("abort",()=>this.fail(d()),{once:!0}),this.cancelPoll=setInterval(()=>{this.resourceCancelled()&&this.fail(d())},250),this.emit("negotiating","正在连接 TFTP 服务端"),this.sendTracked(t,{address:this.options.host,port:this.options.port}),s}clearRetryTimer(){this.retryTimer&&clearTimeout(this.retryTimer),this.retryTimer=null,this.retryCount=0}sendTracked(t,e){var s;if(this.settled)return;const r=null!==(s=null!=e?e:this.endpoint)&&void 0!==s?s:{address:this.options.host,port:this.options.port};this.lastPacket=Buffer.from(t),this.lastTarget=r,this.retryTimer&&clearTimeout(this.retryTimer),this.socket.send(t,r.port,r.address,t=>{t&&this.fail(t)}),this.retryTimer=setTimeout(()=>this.retryLastPacket(),this.options.timeoutMs)}sendUntracked(t,e,s){var r;if(this.settled)return;const i=null!==(r=null!=e?e:this.endpoint)&&void 0!==r?r:{address:this.options.host,port:this.options.port};this.socket.send(t,i.port,i.address,t=>{t?this.fail(t):null==s||s()})}emit(t,e,s){var r;const i=Math.max(1,Date.now()-this.startedAt);null===(r=this.onProgress)||void 0===r||r.call(this,{direction:this.direction,state:t,transferred:this.transferred,total:s,speed:1e3*this.transferred/i,attempt:"retrying"===t?this.retryCount:void 0,message:e})}succeed(){if(this.settled)return;this.settled=!0,this.cleanupTimers(),this.releaseResource(!0),this.socket.close();const t={bytes:this.transferred,blockSize:this.negotiatedBlockSize,durationMs:Date.now()-this.startedAt};this.emit("completed","传输完成",this.transferred),this.resolvePromise(t)}fail(t){if(!this.settled){this.settled=!0,this.cleanupTimers(),this.releaseResource(!1);try{this.socket.close()}catch{}this.emit("AbortError"===t.name?"cancelled":"error",t.message),this.rejectPromise(t)}}async handleMessage(t,e){var s;if(this.settled)return;if((null===(s=this.signal)||void 0===s?void 0:s.aborted)||this.resourceCancelled())throw d();if(this.endpoint&&(e.address!==this.endpoint.address||e.port!==this.endpoint.port))return void this.sendUntracked((0,l.createError)(5,"Unknown transfer ID"),{address:e.address,port:e.port});const r=(0,l.parsePacket)(t);if(this.endpoint||(this.endpoint={address:e.address,port:e.port}),r.opcode===l.Opcode.ERROR)throw new Error(`TFTP 错误 ${r.code}: ${r.message}`);await this.onPacket(r)}retryLastPacket(){var t;this.settled||((null===(t=this.signal)||void 0===t?void 0:t.aborted)||this.resourceCancelled()?this.fail(d()):!this.lastPacket||!this.lastTarget||this.retryCount>=this.options.retries?this.fail(new Error(`TFTP 响应超时,已重试 ${this.options.retries} 次`)):(this.retryCount++,this.emit("retrying",`响应超时,正在进行第 ${this.retryCount} 次重试`),this.socket.send(this.lastPacket,this.lastTarget.port,this.lastTarget.address,t=>{t&&this.fail(t)}),this.retryTimer=setTimeout(()=>this.retryLastPacket(),this.options.timeoutMs)))}cleanupTimers(){this.retryTimer&&clearTimeout(this.retryTimer),this.cancelPoll&&clearInterval(this.cancelPoll),this.retryTimer=null,this.cancelPoll=null}}class h extends u{constructor(t,e,s,r){super("download",t,s,r),this.sinkFactory=e,this.expectedBlock=1,this.sink=null}run(){const t=Math.max(1,Math.min(255,Math.round(this.options.timeoutMs/1e3)));return this.runSession((0,l.createRequest)(l.Opcode.RRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:0}))}async onPacket(t){if(t.opcode===l.Opcode.OACK){if(1!==this.expectedBlock||0!==this.transferred)return;return this.clearRetryTimer(),this.negotiatedBlockSize=(0,l.parseNegotiatedBlockSize)(t.options),this.totalSize=(0,l.parseTransferSize)(t.options),await this.ensureSink(this.totalSize),this.emit("transferring","已协商传输参数",this.totalSize),void this.sendTracked((0,l.createAck)(0))}if(t.opcode!==l.Opcode.DATA)return;if(t.block===this.expectedBlock){this.clearRetryTimer(),this.sink||(this.negotiatedBlockSize=512,await this.ensureSink(void 0)),await this.sink.write(t.data),this.transferred+=t.data.length,this.emit("transferring",`正在接收块 ${t.block}`,this.totalSize);const e=t.data.length<this.negotiatedBlockSize,s=(0,l.createAck)(t.block);return void(e?this.sendUntracked(s,void 0,()=>this.succeed()):(this.expectedBlock=this.expectedBlock+1&65535,this.sendTracked(s)))}const e=this.expectedBlock-1&65535;t.block===e&&this.sendTracked((0,l.createAck)(t.block))}releaseResource(t){this.sink&&(t?this.sink.close():this.sink.abort?this.sink.abort():this.sink.close())}resourceCancelled(){var t,e,s;return null!==(s=null===(e=null===(t=this.sink)||void 0===t?void 0:t.isCancelled)||void 0===e?void 0:e.call(t))&&void 0!==s&&s}async ensureSink(t){if(!this.sink&&(this.sink=await this.sinkFactory(t),!this.sink))throw d()}}class f extends u{constructor(t,e,s,r){super("upload",t,s,r),this.source=e,this.nextBlock=1,this.currentBlock=null,this.currentData=Buffer.alloc(0),this.currentFinal=!1}run(){const t=Math.max(1,Math.min(255,Math.round(this.options.timeoutMs/1e3)));return this.runSession((0,l.createRequest)(l.Opcode.WRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:this.source.size}))}async onPacket(t){return t.opcode===l.Opcode.OACK&&null===this.currentBlock?(this.clearRetryTimer(),this.negotiatedBlockSize=(0,l.parseNegotiatedBlockSize)(t.options),this.emit("transferring","已协商传输参数",this.source.size),void await this.sendNextBlock()):t.opcode===l.Opcode.ACK?0===t.block&&null===this.currentBlock?(this.clearRetryTimer(),this.negotiatedBlockSize=512,this.emit("transferring","服务端使用标准 512 字节块",this.source.size),void await this.sendNextBlock()):null!==this.currentBlock&&t.block===this.currentBlock?(this.clearRetryTimer(),this.transferred+=this.currentData.length,this.emit("transferring",`服务端已确认块 ${t.block}`,this.source.size),void(this.currentFinal?this.succeed():(this.nextBlock=this.nextBlock+1&65535,await this.sendNextBlock()))):void(null!==this.currentBlock&&t.block===(this.currentBlock-1&65535)&&this.sendTracked((0,l.createData)(this.currentBlock,this.currentData))):void 0}releaseResource(t){t?this.source.close():this.source.abort?this.source.abort():this.source.close()}resourceCancelled(){var t,e,s;return null!==(s=null===(e=(t=this.source).isCancelled)||void 0===e?void 0:e.call(t))&&void 0!==s&&s}async sendNextBlock(){var t;if((null===(t=this.signal)||void 0===t?void 0:t.aborted)||this.resourceCancelled())throw d();if(this.currentData=Buffer.from(await this.source.read(this.negotiatedBlockSize)),this.currentData.length>this.negotiatedBlockSize)throw new Error("上传数据源返回的数据超过协商块大小");this.currentBlock=this.nextBlock,this.currentFinal=this.currentData.length<this.negotiatedBlockSize,this.sendTracked((0,l.createData)(this.currentBlock,this.currentData))}}e.TftpClient=class{download(t,e,s,r){return new h(c(t),e,s,r).run()}upload(t,e,s,r){return new f(c(t),e,s,r).run()}}},211(t,e){var s;function r(t){if(t.includes("\0"))throw new Error("TFTP 字段不能包含空字符");return Buffer.concat([Buffer.from(t,"utf8"),Buffer.from([0])])}function i(t,e){const s=[];let r=e;for(let i=e;i<t.length;i++)0===t[i]&&(s.push(t.toString("utf8",r,i)),r=i+1);if(r!==t.length||s.length%2!=0)throw new Error("无效的 TFTP 选项响应");const i={};for(let t=0;t<s.length;t+=2)i[s[t].toLowerCase()]=s[t+1];return i}Object.defineProperty(e,"__esModule",{value:!0}),e.Opcode=void 0,e.createRequest=function(t,e,s={}){if(!e.trim())throw new Error("远端文件名不能为空");const i=Buffer.allocUnsafe(2);i.writeUInt16BE(t,0);const n=[i,r(e),r("octet")];for(const[t,e]of Object.entries(s))n.push(r(t.toLowerCase())),n.push(r(String(e)));return Buffer.concat(n)},e.createData=function(t,e){const r=Buffer.allocUnsafe(4+e.length);return r.writeUInt16BE(s.DATA,0),r.writeUInt16BE(65535&t,2),e.copy(r,4),r},e.createAck=function(t){const e=Buffer.allocUnsafe(4);return e.writeUInt16BE(s.ACK,0),e.writeUInt16BE(65535&t,2),e},e.createError=function(t,e){const i=Buffer.allocUnsafe(4);return i.writeUInt16BE(s.ERROR,0),i.writeUInt16BE(65535&t,2),Buffer.concat([i,r(e)])},e.parsePacket=function(t){if(t.length<2)throw new Error("TFTP 数据包过短");const e=t.readUInt16BE(0);switch(e){case s.DATA:if(t.length<4)throw new Error("无效的 DATA 数据包");return{opcode:e,block:t.readUInt16BE(2),data:t.subarray(4)};case s.ACK:if(4!==t.length)throw new Error("无效的 ACK 数据包");return{opcode:e,block:t.readUInt16BE(2)};case s.ERROR:if(t.length<5||0!==t[t.length-1])throw new Error("无效的 ERROR 数据包");return{opcode:e,code:t.readUInt16BE(2),message:t.toString("utf8",4,t.length-1)};case s.OACK:return{opcode:e,options:i(t,2)};default:throw new Error(`不支持的 TFTP 操作码: ${e}`)}},e.parseNegotiatedBlockSize=function(t,e=512){if(!t.blksize)return e;const s=Number(t.blksize);if(!Number.isInteger(s)||s<8||s>65464)throw new Error(`服务端返回了无效的块大小: ${t.blksize}`);return s},e.parseTransferSize=function(t){if(void 0===t.tsize)return;const e=Number(t.tsize);if(!Number.isSafeInteger(e)||e<0)throw new Error(`服务端返回了无效的文件大小: ${t.tsize}`);return e},function(t){t[t.RRQ=1]="RRQ",t[t.WRQ=2]="WRQ",t[t.DATA=3]="DATA",t[t.ACK=4]="ACK",t[t.ERROR=5]="ERROR",t[t.OACK=6]="OACK"}(s||(e.Opcode=s={}))},108(t,e,s){var r,i=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),n=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__importStar||(r=function(t){return r=Object.getOwnPropertyNames||function(t){var e=[];for(var s in t)Object.prototype.hasOwnProperty.call(t,s)&&(e[e.length]=s);return e},r(t)},function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s=r(t),o=0;o<s.length;o++)"default"!==s[o]&&i(e,t,s[o]);return n(e,t),e});Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServer=void 0,e.parseRequest=u;const a=o(s(935)),l=o(s(383)),c=o(s(3)),d=s(211);function u(t){if(t.length<4)throw new Error("TFTP 请求过短");const e=t.readUInt16BE(0);if(e!==d.Opcode.RRQ&&e!==d.Opcode.WRQ)throw new Error("不是有效的读写请求");const s=function(t){const e=[];let s=2;for(let r=2;r<t.length;r++)0===t[r]&&(e.push(t.toString("utf8",s,r)),s=r+1);if(s!==t.length)throw new Error("请求字段缺少结束符");return e}(t);if(s.length<2||(s.length-2)%2!=0)throw new Error("TFTP 请求字段无效");const r=s[0],i=s[1].toLowerCase();if(!r)throw new Error("请求文件名为空");if("octet"!==i)throw new Error(`不支持的传输模式: ${i}`);const n={};for(let t=2;t<s.length;t+=2)n[s[t].toLowerCase()]=s[t+1];return{opcode:e,filename:r,mode:i,options:n}}class h{constructor(t,e,s,r){this.request=t,this.remote=e,this.options=s,this.onFinish=r,this.blockSize=512,this.transferred=0,this.lastPacket=null,this.timer=null,this.retryCount=0,this.settled=!1,this.failing=!1,this.queue=Promise.resolve(),this.timeoutMs=s.timeoutMs,this.socket=a.createSocket("udp4")}start(){this.socket.on("error",t=>this.fail(t.message)),this.socket.on("message",(t,e)=>{this.queue=this.queue.then(()=>this.handleMessage(t,e)).catch(t=>this.fail(t instanceof Error?t.message:String(t)))}),this.socket.bind(0,this.options.bindAddress,()=>{this.begin().catch(t=>this.fail(t instanceof Error?t.message:String(t)))})}resendLast(){this.lastPacket&&this.sendTracked(this.lastPacket)}cancel(){this.finish(!1,"服务端已停止")}sendTracked(t){this.settled||(this.lastPacket=Buffer.from(t),this.timer&&clearTimeout(this.timer),this.socket.send(t,this.remote.port,this.remote.address,t=>{t&&this.fail(t.message)}),this.timer=setTimeout(()=>this.retry(),this.timeoutMs))}clearTimer(){this.timer&&clearTimeout(this.timer),this.timer=null,this.retryCount=0}complete(t){this.finish(!0,t)}fail(t){if(this.settled||this.failing)return;this.failing=!0,this.timer&&clearTimeout(this.timer);const e=(0,d.createError)(1,t);this.socket.send(e,this.remote.port,this.remote.address,()=>this.finish(!1,t))}async handleMessage(t,e){if(this.settled)return;if(e.address!==this.remote.address||e.port!==this.remote.port){const t=(0,d.createError)(5,"Unknown transfer ID");return void this.socket.send(t,e.port,e.address)}const s=(0,d.parsePacket)(t);if(s.opcode===d.Opcode.ERROR)throw new Error(`客户端错误 ${s.code}: ${s.message}`);await this.onPacket(s)}retry(){this.settled||(!this.lastPacket||this.retryCount>=this.options.retries?this.fail(`等待客户端响应超时,已重试 ${this.options.retries} 次`):(this.retryCount++,this.socket.send(this.lastPacket,this.remote.port,this.remote.address,t=>{t&&this.fail(t.message)}),this.timer=setTimeout(()=>this.retry(),this.timeoutMs)))}finish(t,e){this.settled||(this.settled=!0,this.timer&&clearTimeout(this.timer),this.release(t).catch(()=>{}).finally(()=>{try{this.socket.close()}catch{}this.onFinish(t,e,this.transferred)}))}}class f extends h{constructor(t,e,s,r){super(t,e,s,r),this.file=null,this.fileSize=0,this.position=0,this.currentBlock=0,this.currentData=Buffer.alloc(0),this.finalBlock=!1}async begin(){const t=function(t,e){const s=e.replace(/\\/g,"/").replace(/^\/+/,"").split("/").filter(t=>t&&"."!==t);if(!s.length||s.some(t=>".."===t))throw new Error("拒绝不安全的文件路径");const r=c.resolve(t,...s),i=c.relative(t,r);if(!i||i.startsWith("..")||c.isAbsolute(i))throw new Error("请求文件超出 TFTP 根目录");return r}(this.options.rootDirectory,this.request.filename),e=await l.promises.stat(t);if(!e.isFile())throw new Error("请求路径不是普通文件");this.fileSize=e.size,this.file=await l.promises.open(t,"r");const s={};if(void 0!==this.request.options.blksize){const t=Number(this.request.options.blksize);if(!Number.isInteger(t)||t<8||t>65464)throw new Error("客户端请求了无效的块大小");this.blockSize=Math.min(t,this.options.blockSize),s.blksize=this.blockSize}if(void 0!==this.request.options.timeout){const t=Number(this.request.options.timeout);if(!Number.isInteger(t)||t<1||t>255)throw new Error("客户端请求了无效的超时时间");this.timeoutMs=1e3*t,s.timeout=t}void 0!==this.request.options.tsize&&(s.tsize=this.fileSize),Object.keys(s).length?this.sendTracked(function(t){const e=Buffer.allocUnsafe(2);e.writeUInt16BE(d.Opcode.OACK,0);const s=[e];for(const[e,r]of Object.entries(t))s.push(Buffer.from(`${e}\0${r}\0`,"utf8"));return Buffer.concat(s)}(s)):await this.sendNextBlock()}async onPacket(t){if(t.opcode===d.Opcode.ACK)return 0===this.currentBlock&&0===t.block?(this.clearTimer(),void await this.sendNextBlock()):t.block===this.currentBlock?(this.clearTimer(),this.transferred+=this.currentData.length,void(this.finalBlock?this.complete(`已发送 ${this.transferred} 字节`):await this.sendNextBlock())):void(t.block===(this.currentBlock-1&65535)&&this.resendLast())}async release(){this.file&&await this.file.close(),this.file=null}async sendNextBlock(){const t=this.file;if(!t)throw new Error("TFTP 文件尚未打开");const e=Buffer.allocUnsafe(this.blockSize),s=await t.read(e,0,this.blockSize,this.position);this.currentData=Buffer.from(e.subarray(0,s.bytesRead)),this.position+=s.bytesRead,this.currentBlock=this.currentBlock+1&65535,this.finalBlock=s.bytesRead<this.blockSize,this.sendTracked((0,d.createData)(this.currentBlock,this.currentData))}}e.TftpServer=class{constructor(){this.listener=null,this.options=null,this.transfers=new Map,this.autoStopTimer=null,this.eventHandler=null}get running(){return null!==this.listener}get activeTransfers(){return this.transfers.size}async start(t,e){if(this.running)throw new Error("TFTP 服务端已经在运行");const s=function(t){var e,s,r,i,n,o;const a=c.resolve(t.rootDirectory.trim()),l=(null!==(e=t.bindAddress)&&void 0!==e?e:"0.0.0.0").trim(),d=null!==(s=t.port)&&void 0!==s?s:69,u=null!==(r=t.blockSize)&&void 0!==r?r:8192,h=null!==(i=t.timeoutMs)&&void 0!==i?i:3e3,f=null!==(n=t.retries)&&void 0!==n?n:5,p=null!==(o=t.autoStopSeconds)&&void 0!==o?o:0;if(!t.rootDirectory.trim())throw new Error("请选择 TFTP 根目录");if(!l)throw new Error("监听地址不能为空");if(!Number.isInteger(d)||d<1||d>65535)throw new Error("监听端口必须在 1 到 65535 之间");if(!Number.isInteger(u)||u<8||u>65464)throw new Error("TFTP 缓冲区必须在 8 到 65464 字节之间");if(!Number.isInteger(h)||h<250||h>6e4)throw new Error("超时时间必须在 250 到 60000 毫秒之间");if(!Number.isInteger(f)||f<0||f>20)throw new Error("重试次数必须在 0 到 20 之间");if(!Number.isInteger(p)||p<0||p>86400)throw new Error("自动停止时间必须在 0 到 86400 秒之间");return{rootDirectory:a,bindAddress:l,port:d,blockSize:u,timeoutMs:h,retries:f,autoStopSeconds:p}}(t);if(!(await l.promises.stat(s.rootDirectory)).isDirectory())throw new Error("TFTP 根目录无效");this.options=s,this.eventHandler=null!=e?e:null;const r=a.createSocket("udp4");this.listener=r,r.on("message",(t,e)=>this.handleRequest(t,e));let i=null;r.on("error",t=>{if(i){const e=i;i=null;try{r.close()}catch{}this.listener=null,e(t)}else this.emit("error",t.message)}),await new Promise((t,e)=>{i=e,r.bind(s.port,s.bindAddress,()=>{i=null,t()})}),this.emit("started",`TFTP 服务端已启动:${s.bindAddress}:${s.port}`),s.autoStopSeconds>0&&(this.autoStopTimer=setTimeout(()=>this.stop("达到自动停止时间"),1e3*s.autoStopSeconds))}stop(t="用户停止"){if(this.listener){this.autoStopTimer&&clearTimeout(this.autoStopTimer),this.autoStopTimer=null;for(const t of this.transfers.values())t.cancel();this.transfers.clear();try{this.listener.close()}catch{}this.listener=null,this.emit("stopped",`TFTP 服务端已停止:${t}`)}}handleRequest(t,e){if(!this.listener||!this.options)return;let s;try{s=u(t)}catch(t){const s=t instanceof Error?t.message:String(t);return this.listener.send((0,d.createError)(4,s),e.port,e.address),void this.emit("error",`拒绝来自 ${e.address}:${e.port} 的无效请求:${s}`)}const r=`${e.address}:${e.port}:${s.opcode}:${s.filename}`,i=this.transfers.get(r);if(i)return void i.resendLast();if(s.opcode===d.Opcode.WRQ)return this.listener.send((0,d.createError)(2,"Server is read-only"),e.port,e.address),void this.emit("error",`拒绝 ${e.address}:${e.port} 上传 ${s.filename}:服务端为只读模式`,e,s.filename);const n=`${e.address}:${e.port}`,o=new f(s,e,this.options,(t,i,o)=>{this.transfers.delete(r),this.emit(t?"completed":"error",`${n} ${t?"下载完成":"下载失败"} ${s.filename}:${i}`,e,s.filename,o)});this.transfers.set(r,o),this.emit("request",`${n} 请求下载 ${s.filename}`,e,s.filename),o.start()}emit(t,e,s,r,i){var n;null===(n=this.eventHandler)||void 0===n||n.call(this,{type:t,timestamp:Date.now(),message:e,peer:s?`${s.address}:${s.port}`:void 0,filename:r,bytes:i,activeTransfers:this.transfers.size})}}},282(t){t.exports=require("@angular/common")},976(t){t.exports=require("@angular/core")},98(t){t.exports=require("@angular/forms")},402(t){t.exports=require("@electron/remote")},385(t){t.exports=require("@ng-bootstrap/ng-bootstrap")},935(t){t.exports=require("dgram")},383(t){t.exports=require("fs")},366(t){t.exports=require("os")},3(t){t.exports=require("path")},573(t){t.exports=require("rxjs")},414(t){t.exports=require("tabby-core")}};const e={};let s=function s(r){const i=e[r];if(void 0!==i)return i.exports;const n=e[r]={exports:{}};return t[r].call(n.exports,n,n.exports,s),n.exports}(156);module.exports=s})();
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","mappings":"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","sources":["webpack://tabby-tftp/./src/buttonProvider.ts","webpack://tabby-tftp/./src/components/tftpModal.component.ts","webpack://tabby-tftp/./src/components/tftpServer.component.ts","webpack://tabby-tftp/./src/config.ts","webpack://tabby-tftp/./src/index.ts","webpack://tabby-tftp/./src/services/tftp.service.ts","webpack://tabby-tftp/./src/services/tftpServer.service.ts","webpack://tabby-tftp/./src/tftp/client.ts","webpack://tabby-tftp/./src/tftp/protocol.ts","webpack://tabby-tftp/./src/tftp/server.ts","webpack://tabby-tftp/external commonjs \"@angular/common\"","webpack://tabby-tftp/external commonjs \"@angular/core\"","webpack://tabby-tftp/external commonjs \"@angular/forms\"","webpack://tabby-tftp/external commonjs \"@electron/remote\"","webpack://tabby-tftp/external commonjs \"@ng-bootstrap/ng-bootstrap\"","webpack://tabby-tftp/external commonjs \"dgram\"","webpack://tabby-tftp/external commonjs \"fs\"","webpack://tabby-tftp/external commonjs \"os\"","webpack://tabby-tftp/external commonjs \"path\"","webpack://tabby-tftp/external commonjs \"rxjs\"","webpack://tabby-tftp/external commonjs \"tabby-core\"","webpack://tabby-tftp/webpack/bootstrap","webpack://tabby-tftp/webpack/startup"],"sourcesContent":["import { Injectable } from '@angular/core'\nimport { NgbModal } from '@ng-bootstrap/ng-bootstrap'\nimport { IToolbarButton, ToolbarButtonProvider } from 'tabby-core'\n\nimport { TftpModalComponent } from './components/tftpModal.component'\n\nconst transferIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7 7h11l-3.5-3.5L16 2l6 6-6 6-1.5-1.5L18 9H7V7zm10 10H6l3.5 3.5L8 22l-6-6 6-6 1.5 1.5L6 15h11v2z\"/></svg>`\n\n@Injectable()\nexport class TftpButtonProvider extends ToolbarButtonProvider {\n constructor (private readonly modal: NgbModal) {\n super()\n }\n\n provide (): IToolbarButton[] {\n return [{\n icon: transferIcon,\n title: 'TFTP 文件传输',\n weight: 6,\n click: () => this.modal.open(TftpModalComponent, { size: 'lg' }),\n }]\n }\n}\n","import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from '@angular/core'\nimport { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'\nimport { Subscription } from 'rxjs'\nimport { ConfigService } from 'tabby-core'\n\nimport { defaultTftpServerSettings, defaultTftpSettings, TftpPluginSettings } from '../config'\nimport { TftpService } from '../services/tftp.service'\nimport { TftpProgress } from '../tftp/client'\n\n@Component({\n selector: 'tabby-tftp-modal',\n template: `\n <div class=\"modal-header\">\n <h5 class=\"modal-title\"><i class=\"fa fa-exchange-alt mr-2\"></i>TFTP 文件传输与固件服务</h5>\n <button type=\"button\" class=\"close\" aria-label=\"关闭\" (click)=\"activeModal.dismiss()\">\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n\n <div class=\"mode-tabs px-3 pt-3\">\n <div class=\"btn-group w-100\">\n <button class=\"btn\" [class.btn-primary]=\"mode === 'server'\" [class.btn-outline-secondary]=\"mode !== 'server'\" (click)=\"mode = 'server'\">\n <i class=\"fa fa-server mr-1\"></i>固件 TFTP 服务端\n </button>\n <button class=\"btn\" [class.btn-primary]=\"mode === 'client'\" [class.btn-outline-secondary]=\"mode !== 'client'\" (click)=\"mode = 'client'\">\n <i class=\"fa fa-exchange-alt mr-1\"></i>TFTP 客户端\n </button>\n </div>\n </div>\n\n <div class=\"modal-body\" *ngIf=\"mode === 'server'\">\n <tabby-tftp-server></tabby-tftp-server>\n </div>\n\n <div class=\"modal-body\" *ngIf=\"mode === 'client'\">\n <div class=\"alert alert-warning py-2 small\">\n TFTP 没有身份认证和加密。请仅在可信局域网中使用。\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-8\">\n <label>主机或 IP 地址</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.host\" placeholder=\"例如 192.168.1.10\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>端口</label>\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"65535\" [(ngModel)]=\"settings.port\" [disabled]=\"busy\">\n </div>\n </div>\n\n <div class=\"form-group\">\n <label>远端文件名</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.remoteFilename\"\n placeholder=\"下载时必填;上传留空则使用本地文件名\" [disabled]=\"busy\">\n <small class=\"form-text text-muted\">TFTP 不支持浏览远端目录,路径格式由服务端决定。</small>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">高级选项</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-4\">\n <label>块大小</label>\n <input class=\"form-control\" type=\"number\" min=\"8\" max=\"65464\" [(ngModel)]=\"settings.blockSize\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>超时(毫秒)</label>\n <input class=\"form-control\" type=\"number\" min=\"250\" max=\"60000\" [(ngModel)]=\"settings.timeoutMs\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>重试次数</label>\n <input class=\"form-control\" type=\"number\" min=\"0\" max=\"20\" [(ngModel)]=\"settings.retries\" [disabled]=\"busy\">\n </div>\n </div>\n </details>\n\n <div class=\"transfer-status\" *ngIf=\"status\">\n <div class=\"d-flex align-items-center mb-1\">\n <strong>{{ status.direction === 'upload' ? '上传' : '下载' }}</strong>\n <span class=\"ml-auto text-muted\">{{ formatBytes(status.transferred) }}<span *ngIf=\"status.total !== undefined\"> / {{ formatBytes(status.total) }}</span></span>\n </div>\n <ngb-progressbar [type]=\"progressType\" [value]=\"percent\" [striped]=\"busy\" [animated]=\"busy\"></ngb-progressbar>\n <div class=\"d-flex small mt-1\">\n <span [class.text-danger]=\"status.state === 'error'\">{{ status.message }}</span>\n <span class=\"ml-auto\" *ngIf=\"status.speed > 0\">{{ formatBytes(status.speed) }}/s</span>\n </div>\n </div>\n <div class=\"text-danger mt-2\" *ngIf=\"validationError\">{{ validationError }}</div>\n </div>\n\n <div class=\"modal-footer\">\n <button class=\"btn btn-outline-secondary\" (click)=\"activeModal.close()\">关闭</button>\n <ng-container *ngIf=\"mode === 'client'\">\n <button class=\"btn btn-outline-danger\" *ngIf=\"busy\" (click)=\"cancel()\"><i class=\"fa fa-stop mr-1\"></i>取消传输</button>\n <button class=\"btn btn-primary\" *ngIf=\"!busy\" (click)=\"upload()\"><i class=\"fa fa-upload mr-1\"></i>上传</button>\n <button class=\"btn btn-success\" *ngIf=\"!busy\" (click)=\"download()\"><i class=\"fa fa-download mr-1\"></i>下载</button>\n </ng-container>\n </div>\n `,\n styles: [`\n :host { display: block; }\n summary { cursor: pointer; user-select: none; }\n .transfer-status { border: 1px solid rgba(127, 127, 127, .25); border-radius: .25rem; padding: .75rem; }\n .modal-body label { margin-bottom: .25rem; }\n .mode-tabs .btn { flex: 1; }\n `],\n})\nexport class TftpModalComponent implements OnInit, OnDestroy {\n mode: 'server' | 'client' = 'server'\n settings: TftpPluginSettings = { ...defaultTftpSettings, server: { ...defaultTftpServerSettings } }\n status: TftpProgress | null = null\n validationError = ''\n private subscription?: Subscription\n\n constructor (\n public readonly activeModal: NgbActiveModal,\n private readonly config: ConfigService,\n public readonly tftp: TftpService,\n private readonly zone: NgZone,\n private readonly changeDetector: ChangeDetectorRef,\n ) { }\n\n get busy (): boolean { return this.tftp.busy }\n\n get percent (): number {\n if (!this.status) return 0\n if (this.status.state === 'completed') return 100\n if (this.status.total === undefined) return this.busy ? 100 : 0\n if (this.status.total === 0) return 100\n return Math.min(100, Math.round(this.status.transferred * 100 / this.status.total))\n }\n\n get progressType (): string {\n if (this.status?.state === 'error' || this.status?.state === 'cancelled') return 'danger'\n if (this.status?.state === 'completed') return 'success'\n return 'info'\n }\n\n ngOnInit (): void {\n const stored = this.config.store.tftp ?? {}\n this.settings = {\n ...defaultTftpSettings,\n ...stored,\n server: { ...defaultTftpServerSettings, ...(stored.server ?? {}) },\n }\n this.subscription = this.tftp.progress$.subscribe(status => {\n this.zone.run(() => {\n this.status = status\n this.changeDetector.markForCheck()\n })\n })\n }\n\n ngOnDestroy (): void { this.subscription?.unsubscribe() }\n\n async upload (): Promise<void> {\n if (!this.validate(false)) return\n await this.execute(() => this.tftp.upload(this.settings))\n }\n\n async download (): Promise<void> {\n if (!this.validate(true)) return\n await this.execute(() => this.tftp.download(this.settings))\n }\n\n cancel (): void { this.tftp.cancel() }\n\n formatBytes (value: number): string {\n if (!Number.isFinite(value) || value <= 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n const index = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024)))\n const amount = value / Math.pow(1024, index)\n return `${amount >= 100 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`\n }\n\n private async execute (operation: () => Promise<unknown>): Promise<void> {\n this.validationError = ''\n if (!this.config.store.tftp) this.config.store.tftp = {}\n Object.assign(this.config.store.tftp, {\n host: this.settings.host,\n port: this.settings.port,\n remoteFilename: this.settings.remoteFilename,\n blockSize: this.settings.blockSize,\n timeoutMs: this.settings.timeoutMs,\n retries: this.settings.retries,\n })\n await this.config.save()\n try { await operation() } catch (error) {\n this.validationError = error instanceof Error ? error.message : String(error)\n }\n }\n\n private validate (requireRemoteFilename: boolean): boolean {\n this.validationError = ''\n if (!this.settings.host.trim()) this.validationError = '请填写 TFTP 主机地址'\n else if (requireRemoteFilename && !this.settings.remoteFilename.trim()) this.validationError = '下载时必须填写远端文件名'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.validationError = '端口必须在 1 到 65535 之间'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.validationError = '块大小必须在 8 到 65464 之间'\n else if (!Number.isInteger(Number(this.settings.timeoutMs)) || Number(this.settings.timeoutMs) < 250 || Number(this.settings.timeoutMs) > 60000) this.validationError = '超时时间必须在 250 到 60000 毫秒之间'\n else if (!Number.isInteger(Number(this.settings.retries)) || Number(this.settings.retries) < 0 || Number(this.settings.retries) > 20) this.validationError = '重试次数必须在 0 到 20 之间'\n return !this.validationError\n }\n}\n","import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from '@angular/core'\nimport { Subscription } from 'rxjs'\nimport { ConfigService } from 'tabby-core'\n\nimport { defaultTftpServerSettings, TftpServerSettings } from '../config'\nimport { TftpServerService, TftpServerState } from '../services/tftpServer.service'\n\nconst serverSettingsStorageKey = 'tabby-tftp-server-settings-v1'\n\n@Component({\n selector: 'tabby-tftp-server',\n template: `\n <div class=\"alert alert-info py-2 small\">\n 串口负责给 Bootloader 下命令;本服务端通过网口向设备提供固件。服务端为只读模式,设备不能覆盖电脑文件。\n </div>\n\n <div class=\"form-group\">\n <label>TFTP 根目录</label>\n <div class=\"input-group\">\n <input class=\"form-control\" [(ngModel)]=\"settings.rootDirectory\" placeholder=\"固件所在目录\" [disabled]=\"state.running\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary\" (click)=\"chooseDirectory()\" [disabled]=\"state.running\" title=\"选择目录\">\n <i class=\"fa fa-folder-open\"></i>\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"server.openRootDirectory(settings.rootDirectory)\" [disabled]=\"!settings.rootDirectory\" title=\"打开目录\">\n <i class=\"fa fa-external-link-alt\"></i>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-5\">\n <label>监听地址</label>\n <select class=\"form-control\" [(ngModel)]=\"settings.bindAddress\" [disabled]=\"state.running\">\n <option value=\"0.0.0.0\">0.0.0.0(所有网卡)</option>\n <option *ngFor=\"let address of localAddresses\" [value]=\"address\">{{ address }}</option>\n </select>\n </div>\n <div class=\"form-group col-md-3\">\n <label>监听端口</label>\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"65535\" [(ngModel)]=\"settings.port\" [disabled]=\"state.running\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>TFTP 缓冲区</label>\n <div class=\"input-group\">\n <input class=\"form-control\" type=\"number\" min=\"8\" max=\"65464\" [(ngModel)]=\"settings.blockSize\" [disabled]=\"state.running\">\n <div class=\"input-group-append\"><span class=\"input-group-text\">Bytes</span></div>\n </div>\n </div>\n </div>\n\n <div class=\"local-addresses mb-3\">\n <span class=\"text-muted mr-2\">设备可访问的本机 IP:</span>\n <button class=\"btn btn-sm btn-outline-info mr-1\" *ngFor=\"let address of localAddresses\" (click)=\"server.copyAddress(address)\" title=\"点击复制\">\n {{ address }}\n </button>\n <span class=\"text-warning small\" *ngIf=\"!localAddresses.length\">未检测到可用 IPv4 地址</span>\n </div>\n\n <div class=\"form-row align-items-center mb-3\">\n <div class=\"col-md-5\">\n <div class=\"custom-control custom-checkbox\">\n <input type=\"checkbox\" class=\"custom-control-input\" id=\"tftp-auto-stop\" [(ngModel)]=\"settings.autoStop\" [disabled]=\"state.running\">\n <label class=\"custom-control-label\" for=\"tftp-auto-stop\">自动停止服务端</label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"input-group input-group-sm\">\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"86400\" [(ngModel)]=\"settings.autoStopSeconds\" [disabled]=\"state.running || !settings.autoStop\">\n <div class=\"input-group-append\"><span class=\"input-group-text\">秒</span></div>\n </div>\n </div>\n <div class=\"col-md-4\">\n <div class=\"custom-control custom-checkbox\">\n <input type=\"checkbox\" class=\"custom-control-input\" id=\"tftp-notify\" [(ngModel)]=\"settings.notifyDownloads\">\n <label class=\"custom-control-label\" for=\"tftp-notify\">下载完成后通知</label>\n </div>\n </div>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">可靠性选项</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-6\">\n <label>响应超时(毫秒)</label>\n <input class=\"form-control\" type=\"number\" min=\"250\" max=\"60000\" [(ngModel)]=\"settings.timeoutMs\" [disabled]=\"state.running\">\n </div>\n <div class=\"form-group col-md-6\">\n <label>重试次数</label>\n <input class=\"form-control\" type=\"number\" min=\"0\" max=\"20\" [(ngModel)]=\"settings.retries\" [disabled]=\"state.running\">\n </div>\n </div>\n </details>\n\n <div class=\"d-flex align-items-center mb-2\">\n <span class=\"status-dot mr-2\" [class.running]=\"state.running\"></span>\n <strong>{{ state.running ? 'TFTP 服务端运行中' : 'TFTP 服务端已停止' }}</strong>\n <span class=\"badge badge-info ml-2\" *ngIf=\"state.activeTransfers\">{{ state.activeTransfers }} 个传输</span>\n <div class=\"ml-auto\">\n <button class=\"btn btn-sm btn-link text-muted\" (click)=\"server.clearLogs()\" [disabled]=\"!state.logs.length\">清空日志</button>\n <button class=\"btn btn-success ml-1\" *ngIf=\"!state.running\" (click)=\"start()\" [disabled]=\"starting\">\n <i class=\"fa fa-play mr-1\"></i>{{ starting ? '启动中…' : '启动服务端' }}\n </button>\n <button class=\"btn btn-danger ml-1\" *ngIf=\"state.running\" (click)=\"server.stop()\">\n <i class=\"fa fa-stop mr-1\"></i>停止服务端\n </button>\n </div>\n </div>\n\n <div class=\"server-log\">\n <div *ngIf=\"!state.logs.length\" class=\"text-muted\">服务端输出将显示在这里</div>\n <div *ngFor=\"let log of state.logs\" [class.text-success]=\"log.level === 'success'\" [class.text-danger]=\"log.level === 'error'\">\n [{{ formatTime(log.timestamp) }}] {{ log.message }}\n </div>\n </div>\n <div class=\"text-danger mt-2\" *ngIf=\"error\">{{ error }}</div>\n `,\n styles: [`\n :host { display: block; }\n label { margin-bottom: .25rem; }\n summary { cursor: pointer; user-select: none; }\n .local-addresses { min-height: 31px; display: flex; align-items: center; flex-wrap: wrap; }\n .status-dot { width: 10px; height: 10px; border-radius: 50%; background: #6c757d; display: inline-block; }\n .status-dot.running { background: #28a745; box-shadow: 0 0 7px rgba(40, 167, 69, .8); }\n .server-log { height: 175px; overflow: auto; padding: .65rem; border: 1px solid rgba(127, 127, 127, .3); border-radius: .25rem; background: rgba(0, 0, 0, .18); font-family: Consolas, monospace; font-size: 12px; white-space: pre-wrap; }\n `],\n})\nexport class TftpServerComponent implements OnInit, OnDestroy {\n settings: TftpServerSettings = { ...defaultTftpServerSettings }\n state: TftpServerState = { running: false, activeTransfers: 0, logs: [] }\n localAddresses: string[] = []\n starting = false\n error = ''\n\n private subscription?: Subscription\n\n constructor (\n private readonly config: ConfigService,\n public readonly server: TftpServerService,\n private readonly zone: NgZone,\n private readonly changeDetector: ChangeDetectorRef,\n ) { }\n\n ngOnInit (): void {\n let cached: Partial<TftpServerSettings> = {}\n try {\n cached = JSON.parse(localStorage.getItem(serverSettingsStorageKey) ?? '{}')\n } catch { }\n this.settings = {\n ...defaultTftpServerSettings,\n ...(this.config.store.tftp?.server ?? {}),\n ...cached,\n ...(this.server.activeSettings ?? {}),\n }\n this.localAddresses = this.server.getLocalIPv4Addresses()\n this.subscription = this.server.state$.subscribe(state => {\n this.zone.run(() => {\n this.state = state\n this.changeDetector.markForCheck()\n })\n })\n }\n\n ngOnDestroy (): void {\n this.subscription?.unsubscribe()\n }\n\n async chooseDirectory (): Promise<void> {\n const directory = await this.server.chooseRootDirectory()\n if (directory) {\n this.settings.rootDirectory = directory\n await this.saveSettings()\n }\n }\n\n async start (): Promise<void> {\n this.error = ''\n if (!this.validate()) return\n this.starting = true\n await this.saveSettings()\n try {\n await this.server.start(this.settings)\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n } finally {\n this.starting = false\n }\n }\n\n formatTime (timestamp: number): string {\n return new Date(timestamp).toLocaleTimeString(undefined, { hour12: false })\n }\n\n private validate (): boolean {\n if (!this.settings.rootDirectory.trim()) this.error = '请选择固件所在的 TFTP 根目录'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.error = '监听端口必须在 1 到 65535 之间'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.error = 'TFTP 缓冲区必须在 8 到 65464 字节之间'\n else if (this.settings.autoStop && (!Number.isInteger(Number(this.settings.autoStopSeconds)) || Number(this.settings.autoStopSeconds) < 1 || Number(this.settings.autoStopSeconds) > 86400)) this.error = '自动停止时间必须在 1 到 86400 秒之间'\n return !this.error\n }\n\n private async saveSettings (): Promise<void> {\n localStorage.setItem(serverSettingsStorageKey, JSON.stringify(this.settings))\n if (!this.config.store.tftp) this.config.store.tftp = {}\n if (!this.config.store.tftp.server) this.config.store.tftp.server = {}\n Object.assign(this.config.store.tftp.server, this.settings)\n await this.config.save()\n }\n}\n","import { ConfigProvider } from 'tabby-core'\n\nexport interface TftpPluginSettings {\n host: string\n port: number\n remoteFilename: string\n blockSize: number\n timeoutMs: number\n retries: number\n server: TftpServerSettings\n}\n\nexport interface TftpServerSettings {\n rootDirectory: string\n bindAddress: string\n port: number\n blockSize: number\n timeoutMs: number\n retries: number\n autoStop: boolean\n autoStopSeconds: number\n notifyDownloads: boolean\n}\n\nexport const defaultTftpServerSettings: TftpServerSettings = {\n rootDirectory: '',\n bindAddress: '0.0.0.0',\n port: 69,\n blockSize: 8192,\n timeoutMs: 3000,\n retries: 5,\n autoStop: false,\n autoStopSeconds: 360,\n notifyDownloads: true,\n}\n\nexport const defaultTftpSettings: TftpPluginSettings = {\n host: '',\n port: 69,\n remoteFilename: '',\n blockSize: 1468,\n timeoutMs: 3000,\n retries: 5,\n server: { ...defaultTftpServerSettings },\n}\n\nexport class TftpConfigProvider extends ConfigProvider {\n defaults = {\n tftp: { ...defaultTftpSettings },\n }\n\n platformDefaults = { }\n}\n","import { CommonModule } from '@angular/common'\nimport { NgModule } from '@angular/core'\nimport { FormsModule } from '@angular/forms'\nimport { NgbModule } from '@ng-bootstrap/ng-bootstrap'\nimport TabbyCoreModule, { ConfigProvider, ToolbarButtonProvider } from 'tabby-core'\n\nimport { TftpButtonProvider } from './buttonProvider'\nimport { TftpModalComponent } from './components/tftpModal.component'\nimport { TftpServerComponent } from './components/tftpServer.component'\nimport { TftpConfigProvider } from './config'\nimport { TftpService } from './services/tftp.service'\nimport { TftpServerService } from './services/tftpServer.service'\n\n@NgModule({\n imports: [\n CommonModule,\n FormsModule,\n NgbModule,\n TabbyCoreModule,\n ],\n declarations: [\n TftpModalComponent,\n TftpServerComponent,\n ],\n providers: [\n TftpService,\n TftpServerService,\n { provide: ConfigProvider, useClass: TftpConfigProvider, multi: true },\n { provide: ToolbarButtonProvider, useClass: TftpButtonProvider, multi: true },\n ],\n})\nexport default class TftpModule { }\n\nexport * from './tftp/client'\nexport * from './tftp/protocol'\nexport * from './tftp/server'\n","import { Injectable } from '@angular/core'\nimport { BehaviorSubject } from 'rxjs'\nimport {\n FileDownload,\n FileUpload,\n NotificationsService,\n PlatformService,\n} from 'tabby-core'\n\nimport { TftpPluginSettings } from '../config'\nimport {\n DownloadSink,\n TftpClient,\n TftpProgress,\n TftpResult,\n UploadSource,\n} from '../tftp/client'\n\nconst idleProgress: TftpProgress = {\n direction: 'download',\n state: 'idle',\n transferred: 0,\n speed: 0,\n message: '准备就绪',\n}\n\nclass TabbyUploadSource implements UploadSource {\n readonly size: number\n private pending = Buffer.alloc(0)\n private ended = false\n\n constructor (private readonly upload: FileUpload) {\n this.size = upload.getSize()\n }\n\n async read (maximumBytes: number): Promise<Buffer> {\n while (!this.ended && this.pending.length < maximumBytes) {\n const chunk = await this.upload.read()\n if (!chunk.length) {\n this.ended = true\n break\n }\n this.pending = this.pending.length\n ? Buffer.concat([this.pending, Buffer.from(chunk)])\n : Buffer.from(chunk)\n }\n\n const result = Buffer.from(this.pending.subarray(0, maximumBytes))\n this.pending = this.pending.subarray(result.length)\n return result\n }\n\n close (): void {\n this.upload.close()\n }\n\n abort (): void {\n this.upload.cancel()\n }\n\n isCancelled (): boolean {\n return this.upload.isCancelled()\n }\n}\n\nclass TabbyDownloadSink implements DownloadSink {\n private completed = false\n\n constructor (private readonly download: FileDownload, sizeKnown: boolean) {\n if (!sizeKnown) {\n // Tabby requires a size before the TFTP server has necessarily told us one.\n // Keep the transfer active until close(), then report the actual byte count.\n ;(download as any).getSize = () => this.completed\n ? download.getCompletedBytes()\n : Math.max(1, download.getCompletedBytes() + 1)\n }\n }\n\n write (data: Buffer): Promise<void> {\n return this.download.write(data)\n }\n\n close (): void {\n this.completed = true\n this.download.close()\n }\n\n abort (): void {\n this.download.cancel()\n }\n\n isCancelled (): boolean {\n return this.download.isCancelled()\n }\n}\n\n@Injectable()\nexport class TftpService {\n readonly progress$ = new BehaviorSubject<TftpProgress>(idleProgress)\n\n private controller: AbortController | null = null\n private readonly client = new TftpClient()\n\n constructor (\n private readonly platform: PlatformService,\n private readonly notifications: NotificationsService,\n ) { }\n\n get busy (): boolean {\n return this.controller !== null\n }\n\n async upload (settings: TftpPluginSettings): Promise<TftpResult | null> {\n this.ensureIdle()\n const uploads = await this.platform.startUpload({ multiple: false })\n const upload = uploads[0]\n if (!upload) return null\n\n const remoteFilename = settings.remoteFilename.trim() || upload.getName()\n const source = new TabbyUploadSource(upload)\n return this.run('upload', remoteFilename, controller => this.client.upload(\n this.toClientOptions(settings, remoteFilename),\n source,\n controller.signal,\n progress => this.progress$.next(progress),\n ))\n }\n\n async download (settings: TftpPluginSettings): Promise<TftpResult | null> {\n this.ensureIdle()\n const remoteFilename = settings.remoteFilename.trim()\n if (!remoteFilename) throw new Error('下载时必须填写远端文件名')\n\n const localName = remoteFilename.split(/[\\\\/]/).filter(Boolean).pop() || 'download.bin'\n return this.run('download', remoteFilename, controller => this.client.download(\n this.toClientOptions(settings, remoteFilename),\n async size => {\n const download = await this.platform.startDownload(localName, 0o644, size ?? 1)\n return download ? new TabbyDownloadSink(download, size !== undefined) : null\n },\n controller.signal,\n progress => this.progress$.next(progress),\n ))\n }\n\n cancel (): void {\n this.controller?.abort()\n }\n\n private async run (\n direction: 'upload' | 'download',\n filename: string,\n operation: (controller: AbortController) => Promise<TftpResult>,\n ): Promise<TftpResult> {\n this.ensureIdle()\n const controller = new AbortController()\n this.controller = controller\n try {\n const result = await operation(controller)\n this.notifications.notice(`TFTP ${direction === 'upload' ? '上传' : '下载'}完成:${filename}`)\n return result\n } catch (error) {\n const reason = error instanceof Error ? error : new Error(String(error))\n if (reason.name === 'AbortError') {\n this.notifications.info('TFTP 传输已取消')\n } else {\n this.notifications.error('TFTP 传输失败', reason.message)\n }\n throw reason\n } finally {\n if (this.controller === controller) this.controller = null\n }\n }\n\n private ensureIdle (): void {\n if (this.busy) throw new Error('已有一个 TFTP 传输正在进行')\n }\n\n private toClientOptions (settings: TftpPluginSettings, remoteFilename: string) {\n return {\n host: settings.host,\n port: Number(settings.port),\n remoteFilename,\n blockSize: Number(settings.blockSize),\n timeoutMs: Number(settings.timeoutMs),\n retries: Number(settings.retries),\n }\n }\n}\n","import { Injectable } from '@angular/core'\nimport { BehaviorSubject } from 'rxjs'\nimport { NotificationsService, PlatformService } from 'tabby-core'\nimport * as os from 'os'\n\nimport { TftpServerSettings } from '../config'\nimport { TftpServer, TftpServerEvent } from '../tftp/server'\n\nexport interface TftpServerLog {\n timestamp: number\n level: 'info' | 'success' | 'error'\n message: string\n}\n\nexport interface TftpServerState {\n running: boolean\n activeTransfers: number\n startedAt?: number\n logs: TftpServerLog[]\n}\n\nconst sharedServer = new TftpServer()\nconst sharedState$ = new BehaviorSubject<TftpServerState>({\n running: false,\n activeTransfers: 0,\n logs: [],\n})\nlet sharedSettings: TftpServerSettings | null = null\n\n@Injectable()\nexport class TftpServerService {\n readonly state$ = sharedState$\n private readonly server = sharedServer\n\n constructor (\n private readonly notifications: NotificationsService,\n private readonly platform: PlatformService,\n ) { }\n\n get running (): boolean {\n return this.server.running\n }\n\n get activeSettings (): TftpServerSettings | null {\n return sharedSettings ? { ...sharedSettings } : null\n }\n\n getLocalIPv4Addresses (): string[] {\n const addresses = new Set<string>()\n const interfaces = os.networkInterfaces()\n for (const entries of Object.values(interfaces)) {\n for (const entry of entries ?? []) {\n if (entry.family === 'IPv4' && !entry.internal) addresses.add(entry.address)\n }\n }\n return Array.from(addresses).sort()\n }\n\n async chooseRootDirectory (): Promise<string | null> {\n const remote = require('@electron/remote')\n const result = await remote.dialog.showOpenDialog({\n title: '选择 TFTP 根目录',\n properties: ['openDirectory', 'createDirectory'],\n })\n return result.canceled ? null : (result.filePaths[0] ?? null)\n }\n\n openRootDirectory (directory: string): void {\n if (directory) this.platform.openPath(directory)\n }\n\n copyAddress (address: string): void {\n this.platform.setClipboard({ text: address })\n this.notifications.notice(`已复制本机 IP:${address}`)\n }\n\n async start (settings: TftpServerSettings): Promise<void> {\n sharedSettings = { ...settings }\n try {\n await this.server.start({\n rootDirectory: settings.rootDirectory,\n bindAddress: settings.bindAddress,\n port: Number(settings.port),\n blockSize: Number(settings.blockSize),\n timeoutMs: Number(settings.timeoutMs),\n retries: Number(settings.retries),\n autoStopSeconds: settings.autoStop ? Number(settings.autoStopSeconds) : 0,\n }, event => this.handleEvent(event))\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n this.appendLog('error', `启动失败:${reason}`)\n this.notifications.error('TFTP 服务端启动失败', reason)\n throw error\n }\n }\n\n stop (): void {\n this.server.stop()\n }\n\n clearLogs (): void {\n this.state$.next({ ...this.state$.value, logs: [] })\n }\n\n private handleEvent (event: TftpServerEvent): void {\n const level: TftpServerLog['level'] = event.type === 'completed' ? 'success' : event.type === 'error' ? 'error' : 'info'\n const current = this.state$.value\n const running = event.type === 'started' ? true : event.type === 'stopped' ? false : this.server.running\n const next: TftpServerState = {\n running,\n activeTransfers: event.activeTransfers,\n startedAt: event.type === 'started' ? event.timestamp : current.startedAt,\n logs: [...current.logs, { timestamp: event.timestamp, level, message: event.message }].slice(-300),\n }\n this.state$.next(next)\n\n if (event.type === 'completed' && sharedSettings?.notifyDownloads) {\n this.notifications.notice(`TFTP 下载完成:${event.filename ?? ''}`)\n } else if (event.type === 'stopped') {\n this.notifications.info('TFTP 服务端已停止')\n }\n }\n\n private appendLog (level: TftpServerLog['level'], message: string): void {\n const current = this.state$.value\n this.state$.next({\n ...current,\n logs: [...current.logs, { timestamp: Date.now(), level, message }].slice(-300),\n })\n }\n}\n","import * as dgram from 'dgram'\nimport { RemoteInfo, Socket } from 'dgram'\n\nimport {\n Opcode,\n TftpPacket,\n createAck,\n createData,\n createError,\n createRequest,\n parseNegotiatedBlockSize,\n parsePacket,\n parseTransferSize,\n} from './protocol'\n\nexport interface TftpClientOptions {\n host: string\n port?: number\n remoteFilename: string\n blockSize?: number\n timeoutMs?: number\n retries?: number\n}\n\ninterface NormalizedOptions {\n host: string\n port: number\n remoteFilename: string\n blockSize: number\n timeoutMs: number\n retries: number\n}\n\nexport type TransferDirection = 'upload' | 'download'\nexport type TransferState = 'idle' | 'negotiating' | 'transferring' | 'retrying' | 'completed' | 'cancelled' | 'error'\n\nexport interface TftpProgress {\n direction: TransferDirection\n state: TransferState\n transferred: number\n total?: number\n speed: number\n attempt?: number\n message?: string\n}\n\nexport interface TftpResult {\n bytes: number\n blockSize: number\n durationMs: number\n}\n\nexport interface UploadSource {\n readonly size: number\n read (maximumBytes: number): Promise<Buffer>\n close (): void\n abort? (): void\n isCancelled? (): boolean\n}\n\nexport interface DownloadSink {\n write (data: Buffer): Promise<void>\n close (): void\n abort? (): void\n isCancelled? (): boolean\n}\n\nexport type DownloadSinkFactory = (size?: number) => Promise<DownloadSink | null>\nexport type ProgressHandler = (progress: TftpProgress) => void\n\nfunction normalizeOptions (options: TftpClientOptions): NormalizedOptions {\n const host = options.host.trim()\n const remoteFilename = options.remoteFilename.trim()\n const port = options.port ?? 69\n const blockSize = options.blockSize ?? 1468\n const timeoutMs = options.timeoutMs ?? 3000\n const retries = options.retries ?? 5\n\n if (!host) throw new Error('TFTP 主机不能为空')\n if (!remoteFilename) throw new Error('远端文件名不能为空')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('TFTP 端口必须在 1 到 65535 之间')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP 块大小必须在 8 到 65464 之间')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('超时时间必须在 250 到 60000 毫秒之间')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('重试次数必须在 0 到 20 之间')\n\n return { host, remoteFilename, port, blockSize, timeoutMs, retries }\n}\n\nfunction abortError (): Error {\n const error = new Error('传输已取消')\n error.name = 'AbortError'\n return error\n}\n\nabstract class TransferSession {\n protected readonly socket: Socket\n protected readonly startedAt = Date.now()\n protected transferred = 0\n protected negotiatedBlockSize = 512\n\n private endpoint: { address: string; port: number } | null = null\n private lastPacket: Buffer | null = null\n private lastTarget: { address: string; port: number } | null = null\n private retryCount = 0\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n private cancelPoll: ReturnType<typeof setInterval> | null = null\n private settled = false\n private queue = Promise.resolve()\n private resolvePromise!: (result: TftpResult) => void\n private rejectPromise!: (error: Error) => void\n\n protected constructor (\n protected readonly direction: TransferDirection,\n protected readonly options: NormalizedOptions,\n protected readonly signal?: AbortSignal,\n protected readonly onProgress?: ProgressHandler,\n ) {\n this.socket = dgram.createSocket('udp4')\n }\n\n protected runSession (initialPacket: Buffer): Promise<TftpResult> {\n const result = new Promise<TftpResult>((resolve, reject) => {\n this.resolvePromise = resolve\n this.rejectPromise = reject\n })\n\n this.socket.on('error', error => this.fail(error))\n this.socket.on('message', (message, remote) => {\n this.queue = this.queue\n .then(() => this.handleMessage(message, remote))\n .catch(error => this.fail(error instanceof Error ? error : new Error(String(error))))\n })\n this.signal?.addEventListener('abort', () => this.fail(abortError()), { once: true })\n this.cancelPoll = setInterval(() => {\n if (this.resourceCancelled()) this.fail(abortError())\n }, 250)\n\n this.emit('negotiating', '正在连接 TFTP 服务端')\n this.sendTracked(initialPacket, { address: this.options.host, port: this.options.port })\n return result\n }\n\n protected abstract onPacket (packet: TftpPacket): Promise<void>\n protected abstract releaseResource (successful: boolean): void\n protected abstract resourceCancelled (): boolean\n\n protected clearRetryTimer (): void {\n if (this.retryTimer) clearTimeout(this.retryTimer)\n this.retryTimer = null\n this.retryCount = 0\n }\n\n protected sendTracked (packet: Buffer, explicitTarget?: { address: string; port: number }): void {\n if (this.settled) return\n const target = explicitTarget ?? this.endpoint ?? { address: this.options.host, port: this.options.port }\n this.lastPacket = Buffer.from(packet)\n this.lastTarget = target\n if (this.retryTimer) clearTimeout(this.retryTimer)\n this.socket.send(packet, target.port, target.address, error => {\n if (error) this.fail(error)\n })\n this.retryTimer = setTimeout(() => this.retryLastPacket(), this.options.timeoutMs)\n }\n\n protected sendUntracked (\n packet: Buffer,\n explicitTarget?: { address: string; port: number },\n sent?: () => void,\n ): void {\n if (this.settled) return\n const target = explicitTarget ?? this.endpoint ?? { address: this.options.host, port: this.options.port }\n this.socket.send(packet, target.port, target.address, error => {\n if (error) this.fail(error)\n else sent?.()\n })\n }\n\n protected emit (state: TransferState, message?: string, total?: number): void {\n const duration = Math.max(1, Date.now() - this.startedAt)\n this.onProgress?.({\n direction: this.direction,\n state,\n transferred: this.transferred,\n total,\n speed: this.transferred * 1000 / duration,\n attempt: state === 'retrying' ? this.retryCount : undefined,\n message,\n })\n }\n\n protected succeed (): void {\n if (this.settled) return\n this.settled = true\n this.cleanupTimers()\n this.releaseResource(true)\n this.socket.close()\n const result = {\n bytes: this.transferred,\n blockSize: this.negotiatedBlockSize,\n durationMs: Date.now() - this.startedAt,\n }\n this.emit('completed', '传输完成', this.transferred)\n this.resolvePromise(result)\n }\n\n protected fail (error: Error): void {\n if (this.settled) return\n this.settled = true\n this.cleanupTimers()\n this.releaseResource(false)\n try {\n this.socket.close()\n } catch { }\n this.emit(error.name === 'AbortError' ? 'cancelled' : 'error', error.message)\n this.rejectPromise(error)\n }\n\n private async handleMessage (message: Buffer, remote: RemoteInfo): Promise<void> {\n if (this.settled) return\n if (this.signal?.aborted || this.resourceCancelled()) throw abortError()\n\n if (this.endpoint && (remote.address !== this.endpoint.address || remote.port !== this.endpoint.port)) {\n this.sendUntracked(createError(5, 'Unknown transfer ID'), { address: remote.address, port: remote.port })\n return\n }\n\n const packet = parsePacket(message)\n if (!this.endpoint) this.endpoint = { address: remote.address, port: remote.port }\n if (packet.opcode === Opcode.ERROR) {\n throw new Error(`TFTP 错误 ${packet.code}: ${packet.message}`)\n }\n await this.onPacket(packet)\n }\n\n private retryLastPacket (): void {\n if (this.settled) return\n if (this.signal?.aborted || this.resourceCancelled()) {\n this.fail(abortError())\n return\n }\n if (!this.lastPacket || !this.lastTarget || this.retryCount >= this.options.retries) {\n this.fail(new Error(`TFTP 响应超时,已重试 ${this.options.retries} 次`))\n return\n }\n\n this.retryCount++\n this.emit('retrying', `响应超时,正在进行第 ${this.retryCount} 次重试`)\n this.socket.send(this.lastPacket, this.lastTarget.port, this.lastTarget.address, error => {\n if (error) this.fail(error)\n })\n this.retryTimer = setTimeout(() => this.retryLastPacket(), this.options.timeoutMs)\n }\n\n private cleanupTimers (): void {\n if (this.retryTimer) clearTimeout(this.retryTimer)\n if (this.cancelPoll) clearInterval(this.cancelPoll)\n this.retryTimer = null\n this.cancelPoll = null\n }\n}\n\nclass DownloadSession extends TransferSession {\n private expectedBlock = 1\n private sink: DownloadSink | null = null\n private totalSize: number | undefined\n\n constructor (\n options: NormalizedOptions,\n private readonly sinkFactory: DownloadSinkFactory,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ) {\n super('download', options, signal, onProgress)\n }\n\n run (): Promise<TftpResult> {\n const timeoutSeconds = Math.max(1, Math.min(255, Math.round(this.options.timeoutMs / 1000)))\n return this.runSession(createRequest(Opcode.RRQ, this.options.remoteFilename, {\n blksize: this.options.blockSize,\n timeout: timeoutSeconds,\n tsize: 0,\n }))\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode === Opcode.OACK) {\n if (this.expectedBlock !== 1 || this.transferred !== 0) return\n this.clearRetryTimer()\n this.negotiatedBlockSize = parseNegotiatedBlockSize(packet.options)\n this.totalSize = parseTransferSize(packet.options)\n await this.ensureSink(this.totalSize)\n this.emit('transferring', '已协商传输参数', this.totalSize)\n this.sendTracked(createAck(0))\n return\n }\n if (packet.opcode !== Opcode.DATA) return\n\n if (packet.block === this.expectedBlock) {\n this.clearRetryTimer()\n if (!this.sink) {\n this.negotiatedBlockSize = 512\n await this.ensureSink(undefined)\n }\n await this.sink!.write(packet.data)\n this.transferred += packet.data.length\n this.emit('transferring', `正在接收块 ${packet.block}`, this.totalSize)\n\n const finalBlock = packet.data.length < this.negotiatedBlockSize\n const acknowledgement = createAck(packet.block)\n if (finalBlock) {\n this.sendUntracked(acknowledgement, undefined, () => this.succeed())\n } else {\n this.expectedBlock = (this.expectedBlock + 1) & 0xffff\n this.sendTracked(acknowledgement)\n }\n return\n }\n\n const previousBlock = (this.expectedBlock - 1) & 0xffff\n if (packet.block === previousBlock) {\n this.sendTracked(createAck(packet.block))\n }\n }\n\n protected releaseResource (successful: boolean): void {\n if (!this.sink) return\n if (successful) this.sink.close()\n else if (this.sink.abort) this.sink.abort()\n else this.sink.close()\n }\n\n protected resourceCancelled (): boolean {\n return this.sink?.isCancelled?.() ?? false\n }\n\n private async ensureSink (size?: number): Promise<void> {\n if (this.sink) return\n this.sink = await this.sinkFactory(size)\n if (!this.sink) throw abortError()\n }\n}\n\nclass UploadSession extends TransferSession {\n private nextBlock = 1\n private currentBlock: number | null = null\n private currentData = Buffer.alloc(0)\n private currentFinal = false\n\n constructor (\n options: NormalizedOptions,\n private readonly source: UploadSource,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ) {\n super('upload', options, signal, onProgress)\n }\n\n run (): Promise<TftpResult> {\n const timeoutSeconds = Math.max(1, Math.min(255, Math.round(this.options.timeoutMs / 1000)))\n return this.runSession(createRequest(Opcode.WRQ, this.options.remoteFilename, {\n blksize: this.options.blockSize,\n timeout: timeoutSeconds,\n tsize: this.source.size,\n }))\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode === Opcode.OACK && this.currentBlock === null) {\n this.clearRetryTimer()\n this.negotiatedBlockSize = parseNegotiatedBlockSize(packet.options)\n this.emit('transferring', '已协商传输参数', this.source.size)\n await this.sendNextBlock()\n return\n }\n\n if (packet.opcode !== Opcode.ACK) return\n if (packet.block === 0 && this.currentBlock === null) {\n this.clearRetryTimer()\n this.negotiatedBlockSize = 512\n this.emit('transferring', '服务端使用标准 512 字节块', this.source.size)\n await this.sendNextBlock()\n return\n }\n\n if (this.currentBlock !== null && packet.block === this.currentBlock) {\n this.clearRetryTimer()\n this.transferred += this.currentData.length\n this.emit('transferring', `服务端已确认块 ${packet.block}`, this.source.size)\n if (this.currentFinal) {\n this.succeed()\n } else {\n this.nextBlock = (this.nextBlock + 1) & 0xffff\n await this.sendNextBlock()\n }\n return\n }\n\n if (this.currentBlock !== null && packet.block === ((this.currentBlock - 1) & 0xffff)) {\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n }\n\n protected releaseResource (successful: boolean): void {\n if (successful) this.source.close()\n else if (this.source.abort) this.source.abort()\n else this.source.close()\n }\n\n protected resourceCancelled (): boolean {\n return this.source.isCancelled?.() ?? false\n }\n\n private async sendNextBlock (): Promise<void> {\n if (this.signal?.aborted || this.resourceCancelled()) throw abortError()\n this.currentData = Buffer.from(await this.source.read(this.negotiatedBlockSize))\n if (this.currentData.length > this.negotiatedBlockSize) {\n throw new Error('上传数据源返回的数据超过协商块大小')\n }\n this.currentBlock = this.nextBlock\n this.currentFinal = this.currentData.length < this.negotiatedBlockSize\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n}\n\nexport class TftpClient {\n download (\n options: TftpClientOptions,\n sinkFactory: DownloadSinkFactory,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ): Promise<TftpResult> {\n return new DownloadSession(normalizeOptions(options), sinkFactory, signal, onProgress).run()\n }\n\n upload (\n options: TftpClientOptions,\n source: UploadSource,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ): Promise<TftpResult> {\n return new UploadSession(normalizeOptions(options), source, signal, onProgress).run()\n }\n}\n","export enum Opcode {\n RRQ = 1,\n WRQ = 2,\n DATA = 3,\n ACK = 4,\n ERROR = 5,\n OACK = 6,\n}\n\nexport type TftpPacket =\n | { opcode: Opcode.DATA; block: number; data: Buffer }\n | { opcode: Opcode.ACK; block: number }\n | { opcode: Opcode.ERROR; code: number; message: string }\n | { opcode: Opcode.OACK; options: Record<string, string> }\n\nfunction zeroTerminated (value: string): Buffer {\n if (value.includes('\\0')) {\n throw new Error('TFTP 字段不能包含空字符')\n }\n return Buffer.concat([Buffer.from(value, 'utf8'), Buffer.from([0])])\n}\n\nexport function createRequest (\n opcode: Opcode.RRQ | Opcode.WRQ,\n filename: string,\n options: Record<string, string | number> = {},\n): Buffer {\n if (!filename.trim()) {\n throw new Error('远端文件名不能为空')\n }\n\n const header = Buffer.allocUnsafe(2)\n header.writeUInt16BE(opcode, 0)\n const parts = [header, zeroTerminated(filename), zeroTerminated('octet')]\n\n for (const [key, rawValue] of Object.entries(options)) {\n parts.push(zeroTerminated(key.toLowerCase()))\n parts.push(zeroTerminated(String(rawValue)))\n }\n return Buffer.concat(parts)\n}\n\nexport function createData (block: number, data: Buffer): Buffer {\n const packet = Buffer.allocUnsafe(4 + data.length)\n packet.writeUInt16BE(Opcode.DATA, 0)\n packet.writeUInt16BE(block & 0xffff, 2)\n data.copy(packet, 4)\n return packet\n}\n\nexport function createAck (block: number): Buffer {\n const packet = Buffer.allocUnsafe(4)\n packet.writeUInt16BE(Opcode.ACK, 0)\n packet.writeUInt16BE(block & 0xffff, 2)\n return packet\n}\n\nexport function createError (code: number, message: string): Buffer {\n const packet = Buffer.allocUnsafe(4)\n packet.writeUInt16BE(Opcode.ERROR, 0)\n packet.writeUInt16BE(code & 0xffff, 2)\n return Buffer.concat([packet, zeroTerminated(message)])\n}\n\nfunction parsePairs (packet: Buffer, offset: number): Record<string, string> {\n const values: string[] = []\n let start = offset\n for (let i = offset; i < packet.length; i++) {\n if (packet[i] === 0) {\n values.push(packet.toString('utf8', start, i))\n start = i + 1\n }\n }\n if (start !== packet.length || values.length % 2 !== 0) {\n throw new Error('无效的 TFTP 选项响应')\n }\n\n const options: Record<string, string> = {}\n for (let i = 0; i < values.length; i += 2) {\n options[values[i].toLowerCase()] = values[i + 1]\n }\n return options\n}\n\nexport function parsePacket (packet: Buffer): TftpPacket {\n if (packet.length < 2) {\n throw new Error('TFTP 数据包过短')\n }\n const opcode = packet.readUInt16BE(0)\n\n switch (opcode) {\n case Opcode.DATA:\n if (packet.length < 4) throw new Error('无效的 DATA 数据包')\n return { opcode, block: packet.readUInt16BE(2), data: packet.subarray(4) }\n case Opcode.ACK:\n if (packet.length !== 4) throw new Error('无效的 ACK 数据包')\n return { opcode, block: packet.readUInt16BE(2) }\n case Opcode.ERROR: {\n if (packet.length < 5 || packet[packet.length - 1] !== 0) {\n throw new Error('无效的 ERROR 数据包')\n }\n return {\n opcode,\n code: packet.readUInt16BE(2),\n message: packet.toString('utf8', 4, packet.length - 1),\n }\n }\n case Opcode.OACK:\n return { opcode, options: parsePairs(packet, 2) }\n default:\n throw new Error(`不支持的 TFTP 操作码: ${opcode}`)\n }\n}\n\nexport function parseNegotiatedBlockSize (options: Record<string, string>, fallback = 512): number {\n if (!options.blksize) return fallback\n const value = Number(options.blksize)\n if (!Number.isInteger(value) || value < 8 || value > 65464) {\n throw new Error(`服务端返回了无效的块大小: ${options.blksize}`)\n }\n return value\n}\n\nexport function parseTransferSize (options: Record<string, string>): number | undefined {\n if (options.tsize === undefined) return undefined\n const value = Number(options.tsize)\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new Error(`服务端返回了无效的文件大小: ${options.tsize}`)\n }\n return value\n}\n","import * as dgram from 'dgram'\nimport { RemoteInfo, Socket } from 'dgram'\nimport * as fs from 'fs'\nimport type { FileHandle } from 'fs/promises'\nimport * as path from 'path'\n\nimport {\n Opcode,\n TftpPacket,\n createData,\n createError,\n parsePacket,\n} from './protocol'\n\nexport interface TftpServerOptions {\n rootDirectory: string\n bindAddress?: string\n port?: number\n blockSize?: number\n timeoutMs?: number\n retries?: number\n autoStopSeconds?: number\n}\n\ninterface NormalizedServerOptions {\n rootDirectory: string\n bindAddress: string\n port: number\n blockSize: number\n timeoutMs: number\n retries: number\n autoStopSeconds: number\n}\n\nexport type TftpServerEventType = 'started' | 'stopped' | 'request' | 'progress' | 'completed' | 'error'\n\nexport interface TftpServerEvent {\n type: TftpServerEventType\n timestamp: number\n message: string\n peer?: string\n filename?: string\n bytes?: number\n activeTransfers: number\n}\n\ninterface TftpRequest {\n opcode: Opcode.RRQ | Opcode.WRQ\n filename: string\n mode: string\n options: Record<string, string>\n}\n\nfunction readZeroTerminatedFields (packet: Buffer, offset: number): string[] {\n const fields: string[] = []\n let start = offset\n for (let i = offset; i < packet.length; i++) {\n if (packet[i] === 0) {\n fields.push(packet.toString('utf8', start, i))\n start = i + 1\n }\n }\n if (start !== packet.length) throw new Error('请求字段缺少结束符')\n return fields\n}\n\nexport function parseRequest (packet: Buffer): TftpRequest {\n if (packet.length < 4) throw new Error('TFTP 请求过短')\n const opcode = packet.readUInt16BE(0)\n if (opcode !== Opcode.RRQ && opcode !== Opcode.WRQ) throw new Error('不是有效的读写请求')\n\n const fields = readZeroTerminatedFields(packet, 2)\n if (fields.length < 2 || (fields.length - 2) % 2 !== 0) throw new Error('TFTP 请求字段无效')\n const filename = fields[0]\n const mode = fields[1].toLowerCase()\n if (!filename) throw new Error('请求文件名为空')\n if (mode !== 'octet') throw new Error(`不支持的传输模式: ${mode}`)\n\n const options: Record<string, string> = {}\n for (let i = 2; i < fields.length; i += 2) {\n options[fields[i].toLowerCase()] = fields[i + 1]\n }\n return { opcode, filename, mode, options }\n}\n\nfunction createOack (options: Record<string, string | number>): Buffer {\n const header = Buffer.allocUnsafe(2)\n header.writeUInt16BE(Opcode.OACK, 0)\n const chunks: Buffer[] = [header]\n for (const [key, value] of Object.entries(options)) {\n chunks.push(Buffer.from(`${key}\\0${value}\\0`, 'utf8'))\n }\n return Buffer.concat(chunks)\n}\n\nfunction normalizeOptions (options: TftpServerOptions): NormalizedServerOptions {\n const rootDirectory = path.resolve(options.rootDirectory.trim())\n const bindAddress = (options.bindAddress ?? '0.0.0.0').trim()\n const port = options.port ?? 69\n const blockSize = options.blockSize ?? 8192\n const timeoutMs = options.timeoutMs ?? 3000\n const retries = options.retries ?? 5\n const autoStopSeconds = options.autoStopSeconds ?? 0\n\n if (!options.rootDirectory.trim()) throw new Error('请选择 TFTP 根目录')\n if (!bindAddress) throw new Error('监听地址不能为空')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('监听端口必须在 1 到 65535 之间')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP 缓冲区必须在 8 到 65464 字节之间')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('超时时间必须在 250 到 60000 毫秒之间')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('重试次数必须在 0 到 20 之间')\n if (!Number.isInteger(autoStopSeconds) || autoStopSeconds < 0 || autoStopSeconds > 86400) throw new Error('自动停止时间必须在 0 到 86400 秒之间')\n\n return { rootDirectory, bindAddress, port, blockSize, timeoutMs, retries, autoStopSeconds }\n}\n\nfunction resolveRequestedFile (rootDirectory: string, requestedFilename: string): string {\n const normalized = requestedFilename.replace(/\\\\/g, '/').replace(/^\\/+/, '')\n const segments = normalized.split('/').filter(segment => segment && segment !== '.')\n if (!segments.length || segments.some(segment => segment === '..')) {\n throw new Error('拒绝不安全的文件路径')\n }\n\n const target = path.resolve(rootDirectory, ...segments)\n const relative = path.relative(rootDirectory, target)\n if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {\n throw new Error('请求文件超出 TFTP 根目录')\n }\n return target\n}\n\nabstract class ServerTransfer {\n protected readonly socket: Socket\n protected blockSize = 512\n protected timeoutMs: number\n protected transferred = 0\n\n private lastPacket: Buffer | null = null\n private timer: ReturnType<typeof setTimeout> | null = null\n private retryCount = 0\n private settled = false\n private failing = false\n private queue = Promise.resolve()\n\n protected constructor (\n protected readonly request: TftpRequest,\n protected readonly remote: RemoteInfo,\n protected readonly options: NormalizedServerOptions,\n private readonly onFinish: (successful: boolean, message: string, bytes: number) => void,\n ) {\n this.timeoutMs = options.timeoutMs\n this.socket = dgram.createSocket('udp4')\n }\n\n start (): void {\n this.socket.on('error', error => this.fail(error.message))\n this.socket.on('message', (message, remote) => {\n this.queue = this.queue\n .then(() => this.handleMessage(message, remote))\n .catch(error => this.fail(error instanceof Error ? error.message : String(error)))\n })\n this.socket.bind(0, this.options.bindAddress, () => {\n this.begin().catch(error => this.fail(error instanceof Error ? error.message : String(error)))\n })\n }\n\n resendLast (): void {\n if (this.lastPacket) this.sendTracked(this.lastPacket)\n }\n\n cancel (): void {\n this.finish(false, '服务端已停止')\n }\n\n protected abstract begin (): Promise<void>\n protected abstract onPacket (packet: TftpPacket): Promise<void>\n protected abstract release (successful: boolean): Promise<void>\n\n protected sendTracked (packet: Buffer): void {\n if (this.settled) return\n this.lastPacket = Buffer.from(packet)\n if (this.timer) clearTimeout(this.timer)\n this.socket.send(packet, this.remote.port, this.remote.address, error => {\n if (error) this.fail(error.message)\n })\n this.timer = setTimeout(() => this.retry(), this.timeoutMs)\n }\n\n protected clearTimer (): void {\n if (this.timer) clearTimeout(this.timer)\n this.timer = null\n this.retryCount = 0\n }\n\n protected complete (message: string): void {\n this.finish(true, message)\n }\n\n protected fail (message: string): void {\n if (this.settled || this.failing) return\n this.failing = true\n if (this.timer) clearTimeout(this.timer)\n const errorPacket = createError(1, message)\n this.socket.send(errorPacket, this.remote.port, this.remote.address, () => this.finish(false, message))\n }\n\n private async handleMessage (message: Buffer, remote: RemoteInfo): Promise<void> {\n if (this.settled) return\n if (remote.address !== this.remote.address || remote.port !== this.remote.port) {\n const errorPacket = createError(5, 'Unknown transfer ID')\n this.socket.send(errorPacket, remote.port, remote.address)\n return\n }\n const packet = parsePacket(message)\n if (packet.opcode === Opcode.ERROR) throw new Error(`客户端错误 ${packet.code}: ${packet.message}`)\n await this.onPacket(packet)\n }\n\n private retry (): void {\n if (this.settled) return\n if (!this.lastPacket || this.retryCount >= this.options.retries) {\n this.fail(`等待客户端响应超时,已重试 ${this.options.retries} 次`)\n return\n }\n this.retryCount++\n this.socket.send(this.lastPacket, this.remote.port, this.remote.address, error => {\n if (error) this.fail(error.message)\n })\n this.timer = setTimeout(() => this.retry(), this.timeoutMs)\n }\n\n private finish (successful: boolean, message: string): void {\n if (this.settled) return\n this.settled = true\n if (this.timer) clearTimeout(this.timer)\n this.release(successful)\n .catch(() => undefined)\n .finally(() => {\n try { this.socket.close() } catch { }\n this.onFinish(successful, message, this.transferred)\n })\n }\n}\n\nclass ReadTransfer extends ServerTransfer {\n private file: FileHandle | null = null\n private fileSize = 0\n private position = 0\n private currentBlock = 0\n private currentData = Buffer.alloc(0)\n private finalBlock = false\n\n constructor (\n request: TftpRequest,\n remote: RemoteInfo,\n options: NormalizedServerOptions,\n onFinish: (successful: boolean, message: string, bytes: number) => void,\n ) {\n super(request, remote, options, onFinish)\n }\n\n protected async begin (): Promise<void> {\n const filePath = resolveRequestedFile(this.options.rootDirectory, this.request.filename)\n const stat = await fs.promises.stat(filePath)\n if (!stat.isFile()) throw new Error('请求路径不是普通文件')\n this.fileSize = stat.size\n this.file = await fs.promises.open(filePath, 'r')\n\n const negotiated: Record<string, string | number> = {}\n if (this.request.options.blksize !== undefined) {\n const requested = Number(this.request.options.blksize)\n if (!Number.isInteger(requested) || requested < 8 || requested > 65464) throw new Error('客户端请求了无效的块大小')\n this.blockSize = Math.min(requested, this.options.blockSize)\n negotiated.blksize = this.blockSize\n }\n if (this.request.options.timeout !== undefined) {\n const timeoutSeconds = Number(this.request.options.timeout)\n if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 255) throw new Error('客户端请求了无效的超时时间')\n this.timeoutMs = timeoutSeconds * 1000\n negotiated.timeout = timeoutSeconds\n }\n if (this.request.options.tsize !== undefined) negotiated.tsize = this.fileSize\n\n if (Object.keys(negotiated).length) {\n this.sendTracked(createOack(negotiated))\n } else {\n await this.sendNextBlock()\n }\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode !== Opcode.ACK) return\n if (this.currentBlock === 0 && packet.block === 0) {\n this.clearTimer()\n await this.sendNextBlock()\n return\n }\n if (packet.block === this.currentBlock) {\n this.clearTimer()\n this.transferred += this.currentData.length\n if (this.finalBlock) this.complete(`已发送 ${this.transferred} 字节`)\n else await this.sendNextBlock()\n return\n }\n if (packet.block === ((this.currentBlock - 1) & 0xffff)) this.resendLast()\n }\n\n protected async release (): Promise<void> {\n if (this.file) await this.file.close()\n this.file = null\n }\n\n private async sendNextBlock (): Promise<void> {\n const file = this.file\n if (!file) throw new Error('TFTP 文件尚未打开')\n const buffer = Buffer.allocUnsafe(this.blockSize)\n const result = await file.read(buffer, 0, this.blockSize, this.position)\n this.currentData = Buffer.from(buffer.subarray(0, result.bytesRead))\n this.position += result.bytesRead\n this.currentBlock = (this.currentBlock + 1) & 0xffff\n this.finalBlock = result.bytesRead < this.blockSize\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n}\n\nexport class TftpServer {\n private listener: Socket | null = null\n private options: NormalizedServerOptions | null = null\n private readonly transfers = new Map<string, ServerTransfer>()\n private autoStopTimer: ReturnType<typeof setTimeout> | null = null\n private eventHandler: ((event: TftpServerEvent) => void) | null = null\n\n get running (): boolean {\n return this.listener !== null\n }\n\n get activeTransfers (): number {\n return this.transfers.size\n }\n\n async start (serverOptions: TftpServerOptions, onEvent?: (event: TftpServerEvent) => void): Promise<void> {\n if (this.running) throw new Error('TFTP 服务端已经在运行')\n const options = normalizeOptions(serverOptions)\n const stat = await fs.promises.stat(options.rootDirectory)\n if (!stat.isDirectory()) throw new Error('TFTP 根目录无效')\n\n this.options = options\n this.eventHandler = onEvent ?? null\n const listener = dgram.createSocket('udp4')\n this.listener = listener\n listener.on('message', (message, remote) => this.handleRequest(message, remote))\n\n let rejectBinding: ((error: Error) => void) | null = null\n listener.on('error', error => {\n if (rejectBinding) {\n const reject = rejectBinding\n rejectBinding = null\n try { listener.close() } catch { }\n this.listener = null\n reject(error)\n } else {\n this.emit('error', error.message)\n }\n })\n await new Promise<void>((resolve, reject) => {\n rejectBinding = reject\n listener.bind(options.port, options.bindAddress, () => {\n rejectBinding = null\n resolve()\n })\n })\n\n this.emit('started', `TFTP 服务端已启动:${options.bindAddress}:${options.port}`)\n if (options.autoStopSeconds > 0) {\n this.autoStopTimer = setTimeout(() => this.stop('达到自动停止时间'), options.autoStopSeconds * 1000)\n }\n }\n\n stop (reason = '用户停止'): void {\n if (!this.listener) return\n if (this.autoStopTimer) clearTimeout(this.autoStopTimer)\n this.autoStopTimer = null\n for (const transfer of this.transfers.values()) transfer.cancel()\n this.transfers.clear()\n try { this.listener.close() } catch { }\n this.listener = null\n this.emit('stopped', `TFTP 服务端已停止:${reason}`)\n }\n\n private handleRequest (message: Buffer, remote: RemoteInfo): void {\n if (!this.listener || !this.options) return\n let request: TftpRequest\n try {\n request = parseRequest(message)\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n this.listener.send(createError(4, reason), remote.port, remote.address)\n this.emit('error', `拒绝来自 ${remote.address}:${remote.port} 的无效请求:${reason}`)\n return\n }\n\n const key = `${remote.address}:${remote.port}:${request.opcode}:${request.filename}`\n const existing = this.transfers.get(key)\n if (existing) {\n existing.resendLast()\n return\n }\n\n if (request.opcode === Opcode.WRQ) {\n this.listener.send(createError(2, 'Server is read-only'), remote.port, remote.address)\n this.emit('error', `拒绝 ${remote.address}:${remote.port} 上传 ${request.filename}:服务端为只读模式`, remote, request.filename)\n return\n }\n\n const peer = `${remote.address}:${remote.port}`\n const transfer = new ReadTransfer(request, remote, this.options, (successful, detail, bytes) => {\n this.transfers.delete(key)\n this.emit(successful ? 'completed' : 'error', `${peer} ${successful ? '下载完成' : '下载失败'} ${request.filename}:${detail}`, remote, request.filename, bytes)\n })\n this.transfers.set(key, transfer)\n this.emit('request', `${peer} 请求下载 ${request.filename}`, remote, request.filename)\n transfer.start()\n }\n\n private emit (type: TftpServerEventType, message: string, remote?: RemoteInfo, filename?: string, bytes?: number): void {\n this.eventHandler?.({\n type,\n timestamp: Date.now(),\n message,\n peer: remote ? `${remote.address}:${remote.port}` : undefined,\n filename,\n bytes,\n activeTransfers: this.transfers.size,\n })\n }\n}\n","module.exports = require(\"@angular/common\");","module.exports = require(\"@angular/core\");","module.exports = require(\"@angular/forms\");","module.exports = require(\"@electron/remote\");","module.exports = require(\"@ng-bootstrap/ng-bootstrap\");","module.exports = require(\"dgram\");","module.exports = require(\"fs\");","module.exports = require(\"os\");","module.exports = require(\"path\");","module.exports = require(\"rxjs\");","module.exports = require(\"tabby-core\");","// The module cache\nconst __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tconst cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tconst module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nlet __webpack_exports__ = __webpack_require__(156);\n"],"names":["core_1","__webpack_require__","ng_bootstrap_1","tabby_core_1","tftpModal_component_1","TftpButtonProvider","ToolbarButtonProvider","constructor","modal","super","this","provide","icon","title","weight","click","open","TftpModalComponent","size","exports","__decorate","Injectable","NgbModal","config_1","tftp_service_1","activeModal","config","tftp","zone","changeDetector","mode","settings","defaultTftpSettings","server","defaultTftpServerSettings","status","validationError","busy","percent","state","undefined","total","Math","min","round","transferred","progressType","_a","_b","_c","ngOnInit","stored","store","subscription","progress$","subscribe","run","markForCheck","ngOnDestroy","unsubscribe","upload","validate","execute","download","cancel","formatBytes","value","Number","isFinite","units","index","length","floor","log","amount","pow","toFixed","operation","Object","assign","host","port","remoteFilename","blockSize","timeoutMs","retries","save","error","Error","message","String","requireRemoteFilename","trim","isInteger","Component","selector","template","styles","NgbActiveModal","ConfigService","TftpService","NgZone","ChangeDetectorRef","tftpServer_service_1","serverSettingsStorageKey","TftpServerComponent","running","activeTransfers","logs","localAddresses","starting","cached","JSON","parse","localStorage","getItem","_d","activeSettings","getLocalIPv4Addresses","state$","chooseDirectory","directory","chooseRootDirectory","rootDirectory","saveSettings","start","formatTime","timestamp","Date","toLocaleTimeString","hour12","autoStop","autoStopSeconds","setItem","stringify","TftpServerService","bindAddress","notifyDownloads","TftpConfigProvider","ConfigProvider","defaults","platformDefaults","common_1","forms_1","__importStar","buttonProvider_1","tftpServer_component_1","TftpModule","NgModule","imports","CommonModule","FormsModule","NgbModule","default","declarations","providers","useClass","multi","__exportStar","rxjs_1","client_1","idleProgress","direction","speed","TabbyUploadSource","pending","Buffer","alloc","ended","getSize","read","maximumBytes","chunk","concat","from","result","subarray","close","abort","isCancelled","TabbyDownloadSink","sizeKnown","completed","getCompletedBytes","max","write","data","platform","notifications","BehaviorSubject","controller","client","TftpClient","ensureIdle","startUpload","multiple","getName","source","toClientOptions","signal","progress","next","localName","split","filter","Boolean","pop","async","startDownload","filename","AbortController","notice","reason","name","info","PlatformService","NotificationsService","os","sharedServer","TftpServer","sharedState$","sharedSettings","addresses","Set","interfaces","networkInterfaces","entries","values","entry","family","internal","add","address","Array","sort","remote","dialog","showOpenDialog","properties","canceled","filePaths","openRootDirectory","openPath","copyAddress","setClipboard","text","event","handleEvent","appendLog","stop","clearLogs","level","type","current","startedAt","slice","now","dgram","protocol_1","normalizeOptions","options","abortError","TransferSession","onProgress","negotiatedBlockSize","endpoint","lastPacket","lastTarget","retryCount","retryTimer","cancelPoll","settled","queue","Promise","resolve","socket","createSocket","runSession","initialPacket","reject","resolvePromise","rejectPromise","on","fail","then","handleMessage","catch","addEventListener","once","setInterval","resourceCancelled","emit","sendTracked","clearRetryTimer","clearTimeout","packet","explicitTarget","target","send","setTimeout","retryLastPacket","sendUntracked","sent","duration","call","attempt","succeed","cleanupTimers","releaseResource","bytes","durationMs","aborted","createError","parsePacket","opcode","Opcode","ERROR","code","onPacket","clearInterval","DownloadSession","sinkFactory","expectedBlock","sink","timeoutSeconds","createRequest","RRQ","blksize","timeout","tsize","OACK","parseNegotiatedBlockSize","totalSize","parseTransferSize","ensureSink","createAck","DATA","block","finalBlock","acknowledgement","previousBlock","successful","UploadSession","nextBlock","currentBlock","currentData","currentFinal","WRQ","sendNextBlock","ACK","createData","zeroTerminated","includes","parsePairs","offset","i","push","toString","toLowerCase","header","allocUnsafe","writeUInt16BE","parts","key","rawValue","copy","readUInt16BE","fallback","isSafeInteger","parseRequest","fs","path","fields","readZeroTerminatedFields","ServerTransfer","request","onFinish","timer","failing","bind","begin","resendLast","finish","retry","clearTimer","complete","errorPacket","release","finally","ReadTransfer","file","fileSize","position","filePath","requestedFilename","segments","replace","segment","some","relative","startsWith","isAbsolute","resolveRequestedFile","stat","promises","isFile","negotiated","requested","keys","chunks","createOack","buffer","bytesRead","listener","transfers","Map","autoStopTimer","eventHandler","serverOptions","onEvent","_e","_f","isDirectory","handleRequest","rejectBinding","transfer","clear","existing","get","peer","detail","delete","set","module","require","__webpack_module_cache__","__webpack_exports__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BehaviorSubject } from 'rxjs';
|
|
2
|
+
import { NotificationsService, PlatformService } from 'tabby-core';
|
|
3
|
+
import { TftpPluginSettings } from '../config';
|
|
4
|
+
import { TftpProgress, TftpResult } from '../tftp/client';
|
|
5
|
+
export declare class TftpService {
|
|
6
|
+
private readonly platform;
|
|
7
|
+
private readonly notifications;
|
|
8
|
+
readonly progress$: BehaviorSubject<TftpProgress>;
|
|
9
|
+
private controller;
|
|
10
|
+
private readonly client;
|
|
11
|
+
constructor(platform: PlatformService, notifications: NotificationsService);
|
|
12
|
+
get busy(): boolean;
|
|
13
|
+
upload(settings: TftpPluginSettings): Promise<TftpResult | null>;
|
|
14
|
+
download(settings: TftpPluginSettings): Promise<TftpResult | null>;
|
|
15
|
+
cancel(): void;
|
|
16
|
+
private run;
|
|
17
|
+
private ensureIdle;
|
|
18
|
+
private toClientOptions;
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BehaviorSubject } from 'rxjs';
|
|
2
|
+
import { NotificationsService, PlatformService } from 'tabby-core';
|
|
3
|
+
import { TftpServerSettings } from '../config';
|
|
4
|
+
export interface TftpServerLog {
|
|
5
|
+
timestamp: number;
|
|
6
|
+
level: 'info' | 'success' | 'error';
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TftpServerState {
|
|
10
|
+
running: boolean;
|
|
11
|
+
activeTransfers: number;
|
|
12
|
+
startedAt?: number;
|
|
13
|
+
logs: TftpServerLog[];
|
|
14
|
+
}
|
|
15
|
+
export declare class TftpServerService {
|
|
16
|
+
private readonly notifications;
|
|
17
|
+
private readonly platform;
|
|
18
|
+
readonly state$: BehaviorSubject<TftpServerState>;
|
|
19
|
+
private readonly server;
|
|
20
|
+
constructor(notifications: NotificationsService, platform: PlatformService);
|
|
21
|
+
get running(): boolean;
|
|
22
|
+
get activeSettings(): TftpServerSettings | null;
|
|
23
|
+
getLocalIPv4Addresses(): string[];
|
|
24
|
+
chooseRootDirectory(): Promise<string | null>;
|
|
25
|
+
openRootDirectory(directory: string): void;
|
|
26
|
+
copyAddress(address: string): void;
|
|
27
|
+
start(settings: TftpServerSettings): Promise<void>;
|
|
28
|
+
stop(): void;
|
|
29
|
+
clearLogs(): void;
|
|
30
|
+
private handleEvent;
|
|
31
|
+
private appendLog;
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface TftpClientOptions {
|
|
2
|
+
host: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
remoteFilename: string;
|
|
5
|
+
blockSize?: number;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
retries?: number;
|
|
8
|
+
}
|
|
9
|
+
export type TransferDirection = 'upload' | 'download';
|
|
10
|
+
export type TransferState = 'idle' | 'negotiating' | 'transferring' | 'retrying' | 'completed' | 'cancelled' | 'error';
|
|
11
|
+
export interface TftpProgress {
|
|
12
|
+
direction: TransferDirection;
|
|
13
|
+
state: TransferState;
|
|
14
|
+
transferred: number;
|
|
15
|
+
total?: number;
|
|
16
|
+
speed: number;
|
|
17
|
+
attempt?: number;
|
|
18
|
+
message?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TftpResult {
|
|
21
|
+
bytes: number;
|
|
22
|
+
blockSize: number;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface UploadSource {
|
|
26
|
+
readonly size: number;
|
|
27
|
+
read(maximumBytes: number): Promise<Buffer>;
|
|
28
|
+
close(): void;
|
|
29
|
+
abort?(): void;
|
|
30
|
+
isCancelled?(): boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface DownloadSink {
|
|
33
|
+
write(data: Buffer): Promise<void>;
|
|
34
|
+
close(): void;
|
|
35
|
+
abort?(): void;
|
|
36
|
+
isCancelled?(): boolean;
|
|
37
|
+
}
|
|
38
|
+
export type DownloadSinkFactory = (size?: number) => Promise<DownloadSink | null>;
|
|
39
|
+
export type ProgressHandler = (progress: TftpProgress) => void;
|
|
40
|
+
export declare class TftpClient {
|
|
41
|
+
download(options: TftpClientOptions, sinkFactory: DownloadSinkFactory, signal?: AbortSignal, onProgress?: ProgressHandler): Promise<TftpResult>;
|
|
42
|
+
upload(options: TftpClientOptions, source: UploadSource, signal?: AbortSignal, onProgress?: ProgressHandler): Promise<TftpResult>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare enum Opcode {
|
|
2
|
+
RRQ = 1,
|
|
3
|
+
WRQ = 2,
|
|
4
|
+
DATA = 3,
|
|
5
|
+
ACK = 4,
|
|
6
|
+
ERROR = 5,
|
|
7
|
+
OACK = 6
|
|
8
|
+
}
|
|
9
|
+
export type TftpPacket = {
|
|
10
|
+
opcode: Opcode.DATA;
|
|
11
|
+
block: number;
|
|
12
|
+
data: Buffer;
|
|
13
|
+
} | {
|
|
14
|
+
opcode: Opcode.ACK;
|
|
15
|
+
block: number;
|
|
16
|
+
} | {
|
|
17
|
+
opcode: Opcode.ERROR;
|
|
18
|
+
code: number;
|
|
19
|
+
message: string;
|
|
20
|
+
} | {
|
|
21
|
+
opcode: Opcode.OACK;
|
|
22
|
+
options: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
export declare function createRequest(opcode: Opcode.RRQ | Opcode.WRQ, filename: string, options?: Record<string, string | number>): Buffer;
|
|
25
|
+
export declare function createData(block: number, data: Buffer): Buffer;
|
|
26
|
+
export declare function createAck(block: number): Buffer;
|
|
27
|
+
export declare function createError(code: number, message: string): Buffer;
|
|
28
|
+
export declare function parsePacket(packet: Buffer): TftpPacket;
|
|
29
|
+
export declare function parseNegotiatedBlockSize(options: Record<string, string>, fallback?: number): number;
|
|
30
|
+
export declare function parseTransferSize(options: Record<string, string>): number | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Opcode } from './protocol';
|
|
2
|
+
export interface TftpServerOptions {
|
|
3
|
+
rootDirectory: string;
|
|
4
|
+
bindAddress?: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
blockSize?: number;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
retries?: number;
|
|
9
|
+
autoStopSeconds?: number;
|
|
10
|
+
}
|
|
11
|
+
export type TftpServerEventType = 'started' | 'stopped' | 'request' | 'progress' | 'completed' | 'error';
|
|
12
|
+
export interface TftpServerEvent {
|
|
13
|
+
type: TftpServerEventType;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
message: string;
|
|
16
|
+
peer?: string;
|
|
17
|
+
filename?: string;
|
|
18
|
+
bytes?: number;
|
|
19
|
+
activeTransfers: number;
|
|
20
|
+
}
|
|
21
|
+
interface TftpRequest {
|
|
22
|
+
opcode: Opcode.RRQ | Opcode.WRQ;
|
|
23
|
+
filename: string;
|
|
24
|
+
mode: string;
|
|
25
|
+
options: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export declare function parseRequest(packet: Buffer): TftpRequest;
|
|
28
|
+
export declare class TftpServer {
|
|
29
|
+
private listener;
|
|
30
|
+
private options;
|
|
31
|
+
private readonly transfers;
|
|
32
|
+
private autoStopTimer;
|
|
33
|
+
private eventHandler;
|
|
34
|
+
get running(): boolean;
|
|
35
|
+
get activeTransfers(): number;
|
|
36
|
+
start(serverOptions: TftpServerOptions, onEvent?: (event: TftpServerEvent) => void): Promise<void>;
|
|
37
|
+
stop(reason?: string): void;
|
|
38
|
+
private handleRequest;
|
|
39
|
+
private emit;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 发布到开源社区
|
|
2
|
+
|
|
3
|
+
这个插件通过 npm 包被 Tabby 插件管理器发现。包名保持 `tabby-tftp`(`tabby-` 前缀),并保留 `tabby-plugin` 关键词即可参与搜索。
|
|
4
|
+
|
|
5
|
+
## 首次发布
|
|
6
|
+
|
|
7
|
+
1. 在 GitHub 创建公开仓库,例如 `tabby-tftp`,再把仓库地址填入 `package.json` 的 `repository`、`homepage` 和 `bugs` 字段。
|
|
8
|
+
2. 检查 `package.json` 的版本号。npm 不允许重复发布同一个版本。
|
|
9
|
+
3. 在项目根目录执行:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
npm login
|
|
13
|
+
npm run verify
|
|
14
|
+
npm publish --access public
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
4. 用 `npm view tabby-tftp` 检查包的版本、关键词和 README 是否正确。
|
|
18
|
+
5. 创建对应的 Git tag 和 GitHub Release,并把安装方式写入 Release 说明。
|
|
19
|
+
|
|
20
|
+
## 后续版本
|
|
21
|
+
|
|
22
|
+
每次发布前先修改 `package.json` 版本号(推荐遵循语义化版本),更新 `CHANGELOG.md`,再运行 `npm run verify` 和 `npm publish`。发布前请确认包内没有调试构建、个人路径、访问令牌或固件文件:
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
npm pack --dry-run
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Tabby 集成说明
|
|
29
|
+
|
|
30
|
+
Tabby 的插件管理器按 `tabby-` 包名前缀和 `tabby-plugin` 关键词检索 npm。发布后如果搜索结果尚未更新,等待 npm registry 缓存刷新后再在 Tabby 中重试。
|
|
31
|
+
|
|
32
|
+
这个项目不修改 Tabby 核心代码,也不捆绑 Angular、RxJS 或 `tabby-core`;这些依赖通过 peer dependencies 使用 Tabby 自己提供的版本。
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tabby-tftp",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "TFTP client and read-only firmware server for Tabby terminal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tabby-plugin",
|
|
7
|
+
"tabby",
|
|
8
|
+
"tftp",
|
|
9
|
+
"tftp-server",
|
|
10
|
+
"firmware",
|
|
11
|
+
"serial",
|
|
12
|
+
"bootloader",
|
|
13
|
+
"u-boot",
|
|
14
|
+
"embedded",
|
|
15
|
+
"file-transfer"
|
|
16
|
+
],
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"typings": "dist/index.d.ts",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/lijzijie/tabby-tftp.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/lijzijie/tabby-tftp#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/lijzijie/tabby-tftp/issues"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"README.zh-CN.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"CONTRIBUTING.md",
|
|
33
|
+
"SECURITY.md",
|
|
34
|
+
"docs/PUBLISHING.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "node scripts/build.js",
|
|
39
|
+
"watch": "node scripts/build.js --watch",
|
|
40
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
41
|
+
"test": "tsc -p tsconfig.test.json && node --test tests/protocol.test.js tests/client.test.js tests/server.test.js",
|
|
42
|
+
"verify": "npm run typecheck && npm test && npm run build",
|
|
43
|
+
"pack:plugin": "npm pack",
|
|
44
|
+
"prepublishOnly": "npm run verify"
|
|
45
|
+
},
|
|
46
|
+
"author": "linzi",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"registry": "https://registry.npmjs.org"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@angular/common": ">=15 <17",
|
|
57
|
+
"@angular/core": ">=15 <17",
|
|
58
|
+
"@angular/forms": ">=15 <17",
|
|
59
|
+
"@ng-bootstrap/ng-bootstrap": ">=14 <16",
|
|
60
|
+
"rxjs": ">=7 <8",
|
|
61
|
+
"tabby-core": ">=1.0.200"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@angular/animations": "^15.2.10",
|
|
65
|
+
"@angular/common": "^15.2.10",
|
|
66
|
+
"@angular/core": "^15.2.10",
|
|
67
|
+
"@angular/forms": "^15.2.10",
|
|
68
|
+
"@ng-bootstrap/ng-bootstrap": "^14.2.0",
|
|
69
|
+
"@types/node": "^20.10.0",
|
|
70
|
+
"rxjs": "^7.8.1",
|
|
71
|
+
"tabby-core": "1.0.231-nightly.0",
|
|
72
|
+
"ts-loader": "^9.5.4",
|
|
73
|
+
"typescript": "^5.2.2",
|
|
74
|
+
"webpack": "^5.89.0",
|
|
75
|
+
"webpack-cli": "^5.1.4"
|
|
76
|
+
}
|
|
77
|
+
}
|