tabby-tftp 0.2.1 → 0.2.2
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/dist/buttonProvider.d.ts +7 -7
- package/dist/components/tftpModal.component.d.ts +30 -30
- package/dist/components/tftpServer.component.d.ts +24 -24
- package/dist/config.d.ts +37 -37
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/tftp.service.d.ts +19 -19
- package/dist/services/tftpServer.service.d.ts +32 -32
- package/dist/tftp/client.d.ts +45 -43
- package/dist/tftp/protocol.d.ts +32 -30
- package/dist/tftp/server.d.ts +43 -41
- package/package.json +5 -4
package/dist/buttonProvider.d.ts
CHANGED
|
@@ -1,7 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,30 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,24 +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
|
-
}
|
|
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
CHANGED
|
@@ -1,37 +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
|
-
}
|
|
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export default class TftpModule {
|
|
2
|
-
}
|
|
3
|
-
export * from './tftp/client';
|
|
4
|
-
export * from './tftp/protocol';
|
|
5
|
-
export * from './tftp/server';
|
|
1
|
+
export default class TftpModule {
|
|
2
|
+
}
|
|
3
|
+
export * from './tftp/client';
|
|
4
|
+
export * from './tftp/protocol';
|
|
5
|
+
export * from './tftp/server';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +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})();
|
|
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"})}]}};c=r([(0,n.Injectable)(),i("design:paramtypes",[o.NgbModal])],c),e.TftpButtonProvider=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}};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),e.TftpModalComponent=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()}};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),e.TftpServerComponent=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=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]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),n=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},o=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e},a=this&&this.__exportStar||function(t,e){for(var s in t)"default"===s||Object.prototype.hasOwnProperty.call(e,s)||r(e,t,s)};Object.defineProperty(e,"__esModule",{value:!0});const l=s(282),c=s(976),d=s(98),u=s(385),h=o(s(414)),f=s(983),p=s(263),m=s(153),g=s(28),b=s(306),v=s(21);let y=class{};y=n([(0,c.NgModule)({imports:[l.CommonModule,d.FormsModule,u.NgbModule,h.default],declarations:[p.TftpModalComponent,m.TftpServerComponent],providers:[b.TftpService,v.TftpServerService,{provide:h.ConfigProvider,useClass:g.TftpConfigProvider,multi:!0},{provide:h.ToolbarButtonProvider,useClass:f.TftpButtonProvider,multi:!0}]})],y),e.default=y,a(s(328),e),a(s(211),e),a(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)}}};h=r([(0,n.Injectable)(),i("design:paramtypes",[a.PlatformService,a.NotificationsService])],h),e.TftpService=h},21(t,e,s){var r=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]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),n=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},o=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e},a=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 l=s(976),c=s(573),d=s(414),u=o(s(366)),h=new(s(108).TftpServer),f=new c.BehaviorSubject({running:!1,activeTransfers:0,logs:[]});let p=null,m=class{constructor(t,e){this.notifications=t,this.platform=e,this.state$=f,this.server=h}get running(){return this.server.running}get activeSettings(){return p?{...p}:null}getLocalIPv4Addresses(){const t=new Set,e=u.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){p={...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==p?void 0:p.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)})}};m=n([(0,l.Injectable)(),a("design:paramtypes",[d.NotificationsService,d.PlatformService])],m),e.TftpServerService=m},328(t,e,s){var r=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]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),n=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpClient=void 0;const o=n(s(935)),a=s(211);function l(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 c(){const t=new Error("传输已取消");return t.name="AbortError",t}class d{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=o.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(c()),{once:!0}),this.cancelPoll=setInterval(()=>{this.resourceCancelled()&&this.fail(c())},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 c();if(this.endpoint&&(e.address!==this.endpoint.address||e.port!==this.endpoint.port))return void this.sendUntracked((0,a.createError)(5,"Unknown transfer ID"),{address:e.address,port:e.port});const r=(0,a.parsePacket)(t);if(this.endpoint||(this.endpoint={address:e.address,port:e.port}),r.opcode===a.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(c()):!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 u extends d{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,a.createRequest)(a.Opcode.RRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:0}))}async onPacket(t){if(t.opcode===a.Opcode.OACK){if(1!==this.expectedBlock||0!==this.transferred)return;return this.clearRetryTimer(),this.negotiatedBlockSize=(0,a.parseNegotiatedBlockSize)(t.options),this.totalSize=(0,a.parseTransferSize)(t.options),await this.ensureSink(this.totalSize),this.emit("transferring","已协商传输参数",this.totalSize),void this.sendTracked((0,a.createAck)(0))}if(t.opcode!==a.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,a.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,a.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 c()}}class h extends d{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,a.createRequest)(a.Opcode.WRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:this.source.size}))}async onPacket(t){return t.opcode===a.Opcode.OACK&&null===this.currentBlock?(this.clearRetryTimer(),this.negotiatedBlockSize=(0,a.parseNegotiatedBlockSize)(t.options),this.emit("transferring","已协商传输参数",this.source.size),void await this.sendNextBlock()):t.opcode===a.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,a.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 c();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,a.createData)(this.currentBlock,this.currentData))}}e.TftpClient=class{download(t,e,s,r){return new u(l(t),e,s,r).run()}upload(t,e,s,r){return new h(l(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.parseTransferSize=e.parseNegotiatedBlockSize=e.parsePacket=e.createError=e.createAck=e.createData=e.createRequest=e.Opcode=void 0,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||(e.Opcode={})),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}},108(t,e,s){var r=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]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),n=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServer=e.parseRequest=void 0;const o=n(s(935)),a=n(s(383)),l=n(s(3)),c=s(211);function d(t){if(t.length<4)throw new Error("TFTP 请求过短");const e=t.readUInt16BE(0);if(e!==c.Opcode.RRQ&&e!==c.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}}e.parseRequest=d;class u{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=o.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,c.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,c.createError)(5,"Unknown transfer ID");return void this.socket.send(t,e.port,e.address)}const s=(0,c.parsePacket)(t);if(s.opcode===c.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 h extends u{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=l.resolve(t,...s),i=l.relative(t,r);if(!i||i.startsWith("..")||l.isAbsolute(i))throw new Error("请求文件超出 TFTP 根目录");return r}(this.options.rootDirectory,this.request.filename),e=await a.promises.stat(t);if(!e.isFile())throw new Error("请求路径不是普通文件");this.fileSize=e.size,this.file=await a.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(c.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===c.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,c.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=l.resolve(t.rootDirectory.trim()),c=(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(!c)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:c,port:d,blockSize:u,timeoutMs:h,retries:f,autoStopSeconds:p}}(t);if(!(await a.promises.stat(s.rootDirectory)).isDirectory())throw new Error("TFTP 根目录无效");this.options=s,this.eventHandler=null!=e?e:null;const r=o.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=d(t)}catch(t){const s=t instanceof Error?t.message:String(t);return this.listener.send((0,c.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===c.Opcode.WRQ)return this.listener.send((0,c.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 h(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
2
|
//# sourceMappingURL=index.js.map
|