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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ AppIcon.iconset/
private_key.txt
# 编辑器临时文件
*.swp

.claude/
.codex/
115 changes: 115 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
## Project

LocalShare is a native single-window macOS app written in Swift / SwiftUI. A user chooses files or folders, the window shows a QR code, and phones on the same Wi-Fi can browse the share read-only in a browser. Guest upload and Text Transfer are optional; the default is read-only.

The central engineering rule is **no package-external runtime dylibs**. System frameworks are fine. Pure Swift dependencies such as Swifter are built from SPM source into the binary. Binary frameworks, currently Sparkle, are allowed only when referenced via `@rpath`, bundled inside `.app/Contents/Frameworks/`, signed with the app, and verified by the dependency gate. Absolute package-external dylib paths such as `/opt/homebrew` and `/usr/local` are forbidden.

Related documents:

- Architecture: `docs/ARCHITECTURE.md`
- Visual design system: `DESIGN.md`

## Common Commands

```bash
swift build # Debug build; first run fetches Swifter
swift build -c release # Release build
swift test # XCTest unit tests for pure functions
./build.sh # Release build -> assemble .app -> ad-hoc sign -> dist/LocalShare.app
open "dist/LocalShare.app" # Local GUI smoke test

# Headless end-to-end test for server behavior
LS_HEADLESS=1 LS_FOLDER=/path/to/dir LS_TOKEN=testtoken LS_PORT=8099 .build/debug/LocalShare &
curl -s "http://127.0.0.1:8099/?t=testtoken"

# CLI paths; after CLI/HeadlessServer changes, also retest release because of the documented -O compiler trap
.build/debug/LocalShare --headless /path/a.html /path/dir
.build/debug/LocalShare a.html b.pdf
ln -s "$PWD/dist/LocalShare.app/Contents/MacOS/LocalShare" /tmp/localshare
/tmp/localshare --version

# Dependency rule check: after filtering system libraries, all remaining deps must be bundled @rpath frameworks
otool -L "dist/LocalShare.app/Contents/MacOS/LocalShare" | grep -v "/usr/lib/\|/System/Library/"
# Expected: only @rpath/Sparkle.framework/...; Sparkle.framework must exist in dist/LocalShare.app/Contents/Frameworks
```

Tests have two layers:

- `swift test`: XCTest coverage for pure functions such as `resolveWithinRoot`, `sanitizeFileName`, `Share.makeItems`, `availableURL`, i18n, and text handling.
- `tools/smoke-*.sh` / `smoke-*.cjs`: headless curl smoke tests for token auth and 302 cleanup, traversal blocking, single-file isolation, Chinese/space/percent filename decoding, multi-share virtual roots, upload defanging, interface binding, Markdown link sanitation, Accept-Language routing, and Text Transfer.

CI runs both layers on every PR and `master` push. `release.yml` only handles releases. Requirements: macOS 13+ and a Swift toolchain.

## Commit Messages

Use Conventional Commits and preferably write messages in English:

```text
<type>: <summary>
```

Allowed types: `feat` / `fix` / `refactor` / `chore` / `docs` / `test` / `perf` / `style`.

Use an imperative, lowercase summary under 72 characters, for example:

```text
fix: prevent stale token from reading new share
```

## Architecture Notes

This section is only a navigation map. Full request flow, XSS hardening, lifecycle, CLI, and headless details live in `docs/ARCHITECTURE.md`.

`App.swift` uses `@main enum EntryPoint` with three dispatch layers:

1. `LS_HEADLESS=1` runs `HeadlessServer`.
2. `CLI.parse` handles argv for `localshare <paths>...` and `--headless`.
3. Everything else starts `LocalShareApp`.

All three paths share one `FileServer`, which keeps request logic unified. GUI CLI forwarding uses `NSWorkspace.open(urls, withApplicationAt:)`; `AppDelegate.application(_:open:)` receives paths and hot-swaps `AppState.setShared`. If open events arrive before `AppState` exists, they are buffered in `pendingOpenURLs`.

`AppState` is the `@MainActor ObservableObject` and single source of truth. It owns `FileServer`, network candidates, and derived state such as `primaryURL` and `qrImage`. File shares are represented by `sharedItems: [URL]`: empty, single, or multiple. Cold launch does not automatically replay the previous share; Recent Shares offers manual restart. Closing the window does not quit, so the process and server keep running.

`FileServer` handles all requests through one Swifter middleware closure. It supports:

