Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
107 changes: 107 additions & 0 deletions .agents/skills/localshare-server-deploy/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
name: localshare-server-deploy
description: 在 Linux 主机上安装、配置、升级或排查自托管 LocalShare Server,并验证 systemd、健康检查和一次性配对 key。用于用户要求部署 LocalShare 远程分享 Server;不用于 macOS Client 或纯局域网分享。
license: MIT
metadata:
author: rrbe
version: "1.0.0"
---

# LocalShare Server 部署

部署一个可持久运行的 `localshare-server`,验证公开地址可用,并为 LocalShare Client 生成一次性 Enrollment Key。Server 只转发请求与文件流,不保存 Mac 上的分享内容。

目标环境是使用 systemd 的 Linux,需具备 `curl`、sudo 或 root 权限,以及访问 GitHub Releases 的出站 HTTPS 网络。

## 确认部署边界

- 只操作用户明确指定的主机。用户说“当前服务器”时操作当前 Linux 主机;未指定远程目标时不要猜测 SSH 主机。
- 开始写入前确定公开基地址、监听端口,以及使用 HTTP 还是 Server 直接终止 TLS。公开基地址不能带额外 path 或 query。
- HTTPS 需要域名匹配的证书和私钥路径。不要自行申请证书、配置反向代理或修改 DNS,除非用户明确要求。
- 修改防火墙、覆盖已有 systemd unit、替换已有二进制或调整已有 TLS 文件前,先展示检测结果并取得确认。
- Enrollment Key、Device Token 和 Share Token 不得写入公开日志或代码仓库。

## 只读预检

1. 检查 `uname -s` 为 Linux,并确认 `uname -m`:`x86_64` 映射为 `amd64`,`aarch64` 或 `arm64` 映射为 `arm64`。其他架构停止并报告不支持。
2. 确认 `systemctl`、`curl`、`sha256sum`、`sudo` 或 root 权限可用。
3. 检查目标端口是否被占用,并读取现有的 `localshare-server.service`、`/usr/local/bin/localshare-server` 和 `/var/lib/localshare` 状态。
4. 如果发现已有部署,保留 `/var/lib/localshare/state.json`;在用户确认升级或修复方案之前不要覆盖文件。

## 下载并校验 Release

从 `https://github.com/rrbe/LocalShare/releases/latest` 解析最新 `vX.Y.Z` tag,去掉 `v` 后得到版本号。下载:

```text
LocalShare-Server-<version>-linux-<arch>
LocalShare-Server-<version>-checksums.txt
```

在 `mktemp -d` 创建的临时目录中下载文件,用 checksums 文件和 `sha256sum -c` 校验目标二进制。只有校验通过后,才使用 `sudo install -m 0755` 安装到 `/usr/local/bin/localshare-server`。

如果最新 Release 没有对应 Server 资产,停止并说明情况;可以询问用户是否改为从源码构建,但不要静默下载未经发布的构建。

## 创建运行账户和状态目录

如不存在,创建系统用户:

```bash
sudo useradd --system --home /var/lib/localshare --shell /usr/sbin/nologin localshare
sudo install -d -o localshare -g localshare -m 0700 /var/lib/localshare
```

状态目录必须持久化。不得删除、清空或重新生成已有 `state.json`。保持状态目录权限为 `0700`;Server 会把状态文件和锁文件权限收敛为 `0600`。
Server 默认使用 `/var/lib/localshare`;只有用户明确选择其他目录时才追加 `--state-dir`,并同步调整 `ReadWritePaths`。

## 配置 systemd

使用 `/etc/systemd/system/localshare-server.service`。基础 `ExecStart` 为:

```text
/usr/local/bin/localshare-server serve --listen :<port> --public-url <public-base-url>
```

Server 直接终止 TLS 时追加 `--tls-cert <certificate-path> --tls-key <private-key-path>`,并确认 `localshare` 用户可以读取这两个文件。

Service 至少使用:

```ini
[Unit]
Description=LocalShare Server
After=network-online.target
Wants=network-online.target

[Service]
User=localshare
Group=localshare
ExecStart=<resolved command>
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/localshare

[Install]
WantedBy=multi-user.target
```

写入后运行 `systemctl daemon-reload` 和 `systemctl enable --now localshare-server`。

## 验证并生成配对 key

1. 确认 `systemctl is-active localshare-server` 返回 `active`。
2. 请求 `<public-base-url>/healthz`,必须返回 HTTP 200 和包含 `{"status":"ok"}` 的 JSON。
3. 如果失败,查看 `journalctl -u localshare-server --no-pager -n 100`,修复真实原因后重新验证;不要在健康检查失败时生成 key 或宣布完成。
4. 健康检查通过后运行:

```bash
sudo -u localshare /usr/local/bin/localshare-server key create \
--name <device-name>
```

5. 把一次性 Enrollment Key 只返回给用户,并提醒在 macOS Client 的“设置 → 远程访问”中填写公开地址和 key。

