5 Commits

Author SHA1 Message Date
loveuer
909a016a44 docs: add comprehensive README
Some checks failed
/ build ushare (push) Has been cancelled
/ clean (push) Has been cancelled
- Add project introduction and features
- Add quick start guide (development and production)
- Add configuration documentation
- Add usage instructions
- Add mobile support section
- Add development guide and directory structure
2026-01-18 17:52:39 +08:00
loveuer
96fe642175 fix: correct pnpm setup in GitHub Actions
- Use pnpm/action-setup@v2 instead of npm install -g
- Add proper pnpm cache configuration
- Fix 'Unable to locate executable file: pnpm' error
2026-01-18 17:35:34 +08:00
loveuer
d38fa7a507 feat: optimize mobile responsive layout for share page
- Add responsive design for mobile devices (≤768px)
- Switch from horizontal to vertical layout on mobile
- Hide middle divider panel on mobile
- Optimize button sizes for mobile touch interaction
- Improve input field and button alignment on mobile
2026-01-18 17:12:07 +08:00
loveuer
8a063d15d9 ci: add GitHub Actions workflow for releasing binaries
Some checks failed
/ build ushare (push) Has been cancelled
/ clean (push) Has been cancelled
- Add .github/workflows/release.yml
- Build binaries for multiple platforms (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64)
- Embed frontend into binary
- Generate SHA256 checksums for each binary
- Automatically release on tag push
2026-01-18 14:04:36 +08:00
loveuer
56874b754e feat: implement single binary build and env-based auth
Some checks failed
/ build ushare (push) Has been cancelled
/ clean (push) Has been cancelled
- Unify login page styling with share page
- Add AGENTS.md with build commands and code style guidelines
- Add dev.sh and make.sh for development and production builds
- Implement single binary build with embedded frontend using embed.FS
- Change auth configuration from CLI flag to env variables (USHARE_USERNAME, USHARE_PASSWORD)
- Set default credentials: admin / ushare@123
- Fix static file serving for SPA routes
2026-01-17 23:28:21 +08:00
19 changed files with 1006 additions and 149 deletions

106
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: Release Binaries
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
name: Build and Release
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
suffix: linux-amd64
- goos: linux
goarch: arm64
suffix: linux-arm64
- goos: darwin
goarch: amd64
suffix: darwin-amd64
- goos: darwin
goarch: arm64
suffix: darwin-arm64
- goos: windows
goarch: amd64
suffix: windows-amd64
ext: .exe
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install 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
run: |
echo "Tag: ${{ github.ref_name }}"
echo "GoOS: ${{ matrix.goos }}"
echo "GoArch: ${{ matrix.goarch }}"
- name: Build frontend
working-directory: frontend
run: |
pnpm install --registry=https://registry.npmmirror.com
pnpm run build
- name: Copy frontend dist for embedding
run: |
mkdir -p internal/static/frontend
cp -r frontend/dist internal/static/frontend/
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
OUTPUT_NAME="ushare-${{ matrix.suffix }}${{ matrix.ext }}"
go build -ldflags '-s -w' -o "$OUTPUT_NAME" .
chmod +x "$OUTPUT_NAME"
- name: Generate checksums
run: |
OUTPUT_NAME="ushare-${{ matrix.suffix }}${{ matrix.ext }}"
sha256sum "$OUTPUT_NAME" > "$OUTPUT_NAME.sha256"
- name: Upload to release
uses: softprops/action-gh-release@v1
with:
files: |
ushare-${{ matrix.suffix }}${{ matrix.ext }}
ushare-${{ matrix.suffix }}${{ matrix.ext }}.sha256
draft: false
prerelease: false

16
.gitignore vendored
View File

@@ -1,5 +1,19 @@
# IDE
.idea
.vscode
# OS
.DS_Store
# Build output
dist
xtest
# Data directories
data/
x-*/
# Temporary build files
internal/static/frontend
# Compiled binaries
ushare

250
AGENTS.md Normal file
View File