- token auth through `?t=` or cookie `ls_token`; browser navigations authenticated by query receive a cookie and 302 to a clean URL
- immediate token rotation on `setShared` / `stop`, invalidating old links and cookies
- online presence tracking by client IP with best-effort device-name lookup
- single-file, single-folder, and multi-share virtual-root routing
- traversal protection using decoded paths, standardized file URLs, and resolved symlinks
- directory listing, `index.html` serving, and 64 KB chunked file streaming
- Markdown / JSON / CSV preview shells at the same URL as the original file, with `?raw=1` and curl returning raw content
- guest upload for single-folder shares, guarded by traversal checks, filename sanitation, collision suffixing, atomic rename, 500 MB limit, executable-document defanging, `nosniff`, and quarantine metadata
- optional binding to the selected IPv4 interface when "current network only" is enabled

Utility modules have narrow responsibilities: `NetworkInfo`, `QRCode`, `Token`, `Mime`, `DirectoryListing`, `PreviewPage`, `MarkdownViewer`, `JsonViewer`, `CsvViewer`, `MarkedJS`, `Lang`, `L`, and `LStr`. i18n strings are compiled into the binary; the native app follows `AppState.langPref`, while the web UI is selected per request from `Accept-Language`.

`Updater.swift` wraps Sparkle. `UpdaterController` is constructed only by the GUI path; headless paths never touch Sparkle. Configuration lives in `bundle/Info.plist`. If `SUPublicEDKey` is still the placeholder, the updater does not start.

## Release and Versioning

The version is defined by git tags: `vX.Y.Z` on `master` tip. `bundle/Info.plist` contains placeholder values; CI overwrites `CFBundleShortVersionString` from the tag and `CFBundleVersion` from the run number. Do not edit `Info.plist` just to release a version.

When updating changelogs, update both `CHANGELOG.md` and `CHANGELOG_CN.md` in the same change so the English and Chinese histories stay aligned.

Release flow:

1. Merge all changes to `master`.
2. Write changelog bullets into an annotated tag note: `git tag -a vX.Y.Z -F notes.md`.
3. Push the tag.
4. `.github/workflows/release.yml` builds universal binaries, runs the dependency gate, creates the DMG, creates the GitHub Release, signs the DMG for Sparkle, and uploads `appcast.xml` as a Release asset.

`appcast.xml` is not committed. Sparkle feed URL is `https://github.com/rrbe/LocalShare/releases/latest/download/appcast.xml`, which always points to the latest release asset. Release notes should be user-facing `-` bullets in English, with no `#` heading because `git tag` treats those as comments.

## Cross-File Constraints

- **No package-external dylibs**: after adding or changing dependencies, run the `otool` check and confirm bundled frameworks exist in `Contents/Frameworks`.
- **Swifter 1.5.0 path double-encoding bug**: `req.path` still contains one percent-encoded layer before filesystem resolution. Decode with `removingPercentEncoding`; this is required for spaces, Chinese filenames, and encoded traversal attempts.
- **Traversal guard**: after joining paths, use `standardizedFileURL.resolvingSymlinksInPath`; the result must be equal to `rootPath` or have `rootPath + "/"` as prefix. Retest `../`, `%2e%2e`, and `..%2f` after changing this area.
- **Threading model**: Swifter callbacks run on socket threads, while `AppState` is `@MainActor`. Shared mutable server state is protected by one `NSLock`: `share`, `token`, `uploadEnabled`, `lastSeen`, `nameCache`, and `nameLookupInFlight`.
- **Share changes do not restart the server** when the port is unchanged, but they rotate tokens immediately. Change the key before changing content.
- **`.raw` has no keep-alive and unknown body length**, so file responses must write `Content-Length`.

## Design Constraint

Prefer communicating through the design language over adding explanatory text. When text is necessary, keep it short, common, and natural.
76 changes: 38 additions & 38 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,74 @@
# 更新日志
# Changelog

## [0.9.1] - 2026-06-28

- 设置页改用分组卡片布局,分组层级更清晰、信息更好找
- 「恢复默认窗口尺寸」补充了说明文本
- Reworked Settings into grouped cards so sections are clearer and easier to scan
- Added explanatory copy for restoring the default window size

## [0.9.0] - 2026-06-27

- 新增「传递文本」:Mac 与手机之间双向互传文字,扫码即收发,长文本和链接不必再靠聊天工具中转
- 界面支持中英双语:原生窗口可跟随系统或手动切换,手机网页自动匹配浏览器语言
- 分享文件改为带返回的二级页,文件在后台续传时主页一键回到二维码
- 冷启动不再自动重播上次的分享,改由「最近分享」手动重发,更稳妥
- Added Text Transfer for sending text both ways between Mac and phone; scan once to send or receive long text and links without using a chat app as a relay
- Added Chinese and English UI support: the native app can follow the system language or be switched manually, while the browser UI follows the visitor's browser language
- Moved shared files to a secondary page with back navigation, so the home screen can return to the QR code while transfers continue in the background
- Stopped replaying the last share on cold launch; recent shares can now be restarted manually for a safer default

## [0.8.2] - 2026-06-18

