localshare:一个具有离线优先设计的 Discourse 客户端 + 可进行文件分享的 App

一个可以在局域网分享文件的 App

依托 tauri,支持 Win/Linux/Android 多平台。在 Android 上需要运行需要用户授予管理存储的权限。

@uebianFileShare 启发

文件分享功能基于 dufs,将本地文件通过 HTTP 服务器供内网其他机器通过浏览器访问

项目地址:

下载链接:


Android:
https://dl.xjtu.app/localshare/io.github.fokx.localshare-=VERSION=.apk

Windows:
https://dl.xjtu.app/localshare/localshare_=VERSION=_x64-setup.exe

Linux:
https://dl.xjtu.app/localshare/localshare-=VERSION=-1.x86_64.rpm
https://dl.xjtu.app/localshare/localshare_=VERSION=_amd64.deb
(AppImage 太大就不放了)

安全注意事项

  • 默认启用了用户名/密码 HTTP basic auth 认证,但未要求复杂度。
  • 虽然实现了 peer 间文件分享时的传输加密,但由于互相采用自签名证书并且不校验有效性,建议在物理信道有加密的/不易被嗅探的局域网使用文件分享功能(例如家庭路由器有线局域网,WPA/WPA2/WPA3 加密的无线 WiFi/热点),不建议在无加密的开放式网络(例如 XJTU_STU)进行文件分享。
2 个赞

类似的项目
60k stars

要用 localsend 传送文件,客户端也需要安装 localsend.

得益于 dufs, 本项目更像一个可通过浏览器访问的文件管理器,支持文件上传、搜索、作为压缩包下载文件夹等操作。

1 个赞

仔细研究了 LocalSend 的协议,

觉得用 Tauri(Rust+Svelte) 写一个与之兼容的 App 似乎不难,很有趣.

LocalSend 仓库的 Flutter(Dart) 代码很庞杂,细看 git commit history, 原来 Flutter 自身的源码也被 git 追踪了,不理解 Flutter 的设计为啥要把属于框架的东西作为 submodule 包括进来 :sweat_smile:

今天写完通过 multicast(UDP)/HTTP(TCP) 的 Peer discovery,
正在写 upload 逻辑:借助 LLM 辅助,丢给她 protocol,让她写枯燥无味且耗时的 struct 定义 :grin:

2 个赞

今日实现了 File Transfer (HTTP) aka Upload API, 实现从手机端 LocalSend 发送文件到 LocalShare 所在电脑的/tmp 目录下.

中间发现 axum route handler 若 deserialize query string 失败,会直接返回 HTTP/1.1 422 Unprocessable Entity, 而不会在 stderr 打印错误,
我用 Wireshare 抓包ip.src == 192.168.18.26 || ip.dst == 192.168.18.26看返回的错误内容,
若需自定义 extractor error 还有点复杂:

在搜索错误的时候,撞见另外一个兄弟希望用 Rust 实现 LocalSend 协议:

不过看她的代码库,应该是保留了 Flutter 前端,借助 flutter_rust_bridge 调用 Rust 代码

1 个赞

今天把接受她人发送过来的文件的功能完善了,会询问用户是否接受。前端支持配置接收文件存储的文件夹。

等待用户确认是否接收的代码写得很丑陋:
tokio::select! 一个分支里 loop 每隔 0.5s 检查共享状态,造成了比较多的 Mutex 加锁和释放,另一分支超时退出
本来这一个应该是由 Channel 来实现,可惜 Tauri 通过 emit 实现信息交互的方式不支持阻塞,其实应该也可以配合 oneshot 实现等待前端用户确认的效果的。

这段时间学习了 multicast、axum、Tauri 状态管理, 前后端信息交互

其实 Localsend 协议里还有 5. Reverse File Transfer (HTTP) aka Download API,我觉得这个功能跟我用通过 Dufs 支持的功能重复了,就没写了。