@@ -0,0 +1,250 @@
# AGENTS.md
## Build Commands
### Go Backend (Root)
```bash
# Build the Go backend binary
go build -o ushare .
# Run tests
go test ./...
# Run single test
go test -run TestFunctionName ./internal/pkg/tool
# Run tests in specific package
go test ./internal/pkg/tool
# Run tests with verbose output
go test -v ./...
# Run the application
./ushare -debug -address 0.0.0.0:9119 -data ./data -auth "admin:password"
```
### TypeScript Frontend (frontend/)
```bash
# Install dependencies (uses pnpm)
pnpm install
# Development server
pnpm run dev
# Build for production
pnpm run build
# Lint code
pnpm run lint
# Preview production build
pnpm run preview
```
### Docker Build
```bash
# Build the complete Docker image
docker build -t ushare:latest .
```
## Code Style Guidelines
### Go Backend
#### Imports
- Group imports in three sections: standard library, third-party, internal
- Keep one import per line for readability
- Example:
```go
import (
"context"
"fmt"
"net/http"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/loveuer/ushare/internal/model"
"github.com/loveuer/ushare/internal/opt"
)
```
#### Naming Conventions
- Exported functions/types: PascalCase (e.g., `UserManager`, `NewPassword`)
- Private functions/types: camelCase (e.g., `generateMeta`, `tokenFn`)
- Variables: camelCase (e.g., `filename`, `totalChunks`)
- Constants: PascalCase (e.g., `Meta`, `HeaderSize`, `CodeLength`)
- Interfaces: Usually implied, not explicitly declared unless needed
- Receiver names: Short, 1-2 letters (e.g., `m`, `um`, `c`)
#### Error Handling
- Use `github.com/pkg/errors` for error wrapping
- Check errors immediately after function calls
- Return errors for functions that can fail
- Use `errors.New()` for simple error messages
- Wrap errors with context when propagating up:
```go
if err != nil {
return errors.New("invalid file code")
}
```
#### Struct Tags
- Use `json` tags for JSON serialization
- Use `mapstructure` tags for config parsing (viper)
- Use `-` for fields to exclude from JSON:
```go
type User struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"-"`
}
```
#### Concurrency
- Use `sync.Mutex` for protecting shared state
- Always use `defer mutex.Unlock()` after `mutex.Lock()`
- Use goroutines with select statements for graceful shutdown
#### Testing
- Use standard `testing` package
- Test functions named `Test<FunctionName>`
- Table-driven tests are preferred:
```go
func TestFunction(t *testing.T) {
tests := []struct {
name string
arg ArgType
want ReturnType
}{
{"case 1", arg1, want1},
{"case 2", arg2, want2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Function(tt.arg); got != tt.want {
t.Errorf("Function() = %v, want %v", got, tt.want)
}
})
}
}
```
#### Generics
- Use generics for utility functions with type parameters:
```go
func Min[T ~int | ~uint | ~float64](a, b T) T
```
#### Context
- Use `context.Context` for cancellation signals
- Always check `ctx.Done()` in long-running goroutines
### TypeScript Frontend
#### Imports
- Use ES6 imports
- Third-party imports first, then relative imports:
```typescript
import { useState } from 'react';
import { createUseStyles } from 'react-jss';
import { CloudBackground } from "../component/fluid/cloud.tsx";
```
#### Naming Conventions
- Components: PascalCase (e.g., `UButton`, `Login`, `FileSharing`)
- Functions/hooks: camelCase (e.g., `useFileUpload`, `onLogin`)
- Variables: camelCase (e.g., `progress`, `loading`, `error`)
- Constants: UPPER_SNAKE_CASE (rarely used)
- Types/Interfaces: PascalCase (e.g., `UploadRes`, `LocalStore`)
#### Types
- Explicitly type function parameters and return values
- Use interfaces for object shapes:
```typescript
interface UploadRes {
code: string
}
interface LocalStore {
id: string;
name: string;
channel?: RTCDataChannel;
set: (id: string, name: string) => void;
}
```
#### Components
- Use functional components with hooks
- Props as interface at top of component:
```typescript
type Props = {
onClick?: () => void;
children: ReactNode;
disabled?: boolean;
};
export const UButton: React.FC<Props> = ({ onClick, children, disabled }) => { ... }
```
#### Styling
- Use `react-jss` with `createUseStyles` for component styles
- Define styles object with camelCase properties:
```typescript
const useStyle = createUseStyles({
container: {
display: "flex",
"&:hover": { backgroundColor: "#45a049" }
}
});
```
#### State Management
- Use `useState` for local component state
- Use `zustand` for global state (defined in `store/` directory)
- Always destructure from store hooks:
```typescript
export const useLocalStore = create<LocalStore>()((_set) => ({
id: '',
set: (id: string) => _set({ id })
}))
```
#### Async Patterns
- Use async/await for API calls
- Handle errors with try-catch:
```typescript
try {
const result = await uploadFile(file);
} catch (err) {
setError(err.message);
}
```
#### Configuration
- Vite dev server proxies `/api` and `/ushare` to Go backend at `http://127.0.0.1:9119`
- WebSocket proxy configured for `/api/ulocal/ws`
## Project Structure
```
ushare/
├── internal/
│ ├── api/ # HTTP API setup and routes
│ ├── controller/ # Business logic (user, meta, room management)
│ ├── handler/ # HTTP request handlers
│ ├── model/ # Data models (User, Meta, WS)
│ ├── opt/ # Configuration and constants
│ └── pkg/
│ ├── db/ # Database utilities
│ └── tool/ # Utility functions (password, random, etc.)
├── frontend/
│ └── src/
│ ├── api/ # API calls (auth, upload)
│ ├── component/ # Reusable UI components
│ ├── hook/ # Custom hooks (websocket, message)
│ ├── page/ # Page components (login, share, local)
│ ├── store/ # Zustand state stores
│ └── interface/ # TypeScript interfaces
└── deployment/ # Docker and nginx configs
```