- 新增「自动更新」开关:可关闭后台自动检查,菜单「检查更新…」仍能手动查
- 各主界面标题栏新增设置入口(齿轮按钮),不用先分享文件夹也能打开设置
- 设置项重新分组(网络 / 访问权限 / 外观 / 主界面 / 命令行),更易查找
- 加宽默认窗口,避免地址栏端口号被截断
- 修复进入设置页时的几处界面闪烁
- Added an automatic update setting: background checks can be disabled while the Check for Updates menu item remains available
- Added a Settings entry point to each main title bar, so Settings can be opened without first sharing a folder
- Regrouped Settings into Network, Access, Appearance, Home, and Command Line sections
- Increased the default window width to avoid truncating port numbers in the address field
- Fixed several visual flickers when entering Settings

## [0.8.1] - 2026-06-17

- 修复卡片边缘豁口处在投影下显出的一块亮斑
- 统一卡片、描边按钮与设置分段控件的边框粗细,静止态更协调
- Fixed a bright artifact that appeared around the notched card edge under shadow
- Unified border weights across cards, outline buttons, and Settings segmented controls for a calmer resting state

## [0.8.0] - 2026-06-17

- 分享链接默认只在当前 WiFi 可见,换网或断网后自动失效,别的网络的人摸不到
- 能看到谁正在浏览:显示在线人数和设备名,点开还能看每个人的地址和已经看了多久
- 网页上新增提示,说明局域网传输走的是明文
- 一批网页安全加固:堵住 Markdown 预览里的 XSS、清洗访问令牌、访客上传的网页/图片文件自动去掉可执行性并加隔离标记
- Share links are now visible only on the current Wi-Fi by default; they expire automatically after switching networks or disconnecting
- Added live visitor visibility with online counts and device names; details show each visitor's address and session duration
- Added a browser notice that LAN transfers are sent over plain HTTP
- Hardened browser security: blocked XSS vectors in Markdown preview, cleaned access tokens from browser history, defanged executable uploaded documents and images, and added quarantine metadata

## [0.7.0] - 2026-06-12

- 自动更新换了新源:升级公告(appcast)改为随 GitHub Release 发布,app 从固定地址获取,从本版起生效
- 发布说明随版本标签维护,Release 页自动展示更新内容
- Moved automatic update feeds to GitHub Releases; appcast files are now published with each release and fetched from a stable latest-release URL
- Release notes are now maintained through version tag annotations and displayed automatically on the Release page

## [0.6.0] - 2026-06-12

- 访客上传:单文件夹分享可开启「允许上传」,访客在浏览页直接把文件传到当前目录;默认仍只读、更换分享自动回到只读,单文件上限 500MB
- 浏览器预览 Markdown / JSON / CSV:点开即渲染(Markdown 排版、JSON 折叠树、CSV 表格),文内相对引用照常可用;加 `?raw=1` 或用 curl 仍取原文
- 每次分享都生成新链接:停止或更换分享后旧链接立即失效,窗口里显示的地址与复制到的链接一致
- 分享窗口显示「N 人正在浏览」
- 列表页支持返回上一级,文件改为在新标签打开
- 多选分享时,卡片中的每一项都可在 Finder 中定位
- Added guest uploads for single-folder shares: visitors can upload files into the current folder from the browser; shares remain read-only by default, switch back to read-only after changing shares, and enforce a 500 MB per-file limit
- Added browser previews for Markdown, JSON, and CSV: files render in place, relative references keep working, and `?raw=1` or curl still returns the original text
- Generated a fresh link for every share: old links stop working immediately after stopping or changing a share, and the window URL matches copied links
- Added the "N people browsing" indicator to the share window
- Added parent-directory navigation to listing pages and opened files in new tabs
- Made every selected item in a multi-share card revealable in Finder

## [0.5.0] - 2026-06-10

- 新增 `localshare` 命令行:终端里一条命令即可分享路径,默认转发给 GUI`--headless` 则前台起服务并打印终端二维码
- 设置面板可一键安装 / 卸载命令行工具
- 菜单栏常驻图标(显示 LocalShare / 退出)
- 设置支持隐藏「最近分享」模块
- Added the `localshare` command: share paths from Terminal, forward to the GUI by default, or run a foreground server with `--headless`
- Added one-click install and uninstall controls for the command-line tool in Settings
- Added a persistent menu bar icon with LocalShare and Quit actions
- Added a setting to hide the Recent Shares section

## [0.4.0] - 2026-06-10

- 支持一次分享多个文件 / 目录
- Added support for sharing multiple files and folders at once

## [0.3.0] - 2026-06-09

- 接入 Sparkle 自动更新,这是首个支持自动更新的版本
- Added Sparkle automatic updates; this is the first version that can update itself

## [0.2.0] - 2026-06-08

