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/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","mappings":"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","sources":["webpack://tabby-tftp/./src/buttonProvider.ts","webpack://tabby-tftp/./src/components/tftpModal.component.ts","webpack://tabby-tftp/./src/components/tftpServer.component.ts","webpack://tabby-tftp/./src/config.ts","webpack://tabby-tftp/./src/index.ts","webpack://tabby-tftp/./src/services/tftp.service.ts","webpack://tabby-tftp/./src/services/tftpServer.service.ts","webpack://tabby-tftp/./src/tftp/client.ts","webpack://tabby-tftp/./src/tftp/protocol.ts","webpack://tabby-tftp/./src/tftp/server.ts","webpack://tabby-tftp/external commonjs \"@angular/common\"","webpack://tabby-tftp/external commonjs \"@angular/core\"","webpack://tabby-tftp/external commonjs \"@angular/forms\"","webpack://tabby-tftp/external commonjs \"@electron/remote\"","webpack://tabby-tftp/external commonjs \"@ng-bootstrap/ng-bootstrap\"","webpack://tabby-tftp/external commonjs \"dgram\"","webpack://tabby-tftp/external commonjs \"fs\"","webpack://tabby-tftp/external commonjs \"os\"","webpack://tabby-tftp/external commonjs \"path\"","webpack://tabby-tftp/external commonjs \"rxjs\"","webpack://tabby-tftp/external commonjs \"tabby-core\"","webpack://tabby-tftp/webpack/bootstrap","webpack://tabby-tftp/webpack/startup"],"sourcesContent":["import { Injectable } from '@angular/core'\nimport { NgbModal } from '@ng-bootstrap/ng-bootstrap'\nimport { IToolbarButton, ToolbarButtonProvider } from 'tabby-core'\n\nimport { TftpModalComponent } from './components/tftpModal.component'\n\nconst transferIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7 7h11l-3.5-3.5L16 2l6 6-6 6-1.5-1.5L18 9H7V7zm10 10H6l3.5 3.5L8 22l-6-6 6-6 1.5 1.5L6 15h11v2z\"/></svg>`\n\n@Injectable()\nexport class TftpButtonProvider extends ToolbarButtonProvider {\n constructor (private readonly modal: NgbModal) {\n super()\n }\n\n provide (): IToolbarButton[] {\n return [{\n icon: transferIcon,\n title: 'TFTP 文件传输',\n weight: 6,\n click: () => this.modal.open(TftpModalComponent, { size: 'lg' }),\n }]\n }\n}\n","import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from '@angular/core'\nimport { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'\nimport { Subscription } from 'rxjs'\nimport { ConfigService } from 'tabby-core'\n\nimport { defaultTftpServerSettings, defaultTftpSettings, TftpPluginSettings } from '../config'\nimport { TftpService } from '../services/tftp.service'\nimport { TftpProgress } from '../tftp/client'\n\n@Component({\n selector: 'tabby-tftp-modal',\n template: `\n <div class=\"modal-header\">\n <h5 class=\"modal-title\"><i class=\"fa fa-exchange-alt mr-2\"></i>TFTP 文件传输与固件服务</h5>\n <button type=\"button\" class=\"close\" aria-label=\"关闭\" (click)=\"activeModal.dismiss()\">\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n\n <div class=\"mode-tabs px-3 pt-3\">\n <div class=\"btn-group w-100\">\n <button class=\"btn\" [class.btn-primary]=\"mode === 'server'\" [class.btn-outline-secondary]=\"mode !== 'server'\" (click)=\"mode = 'server'\">\n <i class=\"fa fa-server mr-1\"></i>固件 TFTP 服务端\n </button>\n <button class=\"btn\" [class.btn-primary]=\"mode === 'client'\" [class.btn-outline-secondary]=\"mode !== 'client'\" (click)=\"mode = 'client'\">\n <i class=\"fa fa-exchange-alt mr-1\"></i>TFTP 客户端\n </button>\n </div>\n </div>\n\n <div class=\"modal-body\" *ngIf=\"mode === 'server'\">\n <tabby-tftp-server></tabby-tftp-server>\n </div>\n\n <div class=\"modal-body\" *ngIf=\"mode === 'client'\">\n <div class=\"alert alert-warning py-2 small\">\n TFTP 没有身份认证和加密。请仅在可信局域网中使用。\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-8\">\n <label>主机或 IP 地址</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.host\" placeholder=\"例如 192.168.1.10\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>端口</label>\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"65535\" [(ngModel)]=\"settings.port\" [disabled]=\"busy\">\n </div>\n </div>\n\n <div class=\"form-group\">\n <label>远端文件名</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.remoteFilename\"\n placeholder=\"下载时必填;上传留空则使用本地文件名\" [disabled]=\"busy\">\n <small class=\"form-text text-muted\">TFTP 不支持浏览远端目录,路径格式由服务端决定。</small>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">高级选项</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-4\">\n <label>块大小</label>\n <input class=\"form-control\" type=\"number\" min=\"8\" max=\"65464\" [(ngModel)]=\"settings.blockSize\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>超时(毫秒)</label>\n <input class=\"form-control\" type=\"number\" min=\"250\" max=\"60000\" [(ngModel)]=\"settings.timeoutMs\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>重试次数</label>\n <input class=\"form-control\" type=\"number\" min=\"0\" max=\"20\" [(ngModel)]=\"settings.retries\" [disabled]=\"busy\">\n </div>\n </div>\n </details>\n\n <div class=\"transfer-status\" *ngIf=\"status\">\n <div class=\"d-flex align-items-center mb-1\">\n <strong>{{ status.direction === 'upload' ? '上传' : '下载' }}</strong>\n <span class=\"ml-auto text-muted\">{{ formatBytes(status.transferred) }}<span *ngIf=\"status.total !== undefined\"> / {{ formatBytes(status.total) }}</span></span>\n </div>\n <ngb-progressbar [type]=\"progressType\" [value]=\"percent\" [striped]=\"busy\" [animated]=\"busy\"></ngb-progressbar>\n <div class=\"d-flex small mt-1\">\n <span [class.text-danger]=\"status.state === 'error'\">{{ status.message }}</span>\n <span class=\"ml-auto\" *ngIf=\"status.speed > 0\">{{ formatBytes(status.speed) }}/s</span>\n </div>\n </div>\n <div class=\"text-danger mt-2\" *ngIf=\"validationError\">{{ validationError }}</div>\n </div>\n\n <div class=\"modal-footer\">\n <button class=\"btn btn-outline-secondary\" (click)=\"activeModal.close()\">关闭</button>\n <ng-container *ngIf=\"mode === 'client'\">\n <button class=\"btn btn-outline-danger\" *ngIf=\"busy\" (click)=\"cancel()\"><i class=\"fa fa-stop mr-1\"></i>取消传输</button>\n <button class=\"btn btn-primary\" *ngIf=\"!busy\" (click)=\"upload()\"><i class=\"fa fa-upload mr-1\"></i>上传</button>\n <button class=\"btn btn-success\" *ngIf=\"!busy\" (click)=\"download()\"><i class=\"fa fa-download mr-1\"></i>下载</button>\n </ng-container>\n </div>\n `,\n styles: [`\n :host { display: block; }\n summary { cursor: pointer; user-select: none; }\n .transfer-status { border: 1px solid rgba(127, 127, 127, .25); border-radius: .25rem; padding: .75rem; }\n .modal-body label { margin-bottom: .25rem; }\n .mode-tabs .btn { flex: 1; }\n `],\n})\nexport class TftpModalComponent implements OnInit, OnDestroy {\n mode: 'server' | 'client' = 'server'\n settings: TftpPluginSettings = { ...defaultTftpSettings, server: { ...defaultTftpServerSettings } }\n status: TftpProgress | null = null\n validationError = ''\n private subscription?: Subscription\n\n constructor (\n public readonly activeModal: NgbActiveModal,\n private readonly config: ConfigService,\n public readonly tftp: TftpService,\n private readonly zone: NgZone,\n private readonly changeDetector: ChangeDetectorRef,\n ) { }\n\n get busy (): boolean { return this.tftp.busy }\n\n get percent (): number {\n if (!this.status) return 0\n if (this.status.state === 'completed') return 100\n if (this.status.total === undefined) return this.busy ? 100 : 0\n if (this.status.total === 0) return 100\n return Math.min(100, Math.round(this.status.transferred * 100 / this.status.total))\n }\n\n get progressType (): string {\n if (this.status?.state === 'error' || this.status?.state === 'cancelled') return 'danger'\n if (this.status?.state === 'completed') return 'success'\n return 'info'\n }\n\n ngOnInit (): void {\n const stored = this.config.store.tftp ?? {}\n this.settings = {\n ...defaultTftpSettings,\n ...stored,\n server: { ...defaultTftpServerSettings, ...(stored.server ?? {}) },\n }\n this.subscription = this.tftp.progress$.subscribe(status => {\n this.zone.run(() => {\n this.status = status\n this.changeDetector.markForCheck()\n })\n })\n }\n\n ngOnDestroy (): void { this.subscription?.unsubscribe() }\n\n async upload (): Promise<void> {\n if (!this.validate(false)) return\n await this.execute(() => this.tftp.upload(this.settings))\n }\n\n async download (): Promise<void> {\n if (!this.validate(true)) return\n await this.execute(() => this.tftp.download(this.settings))\n }\n\n cancel (): void { this.tftp.cancel() }\n\n formatBytes (value: number): string {\n if (!Number.isFinite(value) || value <= 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n const index = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024)))\n const amount = value / Math.pow(1024, index)\n return `${amount >= 100 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`\n }\n\n private async execute (operation: () => Promise<unknown>): Promise<void> {\n this.validationError = ''\n if (!this.config.store.tftp) this.config.store.tftp = {}\n Object.assign(this.config.store.tftp, {\n host: this.settings.host,\n port: this.settings.port,\n remoteFilename: this.settings.remoteFilename,\n blockSize: this.settings.blockSize,\n timeoutMs: this.settings.timeoutMs,\n retries: this.settings.retries,\n })\n await this.config.save()\n try { await operation() } catch (error) {\n this.validationError = error instanceof Error ? error.message : String(error)\n }\n }\n\n private validate (requireRemoteFilename: boolean): boolean {\n this.validationError = ''\n if (!this.settings.host.trim()) this.validationError = '请填写 TFTP 主机地址'\n else if (requireRemoteFilename && !this.settings.remoteFilename.trim()) this.validationError = '下载时必须填写远端文件名'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.validationError = '端口必须在 1 到 65535 之间'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.validationError = '块大小必须在 8 到 65464 之间'\n else if (!Number.isInteger(Number(this.settings.timeoutMs)) || Number(this.settings.timeoutMs) < 250 || Number(this.settings.timeoutMs) > 60000) this.validationError = '超时时间必须在 250 到 60000 毫秒之间'\n else if (!Number.isInteger(Number(this.settings.retries)) || Number(this.settings.retries) < 0 || Number(this.settings.retries) > 20) this.validationError = '重试次数必须在 0 到 20 之间'\n return !this.validationError\n }\n}\n","import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from '@angular/core'\nimport { Subscription } from 'rxjs'\nimport { ConfigService } from 'tabby-core'\n\nimport { defaultTftpServerSettings, TftpServerSettings } from '../config'\nimport { TftpServerService, TftpServerState } from '../services/tftpServer.service'\n\nconst serverSettingsStorageKey = 'tabby-tftp-server-settings-v1'\n\n@Component({\n selector: 'tabby-tftp-server',\n template: `\n <div class=\"alert alert-info py-2 small\">\n 串口负责给 Bootloader 下命令;本服务端通过网口向设备提供固件。服务端为只读模式,设备不能覆盖电脑文件。\n </div>\n\n <div class=\"form-group\">\n <label>TFTP 根目录</label>\n <div class=\"input-group\">\n <input class=\"form-control\" [(ngModel)]=\"settings.rootDirectory\" placeholder=\"固件所在目录\" [disabled]=\"state.running\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary\" (click)=\"chooseDirectory()\" [disabled]=\"state.running\" title=\"选择目录\">\n <i class=\"fa fa-folder-open\"></i>\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"server.openRootDirectory(settings.rootDirectory)\" [disabled]=\"!settings.rootDirectory\" title=\"打开目录\">\n <i class=\"fa fa-external-link-alt\"></i>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-5\">\n <label>监听地址</label>\n <select class=\"form-control\" [(ngModel)]=\"settings.bindAddress\" [disabled]=\"state.running\">\n <option value=\"0.0.0.0\">0.0.0.0(所有网卡)</option>\n <option *ngFor=\"let address of localAddresses\" [value]=\"address\">{{ address }}</option>\n </select>\n </div>\n <div class=\"form-group col-md-3\">\n <label>监听端口</label>\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"65535\" [(ngModel)]=\"settings.port\" [disabled]=\"state.running\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>TFTP 缓冲区</label>\n <div class=\"input-group\">\n <input class=\"form-control\" type=\"number\" min=\"8\" max=\"65464\" [(ngModel)]=\"settings.blockSize\" [disabled]=\"state.running\">\n <div class=\"input-group-append\"><span class=\"input-group-text\">Bytes</span></div>\n </div>\n </div>\n </div>\n\n <div class=\"local-addresses mb-3\">\n <span class=\"text-muted mr-2\">设备可访问的本机 IP:</span>\n <button class=\"btn btn-sm btn-outline-info mr-1\" *ngFor=\"let address of localAddresses\" (click)=\"server.copyAddress(address)\" title=\"点击复制\">\n {{ address }}\n </button>\n <span class=\"text-warning small\" *ngIf=\"!localAddresses.length\">未检测到可用 IPv4 地址</span>\n </div>\n\n <div class=\"form-row align-items-center mb-3\">\n <div class=\"col-md-5\">\n <div class=\"custom-control custom-checkbox\">\n <input type=\"checkbox\" class=\"custom-control-input\" id=\"tftp-auto-stop\" [(ngModel)]=\"settings.autoStop\" [disabled]=\"state.running\">\n <label class=\"custom-control-label\" for=\"tftp-auto-stop\">自动停止服务端</label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"input-group input-group-sm\">\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"86400\" [(ngModel)]=\"settings.autoStopSeconds\" [disabled]=\"state.running || !settings.autoStop\">\n <div class=\"input-group-append\"><span class=\"input-group-text\">秒</span></div>\n </div>\n </div>\n <div class=\"col-md-4\">\n <div class=\"custom-control custom-checkbox\">\n <input type=\"checkbox\" class=\"custom-control-input\" id=\"tftp-notify\" [(ngModel)]=\"settings.notifyDownloads\">\n <label class=\"custom-control-label\" for=\"tftp-notify\">下载完成后通知</label>\n </div>\n </div>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">可靠性选项</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-6\">\n <label>响应超时(毫秒)</label>\n <input class=\"form-control\" type=\"number\" min=\"250\" max=\"60000\" [(ngModel)]=\"settings.timeoutMs\" [disabled]=\"state.running\">\n </div>\n <div class=\"form-group col-md-6\">\n <label>重试次数</label>\n <input class=\"form-control\" type=\"number\" min=\"0\" max=\"20\" [(ngModel)]=\"settings.retries\" [disabled]=\"state.running\">\n </div>\n </div>\n </details>\n\n <div class=\"d-flex align-items-center mb-2\">\n <span class=\"status-dot mr-2\" [class.running]=\"state.running\"></span>\n <strong>{{ state.running ? 'TFTP 服务端运行中' : 'TFTP 服务端已停止' }}</strong>\n <span class=\"badge badge-info ml-2\" *ngIf=\"state.activeTransfers\">{{ state.activeTransfers }} 个传输</span>\n <div class=\"ml-auto\">\n <button class=\"btn btn-sm btn-link text-muted\" (click)=\"server.clearLogs()\" [disabled]=\"!state.logs.length\">清空日志</button>\n <button class=\"btn btn-success ml-1\" *ngIf=\"!state.running\" (click)=\"start()\" [disabled]=\"starting\">\n <i class=\"fa fa-play mr-1\"></i>{{ starting ? '启动中…' : '启动服务端' }}\n </button>\n <button class=\"btn btn-danger ml-1\" *ngIf=\"state.running\" (click)=\"server.stop()\">\n <i class=\"fa fa-stop mr-1\"></i>停止服务端\n </button>\n </div>\n </div>\n\n <div class=\"server-log\">\n <div *ngIf=\"!state.logs.length\" class=\"text-muted\">服务端输出将显示在这里</div>\n <div *ngFor=\"let log of state.logs\" [class.text-success]=\"log.level === 'success'\" [class.text-danger]=\"log.level === 'error'\">\n [{{ formatTime(log.timestamp) }}] {{ log.message }}\n </div>\n </div>\n <div class=\"text-danger mt-2\" *ngIf=\"error\">{{ error }}</div>\n `,\n styles: [`\n :host { display: block; }\n label { margin-bottom: .25rem; }\n summary { cursor: pointer; user-select: none; }\n .local-addresses { min-height: 31px; display: flex; align-items: center; flex-wrap: wrap; }\n .status-dot { width: 10px; height: 10px; border-radius: 50%; background: #6c757d; display: inline-block; }\n .status-dot.running { background: #28a745; box-shadow: 0 0 7px rgba(40, 167, 69, .8); }\n .server-log { height: 175px; overflow: auto; padding: .65rem; border: 1px solid rgba(127, 127, 127, .3); border-radius: .25rem; background: rgba(0, 0, 0, .18); font-family: Consolas, monospace; font-size: 12px; white-space: pre-wrap; }\n `],\n})\nexport class TftpServerComponent implements OnInit, OnDestroy {\n settings: TftpServerSettings = { ...defaultTftpServerSettings }\n state: TftpServerState = { running: false, activeTransfers: 0, logs: [] }\n localAddresses: string[] = []\n starting = false\n error = ''\n\n private subscription?: Subscription\n\n constructor (\n private readonly config: ConfigService,\n public readonly server: TftpServerService,\n private readonly zone: NgZone,\n private readonly changeDetector: ChangeDetectorRef,\n ) { }\n\n ngOnInit (): void {\n let cached: Partial<TftpServerSettings> = {}\n try {\n cached = JSON.parse(localStorage.getItem(serverSettingsStorageKey) ?? '{}')\n } catch { }\n this.settings = {\n ...defaultTftpServerSettings,\n ...(this.config.store.tftp?.server ?? {}),\n ...cached,\n ...(this.server.activeSettings ?? {}),\n }\n this.localAddresses = this.server.getLocalIPv4Addresses()\n this.subscription = this.server.state$.subscribe(state => {\n this.zone.run(() => {\n this.state = state\n this.changeDetector.markForCheck()\n })\n })\n }\n\n ngOnDestroy (): void {\n this.subscription?.unsubscribe()\n }\n\n async chooseDirectory (): Promise<void> {\n const directory = await this.server.chooseRootDirectory()\n if (directory) {\n this.settings.rootDirectory = directory\n await this.saveSettings()\n }\n }\n\n async start (): Promise<void> {\n this.error = ''\n if (!this.validate()) return\n this.starting = true\n await this.saveSettings()\n try {\n await this.server.start(this.settings)\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n } finally {\n this.starting = false\n }\n }\n\n formatTime (timestamp: number): string {\n return new Date(timestamp).toLocaleTimeString(undefined, { hour12: false })\n }\n\n private validate (): boolean {\n if (!this.settings.rootDirectory.trim()) this.error = '请选择固件所在的 TFTP 根目录'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.error = '监听端口必须在 1 到 65535 之间'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.error = 'TFTP 缓冲区必须在 8 到 65464 字节之间'\n else if (this.settings.autoStop && (!Number.isInteger(Number(this.settings.autoStopSeconds)) || Number(this.settings.autoStopSeconds) < 1 || Number(this.settings.autoStopSeconds) > 86400)) this.error = '自动停止时间必须在 1 到 86400 秒之间'\n return !this.error\n }\n\n private async saveSettings (): Promise<void> {\n localStorage.setItem(serverSettingsStorageKey, JSON.stringify(this.settings))\n if (!this.config.store.tftp) this.config.store.tftp = {}\n if (!this.config.store.tftp.server) this.config.store.tftp.server = {}\n Object.assign(this.config.store.tftp.server, this.settings)\n await this.config.save()\n }\n}\n","import { ConfigProvider } from 'tabby-core'\n\nexport interface TftpPluginSettings {\n host: string\n port: number\n remoteFilename: string\n blockSize: number\n timeoutMs: number\n retries: number\n server: TftpServerSettings\n}\n\nexport interface TftpServerSettings {\n rootDirectory: string\n bindAddress: string\n port: number\n blockSize: number\n timeoutMs: number\n retries: number\n autoStop: boolean\n autoStopSeconds: number\n notifyDownloads: boolean\n}\n\nexport const defaultTftpServerSettings: TftpServerSettings = {\n rootDirectory: '',\n bindAddress: '0.0.0.0',\n port: 69,\n blockSize: 8192,\n timeoutMs: 3000,\n retries: 5,\n autoStop: false,\n autoStopSeconds: 360,\n notifyDownloads: true,\n}\n\nexport const defaultTftpSettings: TftpPluginSettings = {\n host: '',\n port: 69,\n remoteFilename: '',\n blockSize: 1468,\n timeoutMs: 3000,\n retries: 5,\n server: { ...defaultTftpServerSettings },\n}\n\nexport class TftpConfigProvider extends ConfigProvider {\n defaults = {\n tftp: { ...defaultTftpSettings },\n }\n\n platformDefaults = { }\n}\n","import { CommonModule } from '@angular/common'\nimport { NgModule } from '@angular/core'\nimport { FormsModule } from '@angular/forms'\nimport { NgbModule } from '@ng-bootstrap/ng-bootstrap'\nimport TabbyCoreModule, { ConfigProvider, ToolbarButtonProvider } from 'tabby-core'\n\nimport { TftpButtonProvider } from './buttonProvider'\nimport { TftpModalComponent } from './components/tftpModal.component'\nimport { TftpServerComponent } from './components/tftpServer.component'\nimport { TftpConfigProvider } from './config'\nimport { TftpService } from './services/tftp.service'\nimport { TftpServerService } from './services/tftpServer.service'\n\n@NgModule({\n imports: [\n CommonModule,\n FormsModule,\n NgbModule,\n TabbyCoreModule,\n ],\n declarations: [\n TftpModalComponent,\n TftpServerComponent,\n ],\n providers: [\n TftpService,\n TftpServerService,\n { provide: ConfigProvider, useClass: TftpConfigProvider, multi: true },\n { provide: ToolbarButtonProvider, useClass: TftpButtonProvider, multi: true },\n ],\n})\nexport default class TftpModule { }\n\nexport * from './tftp/client'\nexport * from './tftp/protocol'\nexport * from './tftp/server'\n","import { Injectable } from '@angular/core'\nimport { BehaviorSubject } from 'rxjs'\nimport {\n FileDownload,\n FileUpload,\n NotificationsService,\n PlatformService,\n} from 'tabby-core'\n\nimport { TftpPluginSettings } from '../config'\nimport {\n DownloadSink,\n TftpClient,\n TftpProgress,\n TftpResult,\n UploadSource,\n} from '../tftp/client'\n\nconst idleProgress: TftpProgress = {\n direction: 'download',\n state: 'idle',\n transferred: 0,\n speed: 0,\n message: '准备就绪',\n}\n\nclass TabbyUploadSource implements UploadSource {\n readonly size: number\n private pending = Buffer.alloc(0)\n private ended = false\n\n constructor (private readonly upload: FileUpload) {\n this.size = upload.getSize()\n }\n\n async read (maximumBytes: number): Promise<Buffer> {\n while (!this.ended && this.pending.length < maximumBytes) {\n const chunk = await this.upload.read()\n if (!chunk.length) {\n this.ended = true\n break\n }\n this.pending = this.pending.length\n ? Buffer.concat([this.pending, Buffer.from(chunk)])\n : Buffer.from(chunk)\n }\n\n const result = Buffer.from(this.pending.subarray(0, maximumBytes))\n this.pending = this.pending.subarray(result.length)\n return result\n }\n\n close (): void {\n this.upload.close()\n }\n\n abort (): void {\n this.upload.cancel()\n }\n\n isCancelled (): boolean {\n return this.upload.isCancelled()\n }\n}\n\nclass TabbyDownloadSink implements DownloadSink {\n private completed = false\n\n constructor (private readonly download: FileDownload, sizeKnown: boolean) {\n if (!sizeKnown) {\n // Tabby requires a size before the TFTP server has necessarily told us one.\n // Keep the transfer active until close(), then report the actual byte count.\n ;(download as any).getSize = () => this.completed\n ? download.getCompletedBytes()\n : Math.max(1, download.getCompletedBytes() + 1)\n }\n }\n\n write (data: Buffer): Promise<void> {\n return this.download.write(data)\n }\n\n close (): void {\n this.completed = true\n this.download.close()\n }\n\n abort (): void {\n this.download.cancel()\n }\n\n isCancelled (): boolean {\n return this.download.isCancelled()\n }\n}\n\n@Injectable()\nexport class TftpService {\n readonly progress$ = new BehaviorSubject<TftpProgress>(idleProgress)\n\n private controller: AbortController | null = null\n private readonly client = new TftpClient()\n\n constructor (\n private readonly platform: PlatformService,\n private readonly notifications: NotificationsService,\n ) { }\n\n get busy (): boolean {\n return this.controller !== null\n }\n\n async upload (settings: TftpPluginSettings): Promise<TftpResult | null> {\n this.ensureIdle()\n const uploads = await this.platform.startUpload({ multiple: false })\n const upload = uploads[0]\n if (!upload) return null\n\n const remoteFilename = settings.remoteFilename.trim() || upload.getName()\n const source = new TabbyUploadSource(upload)\n return this.run('upload', remoteFilename, controller => this.client.upload(\n this.toClientOptions(settings, remoteFilename),\n source,\n controller.signal,\n progress => this.progress$.next(progress),\n ))\n }\n\n async download (settings: TftpPluginSettings): Promise<TftpResult | null> {\n this.ensureIdle()\n const remoteFilename = settings.remoteFilename.trim()\n if (!remoteFilename) throw new Error('下载时必须填写远端文件名')\n\n const localName = remoteFilename.split(/[\\\\/]/).filter(Boolean).pop() || 'download.bin'\n return this.run('download', remoteFilename, controller => this.client.download(\n this.toClientOptions(settings, remoteFilename),\n async size => {\n const download = await this.platform.startDownload(localName, 0o644, size ?? 1)\n return download ? new TabbyDownloadSink(download, size !== undefined) : null\n },\n controller.signal,\n progress => this.progress$.next(progress),\n ))\n }\n\n cancel (): void {\n this.controller?.abort()\n }\n\n private async run (\n direction: 'upload' | 'download',\n filename: string,\n operation: (controller: AbortController) => Promise<TftpResult>,\n ): Promise<TftpResult> {\n this.ensureIdle()\n const controller = new AbortController()\n this.controller = controller\n try {\n const result = await operation(controller)\n this.notifications.notice(`TFTP ${direction === 'upload' ? '上传' : '下载'}完成:${filename}`)\n return result\n } catch (error) {\n const reason = error instanceof Error ? error : new Error(String(error))\n if (reason.name === 'AbortError') {\n this.notifications.info('TFTP 传输已取消')\n } else {\n this.notifications.error('TFTP 传输失败', reason.message)\n }\n throw reason\n } finally {\n if (this.controller === controller) this.controller = null\n }\n }\n\n private ensureIdle (): void {\n if (this.busy) throw new Error('已有一个 TFTP 传输正在进行')\n }\n\n private toClientOptions (settings: TftpPluginSettings, remoteFilename: string) {\n return {\n host: settings.host,\n port: Number(settings.port),\n remoteFilename,\n blockSize: Number(settings.blockSize),\n timeoutMs: Number(settings.timeoutMs),\n retries: Number(settings.retries),\n }\n }\n}\n","import { Injectable } from '@angular/core'\nimport { BehaviorSubject } from 'rxjs'\nimport { NotificationsService, PlatformService } from 'tabby-core'\nimport * as os from 'os'\n\nimport { TftpServerSettings } from '../config'\nimport { TftpServer, TftpServerEvent } from '../tftp/server'\n\nexport interface TftpServerLog {\n timestamp: number\n level: 'info' | 'success' | 'error'\n message: string\n}\n\nexport interface TftpServerState {\n running: boolean\n activeTransfers: number\n startedAt?: number\n logs: TftpServerLog[]\n}\n\nconst sharedServer = new TftpServer()\nconst sharedState$ = new BehaviorSubject<TftpServerState>({\n running: false,\n activeTransfers: 0,\n logs: [],\n})\nlet sharedSettings: TftpServerSettings | null = null\n\n@Injectable()\nexport class TftpServerService {\n readonly state$ = sharedState$\n private readonly server = sharedServer\n\n constructor (\n private readonly notifications: NotificationsService,\n private readonly platform: PlatformService,\n ) { }\n\n get running (): boolean {\n return this.server.running\n }\n\n get activeSettings (): TftpServerSettings | null {\n return sharedSettings ? { ...sharedSettings } : null\n }\n\n getLocalIPv4Addresses (): string[] {\n const addresses = new Set<string>()\n const interfaces = os.networkInterfaces()\n for (const entries of Object.values(interfaces)) {\n for (const entry of entries ?? []) {\n if (entry.family === 'IPv4' && !entry.internal) addresses.add(entry.address)\n }\n }\n return Array.from(addresses).sort()\n }\n\n async chooseRootDirectory (): Promise<string | null> {\n const remote = require('@electron/remote')\n const result = await remote.dialog.showOpenDialog({\n title: '选择 TFTP 根目录',\n properties: ['openDirectory', 'createDirectory'],\n })\n return result.canceled ? null : (result.filePaths[0] ?? null)\n }\n\n openRootDirectory (directory: string): void {\n if (directory) this.platform.openPath(directory)\n }\n\n copyAddress (address: string): void {\n this.platform.setClipboard({ text: address })\n this.notifications.notice(`已复制本机 IP:${address}`)\n }\n\n async start (settings: TftpServerSettings): Promise<void> {\n sharedSettings = { ...settings }\n try {\n await this.server.start({\n rootDirectory: settings.rootDirectory,\n bindAddress: settings.bindAddress,\n port: Number(settings.port),\n blockSize: Number(settings.blockSize),\n timeoutMs: Number(settings.timeoutMs),\n retries: Number(settings.retries),\n autoStopSeconds: settings.autoStop ? Number(settings.autoStopSeconds) : 0,\n }, event => this.handleEvent(event))\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n this.appendLog('error', `启动失败:${reason}`)\n this.notifications.error('TFTP 服务端启动失败', reason)\n throw error\n }\n }\n\n stop (): void {\n this.server.stop()\n }\n\n clearLogs (): void {\n this.state$.next({ ...this.state$.value, logs: [] })\n }\n\n private handleEvent (event: TftpServerEvent): void {\n const level: TftpServerLog['level'] = event.type === 'completed' ? 'success' : event.type === 'error' ? 'error' : 'info'\n const current = this.state$.value\n const running = event.type === 'started' ? true : event.type === 'stopped' ? false : this.server.running\n const next: TftpServerState = {\n running,\n activeTransfers: event.activeTransfers,\n startedAt: event.type === 'started' ? event.timestamp : current.startedAt,\n logs: [...current.logs, { timestamp: event.timestamp, level, message: event.message }].slice(-300),\n }\n this.state$.next(next)\n\n if (event.type === 'completed' && sharedSettings?.notifyDownloads) {\n this.notifications.notice(`TFTP 下载完成:${event.filename ?? ''}`)\n } else if (event.type === 'stopped') {\n this.notifications.info('TFTP 服务端已停止')\n }\n }\n\n private appendLog (level: TftpServerLog['level'], message: string): void {\n const current = this.state$.value\n this.state$.next({\n ...current,\n logs: [...current.logs, { timestamp: Date.now(), level, message }].slice(-300),\n })\n }\n}\n","import * as dgram from 'dgram'\nimport { RemoteInfo, Socket } from 'dgram'\n\nimport {\n Opcode,\n TftpPacket,\n createAck,\n createData,\n createError,\n createRequest,\n parseNegotiatedBlockSize,\n parsePacket,\n parseTransferSize,\n} from './protocol'\n\nexport interface TftpClientOptions {\n host: string\n port?: number\n remoteFilename: string\n blockSize?: number\n timeoutMs?: number\n retries?: number\n}\n\ninterface NormalizedOptions {\n host: string\n port: number\n remoteFilename: string\n blockSize: number\n timeoutMs: number\n retries: number\n}\n\nexport type TransferDirection = 'upload' | 'download'\nexport type TransferState = 'idle' | 'negotiating' | 'transferring' | 'retrying' | 'completed' | 'cancelled' | 'error'\n\nexport interface TftpProgress {\n direction: TransferDirection\n state: TransferState\n transferred: number\n total?: number\n speed: number\n attempt?: number\n message?: string\n}\n\nexport interface TftpResult {\n bytes: number\n blockSize: number\n durationMs: number\n}\n\nexport interface UploadSource {\n readonly size: number\n read (maximumBytes: number): Promise<Buffer>\n close (): void\n abort? (): void\n isCancelled? (): boolean\n}\n\nexport interface DownloadSink {\n write (data: Buffer): Promise<void>\n close (): void\n abort? (): void\n isCancelled? (): boolean\n}\n\nexport type DownloadSinkFactory = (size?: number) => Promise<DownloadSink | null>\nexport type ProgressHandler = (progress: TftpProgress) => void\n\nfunction normalizeOptions (options: TftpClientOptions): NormalizedOptions {\n const host = options.host.trim()\n const remoteFilename = options.remoteFilename.trim()\n const port = options.port ?? 69\n const blockSize = options.blockSize ?? 1468\n const timeoutMs = options.timeoutMs ?? 3000\n const retries = options.retries ?? 5\n\n if (!host) throw new Error('TFTP 主机不能为空')\n if (!remoteFilename) throw new Error('远端文件名不能为空')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('TFTP 端口必须在 1 到 65535 之间')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP 块大小必须在 8 到 65464 之间')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('超时时间必须在 250 到 60000 毫秒之间')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('重试次数必须在 0 到 20 之间')\n\n return { host, remoteFilename, port, blockSize, timeoutMs, retries }\n}\n\nfunction abortError (): Error {\n const error = new Error('传输已取消')\n error.name = 'AbortError'\n return error\n}\n\nabstract class TransferSession {\n protected readonly socket: Socket\n protected readonly startedAt = Date.now()\n protected transferred = 0\n protected negotiatedBlockSize = 512\n\n private endpoint: { address: string; port: number } | null = null\n private lastPacket: Buffer | null = null\n private lastTarget: { address: string; port: number } | null = null\n private retryCount = 0\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n private cancelPoll: ReturnType<typeof setInterval> | null = null\n private settled = false\n private queue = Promise.resolve()\n private resolvePromise!: (result: TftpResult) => void\n private rejectPromise!: (error: Error) => void\n\n protected constructor (\n protected readonly direction: TransferDirection,\n protected readonly options: NormalizedOptions,\n protected readonly signal?: AbortSignal,\n protected readonly onProgress?: ProgressHandler,\n ) {\n this.socket = dgram.createSocket('udp4')\n }\n\n protected runSession (initialPacket: Buffer): Promise<TftpResult> {\n const result = new Promise<TftpResult>((resolve, reject) => {\n this.resolvePromise = resolve\n this.rejectPromise = reject\n })\n\n this.socket.on('error', error => this.fail(error))\n this.socket.on('message', (message, remote) => {\n this.queue = this.queue\n .then(() => this.handleMessage(message, remote))\n .catch(error => this.fail(error instanceof Error ? error : new Error(String(error))))\n })\n this.signal?.addEventListener('abort', () => this.fail(abortError()), { once: true })\n this.cancelPoll = setInterval(() => {\n if (this.resourceCancelled()) this.fail(abortError())\n }, 250)\n\n this.emit('negotiating', '正在连接 TFTP 服务端')\n this.sendTracked(initialPacket, { address: this.options.host, port: this.options.port })\n return result\n }\n\n protected abstract onPacket (packet: TftpPacket): Promise<void>\n protected abstract releaseResource (successful: boolean): void\n protected abstract resourceCancelled (): boolean\n\n protected clearRetryTimer (): void {\n if (this.retryTimer) clearTimeout(this.retryTimer)\n this.retryTimer = null\n this.retryCount = 0\n }\n\n protected sendTracked (packet: Buffer, explicitTarget?: { address: string; port: number }): void {\n if (this.settled) return\n const target = explicitTarget ?? this.endpoint ?? { address: this.options.host, port: this.options.port }\n this.lastPacket = Buffer.from(packet)\n this.lastTarget = target\n if (this.retryTimer) clearTimeout(this.retryTimer)\n this.socket.send(packet, target.port, target.address, error => {\n if (error) this.fail(error)\n })\n this.retryTimer = setTimeout(() => this.retryLastPacket(), this.options.timeoutMs)\n }\n\n protected sendUntracked (\n packet: Buffer,\n explicitTarget?: { address: string; port: number },\n sent?: () => void,\n ): void {\n if (this.settled) return\n const target = explicitTarget ?? this.endpoint ?? { address: this.options.host, port: this.options.port }\n this.socket.send(packet, target.port, target.address, error => {\n if (error) this.fail(error)\n else sent?.()\n })\n }\n\n protected emit (state: TransferState, message?: string, total?: number): void {\n const duration = Math.max(1, Date.now() - this.startedAt)\n this.onProgress?.({\n direction: this.direction,\n state,\n transferred: this.transferred,\n total,\n speed: this.transferred * 1000 / duration,\n attempt: state === 'retrying' ? this.retryCount : undefined,\n message,\n })\n }\n\n protected succeed (): void {\n if (this.settled) return\n this.settled = true\n this.cleanupTimers()\n this.releaseResource(true)\n this.socket.close()\n const result = {\n bytes: this.transferred,\n blockSize: this.negotiatedBlockSize,\n durationMs: Date.now() - this.startedAt,\n }\n this.emit('completed', '传输完成', this.transferred)\n this.resolvePromise(result)\n }\n\n protected fail (error: Error): void {\n if (this.settled) return\n this.settled = true\n this.cleanupTimers()\n this.releaseResource(false)\n try {\n this.socket.close()\n } catch { }\n this.emit(error.name === 'AbortError' ? 'cancelled' : 'error', error.message)\n this.rejectPromise(error)\n }\n\n private async handleMessage (message: Buffer, remote: RemoteInfo): Promise<void> {\n if (this.settled) return\n if (this.signal?.aborted || this.resourceCancelled()) throw abortError()\n\n if (this.endpoint && (remote.address !== this.endpoint.address || remote.port !== this.endpoint.port)) {\n this.sendUntracked(createError(5, 'Unknown transfer ID'), { address: remote.address, port: remote.port })\n return\n }\n\n const packet = parsePacket(message)\n if (!this.endpoint) this.endpoint = { address: remote.address, port: remote.port }\n if (packet.opcode === Opcode.ERROR) {\n throw new Error(`TFTP 错误 ${packet.code}: ${packet.message}`)\n }\n await this.onPacket(packet)\n }\n\n private retryLastPacket (): void {\n if (this.settled) return\n if (this.signal?.aborted || this.resourceCancelled()) {\n this.fail(abortError())\n return\n }\n if (!this.lastPacket || !this.lastTarget || this.retryCount >= this.options.retries) {\n this.fail(new Error(`TFTP 响应超时,已重试 ${this.options.retries} 次`))\n return\n }\n\n this.retryCount++\n this.emit('retrying', `响应超时,正在进行第 ${this.retryCount} 次重试`)\n this.socket.send(this.lastPacket, this.lastTarget.port, this.lastTarget.address, error => {\n if (error) this.fail(error)\n })\n this.retryTimer = setTimeout(() => this.retryLastPacket(), this.options.timeoutMs)\n }\n\n private cleanupTimers (): void {\n if (this.retryTimer) clearTimeout(this.retryTimer)\n if (this.cancelPoll) clearInterval(this.cancelPoll)\n this.retryTimer = null\n this.cancelPoll = null\n }\n}\n\nclass DownloadSession extends TransferSession {\n private expectedBlock = 1\n private sink: DownloadSink | null = null\n private totalSize: number | undefined\n\n constructor (\n options: NormalizedOptions,\n private readonly sinkFactory: DownloadSinkFactory,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ) {\n super('download', options, signal, onProgress)\n }\n\n run (): Promise<TftpResult> {\n const timeoutSeconds = Math.max(1, Math.min(255, Math.round(this.options.timeoutMs / 1000)))\n return this.runSession(createRequest(Opcode.RRQ, this.options.remoteFilename, {\n blksize: this.options.blockSize,\n timeout: timeoutSeconds,\n tsize: 0,\n }))\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode === Opcode.OACK) {\n if (this.expectedBlock !== 1 || this.transferred !== 0) return\n this.clearRetryTimer()\n this.negotiatedBlockSize = parseNegotiatedBlockSize(packet.options)\n this.totalSize = parseTransferSize(packet.options)\n await this.ensureSink(this.totalSize)\n this.emit('transferring', '已协商传输参数', this.totalSize)\n this.sendTracked(createAck(0))\n return\n }\n if (packet.opcode !== Opcode.DATA) return\n\n if (packet.block === this.expectedBlock) {\n this.clearRetryTimer()\n if (!this.sink) {\n this.negotiatedBlockSize = 512\n await this.ensureSink(undefined)\n }\n await this.sink!.write(packet.data)\n this.transferred += packet.data.length\n this.emit('transferring', `正在接收块 ${packet.block}`, this.totalSize)\n\n const finalBlock = packet.data.length < this.negotiatedBlockSize\n const acknowledgement = createAck(packet.block)\n if (finalBlock) {\n this.sendUntracked(acknowledgement, undefined, () => this.succeed())\n } else {\n this.expectedBlock = (this.expectedBlock + 1) & 0xffff\n this.sendTracked(acknowledgement)\n }\n return\n }\n\n const previousBlock = (this.expectedBlock - 1) & 0xffff\n if (packet.block === previousBlock) {\n this.sendTracked(createAck(packet.block))\n }\n }\n\n protected releaseResource (successful: boolean): void {\n if (!this.sink) return\n if (successful) this.sink.close()\n else if (this.sink.abort) this.sink.abort()\n else this.sink.close()\n }\n\n protected resourceCancelled (): boolean {\n return this.sink?.isCancelled?.() ?? false\n }\n\n private async ensureSink (size?: number): Promise<void> {\n if (this.sink) return\n this.sink = await this.sinkFactory(size)\n if (!this.sink) throw abortError()\n }\n}\n\nclass UploadSession extends TransferSession {\n private nextBlock = 1\n private currentBlock: number | null = null\n private currentData = Buffer.alloc(0)\n private currentFinal = false\n\n constructor (\n options: NormalizedOptions,\n private readonly source: UploadSource,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ) {\n super('upload', options, signal, onProgress)\n }\n\n run (): Promise<TftpResult> {\n const timeoutSeconds = Math.max(1, Math.min(255, Math.round(this.options.timeoutMs / 1000)))\n return this.runSession(createRequest(Opcode.WRQ, this.options.remoteFilename, {\n blksize: this.options.blockSize,\n timeout: timeoutSeconds,\n tsize: this.source.size,\n }))\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode === Opcode.OACK && this.currentBlock === null) {\n this.clearRetryTimer()\n this.negotiatedBlockSize = parseNegotiatedBlockSize(packet.options)\n this.emit('transferring', '已协商传输参数', this.source.size)\n await this.sendNextBlock()\n return\n }\n\n if (packet.opcode !== Opcode.ACK) return\n if (packet.block === 0 && this.currentBlock === null) {\n this.clearRetryTimer()\n this.negotiatedBlockSize = 512\n this.emit('transferring', '服务端使用标准 512 字节块', this.source.size)\n await this.sendNextBlock()\n return\n }\n\n if (this.currentBlock !== null && packet.block === this.currentBlock) {\n this.clearRetryTimer()\n this.transferred += this.currentData.length\n this.emit('transferring', `服务端已确认块 ${packet.block}`, this.source.size)\n if (this.currentFinal) {\n this.succeed()\n } else {\n this.nextBlock = (this.nextBlock + 1) & 0xffff\n await this.sendNextBlock()\n }\n return\n }\n\n if (this.currentBlock !== null && packet.block === ((this.currentBlock - 1) & 0xffff)) {\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n }\n\n protected releaseResource (successful: boolean): void {\n if (successful) this.source.close()\n else if (this.source.abort) this.source.abort()\n else this.source.close()\n }\n\n protected resourceCancelled (): boolean {\n return this.source.isCancelled?.() ?? false\n }\n\n private async sendNextBlock (): Promise<void> {\n if (this.signal?.aborted || this.resourceCancelled()) throw abortError()\n this.currentData = Buffer.from(await this.source.read(this.negotiatedBlockSize))\n if (this.currentData.length > this.negotiatedBlockSize) {\n throw new Error('上传数据源返回的数据超过协商块大小')\n }\n this.currentBlock = this.nextBlock\n this.currentFinal = this.currentData.length < this.negotiatedBlockSize\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n}\n\nexport class TftpClient {\n download (\n options: TftpClientOptions,\n sinkFactory: DownloadSinkFactory,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ): Promise<TftpResult> {\n return new DownloadSession(normalizeOptions(options), sinkFactory, signal, onProgress).run()\n }\n\n upload (\n options: TftpClientOptions,\n source: UploadSource,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ): Promise<TftpResult> {\n return new UploadSession(normalizeOptions(options), source, signal, onProgress).run()\n }\n}\n","export enum Opcode {\n RRQ = 1,\n WRQ = 2,\n DATA = 3,\n ACK = 4,\n ERROR = 5,\n OACK = 6,\n}\n\nexport type TftpPacket =\n | { opcode: Opcode.DATA; block: number; data: Buffer }\n | { opcode: Opcode.ACK; block: number }\n | { opcode: Opcode.ERROR; code: number; message: string }\n | { opcode: Opcode.OACK; options: Record<string, string> }\n\nfunction zeroTerminated (value: string): Buffer {\n if (value.includes('\\0')) {\n throw new Error('TFTP 字段不能包含空字符')\n }\n return Buffer.concat([Buffer.from(value, 'utf8'), Buffer.from([0])])\n}\n\nexport function createRequest (\n opcode: Opcode.RRQ | Opcode.WRQ,\n filename: string,\n options: Record<string, string | number> = {},\n): Buffer {\n if (!filename.trim()) {\n throw new Error('远端文件名不能为空')\n }\n\n const header = Buffer.allocUnsafe(2)\n header.writeUInt16BE(opcode, 0)\n const parts = [header, zeroTerminated(filename), zeroTerminated('octet')]\n\n for (const [key, rawValue] of Object.entries(options)) {\n parts.push(zeroTerminated(key.toLowerCase()))\n parts.push(zeroTerminated(String(rawValue)))\n }\n return Buffer.concat(parts)\n}\n\nexport function createData (block: number, data: Buffer): Buffer {\n const packet = Buffer.allocUnsafe(4 + data.length)\n packet.writeUInt16BE(Opcode.DATA, 0)\n packet.writeUInt16BE(block & 0xffff, 2)\n data.copy(packet, 4)\n return packet\n}\n\nexport function createAck (block: number): Buffer {\n const packet = Buffer.allocUnsafe(4)\n packet.writeUInt16BE(Opcode.ACK, 0)\n packet.writeUInt16BE(block & 0xffff, 2)\n return packet\n}\n\nexport function createError (code: number, message: string): Buffer {\n const packet = Buffer.allocUnsafe(4)\n packet.writeUInt16BE(Opcode.ERROR, 0)\n packet.writeUInt16BE(code & 0xffff, 2)\n return Buffer.concat([packet, zeroTerminated(message)])\n}\n\nfunction parsePairs (packet: Buffer, offset: number): Record<string, string> {\n const values: string[] = []\n let start = offset\n for (let i = offset; i < packet.length; i++) {\n if (packet[i] === 0) {\n values.push(packet.toString('utf8', start, i))\n start = i + 1\n }\n }\n if (start !== packet.length || values.length % 2 !== 0) {\n throw new Error('无效的 TFTP 选项响应')\n }\n\n const options: Record<string, string> = {}\n for (let i = 0; i < values.length; i += 2) {\n options[values[i].toLowerCase()] = values[i + 1]\n }\n return options\n}\n\nexport function parsePacket (packet: Buffer): TftpPacket {\n if (packet.length < 2) {\n throw new Error('TFTP 数据包过短')\n }\n const opcode = packet.readUInt16BE(0)\n\n switch (opcode) {\n case Opcode.DATA:\n if (packet.length < 4) throw new Error('无效的 DATA 数据包')\n return { opcode, block: packet.readUInt16BE(2), data: packet.subarray(4) }\n case Opcode.ACK:\n if (packet.length !== 4) throw new Error('无效的 ACK 数据包')\n return { opcode, block: packet.readUInt16BE(2) }\n case Opcode.ERROR: {\n if (packet.length < 5 || packet[packet.length - 1] !== 0) {\n throw new Error('无效的 ERROR 数据包')\n }\n return {\n opcode,\n code: packet.readUInt16BE(2),\n message: packet.toString('utf8', 4, packet.length - 1),\n }\n }\n case Opcode.OACK:\n return { opcode, options: parsePairs(packet, 2) }\n default:\n throw new Error(`不支持的 TFTP 操作码: ${opcode}`)\n }\n}\n\nexport function parseNegotiatedBlockSize (options: Record<string, string>, fallback = 512): number {\n if (!options.blksize) return fallback\n const value = Number(options.blksize)\n if (!Number.isInteger(value) || value < 8 || value > 65464) {\n throw new Error(`服务端返回了无效的块大小: ${options.blksize}`)\n }\n return value\n}\n\nexport function parseTransferSize (options: Record<string, string>): number | undefined {\n if (options.tsize === undefined) return undefined\n const value = Number(options.tsize)\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new Error(`服务端返回了无效的文件大小: ${options.tsize}`)\n }\n return value\n}\n","import * as dgram from 'dgram'\nimport { RemoteInfo, Socket } from 'dgram'\nimport * as fs from 'fs'\nimport type { FileHandle } from 'fs/promises'\nimport * as path from 'path'\n\nimport {\n Opcode,\n TftpPacket,\n createData,\n createError,\n parsePacket,\n} from './protocol'\n\nexport interface TftpServerOptions {\n rootDirectory: string\n bindAddress?: string\n port?: number\n blockSize?: number\n timeoutMs?: number\n retries?: number\n autoStopSeconds?: number\n}\n\ninterface NormalizedServerOptions {\n rootDirectory: string\n bindAddress: string\n port: number\n blockSize: number\n timeoutMs: number\n retries: number\n autoStopSeconds: number\n}\n\nexport type TftpServerEventType = 'started' | 'stopped' | 'request' | 'progress' | 'completed' | 'error'\n\nexport interface TftpServerEvent {\n type: TftpServerEventType\n timestamp: number\n message: string\n peer?: string\n filename?: string\n bytes?: number\n activeTransfers: number\n}\n\ninterface TftpRequest {\n opcode: Opcode.RRQ | Opcode.WRQ\n filename: string\n mode: string\n options: Record<string, string>\n}\n\nfunction readZeroTerminatedFields (packet: Buffer, offset: number): string[] {\n const fields: string[] = []\n let start = offset\n for (let i = offset; i < packet.length; i++) {\n if (packet[i] === 0) {\n fields.push(packet.toString('utf8', start, i))\n start = i + 1\n }\n }\n if (start !== packet.length) throw new Error('请求字段缺少结束符')\n return fields\n}\n\nexport function parseRequest (packet: Buffer): TftpRequest {\n if (packet.length < 4) throw new Error('TFTP 请求过短')\n const opcode = packet.readUInt16BE(0)\n if (opcode !== Opcode.RRQ && opcode !== Opcode.WRQ) throw new Error('不是有效的读写请求')\n\n const fields = readZeroTerminatedFields(packet, 2)\n if (fields.length < 2 || (fields.length - 2) % 2 !== 0) throw new Error('TFTP 请求字段无效')\n const filename = fields[0]\n const mode = fields[1].toLowerCase()\n if (!filename) throw new Error('请求文件名为空')\n if (mode !== 'octet') throw new Error(`不支持的传输模式: ${mode}`)\n\n const options: Record<string, string> = {}\n for (let i = 2; i < fields.length; i += 2) {\n options[fields[i].toLowerCase()] = fields[i + 1]\n }\n return { opcode, filename, mode, options }\n}\n\nfunction createOack (options: Record<string, string | number>): Buffer {\n const header = Buffer.allocUnsafe(2)\n header.writeUInt16BE(Opcode.OACK, 0)\n const chunks: Buffer[] = [header]\n for (const [key, value] of Object.entries(options)) {\n chunks.push(Buffer.from(`${key}\\0${value}\\0`, 'utf8'))\n }\n return Buffer.concat(chunks)\n}\n\nfunction normalizeOptions (options: TftpServerOptions): NormalizedServerOptions {\n const rootDirectory = path.resolve(options.rootDirectory.trim())\n const bindAddress = (options.bindAddress ?? '0.0.0.0').trim()\n const port = options.port ?? 69\n const blockSize = options.blockSize ?? 8192\n const timeoutMs = options.timeoutMs ?? 3000\n const retries = options.retries ?? 5\n const autoStopSeconds = options.autoStopSeconds ?? 0\n\n if (!options.rootDirectory.trim()) throw new Error('请选择 TFTP 根目录')\n if (!bindAddress) throw new Error('监听地址不能为空')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('监听端口必须在 1 到 65535 之间')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP 缓冲区必须在 8 到 65464 字节之间')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('超时时间必须在 250 到 60000 毫秒之间')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('重试次数必须在 0 到 20 之间')\n if (!Number.isInteger(autoStopSeconds) || autoStopSeconds < 0 || autoStopSeconds > 86400) throw new Error('自动停止时间必须在 0 到 86400 秒之间')\n\n return { rootDirectory, bindAddress, port, blockSize, timeoutMs, retries, autoStopSeconds }\n}\n\nfunction resolveRequestedFile (rootDirectory: string, requestedFilename: string): string {\n const normalized = requestedFilename.replace(/\\\\/g, '/').replace(/^\\/+/, '')\n const segments = normalized.split('/').filter(segment => segment && segment !== '.')\n if (!segments.length || segments.some(segment => segment === '..')) {\n throw new Error('拒绝不安全的文件路径')\n }\n\n const target = path.resolve(rootDirectory, ...segments)\n const relative = path.relative(rootDirectory, target)\n if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {\n throw new Error('请求文件超出 TFTP 根目录')\n }\n return target\n}\n\nabstract class ServerTransfer {\n protected readonly socket: Socket\n protected blockSize = 512\n protected timeoutMs: number\n protected transferred = 0\n\n private lastPacket: Buffer | null = null\n private timer: ReturnType<typeof setTimeout> | null = null\n private retryCount = 0\n private settled = false\n private failing = false\n private queue = Promise.resolve()\n\n protected constructor (\n protected readonly request: TftpRequest,\n protected readonly remote: RemoteInfo,\n protected readonly options: NormalizedServerOptions,\n private readonly onFinish: (successful: boolean, message: string, bytes: number) => void,\n ) {\n this.timeoutMs = options.timeoutMs\n this.socket = dgram.createSocket('udp4')\n }\n\n start (): void {\n this.socket.on('error', error => this.fail(error.message))\n this.socket.on('message', (message, remote) => {\n this.queue = this.queue\n .then(() => this.handleMessage(message, remote))\n .catch(error => this.fail(error instanceof Error ? error.message : String(error)))\n })\n this.socket.bind(0, this.options.bindAddress, () => {\n this.begin().catch(error => this.fail(error instanceof Error ? error.message : String(error)))\n })\n }\n\n resendLast (): void {\n if (this.lastPacket) this.sendTracked(this.lastPacket)\n }\n\n cancel (): void {\n this.finish(false, '服务端已停止')\n }\n\n protected abstract begin (): Promise<void>\n protected abstract onPacket (packet: TftpPacket): Promise<void>\n protected abstract release (successful: boolean): Promise<void>\n\n protected sendTracked (packet: Buffer): void {\n if (this.settled) return\n this.lastPacket = Buffer.from(packet)\n if (this.timer) clearTimeout(this.timer)\n this.socket.send(packet, this.remote.port, this.remote.address, error => {\n if (error) this.fail(error.message)\n })\n this.timer = setTimeout(() => this.retry(), this.timeoutMs)\n }\n\n protected clearTimer (): void {\n if (this.timer) clearTimeout(this.timer)\n this.timer = null\n this.retryCount = 0\n }\n\n protected complete (message: string): void {\n this.finish(true, message)\n }\n\n protected fail (message: string): void {\n if (this.settled || this.failing) return\n this.failing = true\n if (this.timer) clearTimeout(this.timer)\n const errorPacket = createError(1, message)\n this.socket.send(errorPacket, this.remote.port, this.remote.address, () => this.finish(false, message))\n }\n\n private async handleMessage (message: Buffer, remote: RemoteInfo): Promise<void> {\n if (this.settled) return\n if (remote.address !== this.remote.address || remote.port !== this.remote.port) {\n const errorPacket = createError(5, 'Unknown transfer ID')\n this.socket.send(errorPacket, remote.port, remote.address)\n return\n }\n const packet = parsePacket(message)\n if (packet.opcode === Opcode.ERROR) throw new Error(`客户端错误 ${packet.code}: ${packet.message}`)\n await this.onPacket(packet)\n }\n\n private retry (): void {\n if (this.settled) return\n if (!this.lastPacket || this.retryCount >= this.options.retries) {\n this.fail(`等待客户端响应超时,已重试 ${this.options.retries} 次`)\n return\n }\n this.retryCount++\n this.socket.send(this.lastPacket, this.remote.port, this.remote.address, error => {\n if (error) this.fail(error.message)\n })\n this.timer = setTimeout(() => this.retry(), this.timeoutMs)\n }\n\n private finish (successful: boolean, message: string): void {\n if (this.settled) return\n this.settled = true\n if (this.timer) clearTimeout(this.timer)\n this.release(successful)\n .catch(() => undefined)\n .finally(() => {\n try { this.socket.close() } catch { }\n this.onFinish(successful, message, this.transferred)\n })\n }\n}\n\nclass ReadTransfer extends ServerTransfer {\n private file: FileHandle | null = null\n private fileSize = 0\n private position = 0\n private currentBlock = 0\n private currentData = Buffer.alloc(0)\n private finalBlock = false\n\n constructor (\n request: TftpRequest,\n remote: RemoteInfo,\n options: NormalizedServerOptions,\n onFinish: (successful: boolean, message: string, bytes: number) => void,\n ) {\n super(request, remote, options, onFinish)\n }\n\n protected async begin (): Promise<void> {\n const filePath = resolveRequestedFile(this.options.rootDirectory, this.request.filename)\n const stat = await fs.promises.stat(filePath)\n if (!stat.isFile()) throw new Error('请求路径不是普通文件')\n this.fileSize = stat.size\n this.file = await fs.promises.open(filePath, 'r')\n\n const negotiated: Record<string, string | number> = {}\n if (this.request.options.blksize !== undefined) {\n const requested = Number(this.request.options.blksize)\n if (!Number.isInteger(requested) || requested < 8 || requested > 65464) throw new Error('客户端请求了无效的块大小')\n this.blockSize = Math.min(requested, this.options.blockSize)\n negotiated.blksize = this.blockSize\n }\n if (this.request.options.timeout !== undefined) {\n const timeoutSeconds = Number(this.request.options.timeout)\n if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 255) throw new Error('客户端请求了无效的超时时间')\n this.timeoutMs = timeoutSeconds * 1000\n negotiated.timeout = timeoutSeconds\n }\n if (this.request.options.tsize !== undefined) negotiated.tsize = this.fileSize\n\n if (Object.keys(negotiated).length) {\n this.sendTracked(createOack(negotiated))\n } else {\n await this.sendNextBlock()\n }\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode !== Opcode.ACK) return\n if (this.currentBlock === 0 && packet.block === 0) {\n this.clearTimer()\n await this.sendNextBlock()\n return\n }\n if (packet.block === this.currentBlock) {\n this.clearTimer()\n this.transferred += this.currentData.length\n if (this.finalBlock) this.complete(`已发送 ${this.transferred} 字节`)\n else await this.sendNextBlock()\n return\n }\n if (packet.block === ((this.currentBlock - 1) & 0xffff)) this.resendLast()\n }\n\n protected async release (): Promise<void> {\n if (this.file) await this.file.close()\n this.file = null\n }\n\n private async sendNextBlock (): Promise<void> {\n const file = this.file\n if (!file) throw new Error('TFTP 文件尚未打开')\n const buffer = Buffer.allocUnsafe(this.blockSize)\n const result = await file.read(buffer, 0, this.blockSize, this.position)\n this.currentData = Buffer.from(buffer.subarray(0, result.bytesRead))\n this.position += result.bytesRead\n this.currentBlock = (this.currentBlock + 1) & 0xffff\n this.finalBlock = result.bytesRead < this.blockSize\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n}\n\nexport class TftpServer {\n private listener: Socket | null = null\n private options: NormalizedServerOptions | null = null\n private readonly transfers = new Map<string, ServerTransfer>()\n private autoStopTimer: ReturnType<typeof setTimeout> | null = null\n private eventHandler: ((event: TftpServerEvent) => void) | null = null\n\n get running (): boolean {\n return this.listener !== null\n }\n\n get activeTransfers (): number {\n return this.transfers.size\n }\n\n async start (serverOptions: TftpServerOptions, onEvent?: (event: TftpServerEvent) => void): Promise<void> {\n if (this.running) throw new Error('TFTP 服务端已经在运行')\n const options = normalizeOptions(serverOptions)\n const stat = await fs.promises.stat(options.rootDirectory)\n if (!stat.isDirectory()) throw new Error('TFTP 根目录无效')\n\n this.options = options\n this.eventHandler = onEvent ?? null\n const listener = dgram.createSocket('udp4')\n this.listener = listener\n listener.on('message', (message, remote) => this.handleRequest(message, remote))\n\n let rejectBinding: ((error: Error) => void) | null = null\n listener.on('error', error => {\n if (rejectBinding) {\n const reject = rejectBinding\n rejectBinding = null\n try { listener.close() } catch { }\n this.listener = null\n reject(error)\n } else {\n this.emit('error', error.message)\n }\n })\n await new Promise<void>((resolve, reject) => {\n rejectBinding = reject\n listener.bind(options.port, options.bindAddress, () => {\n rejectBinding = null\n resolve()\n })\n })\n\n this.emit('started', `TFTP 服务端已启动:${options.bindAddress}:${options.port}`)\n if (options.autoStopSeconds > 0) {\n this.autoStopTimer = setTimeout(() => this.stop('达到自动停止时间'), options.autoStopSeconds * 1000)\n }\n }\n\n stop (reason = '用户停止'): void {\n if (!this.listener) return\n if (this.autoStopTimer) clearTimeout(this.autoStopTimer)\n this.autoStopTimer = null\n for (const transfer of this.transfers.values()) transfer.cancel()\n this.transfers.clear()\n try { this.listener.close() } catch { }\n this.listener = null\n this.emit('stopped', `TFTP 服务端已停止:${reason}`)\n }\n\n private handleRequest (message: Buffer, remote: RemoteInfo): void {\n if (!this.listener || !this.options) return\n let request: TftpRequest\n try {\n request = parseRequest(message)\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n this.listener.send(createError(4, reason), remote.port, remote.address)\n this.emit('error', `拒绝来自 ${remote.address}:${remote.port} 的无效请求:${reason}`)\n return\n }\n\n const key = `${remote.address}:${remote.port}:${request.opcode}:${request.filename}`\n const existing = this.transfers.get(key)\n if (existing) {\n existing.resendLast()\n return\n }\n\n if (request.opcode === Opcode.WRQ) {\n this.listener.send(createError(2, 'Server is read-only'), remote.port, remote.address)\n this.emit('error', `拒绝 ${remote.address}:${remote.port} 上传 ${request.filename}:服务端为只读模式`, remote, request.filename)\n return\n }\n\n const peer = `${remote.address}:${remote.port}`\n const transfer = new ReadTransfer(request, remote, this.options, (successful, detail, bytes) => {\n this.transfers.delete(key)\n this.emit(successful ? 'completed' : 'error', `${peer} ${successful ? '下载完成' : '下载失败'} ${request.filename}:${detail}`, remote, request.filename, bytes)\n })\n this.transfers.set(key, transfer)\n this.emit('request', `${peer} 请求下载 ${request.filename}`, remote, request.filename)\n transfer.start()\n }\n\n private emit (type: TftpServerEventType, message: string, remote?: RemoteInfo, filename?: string, bytes?: number): void {\n this.eventHandler?.({\n type,\n timestamp: Date.now(),\n message,\n peer: remote ? `${remote.address}:${remote.port}` : undefined,\n filename,\n bytes,\n activeTransfers: this.transfers.size,\n })\n }\n}\n","module.exports = require(\"@angular/common\");","module.exports = require(\"@angular/core\");","module.exports = require(\"@angular/forms\");","module.exports = require(\"@electron/remote\");","module.exports = require(\"@ng-bootstrap/ng-bootstrap\");","module.exports = require(\"dgram\");","module.exports = require(\"fs\");","module.exports = require(\"os\");","module.exports = require(\"path\");","module.exports = require(\"rxjs\");","module.exports = require(\"tabby-core\");","// The module cache\nconst __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tconst cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tconst module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nlet __webpack_exports__ = __webpack_require__(156);\n"],"names":["core_1","__webpack_require__","ng_bootstrap_1","tabby_core_1","tftpModal_component_1","TftpButtonProvider","ToolbarButtonProvider","constructor","modal","super","this","provide","icon","title","weight","click","open","TftpModalComponent","size","exports","__decorate","Injectable","NgbModal","config_1","tftp_service_1","activeModal","config","tftp","zone","changeDetector","mode","settings","defaultTftpSettings","server","defaultTftpServerSettings","status","validationError","busy","percent","state","undefined","total","Math","min","round","transferred","progressType","_a","_b","_c","ngOnInit","stored","store","subscription","progress$","subscribe","run","markForCheck","ngOnDestroy","unsubscribe","upload","validate","execute","download","cancel","formatBytes","value","Number","isFinite","units","index","length","floor","log","amount","pow","toFixed","operation","Object","assign","host","port","remoteFilename","blockSize","timeoutMs","retries","save","error","Error","message","String","requireRemoteFilename","trim","isInteger","Component","selector","template","styles","NgbActiveModal","ConfigService","TftpService","NgZone","ChangeDetectorRef","tftpServer_service_1","serverSettingsStorageKey","TftpServerComponent","running","activeTransfers","logs","localAddresses","starting","cached","JSON","parse","localStorage","getItem","_d","activeSettings","getLocalIPv4Addresses","state$","chooseDirectory","directory","chooseRootDirectory","rootDirectory","saveSettings","start","formatTime","timestamp","Date","toLocaleTimeString","hour12","autoStop","autoStopSeconds","setItem","stringify","TftpServerService","bindAddress","notifyDownloads","TftpConfigProvider","ConfigProvider","defaults","platformDefaults","common_1","forms_1","__importStar","buttonProvider_1","tftpServer_component_1","TftpModule","NgModule","imports","CommonModule","FormsModule","NgbModule","default","declarations","providers","useClass","multi","__exportStar","rxjs_1","client_1","idleProgress","direction","speed","TabbyUploadSource","pending","Buffer","alloc","ended","getSize","read","maximumBytes","chunk","concat","from","result","subarray","close","abort","isCancelled","TabbyDownloadSink","sizeKnown","completed","getCompletedBytes","max","write","data","platform","notifications","BehaviorSubject","controller","client","TftpClient","ensureIdle","startUpload","multiple","getName","source","toClientOptions","signal","progress","next","localName","split","filter","Boolean","pop","async","startDownload","filename","AbortController","notice","reason","name","info","PlatformService","NotificationsService","os","sharedServer","TftpServer","sharedState$","sharedSettings","addresses","Set","interfaces","networkInterfaces","entries","values","entry","family","internal","add","address","Array","sort","remote","dialog","showOpenDialog","properties","canceled","filePaths","openRootDirectory","openPath","copyAddress","setClipboard","text","event","handleEvent","appendLog","stop","clearLogs","level","type","current","startedAt","slice","now","dgram","protocol_1","normalizeOptions","options","abortError","TransferSession","onProgress","negotiatedBlockSize","endpoint","lastPacket","lastTarget","retryCount","retryTimer","cancelPoll","settled","queue","Promise","resolve","socket","createSocket","runSession","initialPacket","reject","resolvePromise","rejectPromise","on","fail","then","handleMessage","catch","addEventListener","once","setInterval","resourceCancelled","emit","sendTracked","clearRetryTimer","clearTimeout","packet","explicitTarget","target","send","setTimeout","retryLastPacket","sendUntracked","sent","duration","call","attempt","succeed","cleanupTimers","releaseResource","bytes","durationMs","aborted","createError","parsePacket","opcode","Opcode","ERROR","code","onPacket","clearInterval","DownloadSession","sinkFactory","expectedBlock","sink","timeoutSeconds","createRequest","RRQ","blksize","timeout","tsize","OACK","parseNegotiatedBlockSize","totalSize","parseTransferSize","ensureSink","createAck","DATA","block","finalBlock","acknowledgement","previousBlock","successful","UploadSession","nextBlock","currentBlock","currentData","currentFinal","WRQ","sendNextBlock","ACK","createData","zeroTerminated","includes","parsePairs","offset","i","push","toString","toLowerCase","header","allocUnsafe","writeUInt16BE","parts","key","rawValue","copy","readUInt16BE","fallback","isSafeInteger","parseRequest","fs","path","fields","readZeroTerminatedFields","ServerTransfer","request","onFinish","timer","failing","bind","begin","resendLast","finish","retry","clearTimer","complete","errorPacket","release","finally","ReadTransfer","file","fileSize","position","filePath","requestedFilename","segments","replace","segment","some","relative","startsWith","isAbsolute","resolveRequestedFile","stat","promises","isFile","negotiated","requested","keys","chunks","createOack","buffer","bytesRead","listener","transfers","Map","autoStopTimer","eventHandler","serverOptions","onEvent","_e","_f","isDirectory","handleRequest","rejectBinding","transfer","clear","existing","get","peer","detail","delete","set","module","require","__webpack_module_cache__","__webpack_exports__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""}
|
|
1
|
+
{"version":3,"file":"index.js","mappings":"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","sources":["webpack://tabby-tftp/./src/buttonProvider.ts","webpack://tabby-tftp/./src/components/tftpModal.component.ts","webpack://tabby-tftp/./src/components/tftpServer.component.ts","webpack://tabby-tftp/./src/config.ts","webpack://tabby-tftp/./src/index.ts","webpack://tabby-tftp/./src/services/tftp.service.ts","webpack://tabby-tftp/./src/services/tftpServer.service.ts","webpack://tabby-tftp/./src/tftp/client.ts","webpack://tabby-tftp/./src/tftp/protocol.ts","webpack://tabby-tftp/./src/tftp/server.ts","webpack://tabby-tftp/external commonjs \"@angular/common\"","webpack://tabby-tftp/external commonjs \"@angular/core\"","webpack://tabby-tftp/external commonjs \"@angular/forms\"","webpack://tabby-tftp/external commonjs \"@electron/remote\"","webpack://tabby-tftp/external commonjs \"@ng-bootstrap/ng-bootstrap\"","webpack://tabby-tftp/external commonjs \"dgram\"","webpack://tabby-tftp/external commonjs \"fs\"","webpack://tabby-tftp/external commonjs \"os\"","webpack://tabby-tftp/external commonjs \"path\"","webpack://tabby-tftp/external commonjs \"rxjs\"","webpack://tabby-tftp/external commonjs \"tabby-core\"","webpack://tabby-tftp/webpack/bootstrap","webpack://tabby-tftp/webpack/startup"],"sourcesContent":["import { Injectable } from '@angular/core'\nimport { NgbModal } from '@ng-bootstrap/ng-bootstrap'\nimport { IToolbarButton, ToolbarButtonProvider } from 'tabby-core'\n\nimport { TftpModalComponent } from './components/tftpModal.component'\n\nconst transferIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7 7h11l-3.5-3.5L16 2l6 6-6 6-1.5-1.5L18 9H7V7zm10 10H6l3.5 3.5L8 22l-6-6 6-6 1.5 1.5L6 15h11v2z\"/></svg>`\n\n@Injectable()\nexport class TftpButtonProvider extends ToolbarButtonProvider {\n constructor (private readonly modal: NgbModal) {\n super()\n }\n\n provide (): IToolbarButton[] {\n return [{\n icon: transferIcon,\n title: 'TFTP 文件传输',\n weight: 6,\n click: () => this.modal.open(TftpModalComponent, { size: 'lg' }),\n }]\n }\n}\n","import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from '@angular/core'\nimport { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'\nimport { Subscription } from 'rxjs'\nimport { ConfigService } from 'tabby-core'\n\nimport { defaultTftpServerSettings, defaultTftpSettings, TftpPluginSettings } from '../config'\nimport { TftpService } from '../services/tftp.service'\nimport { TftpProgress } from '../tftp/client'\n\n@Component({\n selector: 'tabby-tftp-modal',\n template: `\n <div class=\"modal-header\">\n <h5 class=\"modal-title\"><i class=\"fa fa-exchange-alt mr-2\"></i>TFTP 文件传输与固件服务</h5>\n <button type=\"button\" class=\"close\" aria-label=\"关闭\" (click)=\"activeModal.dismiss()\">\n <span aria-hidden=\"true\">×</span>\n </button>\n </div>\n\n <div class=\"mode-tabs px-3 pt-3\">\n <div class=\"btn-group w-100\">\n <button class=\"btn\" [class.btn-primary]=\"mode === 'server'\" [class.btn-outline-secondary]=\"mode !== 'server'\" (click)=\"mode = 'server'\">\n <i class=\"fa fa-server mr-1\"></i>固件 TFTP 服务端\n </button>\n <button class=\"btn\" [class.btn-primary]=\"mode === 'client'\" [class.btn-outline-secondary]=\"mode !== 'client'\" (click)=\"mode = 'client'\">\n <i class=\"fa fa-exchange-alt mr-1\"></i>TFTP 客户端\n </button>\n </div>\n </div>\n\n <div class=\"modal-body\" *ngIf=\"mode === 'server'\">\n <tabby-tftp-server></tabby-tftp-server>\n </div>\n\n <div class=\"modal-body\" *ngIf=\"mode === 'client'\">\n <div class=\"alert alert-warning py-2 small\">\n TFTP 没有身份认证和加密。请仅在可信局域网中使用。\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-8\">\n <label>主机或 IP 地址</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.host\" placeholder=\"例如 192.168.1.10\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>端口</label>\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"65535\" [(ngModel)]=\"settings.port\" [disabled]=\"busy\">\n </div>\n </div>\n\n <div class=\"form-group\">\n <label>远端文件名</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.remoteFilename\"\n placeholder=\"下载时必填;上传留空则使用本地文件名\" [disabled]=\"busy\">\n <small class=\"form-text text-muted\">TFTP 不支持浏览远端目录,路径格式由服务端决定。</small>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">高级选项</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-4\">\n <label>块大小</label>\n <input class=\"form-control\" type=\"number\" min=\"8\" max=\"65464\" [(ngModel)]=\"settings.blockSize\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>超时(毫秒)</label>\n <input class=\"form-control\" type=\"number\" min=\"250\" max=\"60000\" [(ngModel)]=\"settings.timeoutMs\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>重试次数</label>\n <input class=\"form-control\" type=\"number\" min=\"0\" max=\"20\" [(ngModel)]=\"settings.retries\" [disabled]=\"busy\">\n </div>\n </div>\n </details>\n\n <div class=\"transfer-status\" *ngIf=\"status\">\n <div class=\"d-flex align-items-center mb-1\">\n <strong>{{ status.direction === 'upload' ? '上传' : '下载' }}</strong>\n <span class=\"ml-auto text-muted\">{{ formatBytes(status.transferred) }}<span *ngIf=\"status.total !== undefined\"> / {{ formatBytes(status.total) }}</span></span>\n </div>\n <ngb-progressbar [type]=\"progressType\" [value]=\"percent\" [striped]=\"busy\" [animated]=\"busy\"></ngb-progressbar>\n <div class=\"d-flex small mt-1\">\n <span [class.text-danger]=\"status.state === 'error'\">{{ status.message }}</span>\n <span class=\"ml-auto\" *ngIf=\"status.speed > 0\">{{ formatBytes(status.speed) }}/s</span>\n </div>\n </div>\n <div class=\"text-danger mt-2\" *ngIf=\"validationError\">{{ validationError }}</div>\n </div>\n\n <div class=\"modal-footer\">\n <button class=\"btn btn-outline-secondary\" (click)=\"activeModal.close()\">关闭</button>\n <ng-container *ngIf=\"mode === 'client'\">\n <button class=\"btn btn-outline-danger\" *ngIf=\"busy\" (click)=\"cancel()\"><i class=\"fa fa-stop mr-1\"></i>取消传输</button>\n <button class=\"btn btn-primary\" *ngIf=\"!busy\" (click)=\"upload()\"><i class=\"fa fa-upload mr-1\"></i>上传</button>\n <button class=\"btn btn-success\" *ngIf=\"!busy\" (click)=\"download()\"><i class=\"fa fa-download mr-1\"></i>下载</button>\n </ng-container>\n </div>\n `,\n styles: [`\n :host { display: block; }\n summary { cursor: pointer; user-select: none; }\n .transfer-status { border: 1px solid rgba(127, 127, 127, .25); border-radius: .25rem; padding: .75rem; }\n .modal-body label { margin-bottom: .25rem; }\n .mode-tabs .btn { flex: 1; }\n `],\n})\nexport class TftpModalComponent implements OnInit, OnDestroy {\n mode: 'server' | 'client' = 'server'\n settings: TftpPluginSettings = { ...defaultTftpSettings, server: { ...defaultTftpServerSettings } }\n status: TftpProgress | null = null\n validationError = ''\n private subscription?: Subscription\n\n constructor (\n public readonly activeModal: NgbActiveModal,\n private readonly config: ConfigService,\n public readonly tftp: TftpService,\n private readonly zone: NgZone,\n private readonly changeDetector: ChangeDetectorRef,\n ) { }\n\n get busy (): boolean { return this.tftp.busy }\n\n get percent (): number {\n if (!this.status) return 0\n if (this.status.state === 'completed') return 100\n if (this.status.total === undefined) return this.busy ? 100 : 0\n if (this.status.total === 0) return 100\n return Math.min(100, Math.round(this.status.transferred * 100 / this.status.total))\n }\n\n get progressType (): string {\n if (this.status?.state === 'error' || this.status?.state === 'cancelled') return 'danger'\n if (this.status?.state === 'completed') return 'success'\n return 'info'\n }\n\n ngOnInit (): void {\n const stored = this.config.store.tftp ?? {}\n this.settings = {\n ...defaultTftpSettings,\n ...stored,\n server: { ...defaultTftpServerSettings, ...(stored.server ?? {}) },\n }\n this.subscription = this.tftp.progress$.subscribe(status => {\n this.zone.run(() => {\n this.status = status\n this.changeDetector.markForCheck()\n })\n })\n }\n\n ngOnDestroy (): void { this.subscription?.unsubscribe() }\n\n async upload (): Promise<void> {\n if (!this.validate(false)) return\n await this.execute(() => this.tftp.upload(this.settings))\n }\n\n async download (): Promise<void> {\n if (!this.validate(true)) return\n await this.execute(() => this.tftp.download(this.settings))\n }\n\n cancel (): void { this.tftp.cancel() }\n\n formatBytes (value: number): string {\n if (!Number.isFinite(value) || value <= 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n const index = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024)))\n const amount = value / Math.pow(1024, index)\n return `${amount >= 100 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`\n }\n\n private async execute (operation: () => Promise<unknown>): Promise<void> {\n this.validationError = ''\n if (!this.config.store.tftp) this.config.store.tftp = {}\n Object.assign(this.config.store.tftp, {\n host: this.settings.host,\n port: this.settings.port,\n remoteFilename: this.settings.remoteFilename,\n blockSize: this.settings.blockSize,\n timeoutMs: this.settings.timeoutMs,\n retries: this.settings.retries,\n })\n await this.config.save()\n try { await operation() } catch (error) {\n this.validationError = error instanceof Error ? error.message : String(error)\n }\n }\n\n private validate (requireRemoteFilename: boolean): boolean {\n this.validationError = ''\n if (!this.settings.host.trim()) this.validationError = '请填写 TFTP 主机地址'\n else if (requireRemoteFilename && !this.settings.remoteFilename.trim()) this.validationError = '下载时必须填写远端文件名'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.validationError = '端口必须在 1 到 65535 之间'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.validationError = '块大小必须在 8 到 65464 之间'\n else if (!Number.isInteger(Number(this.settings.timeoutMs)) || Number(this.settings.timeoutMs) < 250 || Number(this.settings.timeoutMs) > 60000) this.validationError = '超时时间必须在 250 到 60000 毫秒之间'\n else if (!Number.isInteger(Number(this.settings.retries)) || Number(this.settings.retries) < 0 || Number(this.settings.retries) > 20) this.validationError = '重试次数必须在 0 到 20 之间'\n return !this.validationError\n }\n}\n","import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from '@angular/core'\nimport { Subscription } from 'rxjs'\nimport { ConfigService } from 'tabby-core'\n\nimport { defaultTftpServerSettings, TftpServerSettings } from '../config'\nimport { TftpServerService, TftpServerState } from '../services/tftpServer.service'\n\nconst serverSettingsStorageKey = 'tabby-tftp-server-settings-v1'\n\n@Component({\n selector: 'tabby-tftp-server',\n template: `\n <div class=\"alert alert-info py-2 small\">\n 串口负责给 Bootloader 下命令;本服务端通过网口向设备提供固件。服务端为只读模式,设备不能覆盖电脑文件。\n </div>\n\n <div class=\"form-group\">\n <label>TFTP 根目录</label>\n <div class=\"input-group\">\n <input class=\"form-control\" [(ngModel)]=\"settings.rootDirectory\" placeholder=\"固件所在目录\" [disabled]=\"state.running\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary\" (click)=\"chooseDirectory()\" [disabled]=\"state.running\" title=\"选择目录\">\n <i class=\"fa fa-folder-open\"></i>\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"server.openRootDirectory(settings.rootDirectory)\" [disabled]=\"!settings.rootDirectory\" title=\"打开目录\">\n <i class=\"fa fa-external-link-alt\"></i>\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-5\">\n <label>监听地址</label>\n <select class=\"form-control\" [(ngModel)]=\"settings.bindAddress\" [disabled]=\"state.running\">\n <option value=\"0.0.0.0\">0.0.0.0(所有网卡)</option>\n <option *ngFor=\"let address of localAddresses\" [value]=\"address\">{{ address }}</option>\n </select>\n </div>\n <div class=\"form-group col-md-3\">\n <label>监听端口</label>\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"65535\" [(ngModel)]=\"settings.port\" [disabled]=\"state.running\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>TFTP 缓冲区</label>\n <div class=\"input-group\">\n <input class=\"form-control\" type=\"number\" min=\"8\" max=\"65464\" [(ngModel)]=\"settings.blockSize\" [disabled]=\"state.running\">\n <div class=\"input-group-append\"><span class=\"input-group-text\">Bytes</span></div>\n </div>\n </div>\n </div>\n\n <div class=\"local-addresses mb-3\">\n <span class=\"text-muted mr-2\">设备可访问的本机 IP:</span>\n <button class=\"btn btn-sm btn-outline-info mr-1\" *ngFor=\"let address of localAddresses\" (click)=\"server.copyAddress(address)\" title=\"点击复制\">\n {{ address }}\n </button>\n <span class=\"text-warning small\" *ngIf=\"!localAddresses.length\">未检测到可用 IPv4 地址</span>\n </div>\n\n <div class=\"form-row align-items-center mb-3\">\n <div class=\"col-md-5\">\n <div class=\"custom-control custom-checkbox\">\n <input type=\"checkbox\" class=\"custom-control-input\" id=\"tftp-auto-stop\" [(ngModel)]=\"settings.autoStop\" [disabled]=\"state.running\">\n <label class=\"custom-control-label\" for=\"tftp-auto-stop\">自动停止服务端</label>\n </div>\n </div>\n <div class=\"col-md-3\">\n <div class=\"input-group input-group-sm\">\n <input class=\"form-control\" type=\"number\" min=\"1\" max=\"86400\" [(ngModel)]=\"settings.autoStopSeconds\" [disabled]=\"state.running || !settings.autoStop\">\n <div class=\"input-group-append\"><span class=\"input-group-text\">秒</span></div>\n </div>\n </div>\n <div class=\"col-md-4\">\n <div class=\"custom-control custom-checkbox\">\n <input type=\"checkbox\" class=\"custom-control-input\" id=\"tftp-notify\" [(ngModel)]=\"settings.notifyDownloads\">\n <label class=\"custom-control-label\" for=\"tftp-notify\">下载完成后通知</label>\n </div>\n </div>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">可靠性选项</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-6\">\n <label>响应超时(毫秒)</label>\n <input class=\"form-control\" type=\"number\" min=\"250\" max=\"60000\" [(ngModel)]=\"settings.timeoutMs\" [disabled]=\"state.running\">\n </div>\n <div class=\"form-group col-md-6\">\n <label>重试次数</label>\n <input class=\"form-control\" type=\"number\" min=\"0\" max=\"20\" [(ngModel)]=\"settings.retries\" [disabled]=\"state.running\">\n </div>\n </div>\n </details>\n\n <div class=\"d-flex align-items-center mb-2\">\n <span class=\"status-dot mr-2\" [class.running]=\"state.running\"></span>\n <strong>{{ state.running ? 'TFTP 服务端运行中' : 'TFTP 服务端已停止' }}</strong>\n <span class=\"badge badge-info ml-2\" *ngIf=\"state.activeTransfers\">{{ state.activeTransfers }} 个传输</span>\n <div class=\"ml-auto\">\n <button class=\"btn btn-sm btn-link text-muted\" (click)=\"server.clearLogs()\" [disabled]=\"!state.logs.length\">清空日志</button>\n <button class=\"btn btn-success ml-1\" *ngIf=\"!state.running\" (click)=\"start()\" [disabled]=\"starting\">\n <i class=\"fa fa-play mr-1\"></i>{{ starting ? '启动中…' : '启动服务端' }}\n </button>\n <button class=\"btn btn-danger ml-1\" *ngIf=\"state.running\" (click)=\"server.stop()\">\n <i class=\"fa fa-stop mr-1\"></i>停止服务端\n </button>\n </div>\n </div>\n\n <div class=\"server-log\">\n <div *ngIf=\"!state.logs.length\" class=\"text-muted\">服务端输出将显示在这里</div>\n <div *ngFor=\"let log of state.logs\" [class.text-success]=\"log.level === 'success'\" [class.text-danger]=\"log.level === 'error'\">\n [{{ formatTime(log.timestamp) }}] {{ log.message }}\n </div>\n </div>\n <div class=\"text-danger mt-2\" *ngIf=\"error\">{{ error }}</div>\n `,\n styles: [`\n :host { display: block; }\n label { margin-bottom: .25rem; }\n summary { cursor: pointer; user-select: none; }\n .local-addresses { min-height: 31px; display: flex; align-items: center; flex-wrap: wrap; }\n .status-dot { width: 10px; height: 10px; border-radius: 50%; background: #6c757d; display: inline-block; }\n .status-dot.running { background: #28a745; box-shadow: 0 0 7px rgba(40, 167, 69, .8); }\n .server-log { height: 175px; overflow: auto; padding: .65rem; border: 1px solid rgba(127, 127, 127, .3); border-radius: .25rem; background: rgba(0, 0, 0, .18); font-family: Consolas, monospace; font-size: 12px; white-space: pre-wrap; }\n `],\n})\nexport class TftpServerComponent implements OnInit, OnDestroy {\n settings: TftpServerSettings = { ...defaultTftpServerSettings }\n state: TftpServerState = { running: false, activeTransfers: 0, logs: [] }\n localAddresses: string[] = []\n starting = false\n error = ''\n\n private subscription?: Subscription\n\n constructor (\n private readonly config: ConfigService,\n public readonly server: TftpServerService,\n private readonly zone: NgZone,\n private readonly changeDetector: ChangeDetectorRef,\n ) { }\n\n ngOnInit (): void {\n let cached: Partial<TftpServerSettings> = {}\n try {\n cached = JSON.parse(localStorage.getItem(serverSettingsStorageKey) ?? '{}')\n } catch { }\n this.settings = {\n ...defaultTftpServerSettings,\n ...(this.config.store.tftp?.server ?? {}),\n ...cached,\n ...(this.server.activeSettings ?? {}),\n }\n this.localAddresses = this.server.getLocalIPv4Addresses()\n this.subscription = this.server.state$.subscribe(state => {\n this.zone.run(() => {\n this.state = state\n this.changeDetector.markForCheck()\n })\n })\n }\n\n ngOnDestroy (): void {\n this.subscription?.unsubscribe()\n }\n\n async chooseDirectory (): Promise<void> {\n const directory = await this.server.chooseRootDirectory()\n if (directory) {\n this.settings.rootDirectory = directory\n await this.saveSettings()\n }\n }\n\n async start (): Promise<void> {\n this.error = ''\n if (!this.validate()) return\n this.starting = true\n await this.saveSettings()\n try {\n await this.server.start(this.settings)\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n } finally {\n this.starting = false\n }\n }\n\n formatTime (timestamp: number): string {\n return new Date(timestamp).toLocaleTimeString(undefined, { hour12: false })\n }\n\n private validate (): boolean {\n if (!this.settings.rootDirectory.trim()) this.error = '请选择固件所在的 TFTP 根目录'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.error = '监听端口必须在 1 到 65535 之间'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.error = 'TFTP 缓冲区必须在 8 到 65464 字节之间'\n else if (this.settings.autoStop && (!Number.isInteger(Number(this.settings.autoStopSeconds)) || Number(this.settings.autoStopSeconds) < 1 || Number(this.settings.autoStopSeconds) > 86400)) this.error = '自动停止时间必须在 1 到 86400 秒之间'\n return !this.error\n }\n\n private async saveSettings (): Promise<void> {\n localStorage.setItem(serverSettingsStorageKey, JSON.stringify(this.settings))\n if (!this.config.store.tftp) this.config.store.tftp = {}\n if (!this.config.store.tftp.server) this.config.store.tftp.server = {}\n Object.assign(this.config.store.tftp.server, this.settings)\n await this.config.save()\n }\n}\n","import { ConfigProvider } from 'tabby-core'\n\nexport interface TftpPluginSettings {\n host: string\n port: number\n remoteFilename: string\n blockSize: number\n timeoutMs: number\n retries: number\n server: TftpServerSettings\n}\n\nexport interface TftpServerSettings {\n rootDirectory: string\n bindAddress: string\n port: number\n blockSize: number\n timeoutMs: number\n retries: number\n autoStop: boolean\n autoStopSeconds: number\n notifyDownloads: boolean\n}\n\nexport const defaultTftpServerSettings: TftpServerSettings = {\n rootDirectory: '',\n bindAddress: '0.0.0.0',\n port: 69,\n blockSize: 8192,\n timeoutMs: 3000,\n retries: 5,\n autoStop: false,\n autoStopSeconds: 360,\n notifyDownloads: true,\n}\n\nexport const defaultTftpSettings: TftpPluginSettings = {\n host: '',\n port: 69,\n remoteFilename: '',\n blockSize: 1468,\n timeoutMs: 3000,\n retries: 5,\n server: { ...defaultTftpServerSettings },\n}\n\nexport class TftpConfigProvider extends ConfigProvider {\n defaults = {\n tftp: { ...defaultTftpSettings },\n }\n\n platformDefaults = { }\n}\n","import { CommonModule } from '@angular/common'\nimport { NgModule } from '@angular/core'\nimport { FormsModule } from '@angular/forms'\nimport { NgbModule } from '@ng-bootstrap/ng-bootstrap'\nimport TabbyCoreModule, { ConfigProvider, ToolbarButtonProvider } from 'tabby-core'\n\nimport { TftpButtonProvider } from './buttonProvider'\nimport { TftpModalComponent } from './components/tftpModal.component'\nimport { TftpServerComponent } from './components/tftpServer.component'\nimport { TftpConfigProvider } from './config'\nimport { TftpService } from './services/tftp.service'\nimport { TftpServerService } from './services/tftpServer.service'\n\n@NgModule({\n imports: [\n CommonModule,\n FormsModule,\n NgbModule,\n TabbyCoreModule,\n ],\n declarations: [\n TftpModalComponent,\n TftpServerComponent,\n ],\n providers: [\n TftpService,\n TftpServerService,\n { provide: ConfigProvider, useClass: TftpConfigProvider, multi: true },\n { provide: ToolbarButtonProvider, useClass: TftpButtonProvider, multi: true },\n ],\n})\nexport default class TftpModule { }\n\nexport * from './tftp/client'\nexport * from './tftp/protocol'\nexport * from './tftp/server'\n","import { Injectable } from '@angular/core'\nimport { BehaviorSubject } from 'rxjs'\nimport {\n FileDownload,\n FileUpload,\n NotificationsService,\n PlatformService,\n} from 'tabby-core'\n\nimport { TftpPluginSettings } from '../config'\nimport {\n DownloadSink,\n TftpClient,\n TftpProgress,\n TftpResult,\n UploadSource,\n} from '../tftp/client'\n\nconst idleProgress: TftpProgress = {\n direction: 'download',\n state: 'idle',\n transferred: 0,\n speed: 0,\n message: '准备就绪',\n}\n\nclass TabbyUploadSource implements UploadSource {\n readonly size: number\n private pending = Buffer.alloc(0)\n private ended = false\n\n constructor (private readonly upload: FileUpload) {\n this.size = upload.getSize()\n }\n\n async read (maximumBytes: number): Promise<Buffer> {\n while (!this.ended && this.pending.length < maximumBytes) {\n const chunk = await this.upload.read()\n if (!chunk.length) {\n this.ended = true\n break\n }\n this.pending = this.pending.length\n ? Buffer.concat([this.pending, Buffer.from(chunk)])\n : Buffer.from(chunk)\n }\n\n const result = Buffer.from(this.pending.subarray(0, maximumBytes))\n this.pending = this.pending.subarray(result.length)\n return result\n }\n\n close (): void {\n this.upload.close()\n }\n\n abort (): void {\n this.upload.cancel()\n }\n\n isCancelled (): boolean {\n return this.upload.isCancelled()\n }\n}\n\nclass TabbyDownloadSink implements DownloadSink {\n private completed = false\n\n constructor (private readonly download: FileDownload, sizeKnown: boolean) {\n if (!sizeKnown) {\n // Tabby requires a size before the TFTP server has necessarily told us one.\n // Keep the transfer active until close(), then report the actual byte count.\n ;(download as any).getSize = () => this.completed\n ? download.getCompletedBytes()\n : Math.max(1, download.getCompletedBytes() + 1)\n }\n }\n\n write (data: Buffer): Promise<void> {\n return this.download.write(data)\n }\n\n close (): void {\n this.completed = true\n this.download.close()\n }\n\n abort (): void {\n this.download.cancel()\n }\n\n isCancelled (): boolean {\n return this.download.isCancelled()\n }\n}\n\n@Injectable()\nexport class TftpService {\n readonly progress$ = new BehaviorSubject<TftpProgress>(idleProgress)\n\n private controller: AbortController | null = null\n private readonly client = new TftpClient()\n\n constructor (\n private readonly platform: PlatformService,\n private readonly notifications: NotificationsService,\n ) { }\n\n get busy (): boolean {\n return this.controller !== null\n }\n\n async upload (settings: TftpPluginSettings): Promise<TftpResult | null> {\n this.ensureIdle()\n const uploads = await this.platform.startUpload({ multiple: false })\n const upload = uploads[0]\n if (!upload) return null\n\n const remoteFilename = settings.remoteFilename.trim() || upload.getName()\n const source = new TabbyUploadSource(upload)\n return this.run('upload', remoteFilename, controller => this.client.upload(\n this.toClientOptions(settings, remoteFilename),\n source,\n controller.signal,\n progress => this.progress$.next(progress),\n ))\n }\n\n async download (settings: TftpPluginSettings): Promise<TftpResult | null> {\n this.ensureIdle()\n const remoteFilename = settings.remoteFilename.trim()\n if (!remoteFilename) throw new Error('下载时必须填写远端文件名')\n\n const localName = remoteFilename.split(/[\\\\/]/).filter(Boolean).pop() || 'download.bin'\n return this.run('download', remoteFilename, controller => this.client.download(\n this.toClientOptions(settings, remoteFilename),\n async size => {\n const download = await this.platform.startDownload(localName, 0o644, size ?? 1)\n return download ? new TabbyDownloadSink(download, size !== undefined) : null\n },\n controller.signal,\n progress => this.progress$.next(progress),\n ))\n }\n\n cancel (): void {\n this.controller?.abort()\n }\n\n private async run (\n direction: 'upload' | 'download',\n filename: string,\n operation: (controller: AbortController) => Promise<TftpResult>,\n ): Promise<TftpResult> {\n this.ensureIdle()\n const controller = new AbortController()\n this.controller = controller\n try {\n const result = await operation(controller)\n this.notifications.notice(`TFTP ${direction === 'upload' ? '上传' : '下载'}完成:${filename}`)\n return result\n } catch (error) {\n const reason = error instanceof Error ? error : new Error(String(error))\n if (reason.name === 'AbortError') {\n this.notifications.info('TFTP 传输已取消')\n } else {\n this.notifications.error('TFTP 传输失败', reason.message)\n }\n throw reason\n } finally {\n if (this.controller === controller) this.controller = null\n }\n }\n\n private ensureIdle (): void {\n if (this.busy) throw new Error('已有一个 TFTP 传输正在进行')\n }\n\n private toClientOptions (settings: TftpPluginSettings, remoteFilename: string) {\n return {\n host: settings.host,\n port: Number(settings.port),\n remoteFilename,\n blockSize: Number(settings.blockSize),\n timeoutMs: Number(settings.timeoutMs),\n retries: Number(settings.retries),\n }\n }\n}\n","import { Injectable } from '@angular/core'\nimport { BehaviorSubject } from 'rxjs'\nimport { NotificationsService, PlatformService } from 'tabby-core'\nimport * as os from 'os'\n\nimport { TftpServerSettings } from '../config'\nimport { TftpServer, TftpServerEvent } from '../tftp/server'\n\nexport interface TftpServerLog {\n timestamp: number\n level: 'info' | 'success' | 'error'\n message: string\n}\n\nexport interface TftpServerState {\n running: boolean\n activeTransfers: number\n startedAt?: number\n logs: TftpServerLog[]\n}\n\nconst sharedServer = new TftpServer()\nconst sharedState$ = new BehaviorSubject<TftpServerState>({\n running: false,\n activeTransfers: 0,\n logs: [],\n})\nlet sharedSettings: TftpServerSettings | null = null\n\n@Injectable()\nexport class TftpServerService {\n readonly state$ = sharedState$\n private readonly server = sharedServer\n\n constructor (\n private readonly notifications: NotificationsService,\n private readonly platform: PlatformService,\n ) { }\n\n get running (): boolean {\n return this.server.running\n }\n\n get activeSettings (): TftpServerSettings | null {\n return sharedSettings ? { ...sharedSettings } : null\n }\n\n getLocalIPv4Addresses (): string[] {\n const addresses = new Set<string>()\n const interfaces = os.networkInterfaces()\n for (const entries of Object.values(interfaces)) {\n for (const entry of entries ?? []) {\n if (entry.family === 'IPv4' && !entry.internal) addresses.add(entry.address)\n }\n }\n return Array.from(addresses).sort()\n }\n\n async chooseRootDirectory (): Promise<string | null> {\n const remote = require('@electron/remote')\n const result = await remote.dialog.showOpenDialog({\n title: '选择 TFTP 根目录',\n properties: ['openDirectory', 'createDirectory'],\n })\n return result.canceled ? null : (result.filePaths[0] ?? null)\n }\n\n openRootDirectory (directory: string): void {\n if (directory) this.platform.openPath(directory)\n }\n\n copyAddress (address: string): void {\n this.platform.setClipboard({ text: address })\n this.notifications.notice(`已复制本机 IP:${address}`)\n }\n\n async start (settings: TftpServerSettings): Promise<void> {\n sharedSettings = { ...settings }\n try {\n await this.server.start({\n rootDirectory: settings.rootDirectory,\n bindAddress: settings.bindAddress,\n port: Number(settings.port),\n blockSize: Number(settings.blockSize),\n timeoutMs: Number(settings.timeoutMs),\n retries: Number(settings.retries),\n autoStopSeconds: settings.autoStop ? Number(settings.autoStopSeconds) : 0,\n }, event => this.handleEvent(event))\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n this.appendLog('error', `启动失败:${reason}`)\n this.notifications.error('TFTP 服务端启动失败', reason)\n throw error\n }\n }\n\n stop (): void {\n this.server.stop()\n }\n\n clearLogs (): void {\n this.state$.next({ ...this.state$.value, logs: [] })\n }\n\n private handleEvent (event: TftpServerEvent): void {\n const level: TftpServerLog['level'] = event.type === 'completed' ? 'success' : event.type === 'error' ? 'error' : 'info'\n const current = this.state$.value\n const running = event.type === 'started' ? true : event.type === 'stopped' ? false : this.server.running\n const next: TftpServerState = {\n running,\n activeTransfers: event.activeTransfers,\n startedAt: event.type === 'started' ? event.timestamp : current.startedAt,\n logs: [...current.logs, { timestamp: event.timestamp, level, message: event.message }].slice(-300),\n }\n this.state$.next(next)\n\n if (event.type === 'completed' && sharedSettings?.notifyDownloads) {\n this.notifications.notice(`TFTP 下载完成:${event.filename ?? ''}`)\n } else if (event.type === 'stopped') {\n this.notifications.info('TFTP 服务端已停止')\n }\n }\n\n private appendLog (level: TftpServerLog['level'], message: string): void {\n const current = this.state$.value\n this.state$.next({\n ...current,\n logs: [...current.logs, { timestamp: Date.now(), level, message }].slice(-300),\n })\n }\n}\n","import * as dgram from 'dgram'\nimport { RemoteInfo, Socket } from 'dgram'\n\nimport {\n Opcode,\n TftpPacket,\n createAck,\n createData,\n createError,\n createRequest,\n parseNegotiatedBlockSize,\n parsePacket,\n parseTransferSize,\n} from './protocol'\n\nexport interface TftpClientOptions {\n host: string\n port?: number\n remoteFilename: string\n blockSize?: number\n timeoutMs?: number\n retries?: number\n}\n\ninterface NormalizedOptions {\n host: string\n port: number\n remoteFilename: string\n blockSize: number\n timeoutMs: number\n retries: number\n}\n\nexport type TransferDirection = 'upload' | 'download'\nexport type TransferState = 'idle' | 'negotiating' | 'transferring' | 'retrying' | 'completed' | 'cancelled' | 'error'\n\nexport interface TftpProgress {\n direction: TransferDirection\n state: TransferState\n transferred: number\n total?: number\n speed: number\n attempt?: number\n message?: string\n}\n\nexport interface TftpResult {\n bytes: number\n blockSize: number\n durationMs: number\n}\n\nexport interface UploadSource {\n readonly size: number\n read (maximumBytes: number): Promise<Buffer>\n close (): void\n abort? (): void\n isCancelled? (): boolean\n}\n\nexport interface DownloadSink {\n write (data: Buffer): Promise<void>\n close (): void\n abort? (): void\n isCancelled? (): boolean\n}\n\nexport type DownloadSinkFactory = (size?: number) => Promise<DownloadSink | null>\nexport type ProgressHandler = (progress: TftpProgress) => void\n\nfunction normalizeOptions (options: TftpClientOptions): NormalizedOptions {\n const host = options.host.trim()\n const remoteFilename = options.remoteFilename.trim()\n const port = options.port ?? 69\n const blockSize = options.blockSize ?? 1468\n const timeoutMs = options.timeoutMs ?? 3000\n const retries = options.retries ?? 5\n\n if (!host) throw new Error('TFTP 主机不能为空')\n if (!remoteFilename) throw new Error('远端文件名不能为空')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('TFTP 端口必须在 1 到 65535 之间')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP 块大小必须在 8 到 65464 之间')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('超时时间必须在 250 到 60000 毫秒之间')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('重试次数必须在 0 到 20 之间')\n\n return { host, remoteFilename, port, blockSize, timeoutMs, retries }\n}\n\nfunction abortError (): Error {\n const error = new Error('传输已取消')\n error.name = 'AbortError'\n return error\n}\n\nabstract class TransferSession {\n protected readonly socket: Socket\n protected readonly startedAt = Date.now()\n protected transferred = 0\n protected negotiatedBlockSize = 512\n\n private endpoint: { address: string; port: number } | null = null\n private lastPacket: Buffer | null = null\n private lastTarget: { address: string; port: number } | null = null\n private retryCount = 0\n private retryTimer: ReturnType<typeof setTimeout> | null = null\n private cancelPoll: ReturnType<typeof setInterval> | null = null\n private settled = false\n private queue = Promise.resolve()\n private resolvePromise!: (result: TftpResult) => void\n private rejectPromise!: (error: Error) => void\n\n protected constructor (\n protected readonly direction: TransferDirection,\n protected readonly options: NormalizedOptions,\n protected readonly signal?: AbortSignal,\n protected readonly onProgress?: ProgressHandler,\n ) {\n this.socket = dgram.createSocket('udp4')\n }\n\n protected runSession (initialPacket: Buffer): Promise<TftpResult> {\n const result = new Promise<TftpResult>((resolve, reject) => {\n this.resolvePromise = resolve\n this.rejectPromise = reject\n })\n\n this.socket.on('error', error => this.fail(error))\n this.socket.on('message', (message, remote) => {\n this.queue = this.queue\n .then(() => this.handleMessage(message, remote))\n .catch(error => this.fail(error instanceof Error ? error : new Error(String(error))))\n })\n this.signal?.addEventListener('abort', () => this.fail(abortError()), { once: true })\n this.cancelPoll = setInterval(() => {\n if (this.resourceCancelled()) this.fail(abortError())\n }, 250)\n\n this.emit('negotiating', '正在连接 TFTP 服务端')\n this.sendTracked(initialPacket, { address: this.options.host, port: this.options.port })\n return result\n }\n\n protected abstract onPacket (packet: TftpPacket): Promise<void>\n protected abstract releaseResource (successful: boolean): void\n protected abstract resourceCancelled (): boolean\n\n protected clearRetryTimer (): void {\n if (this.retryTimer) clearTimeout(this.retryTimer)\n this.retryTimer = null\n this.retryCount = 0\n }\n\n protected sendTracked (packet: Buffer, explicitTarget?: { address: string; port: number }): void {\n if (this.settled) return\n const target = explicitTarget ?? this.endpoint ?? { address: this.options.host, port: this.options.port }\n this.lastPacket = Buffer.from(packet)\n this.lastTarget = target\n if (this.retryTimer) clearTimeout(this.retryTimer)\n this.socket.send(packet, target.port, target.address, error => {\n if (error) this.fail(error)\n })\n this.retryTimer = setTimeout(() => this.retryLastPacket(), this.options.timeoutMs)\n }\n\n protected sendUntracked (\n packet: Buffer,\n explicitTarget?: { address: string; port: number },\n sent?: () => void,\n ): void {\n if (this.settled) return\n const target = explicitTarget ?? this.endpoint ?? { address: this.options.host, port: this.options.port }\n this.socket.send(packet, target.port, target.address, error => {\n if (error) this.fail(error)\n else sent?.()\n })\n }\n\n protected emit (state: TransferState, message?: string, total?: number): void {\n const duration = Math.max(1, Date.now() - this.startedAt)\n this.onProgress?.({\n direction: this.direction,\n state,\n transferred: this.transferred,\n total,\n speed: this.transferred * 1000 / duration,\n attempt: state === 'retrying' ? this.retryCount : undefined,\n message,\n })\n }\n\n protected succeed (): void {\n if (this.settled) return\n this.settled = true\n this.cleanupTimers()\n this.releaseResource(true)\n this.socket.close()\n const result = {\n bytes: this.transferred,\n blockSize: this.negotiatedBlockSize,\n durationMs: Date.now() - this.startedAt,\n }\n this.emit('completed', '传输完成', this.transferred)\n this.resolvePromise(result)\n }\n\n protected fail (error: Error): void {\n if (this.settled) return\n this.settled = true\n this.cleanupTimers()\n this.releaseResource(false)\n try {\n this.socket.close()\n } catch { }\n this.emit(error.name === 'AbortError' ? 'cancelled' : 'error', error.message)\n this.rejectPromise(error)\n }\n\n private async handleMessage (message: Buffer, remote: RemoteInfo): Promise<void> {\n if (this.settled) return\n if (this.signal?.aborted || this.resourceCancelled()) throw abortError()\n\n if (this.endpoint && (remote.address !== this.endpoint.address || remote.port !== this.endpoint.port)) {\n this.sendUntracked(createError(5, 'Unknown transfer ID'), { address: remote.address, port: remote.port })\n return\n }\n\n const packet = parsePacket(message)\n if (!this.endpoint) this.endpoint = { address: remote.address, port: remote.port }\n if (packet.opcode === Opcode.ERROR) {\n throw new Error(`TFTP 错误 ${packet.code}: ${packet.message}`)\n }\n await this.onPacket(packet)\n }\n\n private retryLastPacket (): void {\n if (this.settled) return\n if (this.signal?.aborted || this.resourceCancelled()) {\n this.fail(abortError())\n return\n }\n if (!this.lastPacket || !this.lastTarget || this.retryCount >= this.options.retries) {\n this.fail(new Error(`TFTP 响应超时,已重试 ${this.options.retries} 次`))\n return\n }\n\n this.retryCount++\n this.emit('retrying', `响应超时,正在进行第 ${this.retryCount} 次重试`)\n this.socket.send(this.lastPacket, this.lastTarget.port, this.lastTarget.address, error => {\n if (error) this.fail(error)\n })\n this.retryTimer = setTimeout(() => this.retryLastPacket(), this.options.timeoutMs)\n }\n\n private cleanupTimers (): void {\n if (this.retryTimer) clearTimeout(this.retryTimer)\n if (this.cancelPoll) clearInterval(this.cancelPoll)\n this.retryTimer = null\n this.cancelPoll = null\n }\n}\n\nclass DownloadSession extends TransferSession {\n private expectedBlock = 1\n private sink: DownloadSink | null = null\n private totalSize: number | undefined\n\n constructor (\n options: NormalizedOptions,\n private readonly sinkFactory: DownloadSinkFactory,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ) {\n super('download', options, signal, onProgress)\n }\n\n run (): Promise<TftpResult> {\n const timeoutSeconds = Math.max(1, Math.min(255, Math.round(this.options.timeoutMs / 1000)))\n return this.runSession(createRequest(Opcode.RRQ, this.options.remoteFilename, {\n blksize: this.options.blockSize,\n timeout: timeoutSeconds,\n tsize: 0,\n }))\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode === Opcode.OACK) {\n if (this.expectedBlock !== 1 || this.transferred !== 0) return\n this.clearRetryTimer()\n this.negotiatedBlockSize = parseNegotiatedBlockSize(packet.options)\n this.totalSize = parseTransferSize(packet.options)\n await this.ensureSink(this.totalSize)\n this.emit('transferring', '已协商传输参数', this.totalSize)\n this.sendTracked(createAck(0))\n return\n }\n if (packet.opcode !== Opcode.DATA) return\n\n if (packet.block === this.expectedBlock) {\n this.clearRetryTimer()\n if (!this.sink) {\n this.negotiatedBlockSize = 512\n await this.ensureSink(undefined)\n }\n await this.sink!.write(packet.data)\n this.transferred += packet.data.length\n this.emit('transferring', `正在接收块 ${packet.block}`, this.totalSize)\n\n const finalBlock = packet.data.length < this.negotiatedBlockSize\n const acknowledgement = createAck(packet.block)\n if (finalBlock) {\n this.sendUntracked(acknowledgement, undefined, () => this.succeed())\n } else {\n this.expectedBlock = (this.expectedBlock + 1) & 0xffff\n this.sendTracked(acknowledgement)\n }\n return\n }\n\n const previousBlock = (this.expectedBlock - 1) & 0xffff\n if (packet.block === previousBlock) {\n this.sendTracked(createAck(packet.block))\n }\n }\n\n protected releaseResource (successful: boolean): void {\n if (!this.sink) return\n if (successful) this.sink.close()\n else if (this.sink.abort) this.sink.abort()\n else this.sink.close()\n }\n\n protected resourceCancelled (): boolean {\n return this.sink?.isCancelled?.() ?? false\n }\n\n private async ensureSink (size?: number): Promise<void> {\n if (this.sink) return\n this.sink = await this.sinkFactory(size)\n if (!this.sink) throw abortError()\n }\n}\n\nclass UploadSession extends TransferSession {\n private nextBlock = 1\n private currentBlock: number | null = null\n private currentData = Buffer.alloc(0)\n private currentFinal = false\n\n constructor (\n options: NormalizedOptions,\n private readonly source: UploadSource,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ) {\n super('upload', options, signal, onProgress)\n }\n\n run (): Promise<TftpResult> {\n const timeoutSeconds = Math.max(1, Math.min(255, Math.round(this.options.timeoutMs / 1000)))\n return this.runSession(createRequest(Opcode.WRQ, this.options.remoteFilename, {\n blksize: this.options.blockSize,\n timeout: timeoutSeconds,\n tsize: this.source.size,\n }))\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode === Opcode.OACK && this.currentBlock === null) {\n this.clearRetryTimer()\n this.negotiatedBlockSize = parseNegotiatedBlockSize(packet.options)\n this.emit('transferring', '已协商传输参数', this.source.size)\n await this.sendNextBlock()\n return\n }\n\n if (packet.opcode !== Opcode.ACK) return\n if (packet.block === 0 && this.currentBlock === null) {\n this.clearRetryTimer()\n this.negotiatedBlockSize = 512\n this.emit('transferring', '服务端使用标准 512 字节块', this.source.size)\n await this.sendNextBlock()\n return\n }\n\n if (this.currentBlock !== null && packet.block === this.currentBlock) {\n this.clearRetryTimer()\n this.transferred += this.currentData.length\n this.emit('transferring', `服务端已确认块 ${packet.block}`, this.source.size)\n if (this.currentFinal) {\n this.succeed()\n } else {\n this.nextBlock = (this.nextBlock + 1) & 0xffff\n await this.sendNextBlock()\n }\n return\n }\n\n if (this.currentBlock !== null && packet.block === ((this.currentBlock - 1) & 0xffff)) {\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n }\n\n protected releaseResource (successful: boolean): void {\n if (successful) this.source.close()\n else if (this.source.abort) this.source.abort()\n else this.source.close()\n }\n\n protected resourceCancelled (): boolean {\n return this.source.isCancelled?.() ?? false\n }\n\n private async sendNextBlock (): Promise<void> {\n if (this.signal?.aborted || this.resourceCancelled()) throw abortError()\n this.currentData = Buffer.from(await this.source.read(this.negotiatedBlockSize))\n if (this.currentData.length > this.negotiatedBlockSize) {\n throw new Error('上传数据源返回的数据超过协商块大小')\n }\n this.currentBlock = this.nextBlock\n this.currentFinal = this.currentData.length < this.negotiatedBlockSize\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n}\n\nexport class TftpClient {\n download (\n options: TftpClientOptions,\n sinkFactory: DownloadSinkFactory,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ): Promise<TftpResult> {\n return new DownloadSession(normalizeOptions(options), sinkFactory, signal, onProgress).run()\n }\n\n upload (\n options: TftpClientOptions,\n source: UploadSource,\n signal?: AbortSignal,\n onProgress?: ProgressHandler,\n ): Promise<TftpResult> {\n return new UploadSession(normalizeOptions(options), source, signal, onProgress).run()\n }\n}\n","export enum Opcode {\n RRQ = 1,\n WRQ = 2,\n DATA = 3,\n ACK = 4,\n ERROR = 5,\n OACK = 6,\n}\n\nexport type TftpPacket =\n | { opcode: Opcode.DATA; block: number; data: Buffer }\n | { opcode: Opcode.ACK; block: number }\n | { opcode: Opcode.ERROR; code: number; message: string }\n | { opcode: Opcode.OACK; options: Record<string, string> }\n\nfunction zeroTerminated (value: string): Buffer {\n if (value.includes('\\0')) {\n throw new Error('TFTP 字段不能包含空字符')\n }\n return Buffer.concat([Buffer.from(value, 'utf8'), Buffer.from([0])])\n}\n\nexport function createRequest (\n opcode: Opcode.RRQ | Opcode.WRQ,\n filename: string,\n options: Record<string, string | number> = {},\n): Buffer {\n if (!filename.trim()) {\n throw new Error('远端文件名不能为空')\n }\n\n const header = Buffer.allocUnsafe(2)\n header.writeUInt16BE(opcode, 0)\n const parts = [header, zeroTerminated(filename), zeroTerminated('octet')]\n\n for (const [key, rawValue] of Object.entries(options)) {\n parts.push(zeroTerminated(key.toLowerCase()))\n parts.push(zeroTerminated(String(rawValue)))\n }\n return Buffer.concat(parts)\n}\n\nexport function createData (block: number, data: Buffer): Buffer {\n const packet = Buffer.allocUnsafe(4 + data.length)\n packet.writeUInt16BE(Opcode.DATA, 0)\n packet.writeUInt16BE(block & 0xffff, 2)\n data.copy(packet, 4)\n return packet\n}\n\nexport function createAck (block: number): Buffer {\n const packet = Buffer.allocUnsafe(4)\n packet.writeUInt16BE(Opcode.ACK, 0)\n packet.writeUInt16BE(block & 0xffff, 2)\n return packet\n}\n\nexport function createError (code: number, message: string): Buffer {\n const packet = Buffer.allocUnsafe(4)\n packet.writeUInt16BE(Opcode.ERROR, 0)\n packet.writeUInt16BE(code & 0xffff, 2)\n return Buffer.concat([packet, zeroTerminated(message)])\n}\n\nfunction parsePairs (packet: Buffer, offset: number): Record<string, string> {\n const values: string[] = []\n let start = offset\n for (let i = offset; i < packet.length; i++) {\n if (packet[i] === 0) {\n values.push(packet.toString('utf8', start, i))\n start = i + 1\n }\n }\n if (start !== packet.length || values.length % 2 !== 0) {\n throw new Error('无效的 TFTP 选项响应')\n }\n\n const options: Record<string, string> = {}\n for (let i = 0; i < values.length; i += 2) {\n options[values[i].toLowerCase()] = values[i + 1]\n }\n return options\n}\n\nexport function parsePacket (packet: Buffer): TftpPacket {\n if (packet.length < 2) {\n throw new Error('TFTP 数据包过短')\n }\n const opcode = packet.readUInt16BE(0)\n\n switch (opcode) {\n case Opcode.DATA:\n if (packet.length < 4) throw new Error('无效的 DATA 数据包')\n return { opcode, block: packet.readUInt16BE(2), data: packet.subarray(4) }\n case Opcode.ACK:\n if (packet.length !== 4) throw new Error('无效的 ACK 数据包')\n return { opcode, block: packet.readUInt16BE(2) }\n case Opcode.ERROR: {\n if (packet.length < 5 || packet[packet.length - 1] !== 0) {\n throw new Error('无效的 ERROR 数据包')\n }\n return {\n opcode,\n code: packet.readUInt16BE(2),\n message: packet.toString('utf8', 4, packet.length - 1),\n }\n }\n case Opcode.OACK:\n return { opcode, options: parsePairs(packet, 2) }\n default:\n throw new Error(`不支持的 TFTP 操作码: ${opcode}`)\n }\n}\n\nexport function parseNegotiatedBlockSize (options: Record<string, string>, fallback = 512): number {\n if (!options.blksize) return fallback\n const value = Number(options.blksize)\n if (!Number.isInteger(value) || value < 8 || value > 65464) {\n throw new Error(`服务端返回了无效的块大小: ${options.blksize}`)\n }\n return value\n}\n\nexport function parseTransferSize (options: Record<string, string>): number | undefined {\n if (options.tsize === undefined) return undefined\n const value = Number(options.tsize)\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new Error(`服务端返回了无效的文件大小: ${options.tsize}`)\n }\n return value\n}\n","import * as dgram from 'dgram'\nimport { RemoteInfo, Socket } from 'dgram'\nimport * as fs from 'fs'\nimport type { FileHandle } from 'fs/promises'\nimport * as path from 'path'\n\nimport {\n Opcode,\n TftpPacket,\n createData,\n createError,\n parsePacket,\n} from './protocol'\n\nexport interface TftpServerOptions {\n rootDirectory: string\n bindAddress?: string\n port?: number\n blockSize?: number\n timeoutMs?: number\n retries?: number\n autoStopSeconds?: number\n}\n\ninterface NormalizedServerOptions {\n rootDirectory: string\n bindAddress: string\n port: number\n blockSize: number\n timeoutMs: number\n retries: number\n autoStopSeconds: number\n}\n\nexport type TftpServerEventType = 'started' | 'stopped' | 'request' | 'progress' | 'completed' | 'error'\n\nexport interface TftpServerEvent {\n type: TftpServerEventType\n timestamp: number\n message: string\n peer?: string\n filename?: string\n bytes?: number\n activeTransfers: number\n}\n\ninterface TftpRequest {\n opcode: Opcode.RRQ | Opcode.WRQ\n filename: string\n mode: string\n options: Record<string, string>\n}\n\nfunction readZeroTerminatedFields (packet: Buffer, offset: number): string[] {\n const fields: string[] = []\n let start = offset\n for (let i = offset; i < packet.length; i++) {\n if (packet[i] === 0) {\n fields.push(packet.toString('utf8', start, i))\n start = i + 1\n }\n }\n if (start !== packet.length) throw new Error('请求字段缺少结束符')\n return fields\n}\n\nexport function parseRequest (packet: Buffer): TftpRequest {\n if (packet.length < 4) throw new Error('TFTP 请求过短')\n const opcode = packet.readUInt16BE(0)\n if (opcode !== Opcode.RRQ && opcode !== Opcode.WRQ) throw new Error('不是有效的读写请求')\n\n const fields = readZeroTerminatedFields(packet, 2)\n if (fields.length < 2 || (fields.length - 2) % 2 !== 0) throw new Error('TFTP 请求字段无效')\n const filename = fields[0]\n const mode = fields[1].toLowerCase()\n if (!filename) throw new Error('请求文件名为空')\n if (mode !== 'octet') throw new Error(`不支持的传输模式: ${mode}`)\n\n const options: Record<string, string> = {}\n for (let i = 2; i < fields.length; i += 2) {\n options[fields[i].toLowerCase()] = fields[i + 1]\n }\n return { opcode, filename, mode, options }\n}\n\nfunction createOack (options: Record<string, string | number>): Buffer {\n const header = Buffer.allocUnsafe(2)\n header.writeUInt16BE(Opcode.OACK, 0)\n const chunks: Buffer[] = [header]\n for (const [key, value] of Object.entries(options)) {\n chunks.push(Buffer.from(`${key}\\0${value}\\0`, 'utf8'))\n }\n return Buffer.concat(chunks)\n}\n\nfunction normalizeOptions (options: TftpServerOptions): NormalizedServerOptions {\n const rootDirectory = path.resolve(options.rootDirectory.trim())\n const bindAddress = (options.bindAddress ?? '0.0.0.0').trim()\n const port = options.port ?? 69\n const blockSize = options.blockSize ?? 8192\n const timeoutMs = options.timeoutMs ?? 3000\n const retries = options.retries ?? 5\n const autoStopSeconds = options.autoStopSeconds ?? 0\n\n if (!options.rootDirectory.trim()) throw new Error('请选择 TFTP 根目录')\n if (!bindAddress) throw new Error('监听地址不能为空')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('监听端口必须在 1 到 65535 之间')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP 缓冲区必须在 8 到 65464 字节之间')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('超时时间必须在 250 到 60000 毫秒之间')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('重试次数必须在 0 到 20 之间')\n if (!Number.isInteger(autoStopSeconds) || autoStopSeconds < 0 || autoStopSeconds > 86400) throw new Error('自动停止时间必须在 0 到 86400 秒之间')\n\n return { rootDirectory, bindAddress, port, blockSize, timeoutMs, retries, autoStopSeconds }\n}\n\nfunction resolveRequestedFile (rootDirectory: string, requestedFilename: string): string {\n const normalized = requestedFilename.replace(/\\\\/g, '/').replace(/^\\/+/, '')\n const segments = normalized.split('/').filter(segment => segment && segment !== '.')\n if (!segments.length || segments.some(segment => segment === '..')) {\n throw new Error('拒绝不安全的文件路径')\n }\n\n const target = path.resolve(rootDirectory, ...segments)\n const relative = path.relative(rootDirectory, target)\n if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {\n throw new Error('请求文件超出 TFTP 根目录')\n }\n return target\n}\n\nabstract class ServerTransfer {\n protected readonly socket: Socket\n protected blockSize = 512\n protected timeoutMs: number\n protected transferred = 0\n\n private lastPacket: Buffer | null = null\n private timer: ReturnType<typeof setTimeout> | null = null\n private retryCount = 0\n private settled = false\n private failing = false\n private queue = Promise.resolve()\n\n protected constructor (\n protected readonly request: TftpRequest,\n protected readonly remote: RemoteInfo,\n protected readonly options: NormalizedServerOptions,\n private readonly onFinish: (successful: boolean, message: string, bytes: number) => void,\n ) {\n this.timeoutMs = options.timeoutMs\n this.socket = dgram.createSocket('udp4')\n }\n\n start (): void {\n this.socket.on('error', error => this.fail(error.message))\n this.socket.on('message', (message, remote) => {\n this.queue = this.queue\n .then(() => this.handleMessage(message, remote))\n .catch(error => this.fail(error instanceof Error ? error.message : String(error)))\n })\n this.socket.bind(0, this.options.bindAddress, () => {\n this.begin().catch(error => this.fail(error instanceof Error ? error.message : String(error)))\n })\n }\n\n resendLast (): void {\n if (this.lastPacket) this.sendTracked(this.lastPacket)\n }\n\n cancel (): void {\n this.finish(false, '服务端已停止')\n }\n\n protected abstract begin (): Promise<void>\n protected abstract onPacket (packet: TftpPacket): Promise<void>\n protected abstract release (successful: boolean): Promise<void>\n\n protected sendTracked (packet: Buffer): void {\n if (this.settled) return\n this.lastPacket = Buffer.from(packet)\n if (this.timer) clearTimeout(this.timer)\n this.socket.send(packet, this.remote.port, this.remote.address, error => {\n if (error) this.fail(error.message)\n })\n this.timer = setTimeout(() => this.retry(), this.timeoutMs)\n }\n\n protected clearTimer (): void {\n if (this.timer) clearTimeout(this.timer)\n this.timer = null\n this.retryCount = 0\n }\n\n protected complete (message: string): void {\n this.finish(true, message)\n }\n\n protected fail (message: string): void {\n if (this.settled || this.failing) return\n this.failing = true\n if (this.timer) clearTimeout(this.timer)\n const errorPacket = createError(1, message)\n this.socket.send(errorPacket, this.remote.port, this.remote.address, () => this.finish(false, message))\n }\n\n private async handleMessage (message: Buffer, remote: RemoteInfo): Promise<void> {\n if (this.settled) return\n if (remote.address !== this.remote.address || remote.port !== this.remote.port) {\n const errorPacket = createError(5, 'Unknown transfer ID')\n this.socket.send(errorPacket, remote.port, remote.address)\n return\n }\n const packet = parsePacket(message)\n if (packet.opcode === Opcode.ERROR) throw new Error(`客户端错误 ${packet.code}: ${packet.message}`)\n await this.onPacket(packet)\n }\n\n private retry (): void {\n if (this.settled) return\n if (!this.lastPacket || this.retryCount >= this.options.retries) {\n this.fail(`等待客户端响应超时,已重试 ${this.options.retries} 次`)\n return\n }\n this.retryCount++\n this.socket.send(this.lastPacket, this.remote.port, this.remote.address, error => {\n if (error) this.fail(error.message)\n })\n this.timer = setTimeout(() => this.retry(), this.timeoutMs)\n }\n\n private finish (successful: boolean, message: string): void {\n if (this.settled) return\n this.settled = true\n if (this.timer) clearTimeout(this.timer)\n this.release(successful)\n .catch(() => undefined)\n .finally(() => {\n try { this.socket.close() } catch { }\n this.onFinish(successful, message, this.transferred)\n })\n }\n}\n\nclass ReadTransfer extends ServerTransfer {\n private file: FileHandle | null = null\n private fileSize = 0\n private position = 0\n private currentBlock = 0\n private currentData = Buffer.alloc(0)\n private finalBlock = false\n\n constructor (\n request: TftpRequest,\n remote: RemoteInfo,\n options: NormalizedServerOptions,\n onFinish: (successful: boolean, message: string, bytes: number) => void,\n ) {\n super(request, remote, options, onFinish)\n }\n\n protected async begin (): Promise<void> {\n const filePath = resolveRequestedFile(this.options.rootDirectory, this.request.filename)\n const stat = await fs.promises.stat(filePath)\n if (!stat.isFile()) throw new Error('请求路径不是普通文件')\n this.fileSize = stat.size\n this.file = await fs.promises.open(filePath, 'r')\n\n const negotiated: Record<string, string | number> = {}\n if (this.request.options.blksize !== undefined) {\n const requested = Number(this.request.options.blksize)\n if (!Number.isInteger(requested) || requested < 8 || requested > 65464) throw new Error('客户端请求了无效的块大小')\n this.blockSize = Math.min(requested, this.options.blockSize)\n negotiated.blksize = this.blockSize\n }\n if (this.request.options.timeout !== undefined) {\n const timeoutSeconds = Number(this.request.options.timeout)\n if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 255) throw new Error('客户端请求了无效的超时时间')\n this.timeoutMs = timeoutSeconds * 1000\n negotiated.timeout = timeoutSeconds\n }\n if (this.request.options.tsize !== undefined) negotiated.tsize = this.fileSize\n\n if (Object.keys(negotiated).length) {\n this.sendTracked(createOack(negotiated))\n } else {\n await this.sendNextBlock()\n }\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode !== Opcode.ACK) return\n if (this.currentBlock === 0 && packet.block === 0) {\n this.clearTimer()\n await this.sendNextBlock()\n return\n }\n if (packet.block === this.currentBlock) {\n this.clearTimer()\n this.transferred += this.currentData.length\n if (this.finalBlock) this.complete(`已发送 ${this.transferred} 字节`)\n else await this.sendNextBlock()\n return\n }\n if (packet.block === ((this.currentBlock - 1) & 0xffff)) this.resendLast()\n }\n\n protected async release (): Promise<void> {\n if (this.file) await this.file.close()\n this.file = null\n }\n\n private async sendNextBlock (): Promise<void> {\n const file = this.file\n if (!file) throw new Error('TFTP 文件尚未打开')\n const buffer = Buffer.allocUnsafe(this.blockSize)\n const result = await file.read(buffer, 0, this.blockSize, this.position)\n this.currentData = Buffer.from(buffer.subarray(0, result.bytesRead))\n this.position += result.bytesRead\n this.currentBlock = (this.currentBlock + 1) & 0xffff\n this.finalBlock = result.bytesRead < this.blockSize\n this.sendTracked(createData(this.currentBlock, this.currentData))\n }\n}\n\nexport class TftpServer {\n private listener: Socket | null = null\n private options: NormalizedServerOptions | null = null\n private readonly transfers = new Map<string, ServerTransfer>()\n private autoStopTimer: ReturnType<typeof setTimeout> | null = null\n private eventHandler: ((event: TftpServerEvent) => void) | null = null\n\n get running (): boolean {\n return this.listener !== null\n }\n\n get activeTransfers (): number {\n return this.transfers.size\n }\n\n async start (serverOptions: TftpServerOptions, onEvent?: (event: TftpServerEvent) => void): Promise<void> {\n if (this.running) throw new Error('TFTP 服务端已经在运行')\n const options = normalizeOptions(serverOptions)\n const stat = await fs.promises.stat(options.rootDirectory)\n if (!stat.isDirectory()) throw new Error('TFTP 根目录无效')\n\n this.options = options\n this.eventHandler = onEvent ?? null\n const listener = dgram.createSocket('udp4')\n this.listener = listener\n listener.on('message', (message, remote) => this.handleRequest(message, remote))\n\n let rejectBinding: ((error: Error) => void) | null = null\n listener.on('error', error => {\n if (rejectBinding) {\n const reject = rejectBinding\n rejectBinding = null\n try { listener.close() } catch { }\n this.listener = null\n reject(error)\n } else {\n this.emit('error', error.message)\n }\n })\n await new Promise<void>((resolve, reject) => {\n rejectBinding = reject\n listener.bind(options.port, options.bindAddress, () => {\n rejectBinding = null\n resolve()\n })\n })\n\n this.emit('started', `TFTP 服务端已启动:${options.bindAddress}:${options.port}`)\n if (options.autoStopSeconds > 0) {\n this.autoStopTimer = setTimeout(() => this.stop('达到自动停止时间'), options.autoStopSeconds * 1000)\n }\n }\n\n stop (reason = '用户停止'): void {\n if (!this.listener) return\n if (this.autoStopTimer) clearTimeout(this.autoStopTimer)\n this.autoStopTimer = null\n for (const transfer of this.transfers.values()) transfer.cancel()\n this.transfers.clear()\n try { this.listener.close() } catch { }\n this.listener = null\n this.emit('stopped', `TFTP 服务端已停止:${reason}`)\n }\n\n private handleRequest (message: Buffer, remote: RemoteInfo): void {\n if (!this.listener || !this.options) return\n let request: TftpRequest\n try {\n request = parseRequest(message)\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n this.listener.send(createError(4, reason), remote.port, remote.address)\n this.emit('error', `拒绝来自 ${remote.address}:${remote.port} 的无效请求:${reason}`)\n return\n }\n\n const key = `${remote.address}:${remote.port}:${request.opcode}:${request.filename}`\n const existing = this.transfers.get(key)\n if (existing) {\n existing.resendLast()\n return\n }\n\n if (request.opcode === Opcode.WRQ) {\n this.listener.send(createError(2, 'Server is read-only'), remote.port, remote.address)\n this.emit('error', `拒绝 ${remote.address}:${remote.port} 上传 ${request.filename}:服务端为只读模式`, remote, request.filename)\n return\n }\n\n const peer = `${remote.address}:${remote.port}`\n const transfer = new ReadTransfer(request, remote, this.options, (successful, detail, bytes) => {\n this.transfers.delete(key)\n this.emit(successful ? 'completed' : 'error', `${peer} ${successful ? '下载完成' : '下载失败'} ${request.filename}:${detail}`, remote, request.filename, bytes)\n })\n this.transfers.set(key, transfer)\n this.emit('request', `${peer} 请求下载 ${request.filename}`, remote, request.filename)\n transfer.start()\n }\n\n private emit (type: TftpServerEventType, message: string, remote?: RemoteInfo, filename?: string, bytes?: number): void {\n this.eventHandler?.({\n type,\n timestamp: Date.now(),\n message,\n peer: remote ? `${remote.address}:${remote.port}` : undefined,\n filename,\n bytes,\n activeTransfers: this.transfers.size,\n })\n }\n}\n","module.exports = require(\"@angular/common\");","module.exports = require(\"@angular/core\");","module.exports = require(\"@angular/forms\");","module.exports = require(\"@electron/remote\");","module.exports = require(\"@ng-bootstrap/ng-bootstrap\");","module.exports = require(\"dgram\");","module.exports = require(\"fs\");","module.exports = require(\"os\");","module.exports = require(\"path\");","module.exports = require(\"rxjs\");","module.exports = require(\"tabby-core\");","// The module cache\nconst __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tconst cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tconst module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nlet __webpack_exports__ = __webpack_require__(156);\n"],"names":["core_1","__webpack_require__","ng_bootstrap_1","tabby_core_1","tftpModal_component_1","TftpButtonProvider","ToolbarButtonProvider","constructor","modal","super","this","provide","icon","title","weight","click","open","TftpModalComponent","size","__decorate","Injectable","NgbModal","exports","config_1","tftp_service_1","activeModal","config","tftp","zone","changeDetector","mode","settings","defaultTftpSettings","server","defaultTftpServerSettings","status","validationError","busy","percent","state","undefined","total","Math","min","round","transferred","progressType","_a","_b","_c","ngOnInit","stored","store","subscription","progress$","subscribe","run","markForCheck","ngOnDestroy","unsubscribe","upload","validate","execute","download","cancel","formatBytes","value","Number","isFinite","units","index","length","floor","log","amount","pow","toFixed","operation","Object","assign","host","port","remoteFilename","blockSize","timeoutMs","retries","save","error","Error","message","String","requireRemoteFilename","trim","isInteger","Component","selector","template","styles","NgbActiveModal","ConfigService","TftpService","NgZone","ChangeDetectorRef","tftpServer_service_1","serverSettingsStorageKey","TftpServerComponent","running","activeTransfers","logs","localAddresses","starting","cached","JSON","parse","localStorage","getItem","_d","activeSettings","getLocalIPv4Addresses","state$","chooseDirectory","directory","chooseRootDirectory","rootDirectory","saveSettings","start","formatTime","timestamp","Date","toLocaleTimeString","hour12","autoStop","autoStopSeconds","setItem","stringify","TftpServerService","bindAddress","notifyDownloads","TftpConfigProvider","ConfigProvider","defaults","platformDefaults","common_1","forms_1","__importStar","buttonProvider_1","tftpServer_component_1","TftpModule","NgModule","imports","CommonModule","FormsModule","NgbModule","default","declarations","providers","useClass","multi","__exportStar","rxjs_1","client_1","idleProgress","direction","speed","TabbyUploadSource","pending","Buffer","alloc","ended","getSize","read","maximumBytes","chunk","concat","from","result","subarray","close","abort","isCancelled","TabbyDownloadSink","sizeKnown","completed","getCompletedBytes","max","write","data","platform","notifications","BehaviorSubject","controller","client","TftpClient","ensureIdle","startUpload","multiple","getName","source","toClientOptions","signal","progress","next","localName","split","filter","Boolean","pop","async","startDownload","filename","AbortController","notice","reason","name","info","PlatformService","NotificationsService","os","sharedServer","TftpServer","sharedState$","sharedSettings","addresses","Set","interfaces","networkInterfaces","entries","values","entry","family","internal","add","address","Array","sort","remote","dialog","showOpenDialog","properties","canceled","filePaths","openRootDirectory","openPath","copyAddress","setClipboard","text","event","handleEvent","appendLog","stop","clearLogs","level","type","current","startedAt","slice","now","dgram","protocol_1","normalizeOptions","options","abortError","TransferSession","onProgress","negotiatedBlockSize","endpoint","lastPacket","lastTarget","retryCount","retryTimer","cancelPoll","settled","queue","Promise","resolve","socket","createSocket","runSession","initialPacket","reject","resolvePromise","rejectPromise","on","fail","then","handleMessage","catch","addEventListener","once","setInterval","resourceCancelled","emit","sendTracked","clearRetryTimer","clearTimeout","packet","explicitTarget","target","send","setTimeout","retryLastPacket","sendUntracked","sent","duration","call","attempt","succeed","cleanupTimers","releaseResource","bytes","durationMs","aborted","createError","parsePacket","opcode","Opcode","ERROR","code","onPacket","clearInterval","DownloadSession","sinkFactory","expectedBlock","sink","timeoutSeconds","createRequest","RRQ","blksize","timeout","tsize","OACK","parseNegotiatedBlockSize","totalSize","parseTransferSize","ensureSink","createAck","DATA","block","finalBlock","acknowledgement","previousBlock","successful","UploadSession","nextBlock","currentBlock","currentData","currentFinal","WRQ","sendNextBlock","ACK","createData","zeroTerminated","includes","parsePairs","offset","i","push","toString","toLowerCase","header","allocUnsafe","writeUInt16BE","parts","key","rawValue","copy","readUInt16BE","fallback","isSafeInteger","fs","path","parseRequest","fields","readZeroTerminatedFields","ServerTransfer","request","onFinish","timer","failing","bind","begin","resendLast","finish","retry","clearTimer","complete","errorPacket","release","finally","ReadTransfer","file","fileSize","position","filePath","requestedFilename","segments","replace","segment","some","relative","startsWith","isAbsolute","resolveRequestedFile","stat","promises","isFile","negotiated","requested","keys","chunks","createOack","buffer","bytesRead","listener","transfers","Map","autoStopTimer","eventHandler","serverOptions","onEvent","_e","_f","isDirectory","handleRequest","rejectBinding","transfer","clear","existing","get","peer","detail","delete","set","module","require","__webpack_module_cache__","__webpack_exports__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""}
|