Compare commits
3 Commits
v0.3.1
...
909a016a44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
909a016a44 | ||
|
|
96fe642175 | ||
|
|
d38fa7a507 |
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -44,11 +44,25 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
cache: 'pnpm'
|
|
||||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
run: npm install -g pnpm
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
id: pnpm-cache
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: Print build info
|
- name: Print build info
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
168
README.md
Normal file
168
README.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# UShare
|
||||||
|
|
||||||
|
一个简单易用的文件分享工具,支持大文件分片上传下载,以及局域网内 P2P 文件分享。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 📁 大文件分片上传下载
|
||||||
|
- 🌐 网页端文件分享
|
||||||
|
- 🏠 局域网 P2P 文件分享(WebRTC)
|
||||||
|
- 📱 响应式设计,支持移动端
|
||||||
|
- 🔐 简单的用户认证
|
||||||
|
- 🚀 单一二进制,开箱即用
|
||||||
|
- 🐳 Docker 支持
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/loveuer/ushare.git
|
||||||
|
cd ushare
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
./dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
开发服务器会同时启动后端(http://0.0.0.0:9119)和前端(http://0.0.0.0:5173),前端支持热重载。
|
||||||
|
|
||||||
|
### 生产模式
|
||||||
|
|
||||||
|
#### 使用预编译二进制
|
||||||
|
|
||||||
|
从 [Releases](https://github.com/loveuer/ushare/releases) 下载对应平台的二进制文件。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux amd64
|
||||||
|
wget https://github.com/loveuer/ushare/releases/download/v0.3.2/ushare-linux-amd64
|
||||||
|
chmod +x ushare-linux-amd64
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
./ushare-linux-amd64 -data ./data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 从源码构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建单一二进制(包含嵌入的前端)
|
||||||
|
./make.sh
|
||||||
|
|
||||||
|
# 运行
|
||||||
|
./dist/ushare -data ./data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t ushare:latest .
|
||||||
|
docker run -d -p 80:80 -v $(pwd)/data:/data ushare:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
### 命令行参数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ushare [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-debug 启用调试模式
|
||||||
|
-address string 监听地址 (default "0.0.0.0:9119")
|
||||||
|
-data string 数据目录 (default "/data")
|
||||||
|
-clean int 文件清理周期,单位:小时,0 表示不自动清理 (default 24)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
| 环境变量 | 说明 | 默认值 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| `USHARE_USERNAME` | 用户名 | `admin` |
|
||||||
|
| `USHARE_PASSWORD` | 密码 | `ushare@123` |
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用默认凭据(admin / ushare@123)
|
||||||
|
./ushare -data ./data
|
||||||
|
|
||||||
|
# 使用自定义凭据
|
||||||
|
export USHARE_USERNAME=myuser
|
||||||
|
export USHARE_PASSWORD=mypass
|
||||||
|
./ushare -data ./data
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 文件分享
|
||||||
|
|
||||||
|
1. 访问 http://localhost:9119/share
|
||||||
|
2. 点击"选择文件"选择要上传的文件
|
||||||
|
3. 点击"上传文件",等待上传完成
|
||||||
|
4. 复制生成的下载码分享给他人
|
||||||
|
|
||||||
|
### 文件下载
|
||||||
|
|
||||||
|
1. 访问 http://localhost:9119/share
|
||||||
|
2. 在"获取文件"区域输入下载码
|
||||||
|
3. 点击"获取文件"下载文件
|
||||||
|
|
||||||
|
### 局域网 P2P 分享
|
||||||
|
|
||||||
|
1. 访问 http://localhost:9119
|
||||||
|
2. 自动注册到局域网设备列表
|
||||||
|
3. 选择要分享的设备,建立 P2P 连接
|
||||||
|
4. 开始文件传输
|
||||||
|
|
||||||
|
## 移动端支持
|
||||||
|
|
||||||
|
- 响应式设计,自动适配手机、平板等设备
|
||||||
|
- 移动端(≤768px)采用垂直布局,优化触摸操作
|
||||||
|
- 按钮尺寸优化,适合手指点击
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ushare/
|
||||||
|
├── internal/ # 后端代码
|
||||||
|
│ ├── api/ # HTTP API 路由
|
||||||
|
│ ├── controller/ # 业务逻辑
|
||||||
|
│ ├── handler/ # 请求处理器
|
||||||
|
│ ├── model/ # 数据模型
|
||||||
|
│ ├── opt/ # 配置管理
|
||||||
|
│ └── pkg/ # 工具包
|
||||||
|
├── frontend/ # 前端代码
|
||||||
|
│ └── src/
|
||||||
|
│ ├── api/ # API 调用
|
||||||
|
│ ├── component/ # UI 组件
|
||||||
|
│ ├── hook/ # 自定义 Hooks
|
||||||
|
│ ├── page/ # 页面组件
|
||||||
|
│ └── store/ # 状态管理
|
||||||
|
├── .github/workflows/ # GitHub Actions
|
||||||
|
├── dist/ # 构建输出(.gitignore)
|
||||||
|
└── data/ # 数据目录(.gitignore)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发构建(前后端分离)
|
||||||
|
./dev.sh
|
||||||
|
|
||||||
|
# 生产构建(单一二进制)
|
||||||
|
./make.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码风格
|
||||||
|
|
||||||
|
请参考 [AGENTS.md](./AGENTS.md) 了解代码风格指南。
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request!
|
||||||
@@ -11,6 +11,12 @@ const useUploadStyle = createUseStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
minHeight: "50vh",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
minHeight: "auto",
|
||||||
|
padding: "20px 10px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
backgroundColor: "#C8E6C9",
|
backgroundColor: "#C8E6C9",
|
||||||
@@ -19,10 +25,20 @@ const useUploadStyle = createUseStyles({
|
|||||||
borderRadius: "15px",
|
borderRadius: "15px",
|
||||||
width: "70%",
|
width: "70%",
|
||||||
margin: "20px 60px 20px 0",
|
margin: "20px 60px 20px 0",
|
||||||
/*todo margin 不用 px*/
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
width: "90%",
|
||||||
|
margin: "20px 0",
|
||||||
|
padding: "20px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
color: "#2c9678"
|
color: "#2c9678",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
marginBottom: "15px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
@@ -33,7 +49,11 @@ const useUploadStyle = createUseStyles({
|
|||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
color: "#2c9678",
|
color: "#2c9678",
|
||||||
marginLeft: '10px'
|
marginLeft: '10px',
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
fontSize: "14px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
clean: {
|
clean: {
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
@@ -48,14 +68,12 @@ const useShowStyle = createUseStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
position: "relative", // 为关闭按钮提供定位基准
|
minHeight: "50vh",
|
||||||
},
|
|
||||||
title: {
|
"@media (max-width: 768px)": {
|
||||||
color: "#2c9678",
|
minHeight: "auto",
|
||||||
marginTop: 0,
|
padding: "20px 10px",
|
||||||
display: "flex",
|
},
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
backgroundColor: "#C8E6C9",
|
backgroundColor: "#C8E6C9",
|
||||||
@@ -65,6 +83,25 @@ const useShowStyle = createUseStyles({
|
|||||||
width: "70%",
|
width: "70%",
|
||||||
margin: "20px 60px 20px 0",
|
margin: "20px 60px 20px 0",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
width: "90%",
|
||||||
|
margin: "20px 0",
|
||||||
|
padding: "20px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: "#2c9678",
|
||||||
|
marginTop: 0,
|
||||||
|
marginBottom: "25px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
marginBottom: "15px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
closeButton: {
|
closeButton: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -78,9 +115,7 @@ const useShowStyle = createUseStyles({
|
|||||||
height: "24px",
|
height: "24px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
// background: "#cc0000",
|
|
||||||
boxShadow: "20px 20px 60px #fff, -20px -20px 60px #fff",
|
boxShadow: "20px 20px 60px #fff, -20px -20px 60px #fff",
|
||||||
// boxShadow: "20px 20px 60px #eee",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
codeWrapper: {
|
codeWrapper: {
|
||||||
@@ -89,6 +124,10 @@ const useShowStyle = createUseStyles({
|
|||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
margin: "15px 0",
|
margin: "15px 0",
|
||||||
overflowX: "auto",
|
overflowX: "auto",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
padding: "0 10px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
pre: {
|
pre: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -98,6 +137,12 @@ const useShowStyle = createUseStyles({
|
|||||||
height: '24px',
|
height: '24px',
|
||||||
"& > code": {
|
"& > code": {
|
||||||
marginLeft: "0",
|
marginLeft: "0",
|
||||||
|
fontSize: "14px",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
fontSize: "12px",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
copyButton: {
|
copyButton: {
|
||||||
@@ -112,6 +157,11 @@ const useShowStyle = createUseStyles({
|
|||||||
"&:hover": {
|
"&:hover": {
|
||||||
background: "#1f6d5a",
|
background: "#1f6d5a",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
padding: "6px 12px",
|
||||||
|
fontSize: "12px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ const useStyle = createUseStyles({
|
|||||||
backgroundColor: 'lightgray',
|
backgroundColor: 'lightgray',
|
||||||
position: "relative",
|
position: "relative",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
left: {
|
left: {
|
||||||
backgroundColor: "#e3f2fd",
|
backgroundColor: "#e3f2fd",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {createUseStyles} from "react-jss";
|
import { createUseStyles } from "react-jss";
|
||||||
import {UButton} from "../../../component/button/u-button.tsx";
|
import { UButton } from "../../../component/button/u-button.tsx";
|
||||||
import {useStore} from "../../../store/share.ts";
|
import { useStore } from "../../../store/share.ts";
|
||||||
|
|
||||||
const useStyle = createUseStyles({
|
const useStyle = createUseStyles({
|
||||||
container: {
|
container: {
|
||||||
@@ -8,6 +8,12 @@ const useStyle = createUseStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
minHeight: "50vh",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
minHeight: "auto",
|
||||||
|
padding: "20px 10px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
backgroundColor: "#BBDEFB",
|
backgroundColor: "#BBDEFB",
|
||||||
@@ -16,25 +22,58 @@ const useStyle = createUseStyles({
|
|||||||
borderRadius: "15px",
|
borderRadius: "15px",
|
||||||
width: "70%",
|
width: "70%",
|
||||||
margin: "20px 0 20px 60px",
|
margin: "20px 0 20px 60px",
|
||||||
/*todo margin 不用 px*/
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
width: "90%",
|
||||||
|
margin: "20px 0",
|
||||||
|
padding: "20px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
color: '#1661ab', // 靛青
|
color: '#1661ab',
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
marginBottom: "15px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
padding: '11px',
|
padding: '11px',
|
||||||
margin: '20px 0',
|
margin: '0',
|
||||||
width: '200px',
|
width: '200px',
|
||||||
border: '2px solid #ddd',
|
border: '2px solid #ddd',
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
'&:active': {
|
'&:active': {
|
||||||
border: '2px solid #1661ab',
|
border: '2px solid #1661ab',
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
width: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
padding: '10px',
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
justifyContent: 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export const PanelRight = () => {
|
export const PanelRight = () => {
|
||||||
const style = useStyle()
|
const style = useStyle()
|
||||||
const {code, setCode} = useStore()
|
const { code, setCode } = useStore()
|
||||||
|
|
||||||
function onCodeChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function onCodeChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
setCode(e.currentTarget.value)
|
setCode(e.currentTarget.value)
|
||||||
@@ -52,7 +91,7 @@ export const PanelRight = () => {
|
|||||||
return <div className={style.container}>
|
return <div className={style.container}>
|
||||||
<div className={style.form}>
|
<div className={style.form}>
|
||||||
<h2 className={style.title}>获取文件</h2>
|
<h2 className={style.title}>获取文件</h2>
|
||||||
<div>
|
<div className={style.inputContainer}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={style.code}
|
className={style.code}
|
||||||
@@ -60,7 +99,9 @@ export const PanelRight = () => {
|
|||||||
value={code}
|
value={code}
|
||||||
onChange={onCodeChange}
|
onChange={onCodeChange}
|
||||||
/>
|
/>
|
||||||
<UButton style={{marginLeft: '10px'}} onClick={onFetchFile}>获取文件</UButton>
|
<div className={style.buttonContainer}>
|
||||||
|
<UButton onClick={onFetchFile}>获取文件</UButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ const useStyle = createUseStyles({
|
|||||||
height: "100vh",
|
height: "100vh",
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "40% 20% 40%",
|
gridTemplateColumns: "40% 20% 40%",
|
||||||
|
|
||||||
|
"@media (max-width: 768px)": {
|
||||||
|
gridTemplateColumns: "100%",
|
||||||
|
gridTemplateRows: "auto auto",
|
||||||
|
overflowY: "auto",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -20,7 +26,7 @@ export const FileSharing = () => {
|
|||||||
const style = useStyle()
|
const style = useStyle()
|
||||||
return <div className={style.container}>
|
return <div className={style.container}>
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
<PanelMid/>
|
<PanelMid />
|
||||||
<PanelRight/>
|
<PanelRight/>
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user