- 支持拖拽文件 / 文件夹分享
- 分享卡按类型细分并标注格式,路径可在 Finder 定位或拷贝
- Added drag-and-drop sharing for files and folders
- Split share cards by file type and added format labels, Finder reveal, and path copy actions

## [0.1.2] - 2026-06-05

- 局域网文件分享:选一个文件夹就生成二维码,同 WiFi 的手机扫码即可在浏览器里浏览
- 支持单文件分享,网页端按类型过滤
- 「暖纸 × 广播」编辑风界面,配套网页目录页与深色模式
- 修复干净启动时不出窗口的问题
- Added LAN file sharing: choose a folder, get a QR code, and let phones on the same Wi-Fi browse it in a browser
- Added single-file sharing with browser-side type filtering
- Added the warm-paper broadcast-style interface, matching browser listing pages, and dark mode
- Fixed the window not appearing after a clean launch
74 changes: 74 additions & 0 deletions CHANGELOG_CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# 更新日志

## [0.9.1] - 2026-06-28

- 设置页改用分组卡片布局,分组层级更清晰、信息更好找
- 「恢复默认窗口尺寸」补充了说明文本

## [0.9.0] - 2026-06-27

- 新增「传递文本」:Mac 与手机之间双向互传文字,扫码即收发,长文本和链接不必再靠聊天工具中转
- 界面支持中英双语:原生窗口可跟随系统或手动切换,手机网页自动匹配浏览器语言
- 分享文件改为带返回的二级页,文件在后台续传时主页一键回到二维码
- 冷启动不再自动重播上次的分享,改由「最近分享」手动重发,更稳妥

## [0.8.2] - 2026-06-18

- 新增「自动更新」开关:可关闭后台自动检查,菜单「检查更新…」仍能手动查
- 各主界面标题栏新增设置入口(齿轮按钮),不用先分享文件夹也能打开设置
- 设置项重新分组(网络 / 访问权限 / 外观 / 主界面 / 命令行),更易查找
- 加宽默认窗口,避免地址栏端口号被截断
- 修复进入设置页时的几处界面闪烁

## [0.8.1] - 2026-06-17

- 修复卡片边缘豁口处在投影下显出的一块亮斑
- 统一卡片、描边按钮与设置分段控件的边框粗细,静止态更协调

## [0.8.0] - 2026-06-17

- 分享链接默认只在当前 WiFi 可见,换网或断网后自动失效,别的网络的人摸不到
- 能看到谁正在浏览:显示在线人数和设备名,点开还能看每个人的地址和已经看了多久
- 网页上新增提示,说明局域网传输走的是明文
- 一批网页安全加固:堵住 Markdown 预览里的 XSS、清洗访问令牌、访客上传的网页/图片文件自动去掉可执行性并加隔离标记

## [0.7.0] - 2026-06-12

- 自动更新换了新源:升级公告(appcast)改为随 GitHub Release 发布,app 从固定地址获取,从本版起生效
- 发布说明随版本标签维护,Release 页自动展示更新内容

## [0.6.0] - 2026-06-12

- 访客上传:单文件夹分享可开启「允许上传」,访客在浏览页直接把文件传到当前目录;默认仍只读、更换分享自动回到只读,单文件上限 500MB
- 浏览器预览 Markdown / JSON / CSV:点开即渲染(Markdown 排版、JSON 折叠树、CSV 表格),文内相对引用照常可用;加 `?raw=1` 或用 curl 仍取原文
- 每次分享都生成新链接:停止或更换分享后旧链接立即失效,窗口里显示的地址与复制到的链接一致
- 分享窗口显示「N 人正在浏览」
- 列表页支持返回上一级,文件改为在新标签打开
- 多选分享时,卡片中的每一项都可在 Finder 中定位

## [0.5.0] - 2026-06-10

- 新增 `localshare` 命令行:终端里一条命令即可分享路径,默认转发给 GUI,`--headless` 则前台起服务并打印终端二维码
- 设置面板可一键安装 / 卸载命令行工具
- 菜单栏常驻图标(显示 LocalShare / 退出)
- 设置支持隐藏「最近分享」模块

## [0.4.0] - 2026-06-10

- 支持一次分享多个文件 / 目录

## [0.3.0] - 2026-06-09

- 接入 Sparkle 自动更新,这是首个支持自动更新的版本

## [0.2.0] - 2026-06-08

- 支持拖拽文件 / 文件夹分享
- 分享卡按类型细分并标注格式,路径可在 Finder 定位或拷贝

## [0.1.2] - 2026-06-05

- 局域网文件分享:选一个文件夹就生成二维码,同 WiFi 的手机扫码即可在浏览器里浏览
- 支持单文件分享,网页端按类型过滤
- 「暖纸 × 广播」编辑风界面,配套网页目录页与深色模式
- 修复干净启动时不出窗口的问题
Loading
Loading