Files
waste-go/internal/mesh/transfer.go

515 lines
13 KiB
Go
Raw Normal View History

package mesh
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hash"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync"
"github.com/pion/webrtc/v3"
"github.com/waste-go/internal/proto"
)
const (
fileChunkSize = 64 * 1024 // 64 KiB per chunk
fileBufferHighWater = 512 * 1024 // pause sending above this
fileBufferLowWater = 256 * 1024 // resume when it drops here
)
type outboundTransfer struct {
peerID proto.PeerID
path string
size int64
sha256 string
}
type inboundTransfer struct {
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>
2026-06-28 21:38:28 +02:00
from proto.PeerID
name string
size int64
sha256 string
mu sync.Mutex
tmp *os.File
hasher hash.Hash
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
}
// ScanResumable scans the download directory for .tmp.meta sidecars left by
// interrupted transfers and emits a resumable_transfers IPC event listing them.
// Called once after a network is joined so the UI can show pending transfers.
func (m *Mesh) ScanResumable() {
if m.DownloadDir == "" {
return
}
metas, _ := filepath.Glob(filepath.Join(m.DownloadDir, "*.tmp.meta"))
var files []proto.ResumableFile
for _, mp := range metas {
data, err := os.ReadFile(mp)
if err != nil {
continue
}
var meta partialMeta
if err := json.Unmarshal(data, &meta); err != nil {
continue
}
tp := strings.TrimSuffix(mp, ".meta")
info, err := os.Stat(tp)
if err != nil {
continue
}
files = append(files, proto.ResumableFile{
Name: meta.Name,
SHA256: meta.SHA256,
From: meta.From,
Size: meta.Size,
Offset: info.Size(),
})
}
if len(files) == 0 {
return
}
m.emit(proto.IpcMessage{
Type: proto.EvtResumableTransfers,
ResumableFiles: files,
})
log.Printf("transfer: %d resumable transfer(s) found in %s", len(files), m.DownloadDir)
}
// OfferFile reads filename from ShareDir, computes its SHA-256, and sends a
// file-offer to peerID over the existing "yaw" DataChannel.
func (m *Mesh) OfferFile(peerID proto.PeerID, filename string) error {
if m.ShareDir == "" {
return fmt.Errorf("no share directory configured")
}
if strings.ContainsAny(filename, "/\\") || filename == ".." {
return fmt.Errorf("invalid filename %q", filename)
}
path := filepath.Join(m.ShareDir, filename)
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %q: %w", filename, err)
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return err
}
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return fmt.Errorf("hash %q: %w", filename, err)
}
shaHex := hex.EncodeToString(h.Sum(nil))
xid := newXid()
m.transferMu.Lock()
m.outbound[xid] = &outboundTransfer{
peerID: peerID,
path: path,
size: info.Size(),
sha256: shaHex,
}
m.transferMu.Unlock()
wire, err := json.Marshal(proto.PeerMessage{
Type: proto.MsgFileOffer,
Xid: xid,
Name: filename,
Size: info.Size(),
SHA256: shaHex,
})
if err != nil {
return err
}
if !m.SendTo(peerID, wire) {
m.transferMu.Lock()
delete(m.outbound, xid)
m.transferMu.Unlock()
return fmt.Errorf("peer %s not connected", peerID.Short())
}
log.Printf("transfer: offered %s (%d bytes) to %s xid=%s", filename, info.Size(), peerID.Short(), xid[:8])
return nil
}
// acceptIncoming is called when we receive file-offer from a peer.
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>
2026-06-28 21:38:28 +02:00
// 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) {
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>
2026-06-28 21:38:28 +02:00
t := &inboundTransfer{
from: from,
name: msg.Name,
size: msg.Size,
sha256: msg.SHA256,
}
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>
2026-06-28 21:38:28 +02:00
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()
accept, _ := json.Marshal(proto.PeerMessage{
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>
2026-06-28 21:38:28 +02:00
Type: proto.MsgFileAccept,
Xid: msg.Xid,
ResumeOffset: resumeOffset,
})
m.SendTo(from, accept)
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>
2026-06-28 21:38:28 +02:00
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])
}
}
// startSend opens the binary DataChannel "f:<xid>" and streams the file.
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>
2026-06-28 21:38:28 +02:00
// Called when we receive file-accept from the peer. resumeOffset is non-zero
// when the receiver is resuming a previous partial download.
func (m *Mesh) startSend(xid string, from proto.PeerID, resumeOffset int64) {
m.transferMu.Lock()
t, ok := m.outbound[xid]
m.transferMu.Unlock()
if !ok {
log.Printf("transfer: file-accept for unknown xid %s", xid[:8])
return
}
if t.peerID != from {
log.Printf("transfer: file-accept from unexpected peer %s", from.Short())
return
}
m.mu.RLock()
conn, ok := m.peers[from]
m.mu.RUnlock()
if !ok {
log.Printf("transfer: peer %s not connected for xid %s", from.Short(), xid[:8])
return
}
ordered := true
dc, err := conn.PC.CreateDataChannel("f:"+xid, &webrtc.DataChannelInit{Ordered: &ordered})
if err != nil {
log.Printf("transfer: create file DC xid=%s: %v", xid[:8], err)
return
}
// Wait for receiver to signal "ok" before sending — this ensures the receiver
// has its OnMessage/OnClose handlers registered before any data arrives.
dc.OnOpen(func() {
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
if msg.IsString && string(msg.Data) == "ok" {
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>
2026-06-28 21:38:28 +02:00
go m.sendFileChunks(dc, t, xid, resumeOffset)
}
})
})
}
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>
2026-06-28 21:38:28 +02:00
func (m *Mesh) sendFileChunks(dc *webrtc.DataChannel, t *outboundTransfer, xid string, offset int64) {
defer func() {
m.transferMu.Lock()
delete(m.outbound, xid)
m.transferMu.Unlock()
}()
f, err := os.Open(t.path)
if err != nil {
log.Printf("transfer: open file xid=%s: %v", xid[:8], err)
dc.Close()
return
}
defer f.Close()
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>
2026-06-28 21:38:28 +02:00
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.
resume := make(chan struct{}, 1)
dc.SetBufferedAmountLowThreshold(fileBufferLowWater)
dc.OnBufferedAmountLow(func() {
select {
case resume <- struct{}{}:
default:
}
})
buf := make([]byte, fileChunkSize)
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>
2026-06-28 21:38:28 +02:00
sent := offset // start progress reporting from where the receiver left off
for {
n, readErr := f.Read(buf)
if n > 0 {
for dc.BufferedAmount() > fileBufferHighWater {
<-resume
}
if err := dc.Send(buf[:n]); err != nil {
log.Printf("transfer: send chunk xid=%s: %v", xid[:8], err)
return
}
sent += int64(n)
m.Emit(proto.IpcMessage{
Type: proto.EvtFileProgress,
TransferID: xid,
BytesReceived: sent,
TotalBytes: t.size,
})
}
if readErr == io.EOF {
break
}
if readErr != nil {
log.Printf("transfer: read file xid=%s: %v", xid[:8], readErr)
return
}
}
dc.Close()
done, _ := json.Marshal(proto.PeerMessage{
Type: proto.MsgFileDone,
Xid: xid,
SHA256: t.sha256,
})
m.SendTo(t.peerID, done)
log.Printf("transfer: sent %s (%d bytes) xid=%s", filepath.Base(t.path), sent, xid[:8])
}
// HandleInboundFileDC is called from the anchor when a "f:<xid>" DataChannel
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>
2026-06-28 21:38:28 +02:00
// arrives on a PeerConnection. It writes chunks to a temp file (or resumes an
// 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) {
m.transferMu.Lock()
t, ok := m.inbound[xid]
m.transferMu.Unlock()
if !ok {
log.Printf("transfer: no pending inbound for xid %s", xid[:8])
dc.Close()
return
}
var once sync.Once
doOpen := func() {
if err := os.MkdirAll(m.DownloadDir, 0o755); err != nil {
log.Printf("transfer: mkdir %s: %v", m.DownloadDir, err)
return
}
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>
2026-06-28 21:38:28 +02:00
t.mu.Lock()
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>
2026-06-28 21:38:28 +02:00
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")
if err != nil {
log.Printf("transfer: create temp file: %v", err)
return
}
t.tmp = tmp
t.hasher = sha256.New()
t.metaPath = tmp.Name() + ".meta"
writePartialMeta(t.metaPath, t)
log.Printf("transfer: receiving %s xid=%s", t.name, xid[:8])
}
// Signal sender that we are ready — it will not start streaming until
// it receives this, guaranteeing our OnMessage/OnClose are registered first.
dc.SendText("ok") //nolint:errcheck
}
// Register OnMessage first so pion queues any early binary chunks.
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
if msg.IsString {
return // "ok" echo or any other text; ignore
}
t.mu.Lock()
tmp, h := t.tmp, t.hasher
t.mu.Unlock()
if tmp == nil {
return
}
if _, err := tmp.Write(msg.Data); err != nil {
log.Printf("transfer: write xid=%s: %v", xid[:8], err)
return
}
h.Write(msg.Data)
t.mu.Lock()
t.written += int64(len(msg.Data))
written := t.written
t.mu.Unlock()
m.Emit(proto.IpcMessage{
Type: proto.EvtFileProgress,
TransferID: xid,
BytesReceived: written,
TotalBytes: t.size,
})
})
dc.OnOpen(func() { once.Do(doOpen) })
if dc.ReadyState() == webrtc.DataChannelStateOpen {
once.Do(doOpen)
}
dc.OnClose(func() {
t.mu.Lock()
tmp := t.tmp
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>
2026-06-28 21:38:28 +02:00
metaPath := t.metaPath
actualSha := ""
if t.hasher != nil {
actualSha = hex.EncodeToString(t.hasher.Sum(nil))
}
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>
2026-06-28 21:38:28 +02:00
name, expectedSha, size, written := t.name, t.sha256, t.size, t.written
t.mu.Unlock()
m.transferMu.Lock()
delete(m.inbound, xid)
m.transferMu.Unlock()
if tmp == nil {
return
}
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>
2026-06-28 21:38:28 +02:00
tmpName := tmp.Name()
tmp.Close()
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>
2026-06-28 21:38:28 +02:00
// 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 {
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>
2026-06-28 21:38:28 +02:00
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])
m.Emit(proto.IpcMessage{
Type: proto.EvtError,
TransferID: xid,
ErrorMessage: fmt.Sprintf("file %s: sha256 mismatch", name),
})
return
}
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>
2026-06-28 21:38:28 +02:00
// Success — remove sidecar and move to final path.
if metaPath != "" {
os.Remove(metaPath)
}
final := filepath.Join(m.DownloadDir, name)
if _, err := os.Stat(final); err == nil {
ext := filepath.Ext(name)
base := strings.TrimSuffix(name, ext)
final = filepath.Join(m.DownloadDir, base+"-"+xid[:8]+ext)
}
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>
2026-06-28 21:38:28 +02:00
if err := os.Rename(tmpName, final); err != nil {
log.Printf("transfer: rename xid=%s: %v", xid[:8], err)
return
}
log.Printf("transfer: saved %s -> %s", name, final)
m.Emit(proto.IpcMessage{
Type: proto.EvtFileComplete,
TransferID: xid,
Path: final,
})
})
}
func newXid() string {
b := make([]byte, 16)
rand.Read(b) //nolint:errcheck
return hex.EncodeToString(b)
}