feat: implement single binary build and env-based auth
- 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
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -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
250
AGENTS.md
Normal 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
|
||||
```
|
||||
63
dev.sh
Executable file
63
dev.sh
Executable 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
|
||||
@@ -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)",
|
||||
},
|
||||
"&:hover": {
|
||||
borderColor: "#1a73e8",
|
||||
}
|
||||
},
|
||||
title: {
|
||||
color: "#2c9678",
|
||||
marginTop: 0,
|
||||
marginBottom: "25px",
|
||||
},
|
||||
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,53 +91,48 @@ export const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
return <div className={classes.container}>
|
||||
<CloudBackground/>
|
||||
<div className={classes.login_container}>
|
||||
<div className={classes.form}>
|
||||
<h2>UShare</h2>
|
||||
<div className={classes.form}>
|
||||
<h2 className={classes.title}>UShare</h2>
|
||||
|
||||
{/* 用户名输入框 */}
|
||||
<div className={classes.inputContainer}>
|
||||
<input
|
||||
className={classes.inputField}
|
||||
placeholder="请输入账号"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
{username && (
|
||||
<button
|
||||
className={classes.iconButton}
|
||||
onClick={() => setUsername("")}
|
||||
style={{ right: '10px', fontSize: '16px' }}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 密码输入框 */}
|
||||
<div className={classes.inputContainer}>
|
||||
<input
|
||||
className={classes.inputField}
|
||||
placeholder="请输入密码"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className={classes.inputContainer}>
|
||||
<input
|
||||
className={classes.inputField}
|
||||
placeholder="请输入账号"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
{username && (
|
||||
<button
|
||||
className={classes.iconButton}
|
||||
onMouseDown={() => setShowPassword(true)}
|
||||
onMouseUp={() => setShowPassword(false)}
|
||||
onMouseLeave={() => setShowPassword(false)}
|
||||
style={{ right: '10px', fontSize: '12px' }}
|
||||
onClick={() => setUsername("")}
|
||||
style={{ right: '10px', fontSize: '16px' }}
|
||||
>
|
||||
{showPassword ? "👁" : "👁"}
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={classes.button}>
|
||||
<button onClick={onLogin}>登录</button>
|
||||
</div>
|
||||
<div className={classes.inputContainer}>
|
||||
<input
|
||||
className={classes.inputField}
|
||||
placeholder="请输入密码"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classes.iconButton}
|
||||
onMouseDown={() => setShowPassword(true)}
|
||||
onMouseUp={() => setShowPassword(false)}
|
||||
onMouseLeave={() => setShowPassword(false)}
|
||||
style={{ right: '10px', fontSize: '12px' }}
|
||||
>
|
||||
{showPassword ? "🫣" : "🙈"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={classes.button}>
|
||||
<UButton onClick={onLogin}>登录</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("账号或密码错误")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
114
internal/handler/static.go
Normal 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"
|
||||
}
|
||||
@@ -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
14
internal/static/static.go
Normal 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
|
||||
}
|
||||
3
main.go
3
main.go
@@ -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
49
make.sh
Executable 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 ..."
|
||||
Reference in New Issue
Block a user