168
README.md Normal file
View 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

63
dev.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/bin/bash
set -e
# 捕获 Ctrl+C 信号
trap 'echo ""; echo "Stopping..."; kill $(jobs -p); exit 0' SIGINT SIGTERM
echo "=========================================="
echo " Starting UShare Development Server"
echo "=========================================="
echo ""
# 构建前端(如果需要)
if [ ! -d "frontend/dist" ]; then
echo "[Frontend] Building..."
cd frontend && pnpm run build && cd ..
echo "[Frontend] Build complete!"
fi
# 创建临时嵌入目录用于编译
mkdir -p internal/static/frontend
if [ ! -d "internal/static/frontend/dist" ]; then
echo "[Setup] Creating frontend embed directory..."
cp -r frontend/dist internal/static/frontend/
fi
# 检查后端是否已构建
if [ ! -f "./ushare" ]; then
echo "[Backend] Building..."
go build -o ushare .
echo "[Backend] Build complete!"
fi
# 创建数据目录
mkdir -p ./data
# 启动后端
echo "[Backend] Starting..."
./ushare -debug -address 0.0.0.0:9119 -data ./data &
BACKEND_PID=$!
echo "[Backend] Running on http://0.0.0.0:9119 (PID: $BACKEND_PID)"
echo ""
# 启动前端
echo "[Frontend] Starting..."
cd frontend && pnpm run dev &
FRONTEND_PID=$!
cd ..
echo "[Frontend] Running on http://localhost:5173 (PID: $FRONTEND_PID)"
echo ""
echo "=========================================="
echo " All services started!"
echo " - Backend: http://0.0.0.0:9119"
echo " - Frontend: http://0.0.0.0:5173"
echo "=========================================="
echo ""
echo "Note: Frontend hot-reload is enabled. Changes to backend code require rebuilding."
echo "Press Ctrl+C to stop all services"
echo ""
# 等待所有后台进程
wait

View File

