cdrop — 跨 OS 剪贴板与文件传输服务
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//go:build darwin
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// launchAgentLabel is the reverse-DNS label + plist filename for the per-user
|
||||
// LaunchAgent that starts cdrop at login.
|
||||
const launchAgentLabel = "net.commilitia.cdrop"
|
||||
|
||||
func launchAgentPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, "Library", "LaunchAgents", launchAgentLabel+".plist"), nil
|
||||
}
|
||||
|
||||
// launchTarget returns the program the LaunchAgent should run. For a packaged
|
||||
// .app it launches the bundle via `open` (proper LaunchServices activation);
|
||||
// in a bare dev binary it runs the executable directly. Either way it appends
|
||||
// --hidden so a login launch comes up to the menu bar with no window (see
|
||||
// launchedHidden in main); `open --args` forwards the rest as the app's argv.
|
||||
func launchTarget() ([]string, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = resolved
|
||||
}
|
||||
if bundle := appBundlePath(exe); bundle != "" {
|
||||
return []string{"/usr/bin/open", bundle, "--args", "--hidden"}, nil
|
||||
}
|
||||
return []string{exe, "--hidden"}, nil
|
||||
}
|
||||
|
||||
// appBundlePath returns the enclosing .app path if exe lives inside one
|
||||
// (…/Foo.app/Contents/MacOS/binary), else "".
|
||||
func appBundlePath(exe string) string {
|
||||
const marker = ".app/Contents/MacOS/"
|
||||
if i := strings.LastIndex(exe, marker); i != -1 {
|
||||
return exe[:i+len(".app")]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetLaunchAtLogin installs or removes the LaunchAgent plist. Removing a missing
|
||||
// agent is a no-op success.
|
||||
func SetLaunchAtLogin(enabled bool) error {
|
||||
p, err := launchAgentPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !enabled {
|
||||
if err := os.Remove(p); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
args, err := launchTarget()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(p, []byte(buildLaunchAgentPlist(launchAgentLabel, args)), 0o644)
|
||||
}
|
||||
|
||||
// IsLaunchAtLoginEnabled reports whether the LaunchAgent plist is present.
|
||||
func IsLaunchAtLoginEnabled() bool {
|
||||
p, err := launchAgentPath()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(p)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func buildLaunchAgentPlist(label string, programArgs []string) string {
|
||||
var args strings.Builder
|
||||
for _, a := range programArgs {
|
||||
fmt.Fprintf(&args, "\n <string>%s</string>", xmlEscape(a))
|
||||
}
|
||||
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>%s</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>%s
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
`, xmlEscape(label), args.String())
|
||||
}
|
||||
|
||||
func xmlEscape(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
"&", "&",
|
||||
"<", "<",
|
||||
">", ">",
|
||||
`"`, """,
|
||||
"'", "'",
|
||||
)
|
||||
return r.Replace(s)
|
||||
}
|
||||
Reference in New Issue
Block a user