今日想在 Tauri 工程里用 ORM, 由于前端不支持 ndoe-adapter, 所以只能使用 Rust 里的 ORM: Diesel / SeaORM,前者是 crates.io 在使用,不支持 async,不过这不是什么大缺点;后者支持 async,但看 maintainer 好像是 HK run UK 的两位老兄,文档里的 SeaORM Pro Plus 字样让人稍有不适.

看了看文档,感觉 Rust 里的 ORM 远不如 JS 世界的 Drizzle 易用,除非不得已不想学 :sweat_smile:
于是搜了搜,找到一个没法运行的 Tauri-v1 的项目:

又在 tauri plugin workspace 的 Issues 里搜索 Drizzle, 还真找到一个兄弟今年 3 月份做的尝试:

把里面的 bun 改成 pnpm 后运行成功,这样以后可以爽用 Drizzle 了。

v0.5: 测试可在 Android / Linux 上运行并与 Localsend/Localshare 对端进行发送/接受单/多文件操作.

缺点:

  • UI 太丑陋,当文件名太长时,部分组件会飘出屏幕区域 (edit: fixed)
  • 不显示传输进度
  • 只支持通过 HTTPS 向外传,接收还是通过 HTTP (edit: support https in latest version)
    由于 HTTPS 也是忽略自签名证书导致的错误,所以安全性并未高多少。要想真正安全应该通过其他渠道 (NFC/QR code/3rd party public server with TLS) 交换证书.

花了$173.40 买的 Jetbrains All Products Pack 里的 AI Assistant 最近终于可以用了,算是个赠送福利,当时买的时候套餐里面没有 AI.之前我一直依赖 Github Copilot, $10/month 有点贵,不想续费。虽然 AI Assistant 也支持本地 ollama,但 deepseek-r1/qwen3 质量不行,gemma3/llama4 速度慢,尤其是本地 collect context 特别慢,还特别耗资源,写个代码风扇狂转

由于 Tauri 要求 JS 应用被编译成静态文件 (Static Site Generator), 所以 Sveltekit 的任何需要后端 node 支持的功能都没法用,which causes file-based router cannot know route info / query param. 我通过全局变量传参勉强克服了这一问题.


最近进展很慢,Jetbrains AI 没有 Github Copilot 好用,眼睛难受,白天没空闲

利用 关于本站公开数据提供 SQLite 下载的讨论 , 将全部公众可见的 topics, posts, likes, users 内容导出 (42MiB SQLite, 9MiB after zstd:4), Rust 里用 sqlx 加载并用 Drizzle sqlite-proxy 在 js 侧访问,实现了 Topic 的浏览,以下为纯纯的 POC, 加载了前 50 topics 和前 100 posts:

http://dl.xjtu.app/pool/xap-preview250521.apk (55M)
http://dl.xjtu.app/pool/xap-preview250521.exe (11M)

我早就意识到:针对网站速度慢的问题,怎么更换服务器都不行,
只有把数据都存在本地,才是根本的解决方案.(当然还有一个,我不说)

1 个赞

p2p?

今日:

  • 支持亮/暗两种 UI
  • 通过分页 (pagination) 实现浏览全部话题/帖子,不同于 Discourse 的 Infinite Scroll
    并且记录访问到哪一页,例如下次点开之前访问过的 topic,会进入上次看的那一页
    better off scrolling to last post visited
  • 稳定了底部导航栏,不会再乱跑了

图片/视频等富媒体资料仍然需要从伺服器上拉取,实现分布式存储和获取是一个有趣的下一步

可以先缓存到本地 cache requests to /uploads in rust backend · fokx/localshare@b28da72 · GitHub
然后把所有用户加入 syncthing 同步组里,不过 syncthing 是用 go 写的,集成不到程序里去.

要手写一个分布式存储,还要防止恶意节点任意存储的可能,有点难啊 :sweat_smile:

