tabby-tftp 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## [0.2.6] - 2026-09-10
6
+
7
+ - Add opt-in TFTP server uploads (WRQ) with atomic temporary-file writes.
8
+ - Reject overwriting existing files by default, with a separate explicit override.
9
+ - Add upload progress, completion logs, notifications, and protocol tests.
10
+
5
11
  ## [0.2.5] - 2026-08-31
6
12
 
7
13
  - Translate the complete plugin interface, notifications, validation errors, transfer status, and server logs into English.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  `tabby-tftp` adds firmware-friendly TFTP tools to [Tabby](https://github.com/Eugeny/tabby):
4
4
 
5
5
  - TFTP client for uploads and downloads.
6
- - A local, read-only TFTP server for bootloaders and serial-console firmware updates.
6
+ - A local TFTP server for bootloaders, serial-console firmware updates, and opt-in uploads.
7
7
  - RFC 2347/2348/2349 option negotiation (`blksize`, `timeout`, and `tsize`).
8
8
  - Transfer progress, retry/cancel controls, server logs, and local IPv4 discovery.
9
9
  - Server settings that survive closing and reopening the management window.
@@ -39,7 +39,7 @@ Restart Tabby after installation.
39
39
  4. In the serial bootloader, configure the server address and request the firmware file. For U-Boot this is commonly `serverip`, `ipaddr`, and `tftpboot`.
40
40
  5. Close the dialog when finished. Closing the dialog only hides the UI; the server keeps running until you stop it, its optional auto-stop timer expires, or Tabby exits.
41
41
 
42
- The server is intentionally read-only: devices can download files from the selected root, but cannot upload or overwrite files on the computer.
42
+ The server is read-only by default. Enable **Allow Uploads** before starting it to accept `WRQ` uploads into the selected root directory. Existing files remain protected unless **Allow Uploads to Overwrite Existing Files** is also enabled. Incoming data is written to a temporary file and moved into place only after a successful transfer.
43
43
 
44
44
  ## TFTP client
45
45
 
package/README.zh-CN.md CHANGED
@@ -3,7 +3,7 @@
3
3
  `tabby-tftp` 为 [Tabby](https://github.com/Eugeny/tabby) 提供适合固件下载的 TFTP 工具:
4
4
 
5
5
  - TFTP 文件上传和下载客户端。
6
- - 本地只读 TFTP 服务端,适合串口 Bootloader 烧录固件。
6
+ - 本地 TFTP 服务端,适合串口 Bootloader 烧录固件,并可按需开启上传。
7
7
  - 支持 `blksize`、`timeout`、`tsize` 选项协商。
8
8
  - 显示传输进度、速度、重试/取消状态、服务端日志和本机 IPv4 地址。
9
9
  - 关闭并重新打开管理窗口后,服务端状态和配置仍然保留。
@@ -39,7 +39,7 @@ npm install tabby-tftp
39
39
  4. 在串口 Bootloader 中配置服务端地址并请求固件。U-Boot 常见命令包括 `serverip`、`ipaddr` 和 `tftpboot`。
40
40
  5. 关闭窗口不会停止服务端;只有手动停止、启用的自动停止计时器到期或退出 Tabby 才会停止。
41
41
 
42
- 服务端是只读的:设备可以下载根目录中的文件,但不能向电脑上传或覆盖文件。
42
+ 服务端默认只读。启动前勾选 **Allow Uploads** 后,可以接收设备通过 `WRQ` 上传到根目录的文件。默认仍禁止覆盖已有文件;只有额外勾选 **Allow Uploads to Overwrite Existing Files** 才允许覆盖。上传内容会先写入临时文件,成功完成后才移动到目标文件。
43
43
 
44
44
  ## 本地构建与测试 (开发指南)
45
45
 
package/dist/config.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface TftpServerSettings {
18
18
  autoStop: boolean;
19
19
  autoStopSeconds: number;
20
20
  notifyDownloads: boolean;
21
+ allowUploads: boolean;
22
+ overwriteUploads: boolean;
21
23
  }
22
24
  export declare const defaultTftpServerSettings: TftpServerSettings;
23
25
  export declare const defaultTftpSettings: TftpPluginSettings;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- (()=>{"use strict";var t={983(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpButtonProvider=void 0;const o=s(976),n=s(385),a=s(414),l=s(263);let c=class extends a.ToolbarButtonProvider{constructor(t){super(),this.modal=t}provide(){return[{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M7 7h11l-3.5-3.5L16 2l6 6-6 6-1.5-1.5L18 9H7V7zm10 10H6l3.5 3.5L8 22l-6-6 6-6 1.5 1.5L6 15h11v2z"/></svg>',title:"TFTP File Transfer",weight:6,click:()=>this.modal.open(l.TftpModalComponent,{size:"lg"})}]}};c=r([(0,o.Injectable)(),i("design:paramtypes",[n.NgbModal])],c),e.TftpButtonProvider=c},263(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpModalComponent=void 0;const o=s(976),n=s(385),a=s(414),l=s(28),c=s(306);let d=class{constructor(t,e,s,r,i){this.activeModal=t,this.config=e,this.tftp=s,this.zone=r,this.changeDetector=i,this.mode="server",this.settings={...l.defaultTftpSettings,server:{...l.defaultTftpServerSettings}},this.status=null,this.validationError=""}get busy(){return this.tftp.busy}get percent(){return this.status?"completed"===this.status.state?100:void 0===this.status.total?this.busy?100:0:0===this.status.total?100:Math.min(100,Math.round(100*this.status.transferred/this.status.total)):0}get progressType(){var t,e,s;return"error"===(null===(t=this.status)||void 0===t?void 0:t.state)||"cancelled"===(null===(e=this.status)||void 0===e?void 0:e.state)?"danger":"completed"===(null===(s=this.status)||void 0===s?void 0:s.state)?"success":"info"}ngOnInit(){var t,e;const s=null!==(t=this.config.store.tftp)&&void 0!==t?t:{};this.settings={...l.defaultTftpSettings,...s,server:{...l.defaultTftpServerSettings,...null!==(e=s.server)&&void 0!==e?e:{}}},this.subscription=this.tftp.progress$.subscribe(t=>{this.zone.run(()=>{this.status=t,this.changeDetector.markForCheck()})})}ngOnDestroy(){var t;null===(t=this.subscription)||void 0===t||t.unsubscribe()}async upload(){this.validate(!1)&&await this.execute(()=>this.tftp.upload(this.settings))}async download(){this.validate(!0)&&await this.execute(()=>this.tftp.download(this.settings))}cancel(){this.tftp.cancel()}formatBytes(t){if(!Number.isFinite(t)||t<=0)return"0 B";const e=["B","KB","MB","GB","TB"],s=Math.min(e.length-1,Math.floor(Math.log(t)/Math.log(1024))),r=t/Math.pow(1024,s);return`${r>=100||0===s?r.toFixed(0):r.toFixed(1)} ${e[s]}`}async execute(t){this.validationError="",this.config.store.tftp||(this.config.store.tftp={}),Object.assign(this.config.store.tftp,{host:this.settings.host,port:this.settings.port,remoteFilename:this.settings.remoteFilename,blockSize:this.settings.blockSize,timeoutMs:this.settings.timeoutMs,retries:this.settings.retries}),await this.config.save();try{await t()}catch(t){this.validationError=t instanceof Error?t.message:String(t)}}validate(t){return this.validationError="",this.settings.host.trim()?t&&!this.settings.remoteFilename.trim()?this.validationError="A remote filename is required for downloads":!Number.isInteger(Number(this.settings.port))||Number(this.settings.port)<1||Number(this.settings.port)>65535?this.validationError="Port must be between 1 and 65535":!Number.isInteger(Number(this.settings.blockSize))||Number(this.settings.blockSize)<8||Number(this.settings.blockSize)>65464?this.validationError="Block size must be between 8 and 65464 bytes":!Number.isInteger(Number(this.settings.timeoutMs))||Number(this.settings.timeoutMs)<250||Number(this.settings.timeoutMs)>6e4?this.validationError="Timeout must be between 250 and 60000 milliseconds":(!Number.isInteger(Number(this.settings.retries))||Number(this.settings.retries)<0||Number(this.settings.retries)>20)&&(this.validationError="Retries must be between 0 and 20"):this.validationError="Enter a TFTP host address",!this.validationError}};d=r([(0,o.Component)({selector:"tabby-tftp-modal",template:'\n <div class="modal-header">\n <h5 class="modal-title"><i class="fa fa-exchange-alt mr-2"></i>TFTP File Transfer and Firmware Server</h5>\n <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss()">\n <span aria-hidden="true">&times;</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>Firmware TFTP Server\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 Client\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 provides no authentication or encryption. Use it only on a trusted local network.\n </div>\n\n <div class="form-row">\n <div class="form-group col-md-8">\n <label>Host or IP Address</label>\n <input class="form-control" [(ngModel)]="settings.host" placeholder="e.g. 192.168.1.10" [disabled]="busy">\n </div>\n <div class="form-group col-md-4">\n <label>Port</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>Remote Filename</label>\n <input class="form-control" [(ngModel)]="settings.remoteFilename"\n placeholder="Required for downloads; leave blank on upload to use the local filename" [disabled]="busy">\n <small class="form-text text-muted">TFTP cannot browse remote directories. Path syntax is determined by the server.</small>\n </div>\n\n <details class="mb-3">\n <summary class="text-muted">Advanced Options</summary>\n <div class="form-row mt-2">\n <div class="form-group col-md-4">\n <label>Block Size</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>Timeout (ms)</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>Retries</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\' ? \'Upload\' : \'Download\' }}</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()">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>Cancel Transfer</button>\n <button class="btn btn-primary" *ngIf="!busy" (click)="upload()"><i class="fa fa-upload mr-1"></i>Upload</button>\n <button class="btn btn-success" *ngIf="!busy" (click)="download()"><i class="fa fa-download mr-1"></i>Download</button>\n </ng-container>\n </div>\n ',styles:["\n :host { display: block; }\n summary { cursor: pointer; user-select: none; }\n .transfer-status { border: 1px solid rgba(127, 127, 127, .25); border-radius: .25rem; padding: .75rem; }\n .modal-body label { margin-bottom: .25rem; }\n .mode-tabs .btn { flex: 1; }\n "]}),i("design:paramtypes",[n.NgbActiveModal,a.ConfigService,c.TftpService,o.NgZone,o.ChangeDetectorRef])],d),e.TftpModalComponent=d},153(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServerComponent=void 0;const o=s(976),n=s(414),a=s(28),l=s(21),c="tabby-tftp-server-settings-v1";let d=class{constructor(t,e,s,r){this.config=t,this.server=e,this.zone=s,this.changeDetector=r,this.settings={...a.defaultTftpServerSettings},this.state={running:!1,activeTransfers:0,logs:[]},this.localAddresses=[],this.starting=!1,this.error=""}ngOnInit(){var t,e,s,r;let i={};try{i=JSON.parse(null!==(t=localStorage.getItem(c))&&void 0!==t?t:"{}")}catch{}this.settings={...a.defaultTftpServerSettings,...null!==(s=null===(e=this.config.store.tftp)||void 0===e?void 0:e.server)&&void 0!==s?s:{},...i,...null!==(r=this.server.activeSettings)&&void 0!==r?r:{}},this.localAddresses=this.server.getLocalIPv4Addresses(),this.subscription=this.server.state$.subscribe(t=>{this.zone.run(()=>{this.state=t,this.changeDetector.markForCheck()})})}ngOnDestroy(){var t;null===(t=this.subscription)||void 0===t||t.unsubscribe()}async chooseDirectory(){const t=await this.server.chooseRootDirectory();t&&(this.settings.rootDirectory=t,await this.saveSettings())}async start(){if(this.error="",this.validate()){this.starting=!0,await this.saveSettings();try{await this.server.start(this.settings)}catch(t){this.error=t instanceof Error?t.message:String(t)}finally{this.starting=!1}}}formatTime(t){return new Date(t).toLocaleTimeString(void 0,{hour12:!1})}validate(){return this.settings.rootDirectory.trim()?!Number.isInteger(Number(this.settings.port))||Number(this.settings.port)<1||Number(this.settings.port)>65535?this.error="Listen port must be between 1 and 65535":!Number.isInteger(Number(this.settings.blockSize))||Number(this.settings.blockSize)<8||Number(this.settings.blockSize)>65464?this.error="TFTP block size must be between 8 and 65464 bytes":this.settings.autoStop&&(!Number.isInteger(Number(this.settings.autoStopSeconds))||Number(this.settings.autoStopSeconds)<1||Number(this.settings.autoStopSeconds)>86400)&&(this.error="Automatic stop time must be between 1 and 86400 seconds"):this.error="Choose a TFTP root directory containing the firmware",!this.error}async saveSettings(){localStorage.setItem(c,JSON.stringify(this.settings)),this.config.store.tftp||(this.config.store.tftp={}),this.config.store.tftp.server||(this.config.store.tftp.server={}),Object.assign(this.config.store.tftp.server,this.settings),await this.config.save()}};d=r([(0,o.Component)({selector:"tabby-tftp-server",template:'\n <div class="alert alert-info py-2 small">\n Use the serial terminal to control the bootloader. This read-only server supplies firmware over the network and does not allow devices to overwrite local files.\n </div>\n\n <div class="form-group">\n <label>TFTP Root Directory</label>\n <div class="input-group">\n <input class="form-control" [(ngModel)]="settings.rootDirectory" placeholder="Directory containing firmware files" [disabled]="state.running">\n <div class="input-group-append">\n <button class="btn btn-outline-secondary" (click)="chooseDirectory()" [disabled]="state.running" title="Choose Directory">\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="Open Directory">\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>Listen Address</label>\n <select class="form-control" [(ngModel)]="settings.bindAddress" [disabled]="state.running">\n <option value="0.0.0.0">0.0.0.0 (all interfaces)</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>Listen Port</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 Block Size</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">Local IP addresses available to the device:</span>\n <button class="btn btn-sm btn-outline-info mr-1" *ngFor="let address of localAddresses" (click)="server.copyAddress(address)" title="Click to copy">\n {{ address }}\n </button>\n <span class="text-warning small" *ngIf="!localAddresses.length">No usable IPv4 address detected</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">Stop Server Automatically</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">seconds</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">Notify After Download</label>\n </div>\n </div>\n </div>\n\n <details class="mb-3">\n <summary class="text-muted">Reliability Options</summary>\n <div class="form-row mt-2">\n <div class="form-group col-md-6">\n <label>Response Timeout (ms)</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>Retries</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 Server Running\' : \'TFTP Server Stopped\' }}</strong>\n <span class="badge badge-info ml-2" *ngIf="state.activeTransfers">{{ state.activeTransfers }} active transfer(s)</span>\n <div class="ml-auto">\n <button class="btn btn-sm btn-link text-muted" (click)="server.clearLogs()" [disabled]="!state.logs.length">Clear Logs</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 ? \'Starting…\' : \'Start Server\' }}\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>Stop Server\n </button>\n </div>\n </div>\n\n <div class="server-log">\n <div *ngIf="!state.logs.length" class="text-muted">Server output will appear here</div>\n <div *ngFor="let log of state.logs" [class.text-success]="log.level === \'success\'" [class.text-danger]="log.level === \'error\'">\n [{{ formatTime(log.timestamp) }}] {{ log.message }}\n </div>\n </div>\n <div class="text-danger mt-2" *ngIf="error">{{ error }}</div>\n ',styles:["\n :host { display: block; }\n label { margin-bottom: .25rem; }\n summary { cursor: pointer; user-select: none; }\n .local-addresses { min-height: 31px; display: flex; align-items: center; flex-wrap: wrap; }\n .status-dot { width: 10px; height: 10px; border-radius: 50%; background: #6c757d; display: inline-block; }\n .status-dot.running { background: #28a745; box-shadow: 0 0 7px rgba(40, 167, 69, .8); }\n .server-log { height: 175px; overflow: auto; padding: .65rem; border: 1px solid rgba(127, 127, 127, .3); border-radius: .25rem; background: rgba(0, 0, 0, .18); font-family: Consolas, monospace; font-size: 12px; white-space: pre-wrap; }\n "]}),i("design:paramtypes",[n.ConfigService,l.TftpServerService,o.NgZone,o.ChangeDetectorRef])],d),e.TftpServerComponent=d},28(t,e,s){e.TftpConfigProvider=e.defaultTftpSettings=e.defaultTftpServerSettings=void 0;const r=s(414);e.defaultTftpServerSettings={rootDirectory:"",bindAddress:"0.0.0.0",port:69,blockSize:8192,timeoutMs:3e3,retries:5,autoStop:!1,autoStopSeconds:360,notifyDownloads:!0},e.defaultTftpSettings={host:"",port:69,remoteFilename:"",blockSize:1468,timeoutMs:3e3,retries:5,server:{...e.defaultTftpServerSettings}};class i extends r.ConfigProvider{constructor(){super(...arguments),this.defaults={tftp:{...e.defaultTftpSettings}},this.platformDefaults={}}}e.TftpConfigProvider=i},156(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},n=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e},a=this&&this.__exportStar||function(t,e){for(var s in t)"default"===s||Object.prototype.hasOwnProperty.call(e,s)||r(e,t,s)};Object.defineProperty(e,"__esModule",{value:!0});const l=s(282),c=s(976),d=s(98),u=s(385),h=n(s(414)),f=s(983),p=s(263),m=s(153),g=s(28),b=s(306),v=s(21);let y=class{};y=o([(0,c.NgModule)({imports:[l.CommonModule,d.FormsModule,u.NgbModule,h.default],declarations:[p.TftpModalComponent,m.TftpServerComponent],providers:[b.TftpService,v.TftpServerService,{provide:h.ConfigProvider,useClass:g.TftpConfigProvider,multi:!0},{provide:h.ToolbarButtonProvider,useClass:f.TftpButtonProvider,multi:!0}]})],y),e.default=y,a(s(328),e),a(s(211),e),a(s(108),e)},306(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpService=void 0;const o=s(976),n=s(573),a=s(414),l=s(328),c={direction:"download",state:"idle",transferred:0,speed:0,message:"Ready"};class d{constructor(t){this.upload=t,this.pending=Buffer.alloc(0),this.ended=!1,this.size=t.getSize()}async read(t){for(;!this.ended&&this.pending.length<t;){const t=await this.upload.read();if(!t.length){this.ended=!0;break}this.pending=this.pending.length?Buffer.concat([this.pending,Buffer.from(t)]):Buffer.from(t)}const e=Buffer.from(this.pending.subarray(0,t));return this.pending=this.pending.subarray(e.length),e}close(){this.upload.close()}abort(){this.upload.cancel()}isCancelled(){return this.upload.isCancelled()}}class u{constructor(t,e){this.download=t,this.completed=!1,e||(t.getSize=()=>this.completed?t.getCompletedBytes():Math.max(1,t.getCompletedBytes()+1))}write(t){return this.download.write(t)}close(){this.completed=!0,this.download.close()}abort(){this.download.cancel()}isCancelled(){return this.download.isCancelled()}}let h=class{constructor(t,e){this.platform=t,this.notifications=e,this.progress$=new n.BehaviorSubject(c),this.controller=null,this.client=new l.TftpClient}get busy(){return null!==this.controller}async upload(t){this.ensureIdle();const e=(await this.platform.startUpload({multiple:!1}))[0];if(!e)return null;const s=t.remoteFilename.trim()||e.getName(),r=new d(e);return this.run("upload",s,e=>this.client.upload(this.toClientOptions(t,s),r,e.signal,t=>this.progress$.next(t)))}async download(t){this.ensureIdle();const e=t.remoteFilename.trim();if(!e)throw new Error("A remote filename is required for downloads");const s=e.split(/[\\/]/).filter(Boolean).pop()||"download.bin";return this.run("download",e,r=>this.client.download(this.toClientOptions(t,e),async t=>{const e=await this.platform.startDownload(s,420,null!=t?t:1);return e?new u(e,void 0!==t):null},r.signal,t=>this.progress$.next(t)))}cancel(){var t;null===(t=this.controller)||void 0===t||t.abort()}async run(t,e,s){this.ensureIdle();const r=new AbortController;this.controller=r;try{const i=await s(r);return this.notifications.notice(`TFTP ${"upload"===t?"upload":"download"} completed: ${e}`),i}catch(t){const e=t instanceof Error?t:new Error(String(t));throw"AbortError"===e.name?this.notifications.info("TFTP transfer cancelled"):this.notifications.error("TFTP transfer failed",e.message),e}finally{this.controller===r&&(this.controller=null)}}ensureIdle(){if(this.busy)throw new Error("Another TFTP transfer is already in progress")}toClientOptions(t,e){return{host:t.host,port:Number(t.port),remoteFilename:e,blockSize:Number(t.blockSize),timeoutMs:Number(t.timeoutMs),retries:Number(t.retries)}}};h=r([(0,o.Injectable)(),i("design:paramtypes",[a.PlatformService,a.NotificationsService])],h),e.TftpService=h},21(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},n=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e},a=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServerService=void 0;const l=s(976),c=s(573),d=s(414),u=n(s(366)),h=new(s(108).TftpServer),f=new c.BehaviorSubject({running:!1,activeTransfers:0,logs:[]});let p=null,m=class{constructor(t,e){this.notifications=t,this.platform=e,this.state$=f,this.server=h}get running(){return this.server.running}get activeSettings(){return p?{...p}:null}getLocalIPv4Addresses(){const t=new Set,e=u.networkInterfaces();for(const s of Object.values(e))for(const e of null!=s?s:[])"IPv4"!==e.family||e.internal||t.add(e.address);return Array.from(t).sort()}async chooseRootDirectory(){var t;const e=s(402),r=await e.dialog.showOpenDialog({title:"Choose TFTP Root Directory",properties:["openDirectory","createDirectory"]});return r.canceled?null:null!==(t=r.filePaths[0])&&void 0!==t?t:null}openRootDirectory(t){t&&this.platform.openPath(t)}copyAddress(t){this.platform.setClipboard({text:t}),this.notifications.notice(`Local IP copied: ${t}`)}async start(t){p={...t};try{await this.server.start({rootDirectory:t.rootDirectory,bindAddress:t.bindAddress,port:Number(t.port),blockSize:Number(t.blockSize),timeoutMs:Number(t.timeoutMs),retries:Number(t.retries),autoStopSeconds:t.autoStop?Number(t.autoStopSeconds):0},t=>this.handleEvent(t))}catch(t){const e=t instanceof Error?t.message:String(t);throw this.appendLog("error",`Failed to start: ${e}`),this.notifications.error("Failed to start TFTP server",e),t}}stop(){this.server.stop()}clearLogs(){this.state$.next({...this.state$.value,logs:[]})}handleEvent(t){var e;const s="completed"===t.type?"success":"error"===t.type?"error":"info",r=this.state$.value,i={running:"started"===t.type||"stopped"!==t.type&&this.server.running,activeTransfers:t.activeTransfers,startedAt:"started"===t.type?t.timestamp:r.startedAt,logs:[...r.logs,{timestamp:t.timestamp,level:s,message:t.message}].slice(-300)};this.state$.next(i),"completed"===t.type&&(null==p?void 0:p.notifyDownloads)?this.notifications.notice(`TFTP download completed: ${null!==(e=t.filename)&&void 0!==e?e:""}`):"stopped"===t.type&&this.notifications.info("TFTP server stopped")}appendLog(t,e){const s=this.state$.value;this.state$.next({...s,logs:[...s.logs,{timestamp:Date.now(),level:t,message:e}].slice(-300)})}};m=o([(0,l.Injectable)(),a("design:paramtypes",[d.NotificationsService,d.PlatformService])],m),e.TftpServerService=m},328(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpClient=void 0;const n=o(s(935)),a=s(211);function l(t){var e,s,r,i;const o=t.host.trim(),n=t.remoteFilename.trim(),a=null!==(e=t.port)&&void 0!==e?e:69,l=null!==(s=t.blockSize)&&void 0!==s?s:1468,c=null!==(r=t.timeoutMs)&&void 0!==r?r:3e3,d=null!==(i=t.retries)&&void 0!==i?i:5;if(!o)throw new Error("TFTP host cannot be empty");if(!n)throw new Error("Remote filename cannot be empty");if(!Number.isInteger(a)||a<1||a>65535)throw new Error("TFTP port must be between 1 and 65535");if(!Number.isInteger(l)||l<8||l>65464)throw new Error("TFTP block size must be between 8 and 65464");if(!Number.isInteger(c)||c<250||c>6e4)throw new Error("Timeout must be between 250 and 60000 milliseconds");if(!Number.isInteger(d)||d<0||d>20)throw new Error("Retries must be between 0 and 20");return{host:o,remoteFilename:n,port:a,blockSize:l,timeoutMs:c,retries:d}}function c(){const t=new Error("Transfer cancelled");return t.name="AbortError",t}class d{constructor(t,e,s,r){this.direction=t,this.options=e,this.signal=s,this.onProgress=r,this.startedAt=Date.now(),this.transferred=0,this.negotiatedBlockSize=512,this.endpoint=null,this.lastPacket=null,this.lastTarget=null,this.retryCount=0,this.retryTimer=null,this.cancelPoll=null,this.settled=!1,this.queue=Promise.resolve(),this.socket=n.createSocket("udp4")}runSession(t){var e;const s=new Promise((t,e)=>{this.resolvePromise=t,this.rejectPromise=e});return this.socket.on("error",t=>this.fail(t)),this.socket.on("message",(t,e)=>{this.queue=this.queue.then(()=>this.handleMessage(t,e)).catch(t=>this.fail(t instanceof Error?t:new Error(String(t))))}),null===(e=this.signal)||void 0===e||e.addEventListener("abort",()=>this.fail(c()),{once:!0}),this.cancelPoll=setInterval(()=>{this.resourceCancelled()&&this.fail(c())},250),this.emit("negotiating","Connecting to TFTP server"),this.sendTracked(t,{address:this.options.host,port:this.options.port}),s}clearRetryTimer(){this.retryTimer&&clearTimeout(this.retryTimer),this.retryTimer=null,this.retryCount=0}sendTracked(t,e){var s;if(this.settled)return;const r=null!==(s=null!=e?e:this.endpoint)&&void 0!==s?s:{address:this.options.host,port:this.options.port};this.lastPacket=Buffer.from(t),this.lastTarget=r,this.retryTimer&&clearTimeout(this.retryTimer),this.socket.send(t,r.port,r.address,t=>{t&&this.fail(t)}),this.retryTimer=setTimeout(()=>this.retryLastPacket(),this.options.timeoutMs)}sendUntracked(t,e,s){var r;if(this.settled)return;const i=null!==(r=null!=e?e:this.endpoint)&&void 0!==r?r:{address:this.options.host,port:this.options.port};this.socket.send(t,i.port,i.address,t=>{t?this.fail(t):null==s||s()})}emit(t,e,s){var r;const i=Math.max(1,Date.now()-this.startedAt);null===(r=this.onProgress)||void 0===r||r.call(this,{direction:this.direction,state:t,transferred:this.transferred,total:s,speed:1e3*this.transferred/i,attempt:"retrying"===t?this.retryCount:void 0,message:e})}succeed(){if(this.settled)return;this.settled=!0,this.cleanupTimers(),this.releaseResource(!0),this.socket.close();const t={bytes:this.transferred,blockSize:this.negotiatedBlockSize,durationMs:Date.now()-this.startedAt};this.emit("completed","Transfer completed",this.transferred),this.resolvePromise(t)}fail(t){if(!this.settled){this.settled=!0,this.cleanupTimers(),this.releaseResource(!1);try{this.socket.close()}catch{}this.emit("AbortError"===t.name?"cancelled":"error",t.message),this.rejectPromise(t)}}async handleMessage(t,e){var s;if(this.settled)return;if((null===(s=this.signal)||void 0===s?void 0:s.aborted)||this.resourceCancelled())throw c();if(this.endpoint&&(e.address!==this.endpoint.address||e.port!==this.endpoint.port))return void this.sendUntracked((0,a.createError)(5,"Unknown transfer ID"),{address:e.address,port:e.port});const r=(0,a.parsePacket)(t);if(this.endpoint||(this.endpoint={address:e.address,port:e.port}),r.opcode===a.Opcode.ERROR)throw new Error(`TFTP error ${r.code}: ${r.message}`);await this.onPacket(r)}retryLastPacket(){var t;this.settled||((null===(t=this.signal)||void 0===t?void 0:t.aborted)||this.resourceCancelled()?this.fail(c()):!this.lastPacket||!this.lastTarget||this.retryCount>=this.options.retries?this.fail(new Error(`TFTP response timed out after ${this.options.retries} retries`)):(this.retryCount++,this.emit("retrying",`Response timed out; retry ${this.retryCount}`),this.socket.send(this.lastPacket,this.lastTarget.port,this.lastTarget.address,t=>{t&&this.fail(t)}),this.retryTimer=setTimeout(()=>this.retryLastPacket(),this.options.timeoutMs)))}cleanupTimers(){this.retryTimer&&clearTimeout(this.retryTimer),this.cancelPoll&&clearInterval(this.cancelPoll),this.retryTimer=null,this.cancelPoll=null}}class u extends d{constructor(t,e,s,r){super("download",t,s,r),this.sinkFactory=e,this.expectedBlock=1,this.sink=null}run(){const t=Math.max(1,Math.min(255,Math.round(this.options.timeoutMs/1e3)));return this.runSession((0,a.createRequest)(a.Opcode.RRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:0}))}async onPacket(t){if(t.opcode===a.Opcode.OACK){if(1!==this.expectedBlock||0!==this.transferred)return;return this.clearRetryTimer(),this.negotiatedBlockSize=(0,a.parseNegotiatedBlockSize)(t.options),this.totalSize=(0,a.parseTransferSize)(t.options),await this.ensureSink(this.totalSize),this.emit("transferring","Transfer parameters negotiated",this.totalSize),void this.sendTracked((0,a.createAck)(0))}if(t.opcode!==a.Opcode.DATA)return;if(t.block===this.expectedBlock){this.clearRetryTimer(),this.sink||(this.negotiatedBlockSize=512,await this.ensureSink(void 0)),await this.sink.write(t.data),this.transferred+=t.data.length,this.emit("transferring",`Receiving block ${t.block}`,this.totalSize);const e=t.data.length<this.negotiatedBlockSize,s=(0,a.createAck)(t.block);return void(e?this.sendUntracked(s,void 0,()=>this.succeed()):(this.expectedBlock=this.expectedBlock+1&65535,this.sendTracked(s)))}const e=this.expectedBlock-1&65535;t.block===e&&this.sendTracked((0,a.createAck)(t.block))}releaseResource(t){this.sink&&(t?this.sink.close():this.sink.abort?this.sink.abort():this.sink.close())}resourceCancelled(){var t,e,s;return null!==(s=null===(e=null===(t=this.sink)||void 0===t?void 0:t.isCancelled)||void 0===e?void 0:e.call(t))&&void 0!==s&&s}async ensureSink(t){if(!this.sink&&(this.sink=await this.sinkFactory(t),!this.sink))throw c()}}class h extends d{constructor(t,e,s,r){super("upload",t,s,r),this.source=e,this.nextBlock=1,this.currentBlock=null,this.currentData=Buffer.alloc(0),this.currentFinal=!1}run(){const t=Math.max(1,Math.min(255,Math.round(this.options.timeoutMs/1e3)));return this.runSession((0,a.createRequest)(a.Opcode.WRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:this.source.size}))}async onPacket(t){return t.opcode===a.Opcode.OACK&&null===this.currentBlock?(this.clearRetryTimer(),this.negotiatedBlockSize=(0,a.parseNegotiatedBlockSize)(t.options),this.emit("transferring","Transfer parameters negotiated",this.source.size),void await this.sendNextBlock()):t.opcode===a.Opcode.ACK?0===t.block&&null===this.currentBlock?(this.clearRetryTimer(),this.negotiatedBlockSize=512,this.emit("transferring","Server is using standard 512-byte blocks",this.source.size),void await this.sendNextBlock()):null!==this.currentBlock&&t.block===this.currentBlock?(this.clearRetryTimer(),this.transferred+=this.currentData.length,this.emit("transferring",`Server acknowledged block ${t.block}`,this.source.size),void(this.currentFinal?this.succeed():(this.nextBlock=this.nextBlock+1&65535,await this.sendNextBlock()))):void(null!==this.currentBlock&&t.block===(this.currentBlock-1&65535)&&this.sendTracked((0,a.createData)(this.currentBlock,this.currentData))):void 0}releaseResource(t){t?this.source.close():this.source.abort?this.source.abort():this.source.close()}resourceCancelled(){var t,e,s;return null!==(s=null===(e=(t=this.source).isCancelled)||void 0===e?void 0:e.call(t))&&void 0!==s&&s}async sendNextBlock(){var t;if((null===(t=this.signal)||void 0===t?void 0:t.aborted)||this.resourceCancelled())throw c();if(this.currentData=Buffer.from(await this.source.read(this.negotiatedBlockSize)),this.currentData.length>this.negotiatedBlockSize)throw new Error("Upload source returned more data than the negotiated block size");this.currentBlock=this.nextBlock,this.currentFinal=this.currentData.length<this.negotiatedBlockSize,this.sendTracked((0,a.createData)(this.currentBlock,this.currentData))}}e.TftpClient=class{download(t,e,s,r){return new u(l(t),e,s,r).run()}upload(t,e,s,r){return new h(l(t),e,s,r).run()}}},211(t,e){var s;function r(t){if(t.includes("\0"))throw new Error("TFTP fields cannot contain null characters");return Buffer.concat([Buffer.from(t,"utf8"),Buffer.from([0])])}function i(t,e){const s=[];let r=e;for(let i=e;i<t.length;i++)0===t[i]&&(s.push(t.toString("utf8",r,i)),r=i+1);if(r!==t.length||s.length%2!=0)throw new Error("Invalid TFTP option response");const i={};for(let t=0;t<s.length;t+=2)i[s[t].toLowerCase()]=s[t+1];return i}Object.defineProperty(e,"__esModule",{value:!0}),e.parseTransferSize=e.parseNegotiatedBlockSize=e.parsePacket=e.createError=e.createAck=e.createData=e.createRequest=e.Opcode=void 0,function(t){t[t.RRQ=1]="RRQ",t[t.WRQ=2]="WRQ",t[t.DATA=3]="DATA",t[t.ACK=4]="ACK",t[t.ERROR=5]="ERROR",t[t.OACK=6]="OACK"}(s=e.Opcode||(e.Opcode={})),e.createRequest=function(t,e,s={}){if(!e.trim())throw new Error("Remote filename cannot be empty");const i=Buffer.allocUnsafe(2);i.writeUInt16BE(t,0);const o=[i,r(e),r("octet")];for(const[t,e]of Object.entries(s))o.push(r(t.toLowerCase())),o.push(r(String(e)));return Buffer.concat(o)},e.createData=function(t,e){const r=Buffer.allocUnsafe(4+e.length);return r.writeUInt16BE(s.DATA,0),r.writeUInt16BE(65535&t,2),e.copy(r,4),r},e.createAck=function(t){const e=Buffer.allocUnsafe(4);return e.writeUInt16BE(s.ACK,0),e.writeUInt16BE(65535&t,2),e},e.createError=function(t,e){const i=Buffer.allocUnsafe(4);return i.writeUInt16BE(s.ERROR,0),i.writeUInt16BE(65535&t,2),Buffer.concat([i,r(e)])},e.parsePacket=function(t){if(t.length<2)throw new Error("TFTP packet is too short");const e=t.readUInt16BE(0);switch(e){case s.DATA:if(t.length<4)throw new Error("Invalid DATA packet");return{opcode:e,block:t.readUInt16BE(2),data:t.subarray(4)};case s.ACK:if(4!==t.length)throw new Error("Invalid ACK packet");return{opcode:e,block:t.readUInt16BE(2)};case s.ERROR:if(t.length<5||0!==t[t.length-1])throw new Error("Invalid ERROR packet");return{opcode:e,code:t.readUInt16BE(2),message:t.toString("utf8",4,t.length-1)};case s.OACK:return{opcode:e,options:i(t,2)};default:throw new Error(`Unsupported TFTP opcode: ${e}`)}},e.parseNegotiatedBlockSize=function(t,e=512){if(!t.blksize)return e;const s=Number(t.blksize);if(!Number.isInteger(s)||s<8||s>65464)throw new Error(`Server returned an invalid block size: ${t.blksize}`);return s},e.parseTransferSize=function(t){if(void 0===t.tsize)return;const e=Number(t.tsize);if(!Number.isSafeInteger(e)||e<0)throw new Error(`Server returned an invalid file size: ${t.tsize}`);return e}},108(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServer=e.parseRequest=void 0;const n=o(s(935)),a=o(s(383)),l=o(s(3)),c=s(211);function d(t){if(t.length<4)throw new Error("TFTP request is too short");const e=t.readUInt16BE(0);if(e!==c.Opcode.RRQ&&e!==c.Opcode.WRQ)throw new Error("Not a valid read or write request");const s=function(t){const e=[];let s=2;for(let r=2;r<t.length;r++)0===t[r]&&(e.push(t.toString("utf8",s,r)),s=r+1);if(s!==t.length)throw new Error("Request field is missing a terminator");return e}(t);if(s.length<2||(s.length-2)%2!=0)throw new Error("Invalid TFTP request fields");const r=s[0],i=s[1].toLowerCase();if(!r)throw new Error("Requested filename is empty");if("octet"!==i)throw new Error(`Unsupported transfer mode: ${i}`);const o={};for(let t=2;t<s.length;t+=2)o[s[t].toLowerCase()]=s[t+1];return{opcode:e,filename:r,mode:i,options:o}}e.parseRequest=d;class u{constructor(t,e,s,r){this.request=t,this.remote=e,this.options=s,this.onFinish=r,this.blockSize=512,this.transferred=0,this.lastPacket=null,this.timer=null,this.retryCount=0,this.settled=!1,this.failing=!1,this.queue=Promise.resolve(),this.timeoutMs=s.timeoutMs,this.socket=n.createSocket("udp4")}start(){this.socket.on("error",t=>this.fail(t.message)),this.socket.on("message",(t,e)=>{this.queue=this.queue.then(()=>this.handleMessage(t,e)).catch(t=>this.fail(t instanceof Error?t.message:String(t)))}),this.socket.bind(0,this.options.bindAddress,()=>{this.begin().catch(t=>this.fail(t instanceof Error?t.message:String(t)))})}resendLast(){this.lastPacket&&this.sendTracked(this.lastPacket)}cancel(){this.finish(!1,"Server stopped")}sendTracked(t){this.settled||(this.lastPacket=Buffer.from(t),this.timer&&clearTimeout(this.timer),this.socket.send(t,this.remote.port,this.remote.address,t=>{t&&this.fail(t.message)}),this.timer=setTimeout(()=>this.retry(),this.timeoutMs))}clearTimer(){this.timer&&clearTimeout(this.timer),this.timer=null,this.retryCount=0}complete(t){this.finish(!0,t)}fail(t){if(this.settled||this.failing)return;this.failing=!0,this.timer&&clearTimeout(this.timer);const e=(0,c.createError)(1,t);this.socket.send(e,this.remote.port,this.remote.address,()=>this.finish(!1,t))}async handleMessage(t,e){if(this.settled)return;if(e.address!==this.remote.address||e.port!==this.remote.port){const t=(0,c.createError)(5,"Unknown transfer ID");return void this.socket.send(t,e.port,e.address)}const s=(0,c.parsePacket)(t);if(s.opcode===c.Opcode.ERROR)throw new Error(`Client error ${s.code}: ${s.message}`);await this.onPacket(s)}retry(){this.settled||(!this.lastPacket||this.retryCount>=this.options.retries?this.fail(`Timed out waiting for the client after ${this.options.retries} retries`):(this.retryCount++,this.socket.send(this.lastPacket,this.remote.port,this.remote.address,t=>{t&&this.fail(t.message)}),this.timer=setTimeout(()=>this.retry(),this.timeoutMs)))}finish(t,e){this.settled||(this.settled=!0,this.timer&&clearTimeout(this.timer),this.release(t).catch(()=>{}).finally(()=>{try{this.socket.close()}catch{}this.onFinish(t,e,this.transferred)}))}}class h extends u{constructor(t,e,s,r){super(t,e,s,r),this.file=null,this.fileSize=0,this.position=0,this.currentBlock=0,this.currentData=Buffer.alloc(0),this.finalBlock=!1}async begin(){const t=function(t,e){const s=e.replace(/\\/g,"/").replace(/^\/+/,"").split("/").filter(t=>t&&"."!==t);if(!s.length||s.some(t=>".."===t))throw new Error("Unsafe file path rejected");const r=l.resolve(t,...s),i=l.relative(t,r);if(!i||i.startsWith("..")||l.isAbsolute(i))throw new Error("Requested file is outside the TFTP root directory");return r}(this.options.rootDirectory,this.request.filename),e=await a.promises.stat(t);if(!e.isFile())throw new Error("Requested path is not a regular file");this.fileSize=e.size,this.file=await a.promises.open(t,"r");const s={};if(void 0!==this.request.options.blksize){const t=Number(this.request.options.blksize);if(!Number.isInteger(t)||t<8||t>65464)throw new Error("Client requested an invalid block size");this.blockSize=Math.min(t,this.options.blockSize),s.blksize=this.blockSize}if(void 0!==this.request.options.timeout){const t=Number(this.request.options.timeout);if(!Number.isInteger(t)||t<1||t>255)throw new Error("Client requested an invalid timeout");this.timeoutMs=1e3*t,s.timeout=t}void 0!==this.request.options.tsize&&(s.tsize=this.fileSize),Object.keys(s).length?this.sendTracked(function(t){const e=Buffer.allocUnsafe(2);e.writeUInt16BE(c.Opcode.OACK,0);const s=[e];for(const[e,r]of Object.entries(t))s.push(Buffer.from(`${e}\0${r}\0`,"utf8"));return Buffer.concat(s)}(s)):await this.sendNextBlock()}async onPacket(t){if(t.opcode===c.Opcode.ACK)return 0===this.currentBlock&&0===t.block?(this.clearTimer(),void await this.sendNextBlock()):t.block===this.currentBlock?(this.clearTimer(),this.transferred+=this.currentData.length,void(this.finalBlock?this.complete(`Sent ${this.transferred} bytes`):await this.sendNextBlock())):void(t.block===(this.currentBlock-1&65535)&&this.resendLast())}async release(){this.file&&await this.file.close(),this.file=null}async sendNextBlock(){const t=this.file;if(!t)throw new Error("TFTP file is not open");const e=Buffer.allocUnsafe(this.blockSize),s=await t.read(e,0,this.blockSize,this.position);this.currentData=Buffer.from(e.subarray(0,s.bytesRead)),this.position+=s.bytesRead,this.currentBlock=this.currentBlock+1&65535,this.finalBlock=s.bytesRead<this.blockSize,this.sendTracked((0,c.createData)(this.currentBlock,this.currentData))}}e.TftpServer=class{constructor(){this.listener=null,this.options=null,this.transfers=new Map,this.autoStopTimer=null,this.eventHandler=null}get running(){return null!==this.listener}get activeTransfers(){return this.transfers.size}async start(t,e){if(this.running)throw new Error("TFTP server is already running");const s=function(t){var e,s,r,i,o,n;const a=l.resolve(t.rootDirectory.trim()),c=(null!==(e=t.bindAddress)&&void 0!==e?e:"0.0.0.0").trim(),d=null!==(s=t.port)&&void 0!==s?s:69,u=null!==(r=t.blockSize)&&void 0!==r?r:8192,h=null!==(i=t.timeoutMs)&&void 0!==i?i:3e3,f=null!==(o=t.retries)&&void 0!==o?o:5,p=null!==(n=t.autoStopSeconds)&&void 0!==n?n:0;if(!t.rootDirectory.trim())throw new Error("Choose a TFTP root directory");if(!c)throw new Error("Listen address cannot be empty");if(!Number.isInteger(d)||d<1||d>65535)throw new Error("Listen port must be between 1 and 65535");if(!Number.isInteger(u)||u<8||u>65464)throw new Error("TFTP block size must be between 8 and 65464 bytes");if(!Number.isInteger(h)||h<250||h>6e4)throw new Error("Timeout must be between 250 and 60000 milliseconds");if(!Number.isInteger(f)||f<0||f>20)throw new Error("Retries must be between 0 and 20");if(!Number.isInteger(p)||p<0||p>86400)throw new Error("Automatic stop time must be between 0 and 86400 seconds");return{rootDirectory:a,bindAddress:c,port:d,blockSize:u,timeoutMs:h,retries:f,autoStopSeconds:p}}(t);if(!(await a.promises.stat(s.rootDirectory)).isDirectory())throw new Error("Invalid TFTP root directory");this.options=s,this.eventHandler=null!=e?e:null;const r=n.createSocket("udp4");this.listener=r,r.on("message",(t,e)=>this.handleRequest(t,e));let i=null;r.on("error",t=>{if(i){const e=i;i=null;try{r.close()}catch{}this.listener=null,e(t)}else this.emit("error",t.message)}),await new Promise((t,e)=>{i=e,r.bind(s.port,s.bindAddress,()=>{i=null,t()})}),this.emit("started",`TFTP server started at ${s.bindAddress}:${s.port}`),s.autoStopSeconds>0&&(this.autoStopTimer=setTimeout(()=>this.stop("automatic stop timer expired"),1e3*s.autoStopSeconds))}stop(t="stopped by user"){if(this.listener){this.autoStopTimer&&clearTimeout(this.autoStopTimer),this.autoStopTimer=null;for(const t of this.transfers.values())t.cancel();this.transfers.clear();try{this.listener.close()}catch{}this.listener=null,this.emit("stopped",`TFTP server stopped: ${t}`)}}handleRequest(t,e){if(!this.listener||!this.options)return;let s;try{s=d(t)}catch(t){const s=t instanceof Error?t.message:String(t);return this.listener.send((0,c.createError)(4,s),e.port,e.address),void this.emit("error",`Rejected invalid request from ${e.address}:${e.port}: ${s}`)}const r=`${e.address}:${e.port}:${s.opcode}:${s.filename}`,i=this.transfers.get(r);if(i)return void i.resendLast();if(s.opcode===c.Opcode.WRQ)return this.listener.send((0,c.createError)(2,"Server is read-only"),e.port,e.address),void this.emit("error",`Rejected upload of ${s.filename} from ${e.address}:${e.port}: server is read-only`,e,s.filename);const o=`${e.address}:${e.port}`,n=new h(s,e,this.options,(t,i,n)=>{this.transfers.delete(r),this.emit(t?"completed":"error",`${o} ${t?"completed download of":"failed to download"} ${s.filename}: ${i}`,e,s.filename,n)});this.transfers.set(r,n),this.emit("request",`${o} requested ${s.filename}`,e,s.filename),n.start()}emit(t,e,s,r,i){var o;null===(o=this.eventHandler)||void 0===o||o.call(this,{type:t,timestamp:Date.now(),message:e,peer:s?`${s.address}:${s.port}`:void 0,filename:r,bytes:i,activeTransfers:this.transfers.size})}}},282(t){t.exports=require("@angular/common")},976(t){t.exports=require("@angular/core")},98(t){t.exports=require("@angular/forms")},402(t){t.exports=require("@electron/remote")},385(t){t.exports=require("@ng-bootstrap/ng-bootstrap")},935(t){t.exports=require("dgram")},383(t){t.exports=require("fs")},366(t){t.exports=require("os")},3(t){t.exports=require("path")},573(t){t.exports=require("rxjs")},414(t){t.exports=require("tabby-core")}};const e={};let s=function s(r){const i=e[r];if(void 0!==i)return i.exports;const o=e[r]={exports:{}};return t[r].call(o.exports,o,o.exports,s),o.exports}(156);module.exports=s})();