## 交付结果

报告已安装版本和架构、公开地址、监听端口、TLS 模式、systemd 状态、健康检查结果、状态目录,以及 Enrollment Key。明确列出任何没有执行的防火墙、DNS、证书或外部连通性工作。
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ on:
pull_request:

jobs:
server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: '1.23'
- name: 测试 LocalShare Server
working-directory: server
run: |
go test -race ./...
go vet ./...

smoke:
runs-on: macos-latest # 与 release 同款 runner,自带 Xcode / Swift 工具链与 Node
steps:
Expand Down
24 changes: 23 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ jobs:
}
echo "VERSION=${RELEASE_TAG#v}" >> "$GITHUB_ENV"

- uses: actions/setup-go@v7
if: ${{ hashFiles('server/go.mod') != '' }}
with:
go-version-file: server/go.mod

- name: 编译 LocalShare Server(Linux amd64 / arm64)
if: ${{ hashFiles('server/go.mod') != '' }}
working-directory: server
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags "-s -w" -o "../LocalShare-Server-$VERSION-linux-amd64" .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -ldflags "-s -w" -o "../LocalShare-Server-$VERSION-linux-arm64" .
cd ..
shasum -a 256 LocalShare-Server-* > "LocalShare-Server-$VERSION-checksums.txt"

- name: 写入版本号到 Info.plist
run: |
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" bundle/Info.plist
Expand Down Expand Up @@ -94,6 +110,9 @@ jobs:
CHANGELOG="$(git tag -l --format='%(contents)' "$RELEASE_TAG")"
fi
printf '**LocalShare %s** · 局域网文件 / 文件夹只读分享,扫码即看。\nUniversal(Apple Silicon + Intel),要求 macOS 13+。\n' "$VERSION" > release_body.md
if [ -f "LocalShare-Server-$VERSION-linux-amd64" ]; then
printf '\n远程分享 Server 同时提供 Linux amd64 / arm64 单文件二进制及 SHA-256 校验文件。\n' >> release_body.md
fi
if [ -n "$CHANGELOG" ]; then
printf '\n## 更新内容\n\n%s\n' "$CHANGELOG" >> release_body.md
fi
Expand All @@ -115,7 +134,10 @@ jobs:
with:
tag_name: ${{ env.RELEASE_TAG }}
name: LocalShare ${{ env.VERSION }}
files: LocalShare-${{ env.VERSION }}.dmg
files: |
LocalShare-${{ env.VERSION }}.dmg
LocalShare-Server-${{ env.VERSION }}-linux-*
LocalShare-Server-${{ env.VERSION }}-checksums.txt
body_path: release_body.md

- name: EdDSA 签名 DMG + 生成 appcast.xml 挂到 Release
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.build/
.swiftpm/
/server/localshare-server
dist/
*.app
.DS_Store
Expand Down
193 changes: 193 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# LocalShare Server

LocalShare Server 是远程分享的控制面和数据中继。它只转发请求与文件流,不保存 Mac 上的分享内容。Server 使用一个端口,要求 Go 1.23+。

## 一、下载或编译

GitHub Release 提供 Linux amd64 / arm64 单文件二进制和 SHA-256 校验文件。下载与服务器架构匹配的
`LocalShare-Server-<version>-linux-<arch>` 后安装:

```bash
chmod +x LocalShare-Server-*-linux-*
sudo install -m 0755 LocalShare-Server-*-linux-* /usr/local/bin/localshare-server
```

也可以在仓库根目录从源码编译:

```bash
cd server
go test ./...
go test -race ./...
go build -trimpath -ldflags "-s -w" -o localshare-server .
```

如果部署到 Linux,可以在 macOS 上交叉编译:

```bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags "-s -w" -o localshare-server .
```

把生成的 `localshare-server` 复制到目标服务器即可,不需要安装 SSH/frp/Nginx 或其他 LocalShare 运行时组件。

## 二、本机快速测试

打开两个终端。

终端 A:生成测试目录和一次性配对 key:

```bash
cd server
state_dir=$(mktemp -d)
./localshare-server key create --name test-mac --state-dir "$state_dir"
```

复制命令输出的 `key`。它只允许成功配对一次。

终端 A 继续启动 Server:

```bash
./localshare-server serve \
--listen 127.0.0.1:18080 \
--public-url http://127.0.0.1:18080 \
--state-dir "$state_dir"
```

终端 B 检查 Server:

```bash
curl http://127.0.0.1:18080/healthz
```

预期返回包含:

```json
{"status":"ok"}
```

这里的 `127.0.0.1` 只适合 Server 和 LocalShare Client 在同一台 Mac 上测试。若 Client 在另一台 Mac,Server 必须监听可访问地址,例如 `--listen :18080`,并使用服务器局域网地址作为 Client 的 Server address。

## 三、配置 LocalShare Client

1. 编译并启动 macOS Client:

