Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
702bd88
fix(receive): saveTextAsFile returns explicit HTTP status 400/500/200
bethropolis Jul 14, 2026
7000f57
fix(receive): atomic ClaimFile for concurrent upload integrity
bethropolis Jul 14, 2026
4c679bd
fix(metadata): non-destructive EXIF/metadata strip with temp+rename
bethropolis Jul 14, 2026
9e39128
fix(receive): sanitize control characters in filenames (Issue #23)
bethropolis Jul 14, 2026
0f1704c
fix(receive): reject negative file sizes in prepare-upload (Issue #24)
bethropolis Jul 14, 2026
10cb072
fix(receive): CloseSession release mutex before Progress.Wait()
bethropolis Jul 14, 2026
045d35d
fix(send): only strip image files in private mode (non-image regression)
bethropolis Jul 14, 2026
8a3daa1
fix(receive): reject empty filenames after sanitize, strengthen test
bethropolis Jul 14, 2026
258dcac
fix(clipboard): capture stderr in Read(), treat empty clipboard as em…
bethropolis Jul 15, 2026
ffde0a2
feat(send): send clipboard/stdin as in-memory content (no temp file)
bethropolis Jul 15, 2026
b1351d8
feat(clipboard): send text via Preview field, receiver responds 204
bethropolis Jul 15, 2026
e619e42
fix(test): resolve data race in TestReceiveService_ClaimFile_Concurrent
bethropolis Jul 15, 2026
71b8ed4
feat(config): add env var overrides for shell, clipboard, TLS, notifi…
bethropolis Jul 22, 2026
03fa94d
fix(security): sanitize ANSI escape codes from remote peer data
bethropolis Jul 22, 2026
b116c71
feat(network): add GetInterfaceIPNet and GetUsableSubnetIPs
bethropolis Jul 22, 2026
60e2774
feat(daemon): add serve --daemon and localgo stop
bethropolis Jul 22, 2026
be92836
fix(windows): use filepath.Dir instead of TrimSuffix, use filepath.Jo…
bethropolis Jul 22, 2026
004eace
fix(daemon): move daemon fork to platform-specific files
bethropolis Jul 22, 2026
9017fdc
fix(test): skip read-only dir test on Windows
bethropolis Jul 22, 2026
e479a04
fix(daemon): stop uses liveness polling, guard existing daemon, force…
bethropolis Jul 22, 2026
307d42f
fix(receive): clipboard message detection, fallback, history, and san…
bethropolis Jul 22, 2026
f06b816
fix(clipboard): use cmd.Output() for read, guard whitespace-only conf…
bethropolis Jul 22, 2026
856cc63
fix(network): correct subnet capping threshold and error message
bethropolis Jul 22, 2026
14c84b4
fix(tls): compute and publish fingerprint from custom TLS certificate
bethropolis Jul 22, 2026
30a8fbd
fix(send): count in-memory payloads in --ip progress header
bethropolis Jul 22, 2026
d685050
fix(review): daemon PID lifecycle, clipboard fallback safety, timer r…
bethropolis Jul 22, 2026
ce9394c
fix(exec): replace %-placeholders in exec hook string; fix(stop): han…
bethropolis Jul 22, 2026
ca065eb
feat(send): resolve hostnames and mDNS names in --ip flag
bethropolis Jul 22, 2026
1bfdec7
fix(discovery): update IP/Port/Alias/Protocol on peer rediscovery
bethropolis Jul 22, 2026
d5923d8
feat(network): smart subnet scanning via interface netmask
bethropolis Jul 22, 2026
0a22584
fix(review): guard ANSI clears with TTY check, normalize cross-OS pat…
bethropolis Jul 22, 2026
d4a2fb3
docs(cli): remove duplicated history/version/completion sections
bethropolis Jul 23, 2026
0f8da63
feat(help): add 'config' command to main help and wire SetHelpFunc
bethropolis Jul 23, 2026
792267a
fix(cli): wire --version/-v flag root Run, add version command help
bethropolis Jul 23, 2026
826175d
fix data race in updateDevice: replace in-map pointer instead of muta…
bethropolis Jul 23, 2026
62074ef
fix: exec shell selection, private mode StripTo on Windows, doc comme…
bethropolis Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# Output directories
.localgo_security/
downloads/
scripts/*.log
*.log

# Dependency directories (if vendoring)
vendor/
Expand All @@ -38,7 +38,8 @@ Thumbs.db
coverage.*


/config
/mise.toml
config
test
dist/
.coverage/
Expand Down
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ A Go implementation of the LocalSend v2.1 protocol for secure, cross-platform fi

### Installation

#### Online (macOS, Linux)
#### Quick install (macOS, Linux)
```bash
curl -fsSL https://bethropolis.github.io/localgo/install.sh | bash
```

#### User installation (recommended)
#### User installation
```bash
# clone repo
git clone https://github.com/bethropolis/localgo.git
Expand All @@ -69,6 +69,17 @@ scoop bucket add bethropolis https://github.com/bethropolis/scoop-bucket
scoop install localgo
```

#### using docker / podman
```bash
mkdir -p localgo/downloads localgo/config
docker pull ghcr.io/bethropolis/localgo:latest
docker run -d \
-p 53317:53317 \
-v ./localgo/config:/app/config \
-v ./localgo/downloads:/app/downloads \
ghcr.io/bethropolis/localgo:latest
```

> [!NOTE]
> more install options in [installation documentation](docs/GETTING_STARTED.md)

Expand Down Expand Up @@ -99,7 +110,7 @@ localgo share --file document.pdf

### Docker and Podman

For full details deployment, macvlan networking, read-only root filesystem, watchtower, and more see the [container documentation](docs/CONTAINER.md).
For full details on deployment, macvlan networking, read-only root filesystem, watchtower, and more, see the [container documentation](docs/CONTAINER.md).

## Configuration

Expand All @@ -117,6 +128,17 @@ For full details — deployment, macvlan networking, read-only root filesystem,
| `LOCALSEND_AUTO_ACCEPT` | false | Auto-accept incoming files without prompting |
| `LOCALSEND_NO_CLIPBOARD` | false | Save incoming text as a file instead of clipboard |
| `LOCALSEND_LOG_LEVEL` | info | Log verbosity (debug/info/warn/error) |
| `LOCALSEND_HISTORY` | (auto) | Path to transfer history file |
| `LOCALSEND_EXEC` | — | Shell command to run after each received file |
| `LOCALSEND_QUIET` | false | Minimal output mode |
| `LOCALSEND_CONCURRENCY` | 4 | Max parallel upload workers |
| `LOCALSEND_MULTICAST_INTERFACE` | (all) | Network interface for multicast |
| `LOCALSEND_SHELL` | (auto) | Shell prefix for exec hooks |
| `LOCALSEND_TLS_CERT` | — | Custom TLS certificate path |
| `LOCALSEND_TLS_KEY` | — | Custom TLS private key path |
| `LOCALSEND_NOTIFICATION_CMD` | (auto) | Custom notification command |
| `LOCALSEND_MAX_BODY_SIZE` | 0 | Max request body size (0 = unlimited) |
| `LOCALSEND_SECURITY_DIR` | (auto) | Security context directory |

### Example

Expand All @@ -136,9 +158,12 @@ localgo serve
| `discover` | Find devices via multicast |
| `scan` | Find devices via HTTP scan |
| `send` | Send files to a device |
| `history`| Show file transfer history log |
| `info` | Show device information |
| `devices` | List discovered devices |
| `history` | Show transfer history log |
| `stop` | Stop a running daemon |
| `config` | Manage configuration (get/set/list/path) |
| `version` | Show version information |

Run `localgo help` for more options.

Expand Down Expand Up @@ -180,7 +205,7 @@ Want to build on top of LocalGo or contribute?

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome! Please feel free to submit a Pull Request or report an issue.

## License

Expand Down
9 changes: 8 additions & 1 deletion cmd/localgo/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/bethropolis/localgo/pkg/help"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -90,7 +92,7 @@ var configSetCmd = &cobra.Command{
configPath = os.ExpandEnv("$HOME/.config/localgo/config.yaml")
}

if err := os.MkdirAll(strings.TrimSuffix(configPath, "/config.yaml"), 0700); err != nil {
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}

Expand Down Expand Up @@ -167,5 +169,10 @@ func init() {
configCmd.AddCommand(configSetCmd)
configCmd.AddCommand(configListCmd)
configCmd.AddCommand(configPathCmd)
configCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
if h := help.GetCommandHelp("config"); h != nil {
help.ShowCommandHelp(*h)
}
})
rootCmd.AddCommand(configCmd)
}
57 changes: 57 additions & 0 deletions cmd/localgo/cmd/daemon_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build !windows

package cmd

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
)

func daemonize() error {
// Check if daemon is already running
pidPath, err := pidFilePath()
if err != nil {
return fmt.Errorf("cannot determine pid file path: %w", err)
}
if data, err := os.ReadFile(pidPath); err == nil {
if oldPid, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil {
if p, err := os.FindProcess(oldPid); err == nil && p.Signal(syscall.Signal(0)) == nil {
return fmt.Errorf("daemon already running (PID %d)", oldPid)
}
}
}

var childArgs []string
for _, a := range os.Args[1:] {
if a == "--daemon" || a == "-d" {
continue
}
childArgs = append(childArgs, a)
}
child := exec.Command(os.Args[0], childArgs...)
child.Env = append(os.Environ(), "LOCALGO_DAEMON_CHILD=1")
child.Stdin = nil
child.Stdout = nil
child.Stderr = nil
child.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

if err := child.Start(); err != nil {
return fmt.Errorf("failed to start daemon: %w", err)
}

if err := os.MkdirAll(filepath.Dir(pidPath), 0755); err != nil {
return fmt.Errorf("cannot create pid directory: %w", err)
}
if err := os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", child.Process.Pid)), 0644); err != nil {
return fmt.Errorf("failed to write PID file: %w", err)
}

fmt.Printf("LocalGo daemon started (PID %d)\n", child.Process.Pid)
os.Exit(0)
return nil
}
9 changes: 9 additions & 0 deletions cmd/localgo/cmd/daemon_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build windows

package cmd

import "fmt"

func daemonize() error {
return fmt.Errorf("daemon mode is not supported on Windows; use 'localgo serve' in a terminal or run as a background job")
}
5 changes: 4 additions & 1 deletion cmd/localgo/cmd/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ var discoverCmd = &cobra.Command{
if ipErr == nil && len(localIPs) > 0 {
var scanIps []net.IP
for _, ip := range localIPs {
scanIps = append(scanIps, network.GetSubnetIPs(ip)...)
subnetIPs, err := network.GetUsableSubnetIPsFromIP(ip)
if err == nil {
scanIps = append(scanIps, subnetIPs...)
}
}
registerDto := Cfg.ToRegisterDto()
httpDiscoverer := discovery.NewHTTPDiscovery(nil, registerDto, nil, zap.S())
Expand Down
15 changes: 15 additions & 0 deletions cmd/localgo/cmd/pid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cmd

import (
"os"
"path/filepath"
)

// pidFilePath returns the absolute path to the daemon PID file.
func pidFilePath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(configDir, "localgo", "localgo.pid"), nil
}
18 changes: 17 additions & 1 deletion cmd/localgo/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"os"

"github.com/bethropolis/localgo/pkg/cli"
"github.com/bethropolis/localgo/pkg/clipboard"
"github.com/bethropolis/localgo/pkg/config"
"github.com/bethropolis/localgo/pkg/help"
"github.com/bethropolis/localgo/pkg/logging"
Expand All @@ -29,6 +31,13 @@ var (
var rootCmd = &cobra.Command{
Use: "localgo",
Short: "LocalGo - LocalSend v2.1 Protocol Implementation",
Run: func(cmd *cobra.Command, args []string) {
if versionFlag {
help.ShowVersion(Version, GitCommit, BuildDate)
return
}
cmd.Help()
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if versionFlag {
help.ShowVersion(Version, GitCommit, BuildDate)
Expand Down Expand Up @@ -62,6 +71,13 @@ var rootCmd = &cobra.Command{
Cfg.Private = true
}

if Cfg.ClipboardWriteCmd != "" || Cfg.ClipboardReadCmd != "" {
clipboard.OverrideProvider(Cfg.ClipboardWriteCmd, Cfg.ClipboardReadCmd)
}
if Cfg.NotificationCmd != "" {
cli.SetNotificationCmd(Cfg.NotificationCmd)
}

return nil
},
}
Expand All @@ -73,7 +89,7 @@ func Execute() {
}

func init() {
rootCmd.PersistentFlags().BoolVar(&versionFlag, "version", false, "Show version information")
rootCmd.PersistentFlags().BoolVarP(&versionFlag, "version", "v", false, "Show version information")
rootCmd.PersistentFlags().BoolVarP(&privateMode, "private", "p", false, "Hide device identity (alias, model) during discovery and transfer")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/localgo/config.yaml)")
rootCmd.PersistentFlags().BoolVar(&Verbose, "verbose", false, "Enable debug logging")
Expand Down
6 changes: 4 additions & 2 deletions cmd/localgo/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ var scanCmd = &cobra.Command{
}

for _, ip := range localIPs {
subnetIPs := network.GetSubnetIPs(ip)
ips = append(ips, subnetIPs...)
subnetIPs, err := network.GetUsableSubnetIPsFromIP(ip)
if err == nil {
ips = append(ips, subnetIPs...)
}
}

if !scanquiet {
Expand Down
Loading
Loading