feat: download dirs, file transfer resume, Wails desktop app, PWA, CI
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
120
.gitea/workflows/build.yml
Normal file
120
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Server binaries (no CGo, cross-compile freely) ───────────────────────────
|
||||||
|
|
||||||
|
server:
|
||||||
|
name: Server binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: linux
|
||||||
|
goarch: amd64
|
||||||
|
- goos: linux
|
||||||
|
goarch: arm64
|
||||||
|
- goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm64
|
||||||
|
- goos: windows
|
||||||
|
goarch: amd64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Build daemon + anchor
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
run: |
|
||||||
|
SUFFIX="${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
[ "${{ matrix.goos }}" = "windows" ] && EXT=".exe" || EXT=""
|
||||||
|
go build -trimpath -ldflags="-s -w" -o "dist/waste-daemon-${SUFFIX}${EXT}" ./cmd/daemon
|
||||||
|
go build -trimpath -ldflags="-s -w" -o "dist/waste-anchor-${SUFFIX}${EXT}" ./cmd/anchor
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
# ── Desktop app (Wails, requires CGo + webview libs) ─────────────────────────
|
||||||
|
# Runs only on Linux amd64 with the default runner.
|
||||||
|
# For macOS/Windows desktop builds, add self-hosted runners with those platforms
|
||||||
|
# and duplicate this job (adjusting the runs-on and platform deps).
|
||||||
|
|
||||||
|
desktop-linux:
|
||||||
|
name: Desktop app (Linux amd64)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install Wails CLI
|
||||||
|
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
|
|
||||||
|
- name: Install platform dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libwebkit2gtk-4.0-dev \
|
||||||
|
libayatana-appindicator3-dev
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
cp -r dist ../cmd/app/frontend/dist
|
||||||
|
|
||||||
|
- name: Build desktop app
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
cd cmd/app
|
||||||
|
wails build -trimpath -ldflags="-s -w" -o ../../dist/waste-linux-amd64
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: desktop-linux-amd64
|
||||||
|
path: dist/waste-linux-amd64
|
||||||
|
|
||||||
|
# ── Release: collect all artifacts and publish ────────────────────────────────
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Publish release
|
||||||
|
needs: [server, desktop-linux]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts/
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: https://gitea.com/actions/gitea-release-action@main
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
files: artifacts/*
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-') }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -20,3 +20,5 @@ deploy-web.sh
|
|||||||
deploy-daemon.sh
|
deploy-daemon.sh
|
||||||
serve-web.sh
|
serve-web.sh
|
||||||
web/public/config.js
|
web/public/config.js
|
||||||
|
cmd/app/frontend/dist/*
|
||||||
|
!cmd/app/frontend/dist/.gitkeep
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ proper signed invite (if the network enforces it) to be accepted by peers.
|
|||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{"type":"add_share","path":"/home/alice/Music"} // global
|
{"type":"add_share","path":"/home/alice/Music"} // global
|
||||||
{"type":"add_share","path":"/home/alice/Docs","networks":["abc123"]} // scoped
|
{"type":"add_share","path":"/home/alice/Docs","network_ids":["abc123"]} // scoped
|
||||||
{"type":"remove_share","path":"/home/alice/Music"}
|
{"type":"remove_share","path":"/home/alice/Music"}
|
||||||
{"type":"list_shares"}
|
{"type":"list_shares"}
|
||||||
```
|
```
|
||||||
@@ -148,7 +148,7 @@ entries from all applicable share roots, with relative `path` fields
|
|||||||
|
|
||||||
## EXT-004 — TURN Relay (browser mode)
|
## EXT-004 — TURN Relay (browser mode)
|
||||||
|
|
||||||
**Status:** implemented (browser mode); pending (daemon mode)
|
**Status:** implemented (browser mode + daemon mode)
|
||||||
**Affects:** ICE server configuration only, no wire changes
|
**Affects:** ICE server configuration only, no wire changes
|
||||||
|
|
||||||
The browser adapter reads `WASTE_CONFIG.turnURL` and `WASTE_CONFIG.turnSecret`
|
The browser adapter reads `WASTE_CONFIG.turnURL` and `WASTE_CONFIG.turnSecret`
|
||||||
@@ -171,3 +171,56 @@ this field continue to use `name` for display and download requests.
|
|||||||
|
|
||||||
`MsgFileListReq` / `get` requests use `path` as the lookup key when present,
|
`MsgFileListReq` / `get` requests use `path` as the lookup key when present,
|
||||||
falling back to `name` for backward compat with peers that don't send `path`.
|
falling back to `name` for backward compat with peers that don't send `path`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EXT-006 — File Transfer Resume
|
||||||
|
|
||||||
|
**Status:** implemented (daemon mode)
|
||||||
|
**Affects:** `file-accept` wire message (additive field)
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
A transfer interrupted mid-stream (peer disconnect, network drop) can be
|
||||||
|
continued from where it left off on the next offer of the same file, avoiding
|
||||||
|
a full re-download.
|
||||||
|
|
||||||
|
### Protocol change
|
||||||
|
|
||||||
|
`file-accept` gains one optional field:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "file-accept", "xid": "...", "resume_offset": 65536 }
|
||||||
|
```
|
||||||
|
|
||||||
|
`resume_offset` is the number of bytes the receiver already has on disk.
|
||||||
|
When non-zero, the sender seeks to that byte position before streaming.
|
||||||
|
Peers that don't understand this field ignore it and send from the start —
|
||||||
|
the receiver detects this by comparing incoming data to expected offset and
|
||||||
|
will still verify the final SHA-256, but the partial bytes from the interrupted
|
||||||
|
session will be overwritten (YAW/2-only interop degrades gracefully to a full
|
||||||
|
re-download, not corruption).
|
||||||
|
|
||||||
|
### Receiver behaviour
|
||||||
|
|
||||||
|
1. On `file-offer`, the receiver scans its download directory for a `.tmp.meta`
|
||||||
|
sidecar whose `sha256` matches the offer.
|
||||||
|
2. If found, the corresponding `.tmp` file's size is the resume offset. This is
|
||||||
|
sent back in `file-accept`.
|
||||||
|
3. On DC open, the receiver opens the existing `.tmp` in append mode and
|
||||||
|
re-hashes its existing bytes to restore the SHA-256 state.
|
||||||
|
4. On DC close with all bytes received, SHA-256 is verified. Success → sidecar
|
||||||
|
removed, file renamed to final path. Hash mismatch → both files removed.
|
||||||
|
5. On DC close with fewer bytes than expected (interrupted again) → both files
|
||||||
|
kept for the next resume attempt.
|
||||||
|
|
||||||
|
### Sidecar format
|
||||||
|
|
||||||
|
Each in-progress `.tmp` file has a corresponding `.tmp.meta` JSON sidecar:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "archive.zip", "sha256": "abc...", "from": "<peer-id>", "size": 1048576 }
|
||||||
|
```
|
||||||
|
|
||||||
|
The sidecar is written when the transfer starts and removed on completion or
|
||||||
|
corruption. Interrupted transfers keep the sidecar indefinitely.
|
||||||
|
|||||||
11
FUTURE.md
11
FUTURE.md
@@ -91,10 +91,10 @@ DM rooms (`dm:<peerId>`) appear automatically in both interfaces when messages a
|
|||||||
- Live progress bar per active transfer
|
- Live progress bar per active transfer
|
||||||
- Push (📎) sends directly to a peer without them needing to share a folder
|
- Push (📎) sends directly to a peer without them needing to share a folder
|
||||||
|
|
||||||
**Not yet done:** resume after disconnection, daemon-side download directory.
|
**Not yet done:** daemon-side resume UX (IPC event to surface resumable transfers to the UI on reconnect).
|
||||||
|
|
||||||
### Native UI
|
### Native UI
|
||||||
Web frontend (React, already built) + Tauri shell for native packaging. The IPC protocol is the full boundary — the UI is already a pure consumer. Main work: Tauri setup, system tray, OS notifications.
|
Web frontend (React, already built) + [Wails v2](https://wails.io) shell for native packaging. Wails is Go-native — no Rust toolchain required. The daemon runs embedded in the same process; the webview connects to the existing WebSocket IPC at `ws://127.0.0.1:17338`. Scaffolded in `cmd/app/`; build with `./build-app.sh`. Remaining work: system tray, OS notifications.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,8 +128,11 @@ Web frontend (React, already built) + Tauri shell for native packaging. The IPC
|
|||||||
| ✅ shipped | TURN relay for daemon mode (`-turn-url` / `-turn-secret`) |
|
| ✅ shipped | TURN relay for daemon mode (`-turn-url` / `-turn-secret`) |
|
||||||
| ✅ shipped | TUI room creation + daemon-side room persistence |
|
| ✅ shipped | TUI room creation + daemon-side room persistence |
|
||||||
| ✅ shipped | Unread room indicators in TUI (`*` prefix) |
|
| ✅ shipped | Unread room indicators in TUI (`*` prefix) |
|
||||||
| next | File transfer resume after disconnection |
|
| ✅ shipped | Per-network download directories (`-download-dir` flag + `set_download_dir` IPC) |
|
||||||
| future | Native UI (React + Tauri) |
|
| ✅ shipped | File transfer resume after disconnection |
|
||||||
|
| ✅ shipped | PWA manifest — installable via "Add to Home Screen" on iOS and Android |
|
||||||
|
| ✅ shipped | Native desktop app (Wails 2) — system tray (Linux/Windows), OS notifications, single binary |
|
||||||
|
| ✅ shipped | Gitea Actions CI — server binaries (all platforms via cross-compile) + desktop app (Linux amd64) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
80
README.md
80
README.md
@@ -185,6 +185,8 @@ A user visits your domain, enters their name and a network name, and joins. Invi
|
|||||||
|
|
||||||
**Identity note:** browser mode uses the master identity directly (same keypair on all networks, compatible with yaw2). The daemon derives a separate keypair per network via HKDF. A browser user and a daemon user on the same network will see each other and can chat — they just appear as different peers even if they're the same person.
|
**Identity note:** browser mode uses the master identity directly (same keypair on all networks, compatible with yaw2). The daemon derives a separate keypair per network via HKDF. A browser user and a daemon user on the same network will see each other and can chat — they just appear as different peers even if they're the same person.
|
||||||
|
|
||||||
|
**Mobile:** the web UI ships a PWA manifest (`manifest.json`) so iOS and Android users can pin it to their home screen via "Add to Home Screen" in Safari or Chrome. It runs as a standalone app with no browser chrome. No app store, no native install required.
|
||||||
|
|
||||||
**Session persistence:** the last-used network name, alias, and anchor URL are saved to `localStorage`. On reload, the browser automatically rejoins the saved network — no login screen. To leave a network or switch identity, click the **⏻** button in the top-left of the sidebar. You'll be asked whether to also clear the identity keypair (export a backup first if you want to keep it).
|
**Session persistence:** the last-used network name, alias, and anchor URL are saved to `localStorage`. On reload, the browser automatically rejoins the saved network — no login screen. To leave a network or switch identity, click the **⏻** button in the top-left of the sidebar. You'll be asked whether to also clear the identity keypair (export a backup first if you want to keep it).
|
||||||
|
|
||||||
### Daemon mode (for users running the daemon locally)
|
### Daemon mode (for users running the daemon locally)
|
||||||
@@ -215,8 +217,80 @@ Hover over a peer in the sidebar to reveal action buttons. Click **⊞** to requ
|
|||||||
|
|
||||||
Hover over a peer and click **📎** to open a file picker. The selected file is pushed immediately to that peer — they don't need to be sharing anything. The recipient's browser auto-downloads the file on arrival.
|
Hover over a peer and click **📎** to open a file picker. The selected file is pushed immediately to that peer — they don't need to be sharing anything. The recipient's browser auto-downloads the file on arrival.
|
||||||
|
|
||||||
|
### Transfer resume (daemon mode)
|
||||||
|
|
||||||
|
When a file transfer is interrupted mid-stream — peer disconnects, network drops — the partially-received data is kept on disk. A `.meta` sidecar is written alongside each in-progress `.tmp` file recording the file name, size, and SHA-256.
|
||||||
|
|
||||||
|
When the peer reconnects and re-offers the same file, the daemon finds the matching partial by SHA-256, sends back a `file-accept` with a non-zero `resume_offset`, and the sender seeks to that byte position before streaming. The receiver appends to the existing file and verifies the full SHA-256 at the end.
|
||||||
|
|
||||||
|
Corrupted transfers (all bytes received but hash mismatch) are removed. Interrupted transfers are kept indefinitely until either successfully completed or the `.tmp`/`.meta` files are manually deleted.
|
||||||
|
|
||||||
> In daemon mode, use `add_share` / `remove_share` via IPC to manage share roots. Share configuration is stored in `shares.json` next to `identity.json` in the data directory and survives restarts. `networks: ["*"]` makes a share visible on all networks; omit to scope it to specific network IDs. The legacy `set_share_dir` single-dir command still works alongside it.
|
> In daemon mode, use `add_share` / `remove_share` via IPC to manage share roots. Share configuration is stored in `shares.json` next to `identity.json` in the data directory and survives restarts. `networks: ["*"]` makes a share visible on all networks; omit to scope it to specific network IDs. The legacy `set_share_dir` single-dir command still works alongside it.
|
||||||
|
|
||||||
|
### Daemon download directory
|
||||||
|
|
||||||
|
Received files are saved into a per-network subdirectory so networks stay isolated. By default this is inside the data directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.waste/downloads-<network-id>/
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `-download-dir` at startup to use a different base:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/daemon -download-dir ~/Downloads ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Files then land in `~/Downloads/downloads-<network-id>/`. The path can also be changed at runtime via the `set_download_dir` IPC command (takes effect for the next incoming transfer on that network). The current path is reported in every `state_snapshot` event under `networks[].download_dir`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desktop app (Wails)
|
||||||
|
|
||||||
|
`cmd/app/` is a [Wails v2](https://wails.io) shell that packages the React frontend and the daemon logic into a single native binary for Windows, macOS, and Linux. No Rust, no Electron — just Go + the OS webview.
|
||||||
|
|
||||||
|
The daemon runs embedded in the same process. The webview connects to `ws://127.0.0.1:17338` exactly as it does in browser-daemon mode, so the frontend code is unchanged. Identity and stores use the OS config directory (`~/Library/Application Support/waste` on macOS, `~/.config/waste` on Linux, `%APPDATA%\waste` on Windows).
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.24+
|
||||||
|
- Node.js 20+
|
||||||
|
- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`
|
||||||
|
- Linux platform deps: `sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev`
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production build → dist/waste-linux-amd64 (or waste.exe / waste.app)
|
||||||
|
./build-app.sh
|
||||||
|
|
||||||
|
# Dev mode (hot-reload; start Vite in another terminal: cd web && npm run dev)
|
||||||
|
./build-app.sh dev
|
||||||
|
|
||||||
|
# Build without system tray (no libayatana-appindicator3-dev needed)
|
||||||
|
CGO_ENABLED=1 WAILS_TAGS=notray ./build-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`build-app.sh` builds the React frontend, copies `web/dist/` into `cmd/app/frontend/dist/` for embedding, then runs `wails build`. The result is a single self-contained binary.
|
||||||
|
|
||||||
|
### What the desktop app provides
|
||||||
|
|
||||||
|
- **Single binary** — daemon logic is embedded, no subprocess to manage
|
||||||
|
- **System tray** (Linux/Windows) — hide to tray, reopen from tray menu, Quit
|
||||||
|
- **macOS** — hides to Dock on window close; full menu-bar tray is future work
|
||||||
|
- **OS notifications** — native popup on new message or completed file transfer (uses the browser `Notification` API via Wails events; the app will request permission on first notification)
|
||||||
|
- **Data directory** — OS-appropriate config dir (`~/.config/waste`, `~/Library/Application Support/waste`, `%APPDATA%\waste`)
|
||||||
|
|
||||||
|
### CI / automated builds
|
||||||
|
|
||||||
|
`.gitea/workflows/build.yml` runs on every `v*` tag push:
|
||||||
|
|
||||||
|
- **Server binaries** (daemon + anchor): cross-compiled for Linux amd64/arm64, macOS amd64/arm64, Windows amd64 — no CGo, no special runner needed
|
||||||
|
- **Desktop app**: Linux amd64 on the default runner (installs GTK + WebKit + appindicator deps automatically)
|
||||||
|
- **Releases**: all artifacts attached to the Gitea release
|
||||||
|
|
||||||
|
To add macOS or Windows desktop builds, add self-hosted Gitea runners on those platforms and mirror the `desktop-linux` job.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
@@ -339,17 +413,19 @@ Newline-delimited JSON on TCP port 17337 (or WebSocket on 17338).
|
|||||||
{"type":"get_file_list","peer_id":"<64-hex>"}
|
{"type":"get_file_list","peer_id":"<64-hex>"}
|
||||||
{"type":"send_file","peer_id":"<64-hex>","path":"notes.txt"}
|
{"type":"send_file","peer_id":"<64-hex>","path":"notes.txt"}
|
||||||
{"type":"add_share","path":"/home/alice/Music"} // global share
|
{"type":"add_share","path":"/home/alice/Music"} // global share
|
||||||
{"type":"add_share","path":"/home/alice/Docs","networks":["abc123"]} // network-scoped
|
{"type":"add_share","path":"/home/alice/Docs","network_ids":["abc123"]} // network-scoped
|
||||||
{"type":"remove_share","path":"/home/alice/Music"}
|
{"type":"remove_share","path":"/home/alice/Music"}
|
||||||
{"type":"list_shares"}
|
{"type":"list_shares"}
|
||||||
{"type":"create_room","room":"dev"}
|
{"type":"create_room","room":"dev"}
|
||||||
|
{"type":"set_download_dir","path":"/home/alice/Downloads"} // change download base for current network
|
||||||
|
{"type":"set_download_dir","network_id":"<16-hex>","path":"/home/alice/Downloads/friends"}
|
||||||
{"type":"export_identity","passphrase":"..."}
|
{"type":"export_identity","passphrase":"..."}
|
||||||
{"type":"import_identity","passphrase":"...","backup":"..."}
|
{"type":"import_identity","passphrase":"...","backup":"..."}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Events:**
|
**Events:**
|
||||||
```jsonc
|
```jsonc
|
||||||
{"type":"state_snapshot","local_peer":{...},"connected_peers":[...],"master_alias":"alice","master_id":"<64-hex>"}
|
{"type":"state_snapshot","local_peer":{...},"connected_peers":[...],"master_alias":"alice","master_id":"<64-hex>","networks":[{"network_id":"...","network_name":"friends","share_dir":"...","download_dir":"..."}]}
|
||||||
{"type":"peer_connected","peer":{"id":"<64-hex>","alias":"bob"}}
|
{"type":"peer_connected","peer":{"id":"<64-hex>","alias":"bob"}}
|
||||||
{"type":"session_ready","peer_id":"<64-hex>","nick":"bob"}
|
{"type":"session_ready","peer_id":"<64-hex>","nick":"bob"}
|
||||||
{"type":"peer_disconnected","peer_id":"<64-hex>"}
|
{"type":"peer_disconnected","peer_id":"<64-hex>"}
|
||||||
|
|||||||
30
build-app.sh
Executable file
30
build-app.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the waste desktop app (Wails + embedded React frontend).
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
|
# Node.js 20+
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./build-app.sh # production build → bin/waste (or bin/waste.exe)
|
||||||
|
# ./build-app.sh dev # dev mode (hot-reload; requires Vite dev server)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODE="${1:-build}"
|
||||||
|
|
||||||
|
echo "==> Building web frontend..."
|
||||||
|
(cd web && npm install --silent && npm run build)
|
||||||
|
|
||||||
|
echo "==> Copying frontend dist into app package..."
|
||||||
|
rm -rf cmd/app/frontend/dist
|
||||||
|
cp -r web/dist cmd/app/frontend/dist
|
||||||
|
|
||||||
|
if [ "$MODE" = "dev" ]; then
|
||||||
|
echo "==> Starting Wails dev mode (start 'cd web && npm run dev' in another terminal)..."
|
||||||
|
(cd cmd/app && wails dev)
|
||||||
|
else
|
||||||
|
echo "==> Running Wails build..."
|
||||||
|
mkdir -p bin
|
||||||
|
(cd cmd/app && wails build -o "$(pwd)/../../bin/waste")
|
||||||
|
echo "==> Done: bin/waste"
|
||||||
|
fi
|
||||||
130
cmd/app/app.go
Normal file
130
cmd/app/app.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
|
"github.com/waste-go/internal/crypto"
|
||||||
|
"github.com/waste-go/internal/ipc"
|
||||||
|
"github.com/waste-go/internal/netmgr"
|
||||||
|
"github.com/waste-go/internal/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const wsPort = 17338
|
||||||
|
|
||||||
|
// App is the Wails application backend.
|
||||||
|
// It embeds the daemon directly — no subprocess needed.
|
||||||
|
// The React frontend connects to the daemon's WebSocket IPC at ws://127.0.0.1:17338,
|
||||||
|
// exactly as it does in browser-daemon mode.
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
mgr *netmgr.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func newApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startup is called when the Wails window is ready. It initialises the daemon,
|
||||||
|
// starts the WebSocket IPC listener, sets up the system tray, and begins
|
||||||
|
// forwarding message/file events to the webview as OS notifications.
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
|
||||||
|
dir := dataDir()
|
||||||
|
id, err := crypto.LoadOrCreate(dir, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("app: identity: %v", err)
|
||||||
|
runtime.MessageDialog(ctx, runtime.MessageDialogOptions{
|
||||||
|
Type: runtime.ErrorDialog,
|
||||||
|
Title: "waste — startup error",
|
||||||
|
Message: "Failed to load identity: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("app: peer id: %s alias: %s", id.PeerID().Short(), id.Alias)
|
||||||
|
|
||||||
|
a.mgr = netmgr.New(netmgr.Config{
|
||||||
|
MasterIdentity: id,
|
||||||
|
StoreDir: dir,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Forward daemon events to the webview and generate OS notifications.
|
||||||
|
go a.watchEvents()
|
||||||
|
|
||||||
|
// Start the WebSocket IPC server; the webview connects here (daemon mode).
|
||||||
|
go func() {
|
||||||
|
if err := ipc.RunWS(a.mgr, wsPort); err != nil {
|
||||||
|
log.Printf("app: IPC: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("app: WebSocket IPC listening on 127.0.0.1:%d", wsPort)
|
||||||
|
|
||||||
|
// System tray (Linux/Windows; no-op on macOS — see tray_darwin.go).
|
||||||
|
a.startTray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown is called when the Wails app exits.
|
||||||
|
func (a *App) shutdown(ctx context.Context) {
|
||||||
|
if a.mgr != nil {
|
||||||
|
a.mgr.LeaveAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchEvents subscribes to all daemon events and emits OS notifications for
|
||||||
|
// incoming messages and completed file transfers. The "notify" event is received
|
||||||
|
// by the frontend via Wails EventsOn and displayed using the browser Notification API.
|
||||||
|
func (a *App) watchEvents() {
|
||||||
|
if a.mgr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
events := a.mgr.Subscribe()
|
||||||
|
defer a.mgr.Unsubscribe(events)
|
||||||
|
|
||||||
|
for evt := range events {
|
||||||
|
switch evt.Type {
|
||||||
|
case proto.EvtMessageReceived:
|
||||||
|
if evt.Message == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Don't notify for messages sent by the local peer.
|
||||||
|
if a.mgr.MasterIdentity() != nil && evt.Message.From == a.mgr.MasterIdentity().PeerID() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
runtime.EventsEmit(a.ctx, "notify", map[string]string{
|
||||||
|
"title": "waste — new message",
|
||||||
|
"body": evt.Message.Text,
|
||||||
|
})
|
||||||
|
|
||||||
|
case proto.EvtFileComplete:
|
||||||
|
runtime.EventsEmit(a.ctx, "notify", map[string]string{
|
||||||
|
"title": "waste — file received",
|
||||||
|
"body": filepath.Base(evt.Path),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataDir returns the OS-appropriate config directory for identity and stores.
|
||||||
|
// macOS: ~/Library/Application Support/waste
|
||||||
|
// Linux: ~/.config/waste
|
||||||
|
// Windows: %APPDATA%\waste
|
||||||
|
func dataDir() string {
|
||||||
|
base, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
return filepath.Join(home, ".waste")
|
||||||
|
}
|
||||||
|
return ".waste"
|
||||||
|
}
|
||||||
|
dir := filepath.Join(base, "waste")
|
||||||
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
|
log.Printf("app: mkdir %s: %v", dir, err)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
BIN
cmd/app/appicon.png
Normal file
BIN
cmd/app/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
0
cmd/app/frontend/dist/.gitkeep
vendored
Normal file
0
cmd/app/frontend/dist/.gitkeep
vendored
Normal file
66
cmd/app/main.go
Normal file
66
cmd/app/main.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// waste desktop app — Wails shell wrapping the daemon and React UI.
|
||||||
|
// In dev mode the UI is served from the Vite dev server (http://localhost:5173).
|
||||||
|
// In production the compiled frontend is embedded from frontend/dist/.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/linux"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := newApp()
|
||||||
|
|
||||||
|
logLevel := logger.INFO
|
||||||
|
if os.Getenv("WASTE_DEBUG") != "" {
|
||||||
|
logLevel = logger.DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
err := wails.Run(&options.App{
|
||||||
|
Title: "waste",
|
||||||
|
Width: 1200,
|
||||||
|
Height: 800,
|
||||||
|
MinWidth: 800,
|
||||||
|
MinHeight: 600,
|
||||||
|
DisableResize: false,
|
||||||
|
Fullscreen: false,
|
||||||
|
LogLevel: logLevel,
|
||||||
|
LogLevelProduction: logger.ERROR,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
OnStartup: app.startup,
|
||||||
|
OnShutdown: app.shutdown,
|
||||||
|
Bind: []interface{}{app},
|
||||||
|
// Hide the window instead of quitting when the close button is clicked.
|
||||||
|
// The user can quit via the app menu or by stopping the process.
|
||||||
|
HideWindowOnClose: true,
|
||||||
|
Mac: &mac.Options{
|
||||||
|
TitleBar: mac.TitleBarHiddenInset(),
|
||||||
|
About: &mac.AboutInfo{
|
||||||
|
Title: "waste",
|
||||||
|
Message: "Decentralized friend-to-friend encrypted mesh networking.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Windows: &windows.Options{
|
||||||
|
WebviewIsTransparent: false,
|
||||||
|
WindowIsTranslucent: false,
|
||||||
|
},
|
||||||
|
Linux: &linux.Options{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
cmd/app/tray.go
Normal file
35
cmd/app/tray.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build !darwin && !notray
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/getlantern/systray"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed appicon.png
|
||||||
|
var trayIcon []byte
|
||||||
|
|
||||||
|
func (a *App) startTray() {
|
||||||
|
go systray.Run(func() {
|
||||||
|
systray.SetIcon(trayIcon)
|
||||||
|
systray.SetTooltip("waste")
|
||||||
|
|
||||||
|
mShow := systray.AddMenuItem("Open waste", "Show the waste window")
|
||||||
|
systray.AddSeparator()
|
||||||
|
mQuit := systray.AddMenuItem("Quit", "Quit waste")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-mShow.ClickedCh:
|
||||||
|
runtime.WindowShow(a.ctx)
|
||||||
|
case <-mQuit.ClickedCh:
|
||||||
|
systray.Quit()
|
||||||
|
runtime.Quit(a.ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, func() {})
|
||||||
|
}
|
||||||
14
cmd/app/tray_darwin.go
Normal file
14
cmd/app/tray_darwin.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// On macOS, Cocoa requires the system tray to run on the main thread, which
|
||||||
|
// Wails already owns. Full menu-bar tray integration is future work and would
|
||||||
|
// require an NSStatusItem helper or a fork of systray that supports
|
||||||
|
// RunWithExternalLoop on macOS.
|
||||||
|
//
|
||||||
|
// For now the app hides to the Dock when the window is closed (HideWindowOnClose)
|
||||||
|
// and users can reopen it from the Dock or Cmd+Tab. The standard macOS Cmd+Q
|
||||||
|
// binding quits cleanly via Wails.
|
||||||
|
|
||||||
|
func (a *App) startTray() {}
|
||||||
8
cmd/app/tray_stub.go
Normal file
8
cmd/app/tray_stub.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build notray
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// Build with -tags notray to omit the system tray (no libayatana-appindicator3-dev needed).
|
||||||
|
// Used for headless builds and CI environments that haven't installed the GTK tray headers.
|
||||||
|
|
||||||
|
func (a *App) startTray() {}
|
||||||
12
cmd/app/wails.json
Normal file
12
cmd/app/wails.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||||
|
"name": "waste",
|
||||||
|
"outputfilename": "waste",
|
||||||
|
"frontend:install": "echo 'run: cd web && npm install'",
|
||||||
|
"frontend:build": "echo 'run: ./build-app.sh'",
|
||||||
|
"frontend:dev:watcher": "",
|
||||||
|
"frontend:dev:serverUrl": "http://localhost:5173",
|
||||||
|
"author": {
|
||||||
|
"name": "waste-go"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ func main() {
|
|||||||
wsPort := flag.Int("ws-port", 0, "port for WebSocket IPC (web UI); 0 = disabled")
|
wsPort := flag.Int("ws-port", 0, "port for WebSocket IPC (web UI); 0 = disabled")
|
||||||
anchorURL := flag.String("anchor", "", "anchor WebSocket URL, e.g. ws://your-vps:17339/ws")
|
anchorURL := flag.String("anchor", "", "anchor WebSocket URL, e.g. ws://your-vps:17339/ws")
|
||||||
shareDir := flag.String("share-dir", "", "directory to share with peers on the network")
|
shareDir := flag.String("share-dir", "", "directory to share with peers on the network")
|
||||||
|
downloadDir := flag.String("download-dir", "", "base directory for received files; defaults to data-dir, split by network")
|
||||||
joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup")
|
joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup")
|
||||||
turnURL := flag.String("turn-url", "", "TURN server URL, e.g. turn:your-vps:3478")
|
turnURL := flag.String("turn-url", "", "TURN server URL, e.g. turn:your-vps:3478")
|
||||||
turnSecret := flag.String("turn-secret", "", "shared secret for coturn use-auth-secret HMAC credential")
|
turnSecret := flag.String("turn-secret", "", "shared secret for coturn use-auth-secret HMAC credential")
|
||||||
@@ -76,6 +77,7 @@ func main() {
|
|||||||
StoreDir: dir,
|
StoreDir: dir,
|
||||||
AnchorURL: *anchorURL,
|
AnchorURL: *anchorURL,
|
||||||
ShareDir: expandHome(*shareDir),
|
ShareDir: expandHome(*shareDir),
|
||||||
|
DownloadDir: expandHome(*downloadDir),
|
||||||
TurnURL: *turnURL,
|
TurnURL: *turnURL,
|
||||||
TurnSecret: *turnSecret,
|
TurnSecret: *turnSecret,
|
||||||
})
|
})
|
||||||
|
|||||||
39
go.mod
39
go.mod
@@ -6,14 +6,16 @@ require (
|
|||||||
filippo.io/edwards25519 v1.2.0
|
filippo.io/edwards25519 v1.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/pion/webrtc/v3 v3.3.6
|
github.com/pion/webrtc/v3 v3.3.6
|
||||||
golang.org/x/crypto v0.24.0
|
golang.org/x/crypto v0.33.0
|
||||||
modernc.org/sqlite v1.53.0
|
modernc.org/sqlite v1.53.0
|
||||||
nhooyr.io/websocket v1.8.17
|
nhooyr.io/websocket v1.8.17
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
@@ -27,7 +29,26 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||||
|
github.com/getlantern/systray v1.2.2 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
|
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||||
|
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||||
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
@@ -35,6 +56,7 @@ require (
|
|||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||||
github.com/pion/datachannel v1.5.8 // indirect
|
github.com/pion/datachannel v1.5.8 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||||
github.com/pion/ice/v2 v2.3.38 // indirect
|
github.com/pion/ice/v2 v2.3.38 // indirect
|
||||||
@@ -50,15 +72,24 @@ require (
|
|||||||
github.com/pion/stun v0.6.1 // indirect
|
github.com/pion/stun v0.6.1 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
|
github.com/wailsapp/wails/v2 v2.12.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.3 // indirect
|
github.com/wlynxg/anet v0.0.3 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/net v0.22.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
golang.org/x/text v0.16.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.73.4 // indirect
|
modernc.org/libc v1.73.4 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
|||||||
85
go.sum
85
go.sum
@@ -1,9 +1,13 @@
|
|||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
@@ -31,21 +35,63 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||||
|
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||||
|
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||||
|
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||||
|
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||||
|
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||||
|
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||||
|
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||||
|
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||||
|
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||||
|
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||||
|
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||||
|
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||||
|
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
@@ -60,6 +106,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
|
|||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
|
github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
|
||||||
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
|
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
@@ -102,22 +150,45 @@ github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
|||||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||||
github.com/pion/webrtc/v3 v3.3.6 h1:7XAh4RPtlY1Vul6/GmZrv7z+NnxKA6If0KStXBI2ZLE=
|
github.com/pion/webrtc/v3 v3.3.6 h1:7XAh4RPtlY1Vul6/GmZrv7z+NnxKA6If0KStXBI2ZLE=
|
||||||
github.com/pion/webrtc/v3 v3.3.6/go.mod h1:zyN7th4mZpV27eXybfR/cnUf3J2DRy8zw/mdjD9JTNM=
|
github.com/pion/webrtc/v3 v3.3.6/go.mod h1:zyN7th4mZpV27eXybfR/cnUf3J2DRy8zw/mdjD9JTNM=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
|
github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c=
|
||||||
|
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
||||||
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
|
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
|
||||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
@@ -130,12 +201,15 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y
|
|||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
@@ -144,17 +218,24 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -173,6 +254,7 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
|||||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
@@ -180,6 +262,8 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
@@ -187,6 +271,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -344,6 +344,22 @@ func handleClient(conn net.Conn, mgr *netmgr.Manager) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case proto.CmdSetDownloadDir:
|
||||||
|
n := mgr.Resolve(cmd.NetworkID)
|
||||||
|
if n == nil {
|
||||||
|
send(errMsg("set_download_dir: not joined to any network"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cmd.Path == "" {
|
||||||
|
send(errMsg("set_download_dir: path is required"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !mgr.SetDownloadDir(n.ID, cmd.Path) {
|
||||||
|
send(errMsg("set_download_dir: network not found"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
send(stateSnapshot(mgr))
|
||||||
|
|
||||||
case proto.CmdSendFile:
|
case proto.CmdSendFile:
|
||||||
n := mgr.Resolve(cmd.NetworkID)
|
n := mgr.Resolve(cmd.NetworkID)
|
||||||
if n == nil {
|
if n == nil {
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ func dispatchPeerMessage(msg proto.PeerMessage, from proto.PeerID, m *Mesh) {
|
|||||||
m.acceptIncoming(msg, from)
|
m.acceptIncoming(msg, from)
|
||||||
|
|
||||||
case proto.MsgFileAccept:
|
case proto.MsgFileAccept:
|
||||||
m.startSend(msg.Xid, from)
|
m.startSend(msg.Xid, from, msg.ResumeOffset)
|
||||||
|
|
||||||
case proto.MsgFileCancel:
|
case proto.MsgFileCancel:
|
||||||
log.Printf("mesh: file-cancel from %s xid=%s reason=%s", from.Short(), msg.Xid, msg.Reason)
|
log.Printf("mesh: file-cancel from %s xid=%s reason=%s", from.Short(), msg.Xid, msg.Reason)
|
||||||
|
|||||||
@@ -40,7 +40,55 @@ type inboundTransfer struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
tmp *os.File
|
tmp *os.File
|
||||||
hasher hash.Hash
|
hasher hash.Hash
|
||||||
written int64
|
written int64 // total bytes received; starts at resumeOffset when resuming
|
||||||
|
resumePath string // path to existing .tmp when resuming; empty for new transfers
|
||||||
|
resumeOffset int64 // bytes already present in resumePath
|
||||||
|
metaPath string // path to the .tmp.meta sidecar
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialMeta is written as a JSON sidecar alongside each in-progress .tmp file.
|
||||||
|
// It survives interruptions so the receiver can resume later.
|
||||||
|
type partialMeta struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
SHA256 string `json:"sha256"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// findPartial scans dir for a .tmp.meta sidecar whose sha256 matches.
|
||||||
|
// Returns (tmpPath, metaPath, offset) — all empty/zero if no match.
|
||||||
|
func findPartial(dir, sha256hex string) (tmpPath, metaPath string, offset int64) {
|
||||||
|
metas, _ := filepath.Glob(filepath.Join(dir, "*.tmp.meta"))
|
||||||
|
for _, mp := range metas {
|
||||||
|
data, err := os.ReadFile(mp)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var m partialMeta
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m.SHA256 != sha256hex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tp := strings.TrimSuffix(mp, ".meta")
|
||||||
|
info, err := os.Stat(tp)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return tp, mp, info.Size()
|
||||||
|
}
|
||||||
|
return "", "", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePartialMeta(path string, t *inboundTransfer) {
|
||||||
|
data, _ := json.Marshal(partialMeta{
|
||||||
|
Name: t.name,
|
||||||
|
SHA256: t.sha256,
|
||||||
|
From: string(t.from),
|
||||||
|
Size: t.size,
|
||||||
|
})
|
||||||
|
os.WriteFile(path, data, 0o644) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
// OfferFile reads filename from ShareDir, computes its SHA-256, and sends a
|
// OfferFile reads filename from ShareDir, computes its SHA-256, and sends a
|
||||||
@@ -102,28 +150,49 @@ func (m *Mesh) OfferFile(peerID proto.PeerID, filename string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// acceptIncoming is called when we receive file-offer from a peer.
|
// acceptIncoming is called when we receive file-offer from a peer.
|
||||||
// It stores the inbound state and immediately sends file-accept (auto-accept).
|
// Checks for a resumable partial download matching the offer's sha256.
|
||||||
|
// Sends file-accept immediately, with a non-zero resume_offset when resuming.
|
||||||
func (m *Mesh) acceptIncoming(msg proto.PeerMessage, from proto.PeerID) {
|
func (m *Mesh) acceptIncoming(msg proto.PeerMessage, from proto.PeerID) {
|
||||||
m.transferMu.Lock()
|
t := &inboundTransfer{
|
||||||
m.inbound[msg.Xid] = &inboundTransfer{
|
|
||||||
from: from,
|
from: from,
|
||||||
name: msg.Name,
|
name: msg.Name,
|
||||||
size: msg.Size,
|
size: msg.Size,
|
||||||
sha256: msg.SHA256,
|
sha256: msg.SHA256,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resumeOffset int64
|
||||||
|
if m.DownloadDir != "" {
|
||||||
|
if tp, mp, off := findPartial(m.DownloadDir, msg.SHA256); tp != "" {
|
||||||
|
t.resumePath = tp
|
||||||
|
t.metaPath = mp
|
||||||
|
t.resumeOffset = off
|
||||||
|
t.written = off
|
||||||
|
resumeOffset = off
|
||||||
|
log.Printf("transfer: found partial for %s at %s (%d/%d bytes)", msg.Name, tp, off, msg.Size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.transferMu.Lock()
|
||||||
|
m.inbound[msg.Xid] = t
|
||||||
m.transferMu.Unlock()
|
m.transferMu.Unlock()
|
||||||
|
|
||||||
accept, _ := json.Marshal(proto.PeerMessage{
|
accept, _ := json.Marshal(proto.PeerMessage{
|
||||||
Type: proto.MsgFileAccept,
|
Type: proto.MsgFileAccept,
|
||||||
Xid: msg.Xid,
|
Xid: msg.Xid,
|
||||||
|
ResumeOffset: resumeOffset,
|
||||||
})
|
})
|
||||||
m.SendTo(from, accept)
|
m.SendTo(from, accept)
|
||||||
|
if resumeOffset > 0 {
|
||||||
|
log.Printf("transfer: resuming %s from byte %d xid=%s", msg.Name, resumeOffset, msg.Xid[:8])
|
||||||
|
} else {
|
||||||
log.Printf("transfer: auto-accepted %s (%d bytes) from %s xid=%s", msg.Name, msg.Size, from.Short(), msg.Xid[:8])
|
log.Printf("transfer: auto-accepted %s (%d bytes) from %s xid=%s", msg.Name, msg.Size, from.Short(), msg.Xid[:8])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// startSend opens the binary DataChannel "f:<xid>" and streams the file.
|
// startSend opens the binary DataChannel "f:<xid>" and streams the file.
|
||||||
// Called when we receive file-accept from the peer.
|
// Called when we receive file-accept from the peer. resumeOffset is non-zero
|
||||||
func (m *Mesh) startSend(xid string, from proto.PeerID) {
|
// when the receiver is resuming a previous partial download.
|
||||||
|
func (m *Mesh) startSend(xid string, from proto.PeerID, resumeOffset int64) {
|
||||||
m.transferMu.Lock()
|
m.transferMu.Lock()
|
||||||
t, ok := m.outbound[xid]
|
t, ok := m.outbound[xid]
|
||||||
m.transferMu.Unlock()
|
m.transferMu.Unlock()
|
||||||
@@ -156,13 +225,13 @@ func (m *Mesh) startSend(xid string, from proto.PeerID) {
|
|||||||
dc.OnOpen(func() {
|
dc.OnOpen(func() {
|
||||||
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
|
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||||
if msg.IsString && string(msg.Data) == "ok" {
|
if msg.IsString && string(msg.Data) == "ok" {
|
||||||
go m.sendFileChunks(dc, t, xid)
|
go m.sendFileChunks(dc, t, xid, resumeOffset)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mesh) sendFileChunks(dc *webrtc.DataChannel, t *outboundTransfer, xid string) {
|
func (m *Mesh) sendFileChunks(dc *webrtc.DataChannel, t *outboundTransfer, xid string, offset int64) {
|
||||||
defer func() {
|
defer func() {
|
||||||
m.transferMu.Lock()
|
m.transferMu.Lock()
|
||||||
delete(m.outbound, xid)
|
delete(m.outbound, xid)
|
||||||
@@ -177,6 +246,15 @@ func (m *Mesh) sendFileChunks(dc *webrtc.DataChannel, t *outboundTransfer, xid s
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
|
if offset > 0 {
|
||||||
|
if _, err := f.Seek(offset, io.SeekStart); err != nil {
|
||||||
|
log.Printf("transfer: seek to %d xid=%s: %v", offset, xid[:8], err)
|
||||||
|
dc.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("transfer: resuming send from byte %d xid=%s", offset, xid[:8])
|
||||||
|
}
|
||||||
|
|
||||||
// Backpressure: block when the DC buffer is full.
|
// Backpressure: block when the DC buffer is full.
|
||||||
resume := make(chan struct{}, 1)
|
resume := make(chan struct{}, 1)
|
||||||
dc.SetBufferedAmountLowThreshold(fileBufferLowWater)
|
dc.SetBufferedAmountLowThreshold(fileBufferLowWater)
|
||||||
@@ -188,7 +266,7 @@ func (m *Mesh) sendFileChunks(dc *webrtc.DataChannel, t *outboundTransfer, xid s
|
|||||||
})
|
})
|
||||||
|
|
||||||
buf := make([]byte, fileChunkSize)
|
buf := make([]byte, fileChunkSize)
|
||||||
var sent int64
|
sent := offset // start progress reporting from where the receiver left off
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, readErr := f.Read(buf)
|
n, readErr := f.Read(buf)
|
||||||
@@ -229,8 +307,9 @@ func (m *Mesh) sendFileChunks(dc *webrtc.DataChannel, t *outboundTransfer, xid s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleInboundFileDC is called from the anchor when a "f:<xid>" DataChannel
|
// HandleInboundFileDC is called from the anchor when a "f:<xid>" DataChannel
|
||||||
// arrives on a PeerConnection. It writes chunks to a temp file, verifies the
|
// arrives on a PeerConnection. It writes chunks to a temp file (or resumes an
|
||||||
// SHA-256 on close, and emits EvtFileComplete.
|
// existing partial), verifies the SHA-256 on close, and emits EvtFileComplete.
|
||||||
|
// Interrupted transfers keep their .tmp and .meta files for future resume.
|
||||||
func (m *Mesh) HandleInboundFileDC(dc *webrtc.DataChannel, xid string, from proto.PeerID) {
|
func (m *Mesh) HandleInboundFileDC(dc *webrtc.DataChannel, xid string, from proto.PeerID) {
|
||||||
m.transferMu.Lock()
|
m.transferMu.Lock()
|
||||||
t, ok := m.inbound[xid]
|
t, ok := m.inbound[xid]
|
||||||
@@ -247,16 +326,43 @@ func (m *Mesh) HandleInboundFileDC(dc *webrtc.DataChannel, xid string, from prot
|
|||||||
log.Printf("transfer: mkdir %s: %v", m.DownloadDir, err)
|
log.Printf("transfer: mkdir %s: %v", m.DownloadDir, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
if t.resumePath != "" {
|
||||||
|
// Resume: open existing .tmp in read-write mode, seek to end for appending.
|
||||||
|
f, err := os.OpenFile(t.resumePath, os.O_RDWR, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("transfer: open resume file %s: %v", t.resumePath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Restore the hasher state by re-hashing the bytes already on disk.
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := f.Seek(0, io.SeekStart); err == nil {
|
||||||
|
io.Copy(h, f) //nolint:errcheck
|
||||||
|
}
|
||||||
|
if _, err := f.Seek(0, io.SeekEnd); err != nil {
|
||||||
|
log.Printf("transfer: seek to end xid=%s: %v", xid[:8], err)
|
||||||
|
f.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.tmp = f
|
||||||
|
t.hasher = h
|
||||||
|
log.Printf("transfer: resuming inbound %s at byte %d xid=%s", t.name, t.resumeOffset, xid[:8])
|
||||||
|
} else {
|
||||||
tmp, err := os.CreateTemp(m.DownloadDir, "dl-*.tmp")
|
tmp, err := os.CreateTemp(m.DownloadDir, "dl-*.tmp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("transfer: create temp file: %v", err)
|
log.Printf("transfer: create temp file: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.mu.Lock()
|
|
||||||
t.tmp = tmp
|
t.tmp = tmp
|
||||||
t.hasher = sha256.New()
|
t.hasher = sha256.New()
|
||||||
t.mu.Unlock()
|
t.metaPath = tmp.Name() + ".meta"
|
||||||
|
writePartialMeta(t.metaPath, t)
|
||||||
log.Printf("transfer: receiving %s xid=%s", t.name, xid[:8])
|
log.Printf("transfer: receiving %s xid=%s", t.name, xid[:8])
|
||||||
|
}
|
||||||
|
|
||||||
// Signal sender that we are ready — it will not start streaming until
|
// Signal sender that we are ready — it will not start streaming until
|
||||||
// it receives this, guaranteeing our OnMessage/OnClose are registered first.
|
// it receives this, guaranteeing our OnMessage/OnClose are registered first.
|
||||||
dc.SendText("ok") //nolint:errcheck
|
dc.SendText("ok") //nolint:errcheck
|
||||||
@@ -298,11 +404,12 @@ func (m *Mesh) HandleInboundFileDC(dc *webrtc.DataChannel, xid string, from prot
|
|||||||
dc.OnClose(func() {
|
dc.OnClose(func() {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
tmp := t.tmp
|
tmp := t.tmp
|
||||||
|
metaPath := t.metaPath
|
||||||
actualSha := ""
|
actualSha := ""
|
||||||
if t.hasher != nil {
|
if t.hasher != nil {
|
||||||
actualSha = hex.EncodeToString(t.hasher.Sum(nil))
|
actualSha = hex.EncodeToString(t.hasher.Sum(nil))
|
||||||
}
|
}
|
||||||
name, expectedSha := t.name, t.sha256
|
name, expectedSha, size, written := t.name, t.sha256, t.size, t.written
|
||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
|
|
||||||
m.transferMu.Lock()
|
m.transferMu.Lock()
|
||||||
@@ -312,10 +419,21 @@ func (m *Mesh) HandleInboundFileDC(dc *webrtc.DataChannel, xid string, from prot
|
|||||||
if tmp == nil {
|
if tmp == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
tmpName := tmp.Name()
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
|
|
||||||
|
// Incomplete — keep .tmp and .meta for future resume.
|
||||||
|
if written < size {
|
||||||
|
log.Printf("transfer: interrupted %s at %d/%d bytes xid=%s (resumable)", name, written, size, xid[:8])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full receive — verify integrity.
|
||||||
if actualSha != expectedSha {
|
if actualSha != expectedSha {
|
||||||
os.Remove(tmp.Name())
|
os.Remove(tmpName)
|
||||||
|
if metaPath != "" {
|
||||||
|
os.Remove(metaPath)
|
||||||
|
}
|
||||||
log.Printf("transfer: sha256 mismatch xid=%s got=%s want=%s", xid[:8], actualSha[:8], expectedSha[:8])
|
log.Printf("transfer: sha256 mismatch xid=%s got=%s want=%s", xid[:8], actualSha[:8], expectedSha[:8])
|
||||||
m.Emit(proto.IpcMessage{
|
m.Emit(proto.IpcMessage{
|
||||||
Type: proto.EvtError,
|
Type: proto.EvtError,
|
||||||
@@ -325,13 +443,17 @@ func (m *Mesh) HandleInboundFileDC(dc *webrtc.DataChannel, xid string, from prot
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Success — remove sidecar and move to final path.
|
||||||
|
if metaPath != "" {
|
||||||
|
os.Remove(metaPath)
|
||||||
|
}
|
||||||
final := filepath.Join(m.DownloadDir, name)
|
final := filepath.Join(m.DownloadDir, name)
|
||||||
if _, err := os.Stat(final); err == nil {
|
if _, err := os.Stat(final); err == nil {
|
||||||
ext := filepath.Ext(name)
|
ext := filepath.Ext(name)
|
||||||
base := strings.TrimSuffix(name, ext)
|
base := strings.TrimSuffix(name, ext)
|
||||||
final = filepath.Join(m.DownloadDir, base+"-"+xid[:8]+ext)
|
final = filepath.Join(m.DownloadDir, base+"-"+xid[:8]+ext)
|
||||||
}
|
}
|
||||||
if err := os.Rename(tmp.Name(), final); err != nil {
|
if err := os.Rename(tmpName, final); err != nil {
|
||||||
log.Printf("transfer: rename xid=%s: %v", xid[:8], err)
|
log.Printf("transfer: rename xid=%s: %v", xid[:8], err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
MasterIdentity *crypto.Identity
|
MasterIdentity *crypto.Identity
|
||||||
StoreDir string // base directory for per-network SQLite files
|
StoreDir string // base directory for per-network SQLite files
|
||||||
|
DownloadDir string // base directory for received files; defaults to StoreDir if empty
|
||||||
AnchorURL string // WebSocket anchor URL used for all networks
|
AnchorURL string // WebSocket anchor URL used for all networks
|
||||||
ShareDir string // default share directory; overridden per network via Join or SetShareDir
|
ShareDir string // default share directory; overridden per network via Join or SetShareDir
|
||||||
TurnURL string // optional TURN server URL, e.g. "turn:your-vps:3478"
|
TurnURL string // optional TURN server URL, e.g. "turn:your-vps:3478"
|
||||||
@@ -119,7 +120,7 @@ func (mgr *Manager) Join(name, shareDir string) (string, error) {
|
|||||||
} else if mgr.cfg.ShareDir != "" {
|
} else if mgr.cfg.ShareDir != "" {
|
||||||
m.ShareDir = mgr.cfg.ShareDir
|
m.ShareDir = mgr.cfg.ShareDir
|
||||||
}
|
}
|
||||||
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID_full)
|
m.DownloadDir = filepath.Join(mgr.downloadBase(), "downloads-"+netID_full)
|
||||||
capturedNetID := netID
|
capturedNetID := netID
|
||||||
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID) }
|
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID) }
|
||||||
if ice := mgr.turnICEServers(); ice != nil {
|
if ice := mgr.turnICEServers(); ice != nil {
|
||||||
@@ -207,7 +208,7 @@ func (mgr *Manager) JoinByHash(netHash64, shareDir string) (string, error) {
|
|||||||
} else if mgr.cfg.ShareDir != "" {
|
} else if mgr.cfg.ShareDir != "" {
|
||||||
m.ShareDir = mgr.cfg.ShareDir
|
m.ShareDir = mgr.cfg.ShareDir
|
||||||
}
|
}
|
||||||
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID)
|
m.DownloadDir = filepath.Join(mgr.downloadBase(), "downloads-"+netID)
|
||||||
capturedNetID2 := netID
|
capturedNetID2 := netID
|
||||||
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID2) }
|
m.ScanFiles = func() []proto.FileEntry { return mgr.ScanAllShares(capturedNetID2) }
|
||||||
if ice := mgr.turnICEServers(); ice != nil {
|
if ice := mgr.turnICEServers(); ice != nil {
|
||||||
@@ -293,6 +294,28 @@ func (mgr *Manager) SetShareDir(netID, path string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downloadBase returns the configured download base directory, falling back to StoreDir.
|
||||||
|
func (mgr *Manager) downloadBase() string {
|
||||||
|
if mgr.cfg.DownloadDir != "" {
|
||||||
|
return mgr.cfg.DownloadDir
|
||||||
|
}
|
||||||
|
return mgr.cfg.StoreDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDownloadDir updates the download directory for an already-joined network.
|
||||||
|
// Changes take effect for the next incoming file transfer on that network.
|
||||||
|
func (mgr *Manager) SetDownloadDir(netID, path string) bool {
|
||||||
|
mgr.mu.RLock()
|
||||||
|
net, ok := mgr.networks[netID]
|
||||||
|
mgr.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
net.Mesh.DownloadDir = path
|
||||||
|
log.Printf("netmgr: download dir for %q set to %q", net.Name, path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// LeaveAll leaves every joined network.
|
// LeaveAll leaves every joined network.
|
||||||
func (mgr *Manager) LeaveAll() {
|
func (mgr *Manager) LeaveAll() {
|
||||||
mgr.mu.RLock()
|
mgr.mu.RLock()
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type PeerMessage struct {
|
|||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Size int64 `json:"size,omitempty"`
|
Size int64 `json:"size,omitempty"`
|
||||||
SHA256 string `json:"sha256,omitempty"`
|
SHA256 string `json:"sha256,omitempty"`
|
||||||
|
ResumeOffset int64 `json:"resume_offset,omitempty"` // waste-go ext: non-zero in file-accept when resuming
|
||||||
|
|
||||||
// file-done / file-cancel / file-accept just need xid (already above)
|
// file-done / file-cancel / file-accept just need xid (already above)
|
||||||
Reason string `json:"reason,omitempty"` // file-cancel
|
Reason string `json:"reason,omitempty"` // file-cancel
|
||||||
@@ -237,6 +238,7 @@ const (
|
|||||||
CmdRemoveShare IpcMsgType = "remove_share" // remove a share root; field: path
|
CmdRemoveShare IpcMsgType = "remove_share" // remove a share root; field: path
|
||||||
CmdListShares IpcMsgType = "list_shares" // returns shares_list event
|
CmdListShares IpcMsgType = "list_shares" // returns shares_list event
|
||||||
CmdCreateRoom IpcMsgType = "create_room" // field: room (name)
|
CmdCreateRoom IpcMsgType = "create_room" // field: room (name)
|
||||||
|
CmdSetDownloadDir IpcMsgType = "set_download_dir" // set per-network download directory at runtime; fields: network_id, path
|
||||||
|
|
||||||
// Events (daemon → UI)
|
// Events (daemon → UI)
|
||||||
EvtMessageReceived IpcMsgType = "message_received"
|
EvtMessageReceived IpcMsgType = "message_received"
|
||||||
@@ -312,7 +314,7 @@ type IpcMessage struct {
|
|||||||
InviteGenerated string `json:"invite,omitempty"`
|
InviteGenerated string `json:"invite,omitempty"`
|
||||||
Files []FileEntry `json:"files,omitempty"`
|
Files []FileEntry `json:"files,omitempty"`
|
||||||
Shares []ShareEntry `json:"shares,omitempty"`
|
Shares []ShareEntry `json:"shares,omitempty"`
|
||||||
ShareNetworks []string `json:"networks,omitempty"` // for add_share command
|
ShareNetworks []string `json:"network_ids,omitempty"` // for add_share command: scope to specific network IDs, or ["*"] for global
|
||||||
// export_identity / import_identity
|
// export_identity / import_identity
|
||||||
Passphrase string `json:"passphrase,omitempty"` // import only; never echoed back
|
Passphrase string `json:"passphrase,omitempty"` // import only; never echoed back
|
||||||
Backup string `json:"backup,omitempty"` // JSON backup blob
|
Backup string `json:"backup,omitempty"` // JSON backup blob
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#863bff" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="waste" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title>waste</title>
|
<title>waste</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"wails:dev": "wails dev",
|
||||||
|
"wails:build": "wails build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libsodium-wrappers": "^0.8.4",
|
"libsodium-wrappers": "^0.8.4",
|
||||||
|
|||||||
BIN
web/public/apple-touch-icon.png
Normal file
BIN
web/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
BIN
web/public/icon-192.png
Normal file
BIN
web/public/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
BIN
web/public/icon-512.png
Normal file
BIN
web/public/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 456 KiB |
23
web/public/manifest.json
Normal file
23
web/public/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "waste",
|
||||||
|
"short_name": "waste",
|
||||||
|
"description": "Decentralized friend-to-friend encrypted mesh networking",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0d0d0d",
|
||||||
|
"theme_color": "#863bff",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -10,6 +10,35 @@ const cfg = (window as unknown as { WASTE_CONFIG?: { signalURL?: string } }).WAS
|
|||||||
// Use browser mode if a signal URL is configured, even on localhost
|
// Use browser mode if a signal URL is configured, even on localhost
|
||||||
const useBrowser = !isLocal || !!cfg?.signalURL
|
const useBrowser = !isLocal || !!cfg?.signalURL
|
||||||
|
|
||||||
|
// Wails runtime is injected at /wails/runtime.js when running inside the desktop app.
|
||||||
|
// EventsOn/EventsOff are no-ops in plain browser mode.
|
||||||
|
type WailsRuntime = { EventsOn: (event: string, cb: (data: unknown) => void) => void }
|
||||||
|
const wails = (window as unknown as { runtime?: WailsRuntime }).runtime
|
||||||
|
|
||||||
|
function useWailsNotifications() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wails) return
|
||||||
|
|
||||||
|
const handler = (data: unknown) => {
|
||||||
|
const { title, body } = data as { title: string; body: string }
|
||||||
|
if (Notification.permission === 'granted') {
|
||||||
|
new Notification(title, { body })
|
||||||
|
} else if (Notification.permission !== 'denied') {
|
||||||
|
Notification.requestPermission().then(p => {
|
||||||
|
if (p === 'granted') new Notification(title, { body })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wails.EventsOn('notify', handler)
|
||||||
|
return () => {
|
||||||
|
// EventsOff added in Wails v2.4; guard in case of older runtime.
|
||||||
|
;(window as unknown as { runtime?: { EventsOff?: (...e: string[]) => void } })
|
||||||
|
.runtime?.EventsOff?.('notify')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { connect, connectBrowser, daemonStatus, localPeer } = useWaste()
|
const { connect, connectBrowser, daemonStatus, localPeer } = useWaste()
|
||||||
|
|
||||||
@@ -21,6 +50,8 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}, [connect, connectBrowser])
|
}, [connect, connectBrowser])
|
||||||
|
|
||||||
|
useWailsNotifications()
|
||||||
|
|
||||||
if (daemonStatus !== 'connected' || !localPeer) {
|
if (daemonStatus !== 'connected' || !localPeer) {
|
||||||
return <Onboarding status={daemonStatus} />
|
return <Onboarding status={daemonStatus} />
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user