```bash
cd ..
swift build
.build/debug/LocalShare
```

或使用 `./build.sh` 生成,再执行 `open dist/LocalShare.app`。

2. 在 Client 中选择一个文件或文件夹,让本地分享先运行。
3. 打开「设置 → 远程访问」:

- `Server address`:填写 `http://127.0.0.1:18080`,或远程服务器的可访问地址;
- `Enrollment key`:填写终端 A 生成的 `key`。

4. 点击保存,回到分享票据,点击 `Connect`。
5. 配对成功后,Client 会把 Device Token 保存到 macOS Keychain;Enrollment Key 不会持久化。
6. 连接成功后,分享票据上会显示 Server 生成的远程访问地址。用浏览器打开这个地址即可测试文件浏览和下载。

之后再次使用只需要点击 `Connect`,不需要重新填写 key。`Disconnect` 只断开当前长连接,`Forget Device` 才会删除 Keychain 中的 Device Token。

Client 重启后不会自动公开分享;选择文件并点击 `Connect` 即可使用已经配对的设备凭据重新连接。

## 四、Linux 服务器部署

生产部署只需要持久化二进制、状态目录和一个监听端口:

```bash
sudo install -m 0755 localshare-server /usr/local/bin/localshare-server
sudo useradd --system --home /var/lib/localshare --shell /usr/sbin/nologin localshare
sudo install -d -o localshare -g localshare -m 0700 /var/lib/localshare
```

Server 默认使用 `/var/lib/localshare`;只有使用其他目录时才需要传 `--state-dir`。

先生成配对 key:

```bash
sudo -u localshare /usr/local/bin/localshare-server key create \
--name macbook
```

然后启动:

```bash
sudo -u localshare /usr/local/bin/localshare-server serve \
--listen :18080 \
--public-url http://server.example:18080
```

`--public-url` 必须是浏览器和 Client 使用的 Server 基地址,不能带额外路径或 query。`state-dir` 必须持久化;删除其中的 `state.json` 会使已配对设备全部失效。

如果使用 HTTPS,Server 可以直接终止 TLS:

```bash
sudo -u localshare /usr/local/bin/localshare-server serve \
--listen :18443 \
--public-url https://ls.example.com \
--tls-cert /etc/localshare/server.crt \
--tls-key /etc/localshare/server.key
```

`https` 的 `--public-url` 必须同时提供 `--tls-cert` 和 `--tls-key`;证书的域名必须匹配 Server address。HTTP 配置不应传入 TLS 参数。

如使用 systemd,可创建 `/etc/systemd/system/localshare-server.service`:

```ini
[Unit]
Description=LocalShare Server
After=network-online.target
Wants=network-online.target

[Service]
User=localshare
Group=localshare
ExecStart=/usr/local/bin/localshare-server serve --listen :18080 --public-url http://server.example:18080
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/localshare

[Install]
WantedBy=multi-user.target
```

启动并检查:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now localshare-server
curl http://server.example:18080/healthz
sudo journalctl -u localshare-server -f
```

防火墙只需要允许 Server 的这个端口。Client 不需要把 Mac 的 FileServer 端口暴露给外部;Client 会主动向 Server 建立 WebSocket 长连接。

## 五、管理命令

```bash
sudo -u localshare /usr/local/bin/localshare-server key list
sudo -u localshare /usr/local/bin/localshare-server key revoke <key-id>
sudo -u localshare /usr/local/bin/localshare-server device list
sudo -u localshare /usr/local/bin/localshare-server device revoke <device-id>
```

Server 只在 `state.json` 中保存 Enrollment Key 和 Device Token 的 SHA-256,不保存明文 token。Enrollment Key、Device Token 和 Share Token 都不应写入公开日志或提交到代码仓库。
运行中的 Server 与管理 CLI 通过状态目录里的 `.state.lock` 串行化修改,避免创建 Key、配对和撤销并发时丢更新;目录权限会收敛为 `0700`,`state.json` 与锁文件为 `0600`。

## 六、排查顺序

1. `curl <server-address>/healthz` 是否返回 200。
2. Server address 是否为基地址,例如 `http://server.example:18080`,没有额外 path/query。
3. Enrollment Key 是否已经被使用;已使用时重新运行 `key create`。
4. `device list` 是否能看到配对设备。
5. Client 是否已经选择文件并点击 `Connect`;空分享时 Connect 会被禁用。
6. Client 状态是否为 `Connected`,并且分享票据是否出现远程 URL。
7. 远程 URL 是否仍然对应当前连接;Disconnect、换分享内容或 Server 重启后旧 Share Token 都会失效。

完整架构、消息协议和取舍见 [`docs/REMOTE_SHARING_PLAN.md`](../docs/REMOTE_SHARING_PLAN.md)。
5 changes: 5 additions & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/rrbe/LocalShare/server

go 1.23

require github.com/coder/websocket v1.8.15
2 changes: 2 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
Loading
Loading