1
+ (()=>{"use strict";var t={983(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpButtonProvider=void 0;const o=s(976),n=s(385),a=s(414),l=s(263);let c=class extends a.ToolbarButtonProvider{constructor(t){super(),this.modal=t}provide(){return[{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M7 7h11l-3.5-3.5L16 2l6 6-6 6-1.5-1.5L18 9H7V7zm10 10H6l3.5 3.5L8 22l-6-6 6-6 1.5 1.5L6 15h11v2z"/></svg>',title:"TFTP File Transfer",weight:6,click:()=>this.modal.open(l.TftpModalComponent,{size:"lg"})}]}};c=r([(0,o.Injectable)(),i("design:paramtypes",[n.NgbModal])],c),e.TftpButtonProvider=c},263(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpModalComponent=void 0;const o=s(976),n=s(385),a=s(414),l=s(28),c=s(306);let d=class{constructor(t,e,s,r,i){this.activeModal=t,this.config=e,this.tftp=s,this.zone=r,this.changeDetector=i,this.mode="server",this.settings={...l.defaultTftpSettings,server:{...l.defaultTftpServerSettings}},this.status=null,this.validationError=""}get busy(){return this.tftp.busy}get percent(){return this.status?"completed"===this.status.state?100:void 0===this.status.total?this.busy?100:0:0===this.status.total?100:Math.min(100,Math.round(100*this.status.transferred/this.status.total)):0}get progressType(){var t,e,s;return"error"===(null===(t=this.status)||void 0===t?void 0:t.state)||"cancelled"===(null===(e=this.status)||void 0===e?void 0:e.state)?"danger":"completed"===(null===(s=this.status)||void 0===s?void 0:s.state)?"success":"info"}ngOnInit(){var t,e;const s=null!==(t=this.config.store.tftp)&&void 0!==t?t:{};this.settings={...l.defaultTftpSettings,...s,server:{...l.defaultTftpServerSettings,...null!==(e=s.server)&&void 0!==e?e:{}}},this.subscription=this.tftp.progress$.subscribe(t=>{this.zone.run(()=>{this.status=t,this.changeDetector.markForCheck()})})}ngOnDestroy(){var t;null===(t=this.subscription)||void 0===t||t.unsubscribe()}async upload(){this.validate(!1)&&await this.execute(()=>this.tftp.upload(this.settings))}async download(){this.validate(!0)&&await this.execute(()=>this.tftp.download(this.settings))}cancel(){this.tftp.cancel()}formatBytes(t){if(!Number.isFinite(t)||t<=0)return"0 B";const e=["B","KB","MB","GB","TB"],s=Math.min(e.length-1,Math.floor(Math.log(t)/Math.log(1024))),r=t/Math.pow(1024,s);return`${r>=100||0===s?r.toFixed(0):r.toFixed(1)} ${e[s]}`}async execute(t){this.validationError="",this.config.store.tftp||(this.config.store.tftp={}),Object.assign(this.config.store.tftp,{host:this.settings.host,port:this.settings.port,remoteFilename:this.settings.remoteFilename,blockSize:this.settings.blockSize,timeoutMs:this.settings.timeoutMs,retries:this.settings.retries}),await this.config.save();try{await t()}catch(t){this.validationError=t instanceof Error?t.message:String(t)}}validate(t){return this.validationError="",this.settings.host.trim()?t&&!this.settings.remoteFilename.trim()?this.validationError="A remote filename is required for downloads":!Number.isInteger(Number(this.settings.port))||Number(this.settings.port)<1||Number(this.settings.port)>65535?this.validationError="Port must be between 1 and 65535":!Number.isInteger(Number(this.settings.blockSize))||Number(this.settings.blockSize)<8||Number(this.settings.blockSize)>65464?this.validationError="Block size must be between 8 and 65464 bytes":!Number.isInteger(Number(this.settings.timeoutMs))||Number(this.settings.timeoutMs)<250||Number(this.settings.timeoutMs)>6e4?this.validationError="Timeout must be between 250 and 60000 milliseconds":(!Number.isInteger(Number(this.settings.retries))||Number(this.settings.retries)<0||Number(this.settings.retries)>20)&&(this.validationError="Retries must be between 0 and 20"):this.validationError="Enter a TFTP host address",!this.validationError}};d=r([(0,o.Component)({selector:"tabby-tftp-modal",template:'\n <div class="modal-header">\n <h5 class="modal-title"><i class="fa fa-exchange-alt mr-2"></i>TFTP File Transfer and Firmware Server</h5>\n <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss()">\n <span aria-hidden="true">&times;</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>Firmware TFTP Server\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 Client\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 provides no authentication or encryption. Use it only on a trusted local network.\n </div>\n\n <div class="form-row">\n <div class="form-group col-md-8">\n <label>Host or IP Address</label>\n <input class="form-control" [(ngModel)]="settings.host" placeholder="e.g. 192.168.1.10" [disabled]="busy">\n </div>\n <div class="form-group col-md-4">\n <label>Port</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>Remote Filename</label>\n <input class="form-control" [(ngModel)]="settings.remoteFilename"\n placeholder="Required for downloads; leave blank on upload to use the local filename" [disabled]="busy">\n <small class="form-text text-muted">TFTP cannot browse remote directories. Path syntax is determined by the server.</small>\n </div>\n\n <details class="mb-3">\n <summary class="text-muted">Advanced Options</summary>\n <div class="form-row mt-2">\n <div class="form-group col-md-4">\n <label>Block Size</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>Timeout (ms)</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>Retries</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\' ? \'Upload\' : \'Download\' }}</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()">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>Cancel Transfer</button>\n <button class="btn btn-primary" *ngIf="!busy" (click)="upload()"><i class="fa fa-upload mr-1"></i>Upload</button>\n <button class="btn btn-success" *ngIf="!busy" (click)="download()"><i class="fa fa-download mr-1"></i>Download</button>\n </ng-container>\n </div>\n ',styles:["\n :host { display: block; }\n summary { cursor: pointer; user-select: none; }\n .transfer-status { border: 1px solid rgba(127, 127, 127, .25); border-radius: .25rem; padding: .75rem; }\n .modal-body label { margin-bottom: .25rem; }\n .mode-tabs .btn { flex: 1; }\n "]}),i("design:paramtypes",[n.NgbActiveModal,a.ConfigService,c.TftpService,o.NgZone,o.ChangeDetectorRef])],d),e.TftpModalComponent=d},153(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServerComponent=void 0;const o=s(976),n=s(414),a=s(28),l=s(21),c="tabby-tftp-server-settings-v1";let d=class{constructor(t,e,s,r){this.config=t,this.server=e,this.zone=s,this.changeDetector=r,this.settings={...a.defaultTftpServerSettings},this.state={running:!1,activeTransfers:0,logs:[]},this.localAddresses=[],this.starting=!1,this.error=""}ngOnInit(){var t,e,s,r;let i={};try{i=JSON.parse(null!==(t=localStorage.getItem(c))&&void 0!==t?t:"{}")}catch{}this.settings={...a.defaultTftpServerSettings,...null!==(s=null===(e=this.config.store.tftp)||void 0===e?void 0:e.server)&&void 0!==s?s:{},...i,...null!==(r=this.server.activeSettings)&&void 0!==r?r:{}},this.localAddresses=this.server.getLocalIPv4Addresses(),this.subscription=this.server.state$.subscribe(t=>{this.zone.run(()=>{this.state=t,this.changeDetector.markForCheck()})})}ngOnDestroy(){var t;null===(t=this.subscription)||void 0===t||t.unsubscribe()}async chooseDirectory(){const t=await this.server.chooseRootDirectory();t&&(this.settings.rootDirectory=t,await this.saveSettings())}async start(){if(this.error="",this.validate()){this.starting=!0,await this.saveSettings();try{await this.server.start(this.settings)}catch(t){this.error=t instanceof Error?t.message:String(t)}finally{this.starting=!1}}}formatTime(t){return new Date(t).toLocaleTimeString(void 0,{hour12:!1})}validate(){return this.settings.rootDirectory.trim()?!Number.isInteger(Number(this.settings.port))||Number(this.settings.port)<1||Number(this.settings.port)>65535?this.error="Listen port must be between 1 and 65535":!Number.isInteger(Number(this.settings.blockSize))||Number(this.settings.blockSize)<8||Number(this.settings.blockSize)>65464?this.error="TFTP block size must be between 8 and 65464 bytes":this.settings.autoStop&&(!Number.isInteger(Number(this.settings.autoStopSeconds))||Number(this.settings.autoStopSeconds)<1||Number(this.settings.autoStopSeconds)>86400)&&(this.error="Automatic stop time must be between 1 and 86400 seconds"):this.error="Choose a TFTP root directory containing the firmware",!this.error}async saveSettings(){localStorage.setItem(c,JSON.stringify(this.settings)),this.config.store.tftp||(this.config.store.tftp={}),this.config.store.tftp.server||(this.config.store.tftp.server={}),Object.assign(this.config.store.tftp.server,this.settings),await this.config.save()}};d=r([(0,o.Component)({selector:"tabby-tftp-server",template:'\n <div class="alert alert-info py-2 small">\n Use the serial terminal to control the bootloader. Downloads are always available; uploads must be explicitly enabled below.\n </div>\n\n <div class="form-group">\n <label>TFTP Root Directory</label>\n <div class="input-group">\n <input class="form-control" [(ngModel)]="settings.rootDirectory" placeholder="Directory containing firmware files" [disabled]="state.running">\n <div class="input-group-append">\n <button class="btn btn-outline-secondary" (click)="chooseDirectory()" [disabled]="state.running" title="Choose Directory">\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="Open Directory">\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>Listen Address</label>\n <select class="form-control" [(ngModel)]="settings.bindAddress" [disabled]="state.running">\n <option value="0.0.0.0">0.0.0.0 (all interfaces)</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>Listen Port</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 Block Size</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">Local IP addresses available to the device:</span>\n <button class="btn btn-sm btn-outline-info mr-1" *ngFor="let address of localAddresses" (click)="server.copyAddress(address)" title="Click to copy">\n {{ address }}\n </button>\n <span class="text-warning small" *ngIf="!localAddresses.length">No usable IPv4 address detected</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">Stop Server Automatically</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">seconds</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">Notify After Transfer</label>\n </div>\n </div>\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-allow-uploads" [(ngModel)]="settings.allowUploads" [disabled]="state.running">\n <label class="custom-control-label" for="tftp-allow-uploads">Allow Uploads</label>\n </div>\n </div>\n <div class="col-md-7">\n <div class="custom-control custom-checkbox">\n <input type="checkbox" class="custom-control-input" id="tftp-overwrite-uploads" [(ngModel)]="settings.overwriteUploads" [disabled]="state.running || !settings.allowUploads">\n <label class="custom-control-label" for="tftp-overwrite-uploads">Allow Uploads to Overwrite Existing Files</label>\n </div>\n </div>\n </div>\n <div class="alert alert-warning py-2 small" *ngIf="settings.allowUploads && !state.running">\n Uploaded files are saved under the TFTP root directory. Keep overwrite disabled unless replacing local files is intentional.\n </div>\n\n <details class="mb-3">\n <summary class="text-muted">Reliability Options</summary>\n <div class="form-row mt-2">\n <div class="form-group col-md-6">\n <label>Response Timeout (ms)</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>Retries</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 Server Running\' : \'TFTP Server Stopped\' }}</strong>\n <span class="badge badge-info ml-2" *ngIf="state.activeTransfers">{{ state.activeTransfers }} active transfer(s)</span>\n <div class="ml-auto">\n <button class="btn btn-sm btn-link text-muted" (click)="server.clearLogs()" [disabled]="!state.logs.length">Clear Logs</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 ? \'Starting…\' : \'Start Server\' }}\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>Stop Server\n </button>\n </div>\n </div>\n\n <div class="server-log">\n <div *ngIf="!state.logs.length" class="text-muted">Server output will appear here</div>\n <div *ngFor="let log of state.logs" [class.text-success]="log.level === \'success\'" [class.text-danger]="log.level === \'error\'">\n [{{ formatTime(log.timestamp) }}] {{ log.message }}\n </div>\n </div>\n <div class="text-danger mt-2" *ngIf="error">{{ error }}</div>\n ',styles:["\n :host { display: block; }\n label { margin-bottom: .25rem; }\n summary { cursor: pointer; user-select: none; }\n .local-addresses { min-height: 31px; display: flex; align-items: center; flex-wrap: wrap; }\n .status-dot { width: 10px; height: 10px; border-radius: 50%; background: #6c757d; display: inline-block; }\n .status-dot.running { background: #28a745; box-shadow: 0 0 7px rgba(40, 167, 69, .8); }\n .server-log { height: 175px; overflow: auto; padding: .65rem; border: 1px solid rgba(127, 127, 127, .3); border-radius: .25rem; background: rgba(0, 0, 0, .18); font-family: Consolas, monospace; font-size: 12px; white-space: pre-wrap; }\n "]}),i("design:paramtypes",[n.ConfigService,l.TftpServerService,o.NgZone,o.ChangeDetectorRef])],d),e.TftpServerComponent=d},28(t,e,s){e.TftpConfigProvider=e.defaultTftpSettings=e.defaultTftpServerSettings=void 0;const r=s(414);e.defaultTftpServerSettings={rootDirectory:"",bindAddress:"0.0.0.0",port:69,blockSize:8192,timeoutMs:3e3,retries:5,autoStop:!1,autoStopSeconds:360,notifyDownloads:!0,allowUploads:!1,overwriteUploads:!1},e.defaultTftpSettings={host:"",port:69,remoteFilename:"",blockSize:1468,timeoutMs:3e3,retries:5,server:{...e.defaultTftpServerSettings}};class i extends r.ConfigProvider{constructor(){super(...arguments),this.defaults={tftp:{...e.defaultTftpSettings}},this.platformDefaults={}}}e.TftpConfigProvider=i},156(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},n=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e},a=this&&this.__exportStar||function(t,e){for(var s in t)"default"===s||Object.prototype.hasOwnProperty.call(e,s)||r(e,t,s)};Object.defineProperty(e,"__esModule",{value:!0});const l=s(282),c=s(976),d=s(98),u=s(385),h=n(s(414)),p=s(983),f=s(263),m=s(153),g=s(28),b=s(306),v=s(21);let y=class{};y=o([(0,c.NgModule)({imports:[l.CommonModule,d.FormsModule,u.NgbModule,h.default],declarations:[f.TftpModalComponent,m.TftpServerComponent],providers:[b.TftpService,v.TftpServerService,{provide:h.ConfigProvider,useClass:g.TftpConfigProvider,multi:!0},{provide:h.ToolbarButtonProvider,useClass:p.TftpButtonProvider,multi:!0}]})],y),e.default=y,a(s(328),e),a(s(211),e),a(s(108),e)},306(t,e,s){var r=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},i=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpService=void 0;const o=s(976),n=s(573),a=s(414),l=s(328),c={direction:"download",state:"idle",transferred:0,speed:0,message:"Ready"};class d{constructor(t){this.upload=t,this.pending=Buffer.alloc(0),this.ended=!1,this.size=t.getSize()}async read(t){for(;!this.ended&&this.pending.length<t;){const t=await this.upload.read();if(!t.length){this.ended=!0;break}this.pending=this.pending.length?Buffer.concat([this.pending,Buffer.from(t)]):Buffer.from(t)}const e=Buffer.from(this.pending.subarray(0,t));return this.pending=this.pending.subarray(e.length),e}close(){this.upload.close()}abort(){this.upload.cancel()}isCancelled(){return this.upload.isCancelled()}}class u{constructor(t,e){this.download=t,this.completed=!1,e||(t.getSize=()=>this.completed?t.getCompletedBytes():Math.max(1,t.getCompletedBytes()+1))}write(t){return this.download.write(t)}close(){this.completed=!0,this.download.close()}abort(){this.download.cancel()}isCancelled(){return this.download.isCancelled()}}let h=class{constructor(t,e){this.platform=t,this.notifications=e,this.progress$=new n.BehaviorSubject(c),this.controller=null,this.client=new l.TftpClient}get busy(){return null!==this.controller}async upload(t){this.ensureIdle();const e=(await this.platform.startUpload({multiple:!1}))[0];if(!e)return null;const s=t.remoteFilename.trim()||e.getName(),r=new d(e);return this.run("upload",s,e=>this.client.upload(this.toClientOptions(t,s),r,e.signal,t=>this.progress$.next(t)))}async download(t){this.ensureIdle();const e=t.remoteFilename.trim();if(!e)throw new Error("A remote filename is required for downloads");const s=e.split(/[\\/]/).filter(Boolean).pop()||"download.bin";return this.run("download",e,r=>this.client.download(this.toClientOptions(t,e),async t=>{const e=await this.platform.startDownload(s,420,null!=t?t:1);return e?new u(e,void 0!==t):null},r.signal,t=>this.progress$.next(t)))}cancel(){var t;null===(t=this.controller)||void 0===t||t.abort()}async run(t,e,s){this.ensureIdle();const r=new AbortController;this.controller=r;try{const i=await s(r);return this.notifications.notice(`TFTP ${"upload"===t?"upload":"download"} completed: ${e}`),i}catch(t){const e=t instanceof Error?t:new Error(String(t));throw"AbortError"===e.name?this.notifications.info("TFTP transfer cancelled"):this.notifications.error("TFTP transfer failed",e.message),e}finally{this.controller===r&&(this.controller=null)}}ensureIdle(){if(this.busy)throw new Error("Another TFTP transfer is already in progress")}toClientOptions(t,e){return{host:t.host,port:Number(t.port),remoteFilename:e,blockSize:Number(t.blockSize),timeoutMs:Number(t.timeoutMs),retries:Number(t.retries)}}};h=r([(0,o.Injectable)(),i("design:paramtypes",[a.PlatformService,a.NotificationsService])],h),e.TftpService=h},21(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__decorate||function(t,e,s,r){var i,o=arguments.length,n=o<3?e:null===r?r=Object.getOwnPropertyDescriptor(e,s):r;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,s,r);else for(var a=t.length-1;a>=0;a--)(i=t[a])&&(n=(o<3?i(n):o>3?i(e,s,n):i(e,s))||n);return o>3&&n&&Object.defineProperty(e,s,n),n},n=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e},a=this&&this.__metadata||function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServerService=void 0;const l=s(976),c=s(573),d=s(414),u=n(s(366)),h=new(s(108).TftpServer),p=new c.BehaviorSubject({running:!1,activeTransfers:0,logs:[]});let f=null,m=class{constructor(t,e){this.notifications=t,this.platform=e,this.state$=p,this.server=h}get running(){return this.server.running}get activeSettings(){return f?{...f}:null}getLocalIPv4Addresses(){const t=new Set,e=u.networkInterfaces();for(const s of Object.values(e))for(const e of null!=s?s:[])"IPv4"!==e.family||e.internal||t.add(e.address);return Array.from(t).sort()}async chooseRootDirectory(){var t;const e=s(402),r=await e.dialog.showOpenDialog({title:"Choose TFTP Root Directory",properties:["openDirectory","createDirectory"]});return r.canceled?null:null!==(t=r.filePaths[0])&&void 0!==t?t:null}openRootDirectory(t){t&&this.platform.openPath(t)}copyAddress(t){this.platform.setClipboard({text:t}),this.notifications.notice(`Local IP copied: ${t}`)}async start(t){f={...t};try{await this.server.start({rootDirectory:t.rootDirectory,bindAddress:t.bindAddress,port:Number(t.port),blockSize:Number(t.blockSize),timeoutMs:Number(t.timeoutMs),retries:Number(t.retries),autoStopSeconds:t.autoStop?Number(t.autoStopSeconds):0,allowUploads:t.allowUploads,overwriteUploads:t.overwriteUploads},t=>this.handleEvent(t))}catch(t){const e=t instanceof Error?t.message:String(t);throw this.appendLog("error",`Failed to start: ${e}`),this.notifications.error("Failed to start TFTP server",e),t}}stop(){this.server.stop()}clearLogs(){this.state$.next({...this.state$.value,logs:[]})}handleEvent(t){var e;const s="completed"===t.type?"success":"error"===t.type?"error":"info",r=this.state$.value,i={running:"started"===t.type||"stopped"!==t.type&&this.server.running,activeTransfers:t.activeTransfers,startedAt:"started"===t.type?t.timestamp:r.startedAt,logs:[...r.logs,{timestamp:t.timestamp,level:s,message:t.message}].slice(-300)};this.state$.next(i),"completed"===t.type&&(null==f?void 0:f.notifyDownloads)?this.notifications.notice(`TFTP transfer completed: ${null!==(e=t.filename)&&void 0!==e?e:""}`):"stopped"===t.type&&this.notifications.info("TFTP server stopped")}appendLog(t,e){const s=this.state$.value;this.state$.next({...s,logs:[...s.logs,{timestamp:Date.now(),level:t,message:e}].slice(-300)})}};m=o([(0,l.Injectable)(),a("design:paramtypes",[d.NotificationsService,d.PlatformService])],m),e.TftpServerService=m},328(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpClient=void 0;const n=o(s(935)),a=s(211);function l(t){var e,s,r,i;const o=t.host.trim(),n=t.remoteFilename.trim(),a=null!==(e=t.port)&&void 0!==e?e:69,l=null!==(s=t.blockSize)&&void 0!==s?s:1468,c=null!==(r=t.timeoutMs)&&void 0!==r?r:3e3,d=null!==(i=t.retries)&&void 0!==i?i:5;if(!o)throw new Error("TFTP host cannot be empty");if(!n)throw new Error("Remote filename cannot be empty");if(!Number.isInteger(a)||a<1||a>65535)throw new Error("TFTP port must be between 1 and 65535");if(!Number.isInteger(l)||l<8||l>65464)throw new Error("TFTP block size must be between 8 and 65464");if(!Number.isInteger(c)||c<250||c>6e4)throw new Error("Timeout must be between 250 and 60000 milliseconds");if(!Number.isInteger(d)||d<0||d>20)throw new Error("Retries must be between 0 and 20");return{host:o,remoteFilename:n,port:a,blockSize:l,timeoutMs:c,retries:d}}function c(){const t=new Error("Transfer cancelled");return t.name="AbortError",t}class d{constructor(t,e,s,r){this.direction=t,this.options=e,this.signal=s,this.onProgress=r,this.startedAt=Date.now(),this.transferred=0,this.negotiatedBlockSize=512,this.endpoint=null,this.lastPacket=null,this.lastTarget=null,this.retryCount=0,this.retryTimer=null,this.cancelPoll=null,this.settled=!1,this.queue=Promise.resolve(),this.socket=n.createSocket("udp4")}runSession(t){var e;const s=new Promise((t,e)=>{this.resolvePromise=t,this.rejectPromise=e});return this.socket.on("error",t=>this.fail(t)),this.socket.on("message",(t,e)=>{this.queue=this.queue.then(()=>this.handleMessage(t,e)).catch(t=>this.fail(t instanceof Error?t:new Error(String(t))))}),null===(e=this.signal)||void 0===e||e.addEventListener("abort",()=>this.fail(c()),{once:!0}),this.cancelPoll=setInterval(()=>{this.resourceCancelled()&&this.fail(c())},250),this.emit("negotiating","Connecting to TFTP server"),this.sendTracked(t,{address:this.options.host,port:this.options.port}),s}clearRetryTimer(){this.retryTimer&&clearTimeout(this.retryTimer),this.retryTimer=null,this.retryCount=0}sendTracked(t,e){var s;if(this.settled)return;const r=null!==(s=null!=e?e:this.endpoint)&&void 0!==s?s:{address:this.options.host,port:this.options.port};this.lastPacket=Buffer.from(t),this.lastTarget=r,this.retryTimer&&clearTimeout(this.retryTimer),this.socket.send(t,r.port,r.address,t=>{t&&this.fail(t)}),this.retryTimer=setTimeout(()=>this.retryLastPacket(),this.options.timeoutMs)}sendUntracked(t,e,s){var r;if(this.settled)return;const i=null!==(r=null!=e?e:this.endpoint)&&void 0!==r?r:{address:this.options.host,port:this.options.port};this.socket.send(t,i.port,i.address,t=>{t?this.fail(t):null==s||s()})}emit(t,e,s){var r;const i=Math.max(1,Date.now()-this.startedAt);null===(r=this.onProgress)||void 0===r||r.call(this,{direction:this.direction,state:t,transferred:this.transferred,total:s,speed:1e3*this.transferred/i,attempt:"retrying"===t?this.retryCount:void 0,message:e})}succeed(){if(this.settled)return;this.settled=!0,this.cleanupTimers(),this.releaseResource(!0),this.socket.close();const t={bytes:this.transferred,blockSize:this.negotiatedBlockSize,durationMs:Date.now()-this.startedAt};this.emit("completed","Transfer completed",this.transferred),this.resolvePromise(t)}fail(t){if(!this.settled){this.settled=!0,this.cleanupTimers(),this.releaseResource(!1);try{this.socket.close()}catch{}this.emit("AbortError"===t.name?"cancelled":"error",t.message),this.rejectPromise(t)}}async handleMessage(t,e){var s;if(this.settled)return;if((null===(s=this.signal)||void 0===s?void 0:s.aborted)||this.resourceCancelled())throw c();if(this.endpoint&&(e.address!==this.endpoint.address||e.port!==this.endpoint.port))return void this.sendUntracked((0,a.createError)(5,"Unknown transfer ID"),{address:e.address,port:e.port});const r=(0,a.parsePacket)(t);if(this.endpoint||(this.endpoint={address:e.address,port:e.port}),r.opcode===a.Opcode.ERROR)throw new Error(`TFTP error ${r.code}: ${r.message}`);await this.onPacket(r)}retryLastPacket(){var t;this.settled||((null===(t=this.signal)||void 0===t?void 0:t.aborted)||this.resourceCancelled()?this.fail(c()):!this.lastPacket||!this.lastTarget||this.retryCount>=this.options.retries?this.fail(new Error(`TFTP response timed out after ${this.options.retries} retries`)):(this.retryCount++,this.emit("retrying",`Response timed out; retry ${this.retryCount}`),this.socket.send(this.lastPacket,this.lastTarget.port,this.lastTarget.address,t=>{t&&this.fail(t)}),this.retryTimer=setTimeout(()=>this.retryLastPacket(),this.options.timeoutMs)))}cleanupTimers(){this.retryTimer&&clearTimeout(this.retryTimer),this.cancelPoll&&clearInterval(this.cancelPoll),this.retryTimer=null,this.cancelPoll=null}}class u extends d{constructor(t,e,s,r){super("download",t,s,r),this.sinkFactory=e,this.expectedBlock=1,this.sink=null}run(){const t=Math.max(1,Math.min(255,Math.round(this.options.timeoutMs/1e3)));return this.runSession((0,a.createRequest)(a.Opcode.RRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:0}))}async onPacket(t){if(t.opcode===a.Opcode.OACK){if(1!==this.expectedBlock||0!==this.transferred)return;return this.clearRetryTimer(),this.negotiatedBlockSize=(0,a.parseNegotiatedBlockSize)(t.options),this.totalSize=(0,a.parseTransferSize)(t.options),await this.ensureSink(this.totalSize),this.emit("transferring","Transfer parameters negotiated",this.totalSize),void this.sendTracked((0,a.createAck)(0))}if(t.opcode!==a.Opcode.DATA)return;if(t.block===this.expectedBlock){this.clearRetryTimer(),this.sink||(this.negotiatedBlockSize=512,await this.ensureSink(void 0)),await this.sink.write(t.data),this.transferred+=t.data.length,this.emit("transferring",`Receiving block ${t.block}`,this.totalSize);const e=t.data.length<this.negotiatedBlockSize,s=(0,a.createAck)(t.block);return void(e?this.sendUntracked(s,void 0,()=>this.succeed()):(this.expectedBlock=this.expectedBlock+1&65535,this.sendTracked(s)))}const e=this.expectedBlock-1&65535;t.block===e&&this.sendTracked((0,a.createAck)(t.block))}releaseResource(t){this.sink&&(t?this.sink.close():this.sink.abort?this.sink.abort():this.sink.close())}resourceCancelled(){var t,e,s;return null!==(s=null===(e=null===(t=this.sink)||void 0===t?void 0:t.isCancelled)||void 0===e?void 0:e.call(t))&&void 0!==s&&s}async ensureSink(t){if(!this.sink&&(this.sink=await this.sinkFactory(t),!this.sink))throw c()}}class h extends d{constructor(t,e,s,r){super("upload",t,s,r),this.source=e,this.nextBlock=1,this.currentBlock=null,this.currentData=Buffer.alloc(0),this.currentFinal=!1}run(){const t=Math.max(1,Math.min(255,Math.round(this.options.timeoutMs/1e3)));return this.runSession((0,a.createRequest)(a.Opcode.WRQ,this.options.remoteFilename,{blksize:this.options.blockSize,timeout:t,tsize:this.source.size}))}async onPacket(t){return t.opcode===a.Opcode.OACK&&null===this.currentBlock?(this.clearRetryTimer(),this.negotiatedBlockSize=(0,a.parseNegotiatedBlockSize)(t.options),this.emit("transferring","Transfer parameters negotiated",this.source.size),void await this.sendNextBlock()):t.opcode===a.Opcode.ACK?0===t.block&&null===this.currentBlock?(this.clearRetryTimer(),this.negotiatedBlockSize=512,this.emit("transferring","Server is using standard 512-byte blocks",this.source.size),void await this.sendNextBlock()):null!==this.currentBlock&&t.block===this.currentBlock?(this.clearRetryTimer(),this.transferred+=this.currentData.length,this.emit("transferring",`Server acknowledged block ${t.block}`,this.source.size),void(this.currentFinal?this.succeed():(this.nextBlock=this.nextBlock+1&65535,await this.sendNextBlock()))):void(null!==this.currentBlock&&t.block===(this.currentBlock-1&65535)&&this.sendTracked((0,a.createData)(this.currentBlock,this.currentData))):void 0}releaseResource(t){t?this.source.close():this.source.abort?this.source.abort():this.source.close()}resourceCancelled(){var t,e,s;return null!==(s=null===(e=(t=this.source).isCancelled)||void 0===e?void 0:e.call(t))&&void 0!==s&&s}async sendNextBlock(){var t;if((null===(t=this.signal)||void 0===t?void 0:t.aborted)||this.resourceCancelled())throw c();if(this.currentData=Buffer.from(await this.source.read(this.negotiatedBlockSize)),this.currentData.length>this.negotiatedBlockSize)throw new Error("Upload source returned more data than the negotiated block size");this.currentBlock=this.nextBlock,this.currentFinal=this.currentData.length<this.negotiatedBlockSize,this.sendTracked((0,a.createData)(this.currentBlock,this.currentData))}}e.TftpClient=class{download(t,e,s,r){return new u(l(t),e,s,r).run()}upload(t,e,s,r){return new h(l(t),e,s,r).run()}}},211(t,e){var s;function r(t){if(t.includes("\0"))throw new Error("TFTP fields cannot contain null characters");return Buffer.concat([Buffer.from(t,"utf8"),Buffer.from([0])])}function i(t,e){const s=[];let r=e;for(let i=e;i<t.length;i++)0===t[i]&&(s.push(t.toString("utf8",r,i)),r=i+1);if(r!==t.length||s.length%2!=0)throw new Error("Invalid TFTP option response");const i={};for(let t=0;t<s.length;t+=2)i[s[t].toLowerCase()]=s[t+1];return i}Object.defineProperty(e,"__esModule",{value:!0}),e.parseTransferSize=e.parseNegotiatedBlockSize=e.parsePacket=e.createError=e.createAck=e.createData=e.createRequest=e.Opcode=void 0,function(t){t[t.RRQ=1]="RRQ",t[t.WRQ=2]="WRQ",t[t.DATA=3]="DATA",t[t.ACK=4]="ACK",t[t.ERROR=5]="ERROR",t[t.OACK=6]="OACK"}(s=e.Opcode||(e.Opcode={})),e.createRequest=function(t,e,s={}){if(!e.trim())throw new Error("Remote filename cannot be empty");const i=Buffer.allocUnsafe(2);i.writeUInt16BE(t,0);const o=[i,r(e),r("octet")];for(const[t,e]of Object.entries(s))o.push(r(t.toLowerCase())),o.push(r(String(e)));return Buffer.concat(o)},e.createData=function(t,e){const r=Buffer.allocUnsafe(4+e.length);return r.writeUInt16BE(s.DATA,0),r.writeUInt16BE(65535&t,2),e.copy(r,4),r},e.createAck=function(t){const e=Buffer.allocUnsafe(4);return e.writeUInt16BE(s.ACK,0),e.writeUInt16BE(65535&t,2),e},e.createError=function(t,e){const i=Buffer.allocUnsafe(4);return i.writeUInt16BE(s.ERROR,0),i.writeUInt16BE(65535&t,2),Buffer.concat([i,r(e)])},e.parsePacket=function(t){if(t.length<2)throw new Error("TFTP packet is too short");const e=t.readUInt16BE(0);switch(e){case s.DATA:if(t.length<4)throw new Error("Invalid DATA packet");return{opcode:e,block:t.readUInt16BE(2),data:t.subarray(4)};case s.ACK:if(4!==t.length)throw new Error("Invalid ACK packet");return{opcode:e,block:t.readUInt16BE(2)};case s.ERROR:if(t.length<5||0!==t[t.length-1])throw new Error("Invalid ERROR packet");return{opcode:e,code:t.readUInt16BE(2),message:t.toString("utf8",4,t.length-1)};case s.OACK:return{opcode:e,options:i(t,2)};default:throw new Error(`Unsupported TFTP opcode: ${e}`)}},e.parseNegotiatedBlockSize=function(t,e=512){if(!t.blksize)return e;const s=Number(t.blksize);if(!Number.isInteger(s)||s<8||s>65464)throw new Error(`Server returned an invalid block size: ${t.blksize}`);return s},e.parseTransferSize=function(t){if(void 0===t.tsize)return;const e=Number(t.tsize);if(!Number.isSafeInteger(e)||e<0)throw new Error(`Server returned an invalid file size: ${t.tsize}`);return e}},108(t,e,s){var r=this&&this.__createBinding||(Object.create?function(t,e,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(e,s);i&&!("get"in i?!e.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return e[s]}}),Object.defineProperty(t,r,i)}:function(t,e,s,r){void 0===r&&(r=s),t[r]=e[s]}),i=this&&this.__setModuleDefault||(Object.create?function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}:function(t,e){t.default=e}),o=this&&this.__importStar||function(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var s in t)"default"!==s&&Object.prototype.hasOwnProperty.call(t,s)&&r(e,t,s);return i(e,t),e};Object.defineProperty(e,"__esModule",{value:!0}),e.TftpServer=e.parseRequest=void 0;const n=o(s(935)),a=o(s(383)),l=o(s(3)),c=s(211);function d(t){if(t.length<4)throw new Error("TFTP request is too short");const e=t.readUInt16BE(0);if(e!==c.Opcode.RRQ&&e!==c.Opcode.WRQ)throw new Error("Not a valid read or write request");const s=function(t){const e=[];let s=2;for(let r=2;r<t.length;r++)0===t[r]&&(e.push(t.toString("utf8",s,r)),s=r+1);if(s!==t.length)throw new Error("Request field is missing a terminator");return e}(t);if(s.length<2||(s.length-2)%2!=0)throw new Error("Invalid TFTP request fields");const r=s[0],i=s[1].toLowerCase();if(!r)throw new Error("Requested filename is empty");if("octet"!==i)throw new Error(`Unsupported transfer mode: ${i}`);const o={};for(let t=2;t<s.length;t+=2)o[s[t].toLowerCase()]=s[t+1];return{opcode:e,filename:r,mode:i,options:o}}function u(t){const e=Buffer.allocUnsafe(2);e.writeUInt16BE(c.Opcode.OACK,0);const s=[e];for(const[e,r]of Object.entries(t))s.push(Buffer.from(`${e}\0${r}\0`,"utf8"));return Buffer.concat(s)}function h(t,e){const s=e.replace(/\\/g,"/").replace(/^\/+/,"").split("/").filter(t=>t&&"."!==t);if(!s.length||s.some(t=>".."===t))throw new Error("Unsafe file path rejected");const r=l.resolve(t,...s),i=l.relative(t,r);if(!i||i.startsWith("..")||l.isAbsolute(i))throw new Error("Requested file is outside the TFTP root directory");return r}e.parseRequest=d;class p{constructor(t,e,s,r){this.request=t,this.remote=e,this.options=s,this.onFinish=r,this.blockSize=512,this.transferred=0,this.lastPacket=null,this.timer=null,this.retryCount=0,this.settled=!1,this.failing=!1,this.queue=Promise.resolve(),this.timeoutMs=s.timeoutMs,this.socket=n.createSocket("udp4")}start(){this.socket.on("error",t=>this.fail(t.message)),this.socket.on("message",(t,e)=>{this.queue=this.queue.then(()=>this.handleMessage(t,e)).catch(t=>this.fail(t instanceof Error?t.message:String(t)))}),this.socket.bind(0,this.options.bindAddress,()=>{this.begin().catch(t=>this.fail(t instanceof Error?t.message:String(t)))})}resendLast(){this.lastPacket&&this.sendTracked(this.lastPacket)}cancel(){this.finish(!1,"Server stopped")}sendTracked(t){this.settled||(this.lastPacket=Buffer.from(t),this.timer&&clearTimeout(this.timer),this.socket.send(t,this.remote.port,this.remote.address,t=>{t&&this.fail(t.message)}),this.timer=setTimeout(()=>this.retry(),this.timeoutMs))}clearTimer(){this.timer&&clearTimeout(this.timer),this.timer=null,this.retryCount=0}complete(t){this.finish(!0,t)}fail(t){if(this.settled||this.failing)return;this.failing=!0,this.timer&&clearTimeout(this.timer);const e=(0,c.createError)(1,t);this.socket.send(e,this.remote.port,this.remote.address,()=>this.finish(!1,t))}async handleMessage(t,e){if(this.settled)return;if(e.address!==this.remote.address||e.port!==this.remote.port){const t=(0,c.createError)(5,"Unknown transfer ID");return void this.socket.send(t,e.port,e.address)}const s=(0,c.parsePacket)(t);if(s.opcode===c.Opcode.ERROR)throw new Error(`Client error ${s.code}: ${s.message}`);await this.onPacket(s)}retry(){this.settled||(!this.lastPacket||this.retryCount>=this.options.retries?this.fail(`Timed out waiting for the client after ${this.options.retries} retries`):(this.retryCount++,this.socket.send(this.lastPacket,this.remote.port,this.remote.address,t=>{t&&this.fail(t.message)}),this.timer=setTimeout(()=>this.retry(),this.timeoutMs)))}finish(t,e){this.settled||(this.settled=!0,this.timer&&clearTimeout(this.timer),this.release(t).catch(()=>{}).finally(()=>{try{this.socket.close()}catch{}this.onFinish(t,e,this.transferred)}))}}class f extends p{constructor(t,e,s,r){super(t,e,s,r),this.file=null,this.fileSize=0,this.position=0,this.currentBlock=0,this.currentData=Buffer.alloc(0),this.finalBlock=!1}async begin(){const t=h(this.options.rootDirectory,this.request.filename),e=await a.promises.stat(t);if(!e.isFile())throw new Error("Requested path is not a regular file");this.fileSize=e.size,this.file=await a.promises.open(t,"r");const s={};if(void 0!==this.request.options.blksize){const t=Number(this.request.options.blksize);if(!Number.isInteger(t)||t<8||t>65464)throw new Error("Client requested an invalid block size");this.blockSize=Math.min(t,this.options.blockSize),s.blksize=this.blockSize}if(void 0!==this.request.options.timeout){const t=Number(this.request.options.timeout);if(!Number.isInteger(t)||t<1||t>255)throw new Error("Client requested an invalid timeout");this.timeoutMs=1e3*t,s.timeout=t}void 0!==this.request.options.tsize&&(s.tsize=this.fileSize),Object.keys(s).length?this.sendTracked(u(s)):await this.sendNextBlock()}async onPacket(t){if(t.opcode===c.Opcode.ACK)return 0===this.currentBlock&&0===t.block?(this.clearTimer(),void await this.sendNextBlock()):t.block===this.currentBlock?(this.clearTimer(),this.transferred+=this.currentData.length,void(this.finalBlock?this.complete(`Sent ${this.transferred} bytes`):await this.sendNextBlock())):void(t.block===(this.currentBlock-1&65535)&&this.resendLast())}async release(){this.file&&await this.file.close(),this.file=null}async sendNextBlock(){const t=this.file;if(!t)throw new Error("TFTP file is not open");const e=Buffer.allocUnsafe(this.blockSize),s=await t.read(e,0,this.blockSize,this.position);this.currentData=Buffer.from(e.subarray(0,s.bytesRead)),this.position+=s.bytesRead,this.currentBlock=this.currentBlock+1&65535,this.finalBlock=s.bytesRead<this.blockSize,this.sendTracked((0,c.createData)(this.currentBlock,this.currentData))}}class m extends p{constructor(){super(...arguments),this.file=null,this.targetPath="",this.temporaryPath="",this.expectedBlock=1}async begin(){if(this.targetPath=h(this.options.rootDirectory,this.request.filename),!this.options.overwriteUploads)try{throw await a.promises.access(this.targetPath),new Error("Target file already exists and overwrite is disabled")}catch(t){if(!(t instanceof Error&&"code"in t&&"ENOENT"===t.code))throw t}this.temporaryPath=`${this.targetPath}.tabby-tftp-${process.pid}-${Date.now()}.part`,this.file=await a.promises.open(this.temporaryPath,"wx");const t={};if(void 0!==this.request.options.blksize){const e=Number(this.request.options.blksize);if(!Number.isInteger(e)||e<8||e>65464)throw new Error("Client requested an invalid block size");this.blockSize=Math.min(e,this.options.blockSize),t.blksize=this.blockSize}if(void 0!==this.request.options.timeout){const e=Number(this.request.options.timeout);if(!Number.isInteger(e)||e<1||e>255)throw new Error("Client requested an invalid timeout");this.timeoutMs=1e3*e,t.timeout=e}void 0!==this.request.options.tsize&&(t.tsize=this.request.options.tsize),this.sendTracked(Object.keys(t).length?u(t):(0,c.createAck)(0))}async onPacket(t){if(t.opcode!==c.Opcode.DATA)return;if(t.block===(this.expectedBlock-1&65535))return void this.sendTracked((0,c.createAck)(t.block));if(t.block!==this.expectedBlock)return;this.clearTimer();const e=this.file;if(!e)throw new Error("TFTP upload file is not open");await e.write(t.data),this.transferred+=t.data.length,this.expectedBlock=this.expectedBlock+1&65535,this.sendTracked((0,c.createAck)(t.block)),t.data.length<this.blockSize&&this.complete(`Received ${this.transferred} bytes`)}async release(t){this.file&&await this.file.close(),this.file=null,this.temporaryPath&&(t?(this.options.overwriteUploads&&await a.promises.unlink(this.targetPath).catch(t=>{if("ENOENT"!==(null==t?void 0:t.code))throw t}),await a.promises.rename(this.temporaryPath,this.targetPath)):await a.promises.unlink(this.temporaryPath).catch(()=>{}))}}e.TftpServer=class{constructor(){this.listener=null,this.options=null,this.transfers=new Map,this.autoStopTimer=null,this.eventHandler=null}get running(){return null!==this.listener}get activeTransfers(){return this.transfers.size}async start(t,e){if(this.running)throw new Error("TFTP server is already running");const s=function(t){var e,s,r,i,o,n,a,c;const d=l.resolve(t.rootDirectory.trim()),u=(null!==(e=t.bindAddress)&&void 0!==e?e:"0.0.0.0").trim(),h=null!==(s=t.port)&&void 0!==s?s:69,p=null!==(r=t.blockSize)&&void 0!==r?r:8192,f=null!==(i=t.timeoutMs)&&void 0!==i?i:3e3,m=null!==(o=t.retries)&&void 0!==o?o:5,g=null!==(n=t.autoStopSeconds)&&void 0!==n?n:0,b=null!==(a=t.allowUploads)&&void 0!==a&&a,v=null!==(c=t.overwriteUploads)&&void 0!==c&&c;if(!t.rootDirectory.trim())throw new Error("Choose a TFTP root directory");if(!u)throw new Error("Listen address cannot be empty");if(!Number.isInteger(h)||h<1||h>65535)throw new Error("Listen port must be between 1 and 65535");if(!Number.isInteger(p)||p<8||p>65464)throw new Error("TFTP block size must be between 8 and 65464 bytes");if(!Number.isInteger(f)||f<250||f>6e4)throw new Error("Timeout must be between 250 and 60000 milliseconds");if(!Number.isInteger(m)||m<0||m>20)throw new Error("Retries must be between 0 and 20");if(!Number.isInteger(g)||g<0||g>86400)throw new Error("Automatic stop time must be between 0 and 86400 seconds");return{rootDirectory:d,bindAddress:u,port:h,blockSize:p,timeoutMs:f,retries:m,autoStopSeconds:g,allowUploads:b,overwriteUploads:v}}(t);if(!(await a.promises.stat(s.rootDirectory)).isDirectory())throw new Error("Invalid TFTP root directory");this.options=s,this.eventHandler=null!=e?e:null;const r=n.createSocket("udp4");this.listener=r,r.on("message",(t,e)=>this.handleRequest(t,e));let i=null;r.on("error",t=>{if(i){const e=i;i=null;try{r.close()}catch{}this.listener=null,e(t)}else this.emit("error",t.message)}),await new Promise((t,e)=>{i=e,r.bind(s.port,s.bindAddress,()=>{i=null,t()})}),this.emit("started",`TFTP server started at ${s.bindAddress}:${s.port}`),s.autoStopSeconds>0&&(this.autoStopTimer=setTimeout(()=>this.stop("automatic stop timer expired"),1e3*s.autoStopSeconds))}stop(t="stopped by user"){if(this.listener){this.autoStopTimer&&clearTimeout(this.autoStopTimer),this.autoStopTimer=null;for(const t of this.transfers.values())t.cancel();this.transfers.clear();try{this.listener.close()}catch{}this.listener=null,this.emit("stopped",`TFTP server stopped: ${t}`)}}handleRequest(t,e){if(!this.listener||!this.options)return;let s;try{s=d(t)}catch(t){const s=t instanceof Error?t.message:String(t);return this.listener.send((0,c.createError)(4,s),e.port,e.address),void this.emit("error",`Rejected invalid request from ${e.address}:${e.port}: ${s}`)}const r=`${e.address}:${e.port}:${s.opcode}:${s.filename}`,i=this.transfers.get(r);if(i)return void i.resendLast();if(s.opcode===c.Opcode.WRQ&&!this.options.allowUploads)return this.listener.send((0,c.createError)(2,"Server is read-only"),e.port,e.address),void this.emit("error",`Rejected upload of ${s.filename} from ${e.address}:${e.port}: server is read-only`,e,s.filename);const o=`${e.address}:${e.port}`,n=s.opcode===c.Opcode.WRQ,a=new(n?m:f)(s,e,this.options,(t,i,a)=>{this.transfers.delete(r);const l=n?"upload":"download";this.emit(t?"completed":"error",`${o} ${t?`completed ${l} of`:`failed to ${l}`} ${s.filename}: ${i}`,e,s.filename,a)});this.transfers.set(r,a),this.emit("request",`${o} requested ${n?"upload of":"download of"} ${s.filename}`,e,s.filename),a.start()}emit(t,e,s,r,i){var o;null===(o=this.eventHandler)||void 0===o||o.call(this,{type:t,timestamp:Date.now(),message:e,peer:s?`${s.address}:${s.port}`:void 0,filename:r,bytes:i,activeTransfers:this.transfers.size})}}},282(t){t.exports=require("@angular/common")},976(t){t.exports=require("@angular/core")},98(t){t.exports=require("@angular/forms")},402(t){t.exports=require("@electron/remote")},385(t){t.exports=require("@ng-bootstrap/ng-bootstrap")},935(t){t.exports=require("dgram")},383(t){t.exports=require("fs")},366(t){t.exports=require("os")},3(t){t.exports=require("path")},573(t){t.exports=require("rxjs")},414(t){t.exports=require("tabby-core")}};const e={};let s=function s(r){const i=e[r];if(void 0!==i)return i.exports;const o=e[r]={exports:{}};return t[r].call(o.exports,o,o.exports,s),o.exports}(156);module.exports=s})();
2
2
  //# sourceMappingURL=index.js.map
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 File Transfer',\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 File Transfer and Firmware Server</h5>\n <button type=\"button\" class=\"close\" aria-label=\"Close\" (click)=\"activeModal.dismiss()\">\n <span aria-hidden=\"true\">&times;</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>Firmware TFTP Server\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 Client\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 provides no authentication or encryption. Use it only on a trusted local network.\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-8\">\n <label>Host or IP Address</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.host\" placeholder=\"e.g. 192.168.1.10\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>Port</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>Remote Filename</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.remoteFilename\"\n placeholder=\"Required for downloads; leave blank on upload to use the local filename\" [disabled]=\"busy\">\n <small class=\"form-text text-muted\">TFTP cannot browse remote directories. Path syntax is determined by the server.</small>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">Advanced Options</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-4\">\n <label>Block Size</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>Timeout (ms)</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>Retries</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' ? 'Upload' : 'Download' }}</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()\">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>Cancel Transfer</button>\n <button class=\"btn btn-primary\" *ngIf=\"!busy\" (click)=\"upload()\"><i class=\"fa fa-upload mr-1\"></i>Upload</button>\n <button class=\"btn btn-success\" *ngIf=\"!busy\" (click)=\"download()\"><i class=\"fa fa-download mr-1\"></i>Download</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 = 'Enter a TFTP host address'\n else if (requireRemoteFilename && !this.settings.remoteFilename.trim()) this.validationError = 'A remote filename is required for downloads'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.validationError = 'Port must be between 1 and 65535'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.validationError = 'Block size must be between 8 and 65464 bytes'\n else if (!Number.isInteger(Number(this.settings.timeoutMs)) || Number(this.settings.timeoutMs) < 250 || Number(this.settings.timeoutMs) > 60000) this.validationError = 'Timeout must be between 250 and 60000 milliseconds'\n else if (!Number.isInteger(Number(this.settings.retries)) || Number(this.settings.retries) < 0 || Number(this.settings.retries) > 20) this.validationError = 'Retries must be between 0 and 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 Use the serial terminal to control the bootloader. This read-only server supplies firmware over the network and does not allow devices to overwrite local files.\n </div>\n\n <div class=\"form-group\">\n <label>TFTP Root Directory</label>\n <div class=\"input-group\">\n <input class=\"form-control\" [(ngModel)]=\"settings.rootDirectory\" placeholder=\"Directory containing firmware files\" [disabled]=\"state.running\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary\" (click)=\"chooseDirectory()\" [disabled]=\"state.running\" title=\"Choose Directory\">\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=\"Open Directory\">\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>Listen Address</label>\n <select class=\"form-control\" [(ngModel)]=\"settings.bindAddress\" [disabled]=\"state.running\">\n <option value=\"0.0.0.0\">0.0.0.0 (all interfaces)</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>Listen Port</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 Block Size</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\">Local IP addresses available to the device:</span>\n <button class=\"btn btn-sm btn-outline-info mr-1\" *ngFor=\"let address of localAddresses\" (click)=\"server.copyAddress(address)\" title=\"Click to copy\">\n {{ address }}\n </button>\n <span class=\"text-warning small\" *ngIf=\"!localAddresses.length\">No usable IPv4 address detected</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\">Stop Server Automatically</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\">seconds</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\">Notify After Download</label>\n </div>\n </div>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">Reliability Options</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-6\">\n <label>Response Timeout (ms)</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>Retries</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 Server Running' : 'TFTP Server Stopped' }}</strong>\n <span class=\"badge badge-info ml-2\" *ngIf=\"state.activeTransfers\">{{ state.activeTransfers }} active transfer(s)</span>\n <div class=\"ml-auto\">\n <button class=\"btn btn-sm btn-link text-muted\" (click)=\"server.clearLogs()\" [disabled]=\"!state.logs.length\">Clear Logs</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 ? 'Starting…' : 'Start Server' }}\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>Stop Server\n </button>\n </div>\n </div>\n\n <div class=\"server-log\">\n <div *ngIf=\"!state.logs.length\" class=\"text-muted\">Server output will appear here</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 = 'Choose a TFTP root directory containing the firmware'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.error = 'Listen port must be between 1 and 65535'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.error = 'TFTP block size must be between 8 and 65464 bytes'\n else if (this.settings.autoStop && (!Number.isInteger(Number(this.settings.autoStopSeconds)) || Number(this.settings.autoStopSeconds) < 1 || Number(this.settings.autoStopSeconds) > 86400)) this.error = 'Automatic stop time must be between 1 and 86400 seconds'\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: 'Ready',\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('A remote filename is required for downloads')\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' ? 'upload' : 'download'} completed: ${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 transfer cancelled')\n } else {\n this.notifications.error('TFTP transfer failed', 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('Another TFTP transfer is already in progress')\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: 'Choose TFTP Root Directory',\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(`Local IP copied: ${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', `Failed to start: ${reason}`)\n this.notifications.error('Failed to start TFTP server', 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 download completed: ${event.filename ?? ''}`)\n } else if (event.type === 'stopped') {\n this.notifications.info('TFTP server stopped')\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 host cannot be empty')\n if (!remoteFilename) throw new Error('Remote filename cannot be empty')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('TFTP port must be between 1 and 65535')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP block size must be between 8 and 65464')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('Timeout must be between 250 and 60000 milliseconds')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('Retries must be between 0 and 20')\n\n return { host, remoteFilename, port, blockSize, timeoutMs, retries }\n}\n\nfunction abortError (): Error {\n const error = new Error('Transfer cancelled')\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', 'Connecting to TFTP server')\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', 'Transfer 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 error ${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 response timed out after ${this.options.retries} retries`))\n return\n }\n\n this.retryCount++\n this.emit('retrying', `Response timed out; retry ${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', 'Transfer parameters negotiated', 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', `Receiving block ${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', 'Transfer parameters negotiated', 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', 'Server is using standard 512-byte blocks', 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', `Server acknowledged block ${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('Upload source returned more data than the negotiated block size')\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 fields cannot contain null characters')\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('Remote filename cannot be empty')\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('Invalid TFTP option response')\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 packet is too short')\n }\n const opcode = packet.readUInt16BE(0)\n\n switch (opcode) {\n case Opcode.DATA:\n if (packet.length < 4) throw new Error('Invalid DATA packet')\n return { opcode, block: packet.readUInt16BE(2), data: packet.subarray(4) }\n case Opcode.ACK:\n if (packet.length !== 4) throw new Error('Invalid ACK packet')\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('Invalid ERROR packet')\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(`Unsupported TFTP opcode: ${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(`Server returned an invalid block size: ${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(`Server returned an invalid file size: ${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('Request field is missing a terminator')\n return fields\n}\n\nexport function parseRequest (packet: Buffer): TftpRequest {\n if (packet.length < 4) throw new Error('TFTP request is too short')\n const opcode = packet.readUInt16BE(0)\n if (opcode !== Opcode.RRQ && opcode !== Opcode.WRQ) throw new Error('Not a valid read or write request')\n\n const fields = readZeroTerminatedFields(packet, 2)\n if (fields.length < 2 || (fields.length - 2) % 2 !== 0) throw new Error('Invalid TFTP request fields')\n const filename = fields[0]\n const mode = fields[1].toLowerCase()\n if (!filename) throw new Error('Requested filename is empty')\n if (mode !== 'octet') throw new Error(`Unsupported transfer mode: ${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('Choose a TFTP root directory')\n if (!bindAddress) throw new Error('Listen address cannot be empty')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Listen port must be between 1 and 65535')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP block size must be between 8 and 65464 bytes')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('Timeout must be between 250 and 60000 milliseconds')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('Retries must be between 0 and 20')\n if (!Number.isInteger(autoStopSeconds) || autoStopSeconds < 0 || autoStopSeconds > 86400) throw new Error('Automatic stop time must be between 0 and 86400 seconds')\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('Unsafe file path rejected')\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('Requested file is outside the TFTP root directory')\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, 'Server stopped')\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(`Client 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(`Timed out waiting for the client after ${this.options.retries} 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('Requested path is not a regular file')\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('Client requested an invalid block size')\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('Client requested an invalid timeout')\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(`Sent ${this.transferred} bytes`)\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 file is not open')\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 server is already running')\n const options = normalizeOptions(serverOptions)\n const stat = await fs.promises.stat(options.rootDirectory)\n if (!stat.isDirectory()) throw new Error('Invalid TFTP root directory')\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 server started at ${options.bindAddress}:${options.port}`)\n if (options.autoStopSeconds > 0) {\n this.autoStopTimer = setTimeout(() => this.stop('automatic stop timer expired'), options.autoStopSeconds * 1000)\n }\n }\n\n stop (reason = 'stopped by user'): 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 server stopped: ${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', `Rejected invalid request from ${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', `Rejected upload of ${request.filename} from ${remote.address}:${remote.port}: server is read-only`, 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 ? 'completed download of' : 'failed to download'} ${request.filename}: ${detail}`, remote, request.filename, bytes)\n })\n this.transfers.set(key, transfer)\n this.emit('request', `${peer} requested ${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":""}
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 File Transfer',\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 File Transfer and Firmware Server</h5>\n <button type=\"button\" class=\"close\" aria-label=\"Close\" (click)=\"activeModal.dismiss()\">\n <span aria-hidden=\"true\">&times;</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>Firmware TFTP Server\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 Client\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 provides no authentication or encryption. Use it only on a trusted local network.\n </div>\n\n <div class=\"form-row\">\n <div class=\"form-group col-md-8\">\n <label>Host or IP Address</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.host\" placeholder=\"e.g. 192.168.1.10\" [disabled]=\"busy\">\n </div>\n <div class=\"form-group col-md-4\">\n <label>Port</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>Remote Filename</label>\n <input class=\"form-control\" [(ngModel)]=\"settings.remoteFilename\"\n placeholder=\"Required for downloads; leave blank on upload to use the local filename\" [disabled]=\"busy\">\n <small class=\"form-text text-muted\">TFTP cannot browse remote directories. Path syntax is determined by the server.</small>\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">Advanced Options</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-4\">\n <label>Block Size</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>Timeout (ms)</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>Retries</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' ? 'Upload' : 'Download' }}</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()\">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>Cancel Transfer</button>\n <button class=\"btn btn-primary\" *ngIf=\"!busy\" (click)=\"upload()\"><i class=\"fa fa-upload mr-1\"></i>Upload</button>\n <button class=\"btn btn-success\" *ngIf=\"!busy\" (click)=\"download()\"><i class=\"fa fa-download mr-1\"></i>Download</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 = 'Enter a TFTP host address'\n else if (requireRemoteFilename && !this.settings.remoteFilename.trim()) this.validationError = 'A remote filename is required for downloads'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.validationError = 'Port must be between 1 and 65535'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.validationError = 'Block size must be between 8 and 65464 bytes'\n else if (!Number.isInteger(Number(this.settings.timeoutMs)) || Number(this.settings.timeoutMs) < 250 || Number(this.settings.timeoutMs) > 60000) this.validationError = 'Timeout must be between 250 and 60000 milliseconds'\n else if (!Number.isInteger(Number(this.settings.retries)) || Number(this.settings.retries) < 0 || Number(this.settings.retries) > 20) this.validationError = 'Retries must be between 0 and 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 Use the serial terminal to control the bootloader. Downloads are always available; uploads must be explicitly enabled below.\n </div>\n\n <div class=\"form-group\">\n <label>TFTP Root Directory</label>\n <div class=\"input-group\">\n <input class=\"form-control\" [(ngModel)]=\"settings.rootDirectory\" placeholder=\"Directory containing firmware files\" [disabled]=\"state.running\">\n <div class=\"input-group-append\">\n <button class=\"btn btn-outline-secondary\" (click)=\"chooseDirectory()\" [disabled]=\"state.running\" title=\"Choose Directory\">\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=\"Open Directory\">\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>Listen Address</label>\n <select class=\"form-control\" [(ngModel)]=\"settings.bindAddress\" [disabled]=\"state.running\">\n <option value=\"0.0.0.0\">0.0.0.0 (all interfaces)</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>Listen Port</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 Block Size</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\">Local IP addresses available to the device:</span>\n <button class=\"btn btn-sm btn-outline-info mr-1\" *ngFor=\"let address of localAddresses\" (click)=\"server.copyAddress(address)\" title=\"Click to copy\">\n {{ address }}\n </button>\n <span class=\"text-warning small\" *ngIf=\"!localAddresses.length\">No usable IPv4 address detected</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\">Stop Server Automatically</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\">seconds</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\">Notify After Transfer</label>\n </div>\n </div>\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-allow-uploads\" [(ngModel)]=\"settings.allowUploads\" [disabled]=\"state.running\">\n <label class=\"custom-control-label\" for=\"tftp-allow-uploads\">Allow Uploads</label>\n </div>\n </div>\n <div class=\"col-md-7\">\n <div class=\"custom-control custom-checkbox\">\n <input type=\"checkbox\" class=\"custom-control-input\" id=\"tftp-overwrite-uploads\" [(ngModel)]=\"settings.overwriteUploads\" [disabled]=\"state.running || !settings.allowUploads\">\n <label class=\"custom-control-label\" for=\"tftp-overwrite-uploads\">Allow Uploads to Overwrite Existing Files</label>\n </div>\n </div>\n </div>\n <div class=\"alert alert-warning py-2 small\" *ngIf=\"settings.allowUploads && !state.running\">\n Uploaded files are saved under the TFTP root directory. Keep overwrite disabled unless replacing local files is intentional.\n </div>\n\n <details class=\"mb-3\">\n <summary class=\"text-muted\">Reliability Options</summary>\n <div class=\"form-row mt-2\">\n <div class=\"form-group col-md-6\">\n <label>Response Timeout (ms)</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>Retries</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 Server Running' : 'TFTP Server Stopped' }}</strong>\n <span class=\"badge badge-info ml-2\" *ngIf=\"state.activeTransfers\">{{ state.activeTransfers }} active transfer(s)</span>\n <div class=\"ml-auto\">\n <button class=\"btn btn-sm btn-link text-muted\" (click)=\"server.clearLogs()\" [disabled]=\"!state.logs.length\">Clear Logs</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 ? 'Starting…' : 'Start Server' }}\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>Stop Server\n </button>\n </div>\n </div>\n\n <div class=\"server-log\">\n <div *ngIf=\"!state.logs.length\" class=\"text-muted\">Server output will appear here</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 = 'Choose a TFTP root directory containing the firmware'\n else if (!Number.isInteger(Number(this.settings.port)) || Number(this.settings.port) < 1 || Number(this.settings.port) > 65535) this.error = 'Listen port must be between 1 and 65535'\n else if (!Number.isInteger(Number(this.settings.blockSize)) || Number(this.settings.blockSize) < 8 || Number(this.settings.blockSize) > 65464) this.error = 'TFTP block size must be between 8 and 65464 bytes'\n else if (this.settings.autoStop && (!Number.isInteger(Number(this.settings.autoStopSeconds)) || Number(this.settings.autoStopSeconds) < 1 || Number(this.settings.autoStopSeconds) > 86400)) this.error = 'Automatic stop time must be between 1 and 86400 seconds'\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 allowUploads: boolean\n overwriteUploads: 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 allowUploads: false,\n overwriteUploads: false,\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: 'Ready',\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('A remote filename is required for downloads')\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' ? 'upload' : 'download'} completed: ${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 transfer cancelled')\n } else {\n this.notifications.error('TFTP transfer failed', 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('Another TFTP transfer is already in progress')\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: 'Choose TFTP Root Directory',\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(`Local IP copied: ${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 allowUploads: settings.allowUploads,\n overwriteUploads: settings.overwriteUploads,\n }, event => this.handleEvent(event))\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n this.appendLog('error', `Failed to start: ${reason}`)\n this.notifications.error('Failed to start TFTP server', 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 transfer completed: ${event.filename ?? ''}`)\n } else if (event.type === 'stopped') {\n this.notifications.info('TFTP server stopped')\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 host cannot be empty')\n if (!remoteFilename) throw new Error('Remote filename cannot be empty')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('TFTP port must be between 1 and 65535')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP block size must be between 8 and 65464')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('Timeout must be between 250 and 60000 milliseconds')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('Retries must be between 0 and 20')\n\n return { host, remoteFilename, port, blockSize, timeoutMs, retries }\n}\n\nfunction abortError (): Error {\n const error = new Error('Transfer cancelled')\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', 'Connecting to TFTP server')\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', 'Transfer 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 error ${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 response timed out after ${this.options.retries} retries`))\n return\n }\n\n this.retryCount++\n this.emit('retrying', `Response timed out; retry ${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', 'Transfer parameters negotiated', 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', `Receiving block ${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', 'Transfer parameters negotiated', 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', 'Server is using standard 512-byte blocks', 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', `Server acknowledged block ${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('Upload source returned more data than the negotiated block size')\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 fields cannot contain null characters')\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('Remote filename cannot be empty')\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('Invalid TFTP option response')\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 packet is too short')\n }\n const opcode = packet.readUInt16BE(0)\n\n switch (opcode) {\n case Opcode.DATA:\n if (packet.length < 4) throw new Error('Invalid DATA packet')\n return { opcode, block: packet.readUInt16BE(2), data: packet.subarray(4) }\n case Opcode.ACK:\n if (packet.length !== 4) throw new Error('Invalid ACK packet')\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('Invalid ERROR packet')\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(`Unsupported TFTP opcode: ${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(`Server returned an invalid block size: ${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(`Server returned an invalid file size: ${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 createAck,\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 allowUploads?: boolean\n overwriteUploads?: boolean\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 allowUploads: boolean\n overwriteUploads: boolean\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('Request field is missing a terminator')\n return fields\n}\n\nexport function parseRequest (packet: Buffer): TftpRequest {\n if (packet.length < 4) throw new Error('TFTP request is too short')\n const opcode = packet.readUInt16BE(0)\n if (opcode !== Opcode.RRQ && opcode !== Opcode.WRQ) throw new Error('Not a valid read or write request')\n\n const fields = readZeroTerminatedFields(packet, 2)\n if (fields.length < 2 || (fields.length - 2) % 2 !== 0) throw new Error('Invalid TFTP request fields')\n const filename = fields[0]\n const mode = fields[1].toLowerCase()\n if (!filename) throw new Error('Requested filename is empty')\n if (mode !== 'octet') throw new Error(`Unsupported transfer mode: ${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 const allowUploads = options.allowUploads ?? false\n const overwriteUploads = options.overwriteUploads ?? false\n\n if (!options.rootDirectory.trim()) throw new Error('Choose a TFTP root directory')\n if (!bindAddress) throw new Error('Listen address cannot be empty')\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Listen port must be between 1 and 65535')\n if (!Number.isInteger(blockSize) || blockSize < 8 || blockSize > 65464) throw new Error('TFTP block size must be between 8 and 65464 bytes')\n if (!Number.isInteger(timeoutMs) || timeoutMs < 250 || timeoutMs > 60000) throw new Error('Timeout must be between 250 and 60000 milliseconds')\n if (!Number.isInteger(retries) || retries < 0 || retries > 20) throw new Error('Retries must be between 0 and 20')\n if (!Number.isInteger(autoStopSeconds) || autoStopSeconds < 0 || autoStopSeconds > 86400) throw new Error('Automatic stop time must be between 0 and 86400 seconds')\n\n return { rootDirectory, bindAddress, port, blockSize, timeoutMs, retries, autoStopSeconds, allowUploads, overwriteUploads }\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('Unsafe file path rejected')\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('Requested file is outside the TFTP root directory')\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, 'Server stopped')\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(`Client 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(`Timed out waiting for the client after ${this.options.retries} 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('Requested path is not a regular file')\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('Client requested an invalid block size')\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('Client requested an invalid timeout')\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(`Sent ${this.transferred} bytes`)\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 file is not open')\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\nclass WriteTransfer extends ServerTransfer {\n private file: FileHandle | null = null\n private targetPath = ''\n private temporaryPath = ''\n private expectedBlock = 1\n\n protected async begin (): Promise<void> {\n this.targetPath = resolveRequestedFile(this.options.rootDirectory, this.request.filename)\n if (!this.options.overwriteUploads) {\n try {\n await fs.promises.access(this.targetPath)\n throw new Error('Target file already exists and overwrite is disabled')\n } catch (error) {\n if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') throw error\n }\n }\n\n this.temporaryPath = `${this.targetPath}.tabby-tftp-${process.pid}-${Date.now()}.part`\n this.file = await fs.promises.open(this.temporaryPath, 'wx')\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('Client requested an invalid block size')\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('Client requested an invalid timeout')\n this.timeoutMs = timeoutSeconds * 1000\n negotiated.timeout = timeoutSeconds\n }\n if (this.request.options.tsize !== undefined) negotiated.tsize = this.request.options.tsize\n\n this.sendTracked(Object.keys(negotiated).length ? createOack(negotiated) : createAck(0))\n }\n\n protected async onPacket (packet: TftpPacket): Promise<void> {\n if (packet.opcode !== Opcode.DATA) return\n if (packet.block === ((this.expectedBlock - 1) & 0xffff)) {\n this.sendTracked(createAck(packet.block))\n return\n }\n if (packet.block !== this.expectedBlock) return\n\n this.clearTimer()\n const file = this.file\n if (!file) throw new Error('TFTP upload file is not open')\n await file.write(packet.data)\n this.transferred += packet.data.length\n this.expectedBlock = (this.expectedBlock + 1) & 0xffff\n this.sendTracked(createAck(packet.block))\n if (packet.data.length < this.blockSize) this.complete(`Received ${this.transferred} bytes`)\n }\n\n protected async release (successful: boolean): Promise<void> {\n if (this.file) await this.file.close()\n this.file = null\n if (!this.temporaryPath) return\n if (!successful) {\n await fs.promises.unlink(this.temporaryPath).catch(() => undefined)\n return\n }\n if (this.options.overwriteUploads) await fs.promises.unlink(this.targetPath).catch(error => {\n if (error?.code !== 'ENOENT') throw error\n })\n await fs.promises.rename(this.temporaryPath, this.targetPath)\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 server is already running')\n const options = normalizeOptions(serverOptions)\n const stat = await fs.promises.stat(options.rootDirectory)\n if (!stat.isDirectory()) throw new Error('Invalid TFTP root directory')\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 server started at ${options.bindAddress}:${options.port}`)\n if (options.autoStopSeconds > 0) {\n this.autoStopTimer = setTimeout(() => this.stop('automatic stop timer expired'), options.autoStopSeconds * 1000)\n }\n }\n\n stop (reason = 'stopped by user'): 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 server stopped: ${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', `Rejected invalid request from ${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 && !this.options.allowUploads) {\n this.listener.send(createError(2, 'Server is read-only'), remote.port, remote.address)\n this.emit('error', `Rejected upload of ${request.filename} from ${remote.address}:${remote.port}: server is read-only`, remote, request.filename)\n return\n }\n\n const peer = `${remote.address}:${remote.port}`\n const uploading = request.opcode === Opcode.WRQ\n const Transfer = uploading ? WriteTransfer : ReadTransfer\n const transfer = new Transfer(request, remote, this.options, (successful, detail, bytes) => {\n this.transfers.delete(key)\n const operation = uploading ? 'upload' : 'download'\n this.emit(successful ? 'completed' : 'error', `${peer} ${successful ? `completed ${operation} of` : `failed to ${operation}`} ${request.filename}: ${detail}`, remote, request.filename, bytes)\n })\n this.transfers.set(key, transfer)\n this.emit('request', `${peer} requested ${uploading ? 'upload of' : 'download of'} ${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","allowUploads","overwriteUploads","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","createOack","chunks","resolveRequestedFile","requestedFilename","segments","replace","segment","some","relative","startsWith","isAbsolute","ServerTransfer","request","onFinish","timer","failing","bind","begin","resendLast","finish","retry","clearTimer","complete","errorPacket","release","finally","ReadTransfer","file","fileSize","position","filePath","stat","promises","isFile","negotiated","requested","keys","buffer","bytesRead","WriteTransfer","targetPath","temporaryPath","access","process","pid","unlink","rename","listener","transfers","Map","autoStopTimer","eventHandler","serverOptions","onEvent","_e","_f","_g","_h","isDirectory","handleRequest","rejectBinding","transfer","clear","existing","get","peer","uploading","detail","delete","set","module","require","__webpack_module_cache__","__webpack_exports__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""}
@@ -9,6 +9,8 @@ export interface TftpServerOptions {
9
9
  timeoutMs?: number;
10
10
  retries?: number;
11
11
  autoStopSeconds?: number;
12
+ allowUploads?: boolean;
13
+ overwriteUploads?: boolean;
12
14
  }
13
15
  export type TftpServerEventType = 'started' | 'stopped' | 'request' | 'progress' | 'completed' | 'error';
14
16
  export interface TftpServerEvent {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tabby-tftp",
3
- "version": "0.2.5",
4
- "description": "TFTP client and read-only firmware server for Tabby terminal",
3
+ "version": "0.2.6",
4
+ "description": "TFTP client and firmware server with controlled uploads for Tabby terminal",
5
5
  "keywords": [
6
6
  "tabby-plugin",
7
7
  "tabby",