@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { createUseStyles } from "react-jss";
import { CloudBackground } from "../component/fluid/cloud.tsx";
import {useAuth} from "../api/auth.ts";
import { useAuth } from "../api/auth.ts";
import { UButton } from "../component/button/u-button.tsx";
const useClass = createUseStyles({
container: {
@@ -14,88 +14,42 @@ const useClass = createUseStyles({
display: "flex",
justifyContent: "center",
alignItems: "center",
position: 'relative',
},
login_container: {
background: "rgba(255,255,255,.5)",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
width: "350px",
height: '100%',
position: 'absolute',
left: '70%',
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#e3f2fd",
},
form: {
height: '100%',
width: '100%',
backgroundColor: "#C8E6C9",
boxShadow: "inset 0 0 15px rgba(56, 142, 60, 0.15)",
padding: "30px",
borderRadius: "15px",
width: "350px",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: 'column',
color: "#1a73e8",
padding: '40px',
flexDirection: "column",
},
input: {
width: '100%',
marginTop: '20px',
"& > input": {
width: "calc(100% - 30px)",
padding: "12px 15px",
border: "1px solid #ddd",
borderRadius: "6px",
fontSize: "16px",
transition: "border-color 0.3s",
"&:focus": {
outline: "none",
borderColor: "#1a73e8",
boxShadow: "0 0 0 2px rgba(26, 115, 232, 0.2)",
title: {
color: "#2c9678",
marginTop: 0,
marginBottom: "25px",
},
"&:hover": {
borderColor: "#1a73e8",
}
},
},
button: {
marginTop: '20px',
width: '100%',
"& > button": {
width: "100%",
padding: "12px",
background: "#1a73e8",
color: "white",
border: "none",
borderRadius: "6px",
fontSize: "16px",
cursor: "pointer",
transition: "background 0.3s",
"&:hover": {
background: "#1557b0",
},
},
},
inputContainer: {
position: 'relative',
width: '100%',
marginTop: '20px',
marginTop: '15px',
},
inputField: {
width: "calc(100% - 52px)",
padding: "12px 35px 12px 15px",
border: "1px solid #ddd",
borderRadius: "6px",
width: "100%",
padding: "11px",
border: "2px solid #ddd",
borderRadius: "5px",
fontSize: "16px",
boxSizing: "border-box",
transition: "border-color 0.3s",
background: "rgba(255,255,255,0.8)",
"&:focus": {
outline: "none",
borderColor: "#1a73e8",
boxShadow: "0 0 0 2px rgba(26, 115, 232, 0.2)",
borderColor: "#2c9678",
},
"&:hover": {
borderColor: "#1a73e8",
borderColor: "#2c9678",
}
},
iconButton: {
@@ -110,12 +64,19 @@ const useClass = createUseStyles({
"&:hover": {
color: '#333',
}
},
button: {
marginTop: '25px',
width: '100%',
"& > button": {
width: "100%",
},
}
})
export const Login: React.FC = () => {
const classes = useClass()
const {login} = useAuth()
const { login } = useAuth()
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [showPassword, setShowPassword] = useState(false)
@@ -130,12 +91,9 @@ export const Login: React.FC = () => {
}
return <div className={classes.container}>
<CloudBackground/>
<div className={classes.login_container}>
<div className={classes.form}>
<h2>UShare</h2>
<h2 className={classes.title}>UShare</h2>
{/* 用户名输入框 */}
<div className={classes.inputContainer}>
<input
className={classes.inputField}
@@ -154,7 +112,6 @@ export const Login: React.FC = () => {
)}
</div>
{/* 密码输入框 */}
<div className={classes.inputContainer}>
<input
className={classes.inputField}
@@ -170,13 +127,12 @@ export const Login: React.FC = () => {
onMouseLeave={() => setShowPassword(false)}
style={{ right: '10px', fontSize: '12px' }}
>
{showPassword ? "👁" : "👁"}
{showPassword ? "🫣" : "🙈"}
</button>
</div>
<div className={classes.button}>
<button onClick={onLogin}></button>
</div>
<UButton onClick={onLogin}></UButton>
</div>
</div>
</div>

View File

@@ -11,6 +11,12 @@ const useUploadStyle = createUseStyles({
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "50vh",
"@media (max-width: 768px)": {
minHeight: "auto",
padding: "20px 10px",
},
},
form: {
backgroundColor: "#C8E6C9",
@@ -19,10 +25,20 @@ const useUploadStyle = createUseStyles({
borderRadius: "15px",
width: "70%",
margin: "20px 60px 20px 0",
/*todo margin 不用 px*/
"@media (max-width: 768px)": {
width: "90%",
margin: "20px 0",
padding: "20px",
},
},
title: {
color: "#2c9678"
color: "#2c9678",
"@media (max-width: 768px)": {
fontSize: "1.5rem",
marginBottom: "15px",
},
},
file: {
display: 'none',
@@ -33,7 +49,11 @@ const useUploadStyle = createUseStyles({
},
name: {
color: "#2c9678",
marginLeft: '10px'
marginLeft: '10px',
"@media (max-width: 768px)": {
fontSize: "14px",
},
},
clean: {
borderRadius: '50%',
@@ -48,14 +68,12 @@ const useShowStyle = createUseStyles({
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative", // 为关闭按钮提供定位基准
minHeight: "50vh",
"@media (max-width: 768px)": {
minHeight: "auto",
padding: "20px 10px",
},
title: {
color: "#2c9678",
marginTop: 0,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
form: {
backgroundColor: "#C8E6C9",
@@ -65,6 +83,25 @@ const useShowStyle = createUseStyles({
width: "70%",
margin: "20px 60px 20px 0",
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: {
position: "absolute",
@@ -78,9 +115,7 @@ const useShowStyle = createUseStyles({
height: "24px",
cursor: "pointer",
"&:hover": {
// background: "#cc0000",
boxShadow: "20px 20px 60px #fff, -20px -20px 60px #fff",
// boxShadow: "20px 20px 60px #eee",
},
},
codeWrapper: {
@@ -89,6 +124,10 @@ const useShowStyle = createUseStyles({
borderRadius: "8px",
margin: "15px 0",
overflowX: "auto",
"@media (max-width: 768px)": {
padding: "0 10px",
},
},
pre: {
display: 'flex',
@@ -98,6 +137,12 @@ const useShowStyle = createUseStyles({
height: '24px',
"& > code": {
marginLeft: "0",
fontSize: "14px",
wordBreak: "break-all",
"@media (max-width: 768px)": {
fontSize: "12px",
},
}
},
copyButton: {
@@ -112,6 +157,11 @@ const useShowStyle = createUseStyles({
"&:hover": {
background: "#1f6d5a",
},
"@media (max-width: 768px)": {
padding: "6px 12px",
fontSize: "12px",
},
},
});

View File

@@ -5,6 +5,10 @@ const useStyle = createUseStyles({
backgroundColor: 'lightgray',
position: "relative",
overflow: "hidden",
"@media (max-width: 768px)": {
display: "none",
},
},
left: {
backgroundColor: "#e3f2fd",

View File

@@ -1,6 +1,6 @@
import {createUseStyles} from "react-jss";
import {UButton} from "../../../component/button/u-button.tsx";
import {useStore} from "../../../store/share.ts";
import { createUseStyles } from "react-jss";
import { UButton } from "../../../component/button/u-button.tsx";
import { useStore } from "../../../store/share.ts";
const useStyle = createUseStyles({
container: {
@@ -8,6 +8,12 @@ const useStyle = createUseStyles({
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "50vh",
"@media (max-width: 768px)": {
minHeight: "auto",
padding: "20px 10px",
},
},
form: {
backgroundColor: "#BBDEFB",
@@ -16,25 +22,58 @@ const useStyle = createUseStyles({
borderRadius: "15px",
width: "70%",
margin: "20px 0 20px 60px",
/*todo margin 不用 px*/
"@media (max-width: 768px)": {
width: "90%",
margin: "20px 0",
padding: "20px",
},
},
title: {
color: '#1661ab', // 靛青
color: '#1661ab',
"@media (max-width: 768px)": {
fontSize: "1.5rem",
marginBottom: "15px",
},
},
code: {
padding: '11px',
margin: '20px 0',
margin: '0',
width: '200px',
border: '2px solid #ddd',
borderRadius: '5px',
'&:active': {
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 = () => {
const style = useStyle()
const {code, setCode} = useStore()
const { code, setCode } = useStore()
function onCodeChange(e: React.ChangeEvent<HTMLInputElement>) {
setCode(e.currentTarget.value)
@@ -52,7 +91,7 @@ export const PanelRight = () => {
return <div className={style.container}>
<div className={style.form}>
<h2 className={style.title}></h2>
<div>
<div className={style.inputContainer}>
<input
type="text"
className={style.code}
@@ -60,7 +99,9 @@ export const PanelRight = () => {
value={code}
onChange={onCodeChange}
/>
<UButton style={{marginLeft: '10px'}} onClick={onFetchFile}></UButton>
<div className={style.buttonContainer}>
<UButton onClick={onFetchFile}></UButton>
</div>
</div>
</div>
</div>

View File

@@ -13,6 +13,12 @@ const useStyle = createUseStyles({
height: "100vh",
display: "grid",
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()
return <div className={style.container}>
<PanelLeft />
<PanelMid/>
<PanelMid />
<PanelRight/>
</div>
};

View File

@@ -5,6 +5,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://127.0.0.1:9119',

View File

@@ -2,19 +2,20 @@ package api
import (
"context"
"net"
"net/http"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/nf/nft/tool"
"github.com/loveuer/ushare/internal/handler"
"github.com/loveuer/ushare/internal/opt"
"net"
"net/http"
)
func Start(ctx context.Context) <-chan struct{} {
app := nf.New(nf.Config{BodyLimit: 10 * 1024 * 1024 * 1024})
app.Get("/api/available", func(c *nf.Ctx) error {
app.Get("/api/healthz", func(c *nf.Ctx) error {
return c.SendStatus(http.StatusOK)
})
@@ -33,6 +34,9 @@ func Start(ctx context.Context) <-chan struct{} {
api.Get("/ws", handler.LocalWS())
}
// 静态文件服务 - 作为中间件处理
app.Use(handler.ServeFrontendMiddleware())
ready := make(chan struct{})
ln, err := net.Listen("tcp", opt.Cfg.Address)
if err != nil {

View File

@@ -21,11 +21,11 @@ func (um *userManager) Login(username string, password string) (*model.User, err
now = time.Now()
)
if username != "admin" {
if username != opt.Cfg.Username {
return nil, errors.New("账号或密码错误")
}
if !tool.ComparePassword(password, opt.Cfg.Auth) {
if !tool.ComparePassword(password, opt.Cfg.Password) {
return nil, errors.New("账号或密码错误")
}

View File

@@ -21,7 +21,7 @@ func AuthVerify() nf.HandlerFunc {
}
return func(c *nf.Ctx) error {
if opt.Cfg.Auth == "" {
if opt.Cfg.Username == "" || opt.Cfg.Password == "" {
return c.Next()
}

114
internal/handler/static.go Normal file
View File

@@ -0,0 +1,114 @@
package handler
import (
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/static"
"io"
"io/fs"
"net/http"
"strings"
)
func ServeFrontend() nf.HandlerFunc {
assets := static.Frontend()
return func(c *nf.Ctx) error {
path := strings.TrimPrefix(c.Path(), "/")
if path == "" || path == "/" {
path = "index.html"
}
file, err := assets.Open(path)
if err != nil {
if err.Error() == "file does not exist" {
return serveIndex(assets, c)
}
return c.SendStatus(http.StatusNotFound)
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return c.SendStatus(http.StatusInternalServerError)
}
if stat.IsDir() {
return serveIndex(assets, c)
}
io.Copy(c.Writer, file)
return nil
}
}
func ServeFrontendMiddleware() nf.HandlerFunc {
assets := static.Frontend()
return func(c *nf.Ctx) error {
path := c.Path()
if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/ushare") {
return c.Next()
}
filePath := strings.TrimPrefix(path, "/")
if filePath == "" || filePath == "/" {
filePath = "index.html"
}
file, err := assets.Open(filePath)
if err != nil {
return serveIndex(assets, c)
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return c.SendStatus(http.StatusInternalServerError)
}
if stat.IsDir() {
return serveIndex(assets, c)
}
c.SetHeader("Content-Type", getContentType(filePath))
io.Copy(c.Writer, file)
return nil
}
}
func serveIndex(assets fs.FS, c *nf.Ctx) error {
index, err := assets.Open("index.html")
if err != nil {
log.Error("failed to open index.html: %v", err)
return c.SendStatus(http.StatusInternalServerError)
}
defer index.Close()
c.SetHeader("Content-Type", "text/html; charset=utf-8")
io.Copy(c.Writer, index)
return nil
}
func getContentType(path string) string {
if strings.HasSuffix(path, ".html") {
return "text/html; charset=utf-8"
}
if strings.HasSuffix(path, ".css") {
return "text/css; charset=utf-8"
}
if strings.HasSuffix(path, ".js") {
return "application/javascript; charset=utf-8"
}
if strings.HasSuffix(path, ".png") {
return "image/png"
}
if strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".jpeg") {
return "image/jpeg"
}
if strings.HasSuffix(path, ".svg") {
return "image/svg+xml"
}
return "application/octet-stream"
}

View File

@@ -4,13 +4,15 @@ import (
"context"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/pkg/tool"
"os"
)
type config struct {
Debug bool
Address string
DataPath string
Auth string
Username string
Password string
CleanInterval int
}
@@ -19,8 +21,22 @@ var (
)
func Init(_ context.Context) {
if Cfg.Auth != "" {
Cfg.Auth = tool.NewPassword(Cfg.Auth)
log.Debug("opt.Init: encrypted password = %s", Cfg.Auth)
if Cfg.Username == "" {
Cfg.Username = "admin"
}
if Cfg.Password == "" {
Cfg.Password = "ushare@123"
}
Cfg.Password = tool.NewPassword(Cfg.Password)
log.Debug("opt.Init: username = %s, encrypted password = %s", Cfg.Username, Cfg.Password)
}
func LoadFromEnv() {
if username := os.Getenv("USHARE_USERNAME"); username != "" {
Cfg.Username = username
}
if password := os.Getenv("USHARE_PASSWORD"); password != "" {
Cfg.Password = password
}
}

14
internal/static/static.go Normal file
View File

@@ -0,0 +1,14 @@
package static
import (
"embed"
"io/fs"
)
//go:embed frontend/dist
var FrontendFS embed.FS
func Frontend() fs.FS {
sub, _ := fs.Sub(FrontendFS, "frontend/dist")
return sub
}

View File

@@ -16,10 +16,11 @@ func init() {
flag.BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode")
flag.StringVar(&opt.Cfg.Address, "address", "0.0.0.0:9119", "")
flag.StringVar(&opt.Cfg.DataPath, "data", "/data", "")
flag.StringVar(&opt.Cfg.Auth, "auth", "", "auth required(admin, password)")
flag.IntVar(&opt.Cfg.CleanInterval, "clean", 24, "清理文件的周期, 单位: 小时, 0 则表示不自动清理")
flag.Parse()
opt.LoadFromEnv()
if opt.Cfg.Debug {
log.SetLogLevel(log.LogLevelDebug)
tool.TablePrinter(opt.Cfg)

49
make.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
set -e
echo "=========================================="
echo " Building UShare Single Binary"
echo "=========================================="
echo ""
# 清理旧的构建产物
echo "[Cleanup] Removing old build files..."
rm -rf dist
rm -f ushare
rm -rf internal/static/frontend
# 构建前端
echo ""
echo "[Frontend] Building..."
cd frontend
pnpm run build
cd ..
# 复制前端构建产物到 internal/static
echo "[Frontend] Copying dist files..."
mkdir -p internal/static/frontend
cp -r frontend/dist internal/static/frontend/
# 构建后端(包含嵌入的前端文件)
echo ""
echo "[Backend] Building with embedded frontend..."
mkdir -p dist
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '-s -w' -o dist/ushare .
# 清理临时文件
echo ""
echo "[Cleanup] Removing temporary files..."
rm -rf internal/static/frontend
echo ""
echo "=========================================="
echo " Build Complete!"
echo " Binary: dist/ushare"
echo "=========================================="
echo ""
echo "Usage:"
echo " ./dist/ushare -debug -address 0.0.0.0:9119 -data ./data -auth \"admin:password\""
echo ""
echo " Development: ./dev.sh"
echo " Production: ./make.sh && ./dist/ushare ..."