今日:

  • 本地的 Localsend 绑定在 https://0.0.0.0:53317 ,首次使用时会自动生成自签名证书,加密向传输的流量,避免了一般攻击者抓取流量直接看到明文.
    但这依然是不安全的,技术稍高的攻击者依然有可乘之机:对未部署 SSL 的网站能否保证数据传输安全的探究 - EAimTY's Blog

  • peer 之间互传缓存也用 HTTPS 了,但由于不验证证书真实性,需要别的方式校验文件,例如上传的文件的文件名就是该文件的 sha1


说到导出,做到现在,也只是做了对导出历史数据的浏览,总不能让用户定期下载包含全量 sqlite 的安装包吧.

peer 间,以及 peer 与服务器之间,应该要能自动拉取 peer’s post id > my current height 的 post 以及 topic

v0.6 Finally a working version of webview via proxy
following the idea in 桌面 App(墙裂建议网不好的用户使用)xjtu-app-tauri-universal - #13,来自 anonymous_warrior

starts a local socks5 proxy,
the user loads localhost:port in webview,
intercept the request,
using reqwest to send the request using proxy,
for all plain text responses, replace hostname to localhost:port

Problems:

  • no proxy for post request, so login does not work
  • webview will replace button navbar, better load in iframe
2 个赞

v0.6.1 can load latest topics and posts (and save into local SQLite DB, and cache media)

Good Samsung has curl included in built-in shell

e2q:/ $ curl --version                                                                                                    
curl 8.8.0-DEV (Android) libcurl/8.8.0-DEV BoringSSL zlib/1.3.0.1-motley
Release-Date: [unreleased]
Protocols: file http https ipfs ipns mqtt
Features: alt-svc AsynchDNS HSTS HTTPS-proxy IPv6 libz NTLM SSL threadsafe UnixSockets

悲:Android production build have 500 internal error (none in debug build)

1 个赞

v0.6.2
basically a Discourse browser (of latest posts) plus file sharing functionalities,
with built-in network acceleration and cache
(work on Android and Windows)

TBD: login and post posts

Download links are in the original post:

1 个赞

今日实现在 Tauri App 里通过 Discourse 进行 oauth 身份认证(通过 distrust

进而实现回复(不再是 read-only browser),前端写好了:using md editor carta to reply to post(backend not implemented yet) · fokx/localshare@5c50cfb · GitHub
但不知道后端怎么写。要回复需要 Api-Key,怎样通过网页登录认证获取 Api-Key?
seems not a good idea to load a full web app: User API keys specification - Integrations - Discourse Meta
maybe:
listen on localhost:8000 for result,
user use distrust to do oauth,
will callback localhost:8000,
should return username, email, etc.
(+what if it also returns Api-Key, which can be pre-generated for all users / at registration)

safety:

  • Is hardcoding client_secret in user-side app safe?
3 个赞

今晚写疯了,同时打开了 Rubymine, Goland, Rustrover,
改了 Discourse 和 distrust 的代码,使得 oauth 能返回 user api key,从而在 Tauri 后端里能调用 api key 进行回帖等操作

关于前面的问题:

用户可以分析二进制文件/截获流量分析 oauth app id & key,从而可以做一个仿冒的 app 进行 oauth,
但只要你下载"官方"的软件包,就不影响你的登陆凭据安全性

AI 建议说:

  • Use PKCE flow specifically designed for public clients
  • Register your app as a public client that doesn’t require a client secret
  • Consider using a backend-for-frontend (BFF) pattern for sensitive operations
1 个赞

今日:


我发现 Jetbrains 的 Junie 插件真的强大,相比于 AI Assistant 插件,Junie 可以跨文件进行分析和修改,缺点就是慢 (但总比人快 :grin:)

另外我多次用 AI 帮助我这种弱智修复 Rust 代码无法编译的错误

下载 v0.6.4 版本即可体验

1 个赞