11 Commits
v0.3.0 ... dev

Author SHA1 Message Date
loveuer
9fb248dff0 wip: v0.2.8 dialog 美化 2025-06-18 23:01:08 +08:00
loveuer
1b4ba1cb61 wip: v0.2.7(发送消息) 2025-06-17 22:45:22 +08:00
loveuer
e5f7b5e6dc wip: 0.2.8
发送成功
2025-05-30 16:24:42 +08:00
loveuer
21287e0874 wip: 0.2.7
实现了 rtc 握手和打开数据通道
2025-05-26 17:40:06 +08:00
loveuer
5bc695bde3 wip: v0.2.6 2025-05-26 09:37:23 +08:00
loveuer
b8645a68ed wip: 0.2.5
还未实现 rtc 握手
2025-05-23 18:01:16 +08:00
loveuer
013670b78f wip: 0.2.4
还未实现 rtc 握手
2025-05-22 17:57:36 +08:00
loveuer
16e9d663f4 wip: 0.2.3
1. websocket hook
  2. rtc init ok
2025-05-20 18:04:37 +08:00
loveuer
becbc137c5 Merge tag 'v0.1.4' 2025-05-16 16:52:17 +08:00
loveuer
d41d516f19 fix: 0.1.4
All checks were successful
/ build ushare (push) Successful in 36s
/ clean (push) Successful in 0s
1. meta clean goroutine walk error
  2. clean interval to args(--clean)
2025-05-16 16:50:22 +08:00
loveuer
0a24393dcb fix: 0.1.4
All checks were successful
/ build ushare (push) Successful in 51s
/ clean (push) Successful in 0s
1. meta clean goroutine walk error
  2. clean interval to args(--clean)
2025-05-16 16:48:28 +08:00
32 changed files with 286 additions and 945 deletions

16
.gitignore vendored
View File

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

250
AGENTS.md
View File

@@ -1,250 +0,0 @@
# 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
```

View File

@@ -25,7 +25,8 @@ COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
COPY --from=backend-builder /app/ushare /usr/local/bin/ushare COPY --from=backend-builder /app/ushare /usr/local/bin/ushare
# 配置 Nginx # 配置 Nginx
COPY deployment/nginx.conf /etc/nginx/nginx.conf RUN rm /etc/nginx/conf.d/default.conf
COPY deployment/nginx.conf /etc/nginx/conf.d
COPY deployment/entrypoint.sh /usr/local/bin/entrypoint.sh COPY deployment/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh

View File

@@ -1,26 +1,3 @@
user root;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server { server {
listen 80; listen 80;
@@ -36,25 +13,11 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
location /api/ulocal/ws {
proxy_pass http://localhost:9119;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /ushare { location /ushare {
proxy_pass http://localhost:9119; proxy_pass http://localhost:9119;
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_max_temp_file_size 0;
proxy_request_buffering off;
proxy_buffering off;
client_max_body_size 5M;
}
} }
} }

63
dev.sh
View File

@@ -1,63 +0,0 @@
#!/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

@@ -16,6 +16,7 @@ export const useFileUpload = () => {
setProgress(0); setProgress(0);
try { try {
console.log(`[D] api.Upload: upload file = ${file.name}, size = ${file.size}`, file);
const url = `/api/ushare/${file.name}`; const url = `/api/ushare/${file.name}`;
// 1. 初始化上传 // 1. 初始化上传
@@ -25,7 +26,7 @@ export const useFileUpload = () => {
}); });
if (!res1.ok) { if (!res1.ok) {
console.log(`[W] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`) console.log(`[D] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`)
if (res1.status === 401) { if (res1.status === 401) {
window.location.href = "/login?next=/share" window.location.href = "/login?next=/share"
return "" return ""

View File

@@ -17,8 +17,7 @@ const useClass = createUseStyles({
background: "rgba(0, 0, 0, 0.5)", background: "rgba(0, 0, 0, 0.5)",
backdropFilter: "blur(2px)" backdropFilter: "blur(2px)"
}, },
background: "rgba(212,212,212,0.85)", background: "rgba(255, 255, 255, 0.4)",
backdropFilter: "blur(8px)",
}, },
dialog_content: { dialog_content: {
padding: "1.5rem", padding: "1.5rem",

View File

@@ -50,6 +50,7 @@ export const Drawer: React.FC<DrawerProps> = ({
useEffect(() => { useEffect(() => {
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('[D] escape close:', close)
close() close()
} }
}; };

View File

@@ -1,87 +1,6 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import {Dialog} from "../../../component/dialog/dialog.tsx"; import {Dialog} from "../../../component/dialog/dialog.tsx";
import {ReceivedMessage} from "./types.ts"; import {ReceivedMessage} from "./types.ts";
import {createUseStyles} from "react-jss";
const useStyles = createUseStyles({
root: {
marginBottom: '1rem',
},
sender: {
margin: '0 0 0.5rem 0',
fontSize: '0.9rem',
color: '#666',
},
msgBox: {
background: 'rgba(255,255,255,0.96)',
color: '#222',
padding: '1rem',
borderRadius: '4px',
border: '1px solid #ddd',
maxHeight: '400px',
minHeight: '80px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
},
downloadLink: {
color: '#007aff',
textDecoration: 'underline',
},
btnRow: {
display: 'flex',
flexDirection: 'row',
gap: '0.5rem',
justifyContent: 'flex-end',
alignItems: 'center',
marginTop: '8px',
},
copySuccess: {
color: '#28a745',
fontSize: '0.9rem',
marginRight: '0.5rem',
animation: 'fadeIn 0.3s ease-in',
},
copyBtn: {
padding: '8px 16px',
background: '#007aff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s',
'&:hover': {
background: '#0056b3',
},
},
copyBtnSuccess: {
background: '#28a745',
},
closeBtn: {
padding: '8px 16px',
background: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s',
},
progressBar: {
width: '100%',
height: 8,
background: '#eee',
borderRadius: 4,
margin: '12px 0',
overflow: 'hidden',
},
progressInner: {
height: '100%',
background: '#4dabf7',
transition: 'width 0.3s',
},
});
interface MessageDialogProps { interface MessageDialogProps {
open: boolean; open: boolean;
@@ -91,14 +10,16 @@ interface MessageDialogProps {
export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onClose}) => { export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onClose}) => {
const [copySuccess, setCopySuccess] = useState(false); const [copySuccess, setCopySuccess] = useState(false);
const classes = useStyles();
const handleCopyMessage = () => { const handleCopyMessage = () => {
if (message) { if (message) {
navigator.clipboard.writeText(message.text || '').then(() => { navigator.clipboard.writeText(message.text || '').then(() => {
console.log('消息已复制到剪贴板');
setCopySuccess(true); setCopySuccess(true);
// 2秒后隐藏成功提示
setTimeout(() => setCopySuccess(false), 2000); setTimeout(() => setCopySuccess(false), 2000);
}).catch(() => { }).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动复制'); alert('复制失败,请手动复制');
}); });
} }
@@ -111,49 +32,82 @@ export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onCl
onClose={onClose} onClose={onClose}
footer={false} footer={false}
> >
<div className={classes.root}> <div style={{ marginBottom: '1rem' }}>
<p className={classes.sender}> <p style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem', color: '#666' }}>
: {message?.sender} : {message?.sender}
</p> </p>
<div className={classes.msgBox}> <div style={{
background: '#f5f5f5',
padding: '1rem',
borderRadius: '4px',
border: '1px solid #ddd',
maxHeight: '200px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{message?.isFile ? ( {message?.isFile ? (
<> <>
<div>📎 : {message.fileName} ({message.fileSize ? (message.fileSize/1024).toFixed(1) : ''} KB)</div> <div>📎 : {message.fileName} ({message.fileSize ? (message.fileSize/1024).toFixed(1) : ''} KB)</div>
{message.receiving ? ( <a href={message.fileBlobUrl} download={message.fileName} style={{color:'#007aff',textDecoration:'underline'}}>
<>
<div style={{fontSize: '0.95em', color: '#888'}}>... {Math.round((message.progress||0)*100)}%</div>
<div className={classes.progressBar}>
<div className={classes.progressInner} style={{width: `${Math.round((message.progress||0)*100)}%`}} />
</div>
</>
) : (
<a href={message.fileBlobUrl} download={message.fileName} className={classes.downloadLink}>
</a> </a>
)}
</> </>
) : ( ) : (
message?.text message?.text
)} )}
</div> </div>
</div> </div>
<div className={classes.btnRow}> <div style={{ display: 'flex', flexDirection: 'row', gap: '0.5rem', justifyContent: 'flex-end', alignItems: 'center', marginTop: '8px' }}>
{copySuccess && ( {copySuccess && (
<span className={classes.copySuccess}> <span style={{
color: '#28a745',
fontSize: '0.9rem',
marginRight: '0.5rem',
animation: 'fadeIn 0.3s ease-in'
}}>
</span> </span>
)} )}
{!message?.isFile && ( {!message?.isFile && (
<button <button
onClick={handleCopyMessage} onClick={handleCopyMessage}
className={copySuccess ? `${classes.copyBtn} ${classes.copyBtnSuccess}` : classes.copyBtn} style={{
padding: '8px 16px',
background: copySuccess ? '#28a745' : '#007aff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
if (!copySuccess) {
e.currentTarget.style.backgroundColor = '#0056b3';
}
}}
onMouseLeave={(e) => {
if (!copySuccess) {
e.currentTarget.style.backgroundColor = '#007aff';
}
}}
> >
{copySuccess ? '已复制' : '复制消息'} {copySuccess ? '已复制' : '复制消息'}
</button> </button>
)} )}
<button <button
onClick={onClose} onClick={onClose}
className={classes.closeBtn} style={{
padding: '8px 16px',
background: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s'
}}
> >
</button> </button>

View File

@@ -1,4 +1,5 @@
import { WSMessage, ReceivedMessage} from "./types.ts"; import {Resp} from "../../../interface/response.ts";
import {Client, WSMessage, ReceivedMessage} from "./types.ts";
import {useLocalStore} from "../../../store/local.ts"; import {useLocalStore} from "../../../store/local.ts";
// 文件接收缓存 // 文件接收缓存
@@ -8,7 +9,7 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
if (chunk.type === 'file') { if (chunk.type === 'file') {
// 文件元信息,初始化缓存 // 文件元信息,初始化缓存
fileReceiveCache[chunk.name + '_' + chunk.timestamp] = { fileReceiveCache[chunk.name + '_' + chunk.timestamp] = {
chunks: new Array(chunk.totalChunks), chunks: [],
total: chunk.totalChunks, total: chunk.totalChunks,
received: 0, received: 0,
name: chunk.name, name: chunk.name,
@@ -16,16 +17,6 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
sender: chunk.sender, sender: chunk.sender,
timestamp: chunk.timestamp timestamp: chunk.timestamp
}; };
// 首次弹出进度
onFileReceived({
sender: chunk.sender,
timestamp: chunk.timestamp,
fileName: chunk.name,
fileSize: chunk.size,
isFile: true,
progress: 0,
receiving: true
});
} else if (chunk.type === 'file-chunk') { } else if (chunk.type === 'file-chunk') {
const key = chunk.name + '_' + chunk.timestamp; const key = chunk.name + '_' + chunk.timestamp;
const cache = fileReceiveCache[key]; const cache = fileReceiveCache[key];
@@ -34,23 +25,8 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
const uint8 = new Uint8Array(chunk.data); const uint8 = new Uint8Array(chunk.data);
cache.chunks[chunk.chunkIndex] = uint8.buffer; cache.chunks[chunk.chunkIndex] = uint8.buffer;
cache.received++; cache.received++;
// 实时回调进度 // 全部收到
if (cache.received < cache.total) { if (cache.received === cache.total) {
onFileReceived({
sender: cache.sender,
timestamp: cache.timestamp,
fileName: cache.name,
fileSize: cache.size,
isFile: true,
progress: cache.received / cache.total,
receiving: true
});
}
}
} else if (chunk.type === 'file-end') {
const key = chunk.name + '_' + chunk.timestamp;
const cache = fileReceiveCache[key];
if (cache && cache.received === cache.total) {
const blob = new Blob(cache.chunks); const blob = new Blob(cache.chunks);
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
onFileReceived({ onFileReceived({
@@ -59,23 +35,10 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
fileName: cache.name, fileName: cache.name,
fileSize: cache.size, fileSize: cache.size,
fileBlobUrl: url, fileBlobUrl: url,
isFile: true, isFile: true
progress: 1,
receiving: false
}); });
delete fileReceiveCache[key]; delete fileReceiveCache[key];
} else if (cache) { }
// 分块未齐全,提示异常
onFileReceived({
sender: cache.sender,
timestamp: cache.timestamp,
fileName: cache.name,
fileSize: cache.size,
isFile: true,
progress: cache.received / cache.total,
receiving: true,
text: '文件分块未齐全,接收失败'
});
} }
} }
}; };
@@ -97,18 +60,21 @@ export class RTCHandler {
// 更新回调函数的方法 // 更新回调函数的方法
updateCallbacks = (newCallbacks: RTCHandlerCallbacks) => { updateCallbacks = (newCallbacks: RTCHandlerCallbacks) => {
console.log('[D] Updating RTC handler callbacks');
this.callbacks = newCallbacks; this.callbacks = newCallbacks;
}; };
setupDataChannel = async (ch: RTCDataChannel, type: 'sender' | 'receiver') => { setupDataChannel = async (ch: RTCDataChannel, type: 'sender' | 'receiver') => {
console.log(`[D] Setting up data channel for type: ${type}`);
ch.onopen = () => { ch.onopen = () => {
console.log(`[D] 通道已打开!类型: ${type}`); console.log(`[D] 通道已打开!类型: ${type}`);
console.log('[D] Calling onChannelOpen callback with type:', type);
this.callbacks.onChannelOpen(type); this.callbacks.onChannelOpen(type);
useLocalStore.getState().setChannel(ch); useLocalStore.getState().setChannel(ch);
}; };
ch.onmessage = (e) => { ch.onmessage = (e) => {
// console.log('[D] Received message:', e.data); console.log('[D] Received message:', e.data);
try { try {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'message') { if (data.type === 'message') {
@@ -118,9 +84,10 @@ export class RTCHandler {
timestamp: Date.now(), timestamp: Date.now(),
sender: data.sender || '未知用户' sender: data.sender || '未知用户'
}; };
console.log('[D] Calling onMessageReceived callback with message:', message);
this.callbacks.onMessageReceived(message); this.callbacks.onMessageReceived(message);
} else if (data.type === 'file' || data.type === 'file-chunk' || data.type === 'file-end') { } else if (data.type === 'file') {
// 处理文件相关消息 // 处理文件消息
handleFileChunk(data, this.callbacks.onMessageReceived); handleFileChunk(data, this.callbacks.onMessageReceived);
} }
} catch (error) { } catch (error) {
@@ -130,23 +97,27 @@ export class RTCHandler {
timestamp: Date.now(), timestamp: Date.now(),
sender: '未知用户' sender: '未知用户'
}; };
console.log('[D] Calling onMessageReceived callback with plain text message:', message);
this.callbacks.onMessageReceived(message); this.callbacks.onMessageReceived(message);
} }
}; };
ch.onclose = () => { ch.onclose = () => {
console.log('[D] 通道关闭'); console.log('[D] 通道关闭');
console.log('[D] Calling onChannelClose callback');
this.callbacks.onChannelClose(); this.callbacks.onChannelClose();
useLocalStore.getState().setChannel(); useLocalStore.getState().setChannel();
}; };
}; };
handleBubbleClick = async (bubbleId: string, currentUserId: string) => { handleBubbleClick = async (bubbleId: string, currentUserId: string) => {
console.log(`[D] click id = ${bubbleId}`);
const current_rtc = this.rtcRef.current; const current_rtc = this.rtcRef.current;
if (!current_rtc) return; if (!current_rtc) return;
current_rtc.onnegotiationneeded = async () => { current_rtc.onnegotiationneeded = async () => {
const offer = await current_rtc.createOffer(); const offer = await current_rtc.createOffer();
console.log('[D] offer created', offer);
await current_rtc.setLocalDescription(offer); await current_rtc.setLocalDescription(offer);
const data = { const data = {
id: bubbleId, id: bubbleId,
@@ -161,6 +132,7 @@ export class RTCHandler {
}; };
current_rtc.onicecandidate = async (e) => { current_rtc.onicecandidate = async (e) => {
console.log('[D] on candidate ', e);
await fetch('/api/ulocal/candidate', { await fetch('/api/ulocal/candidate', {
method: 'POST', method: 'POST',
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
@@ -176,7 +148,7 @@ export class RTCHandler {
let current_id: string; let current_id: string;
let current_rtc: RTCPeerConnection | null; let current_rtc: RTCPeerConnection | null;
const msg = JSON.parse(e.data) as WSMessage; const msg = JSON.parse(e.data) as WSMessage;
// console.log('[D] ws event msg =', msg); console.log('[D] ws event msg =', msg);
switch (msg.type) { switch (msg.type) {
case "enter": case "enter":
@@ -241,6 +213,7 @@ export class RTCHandler {
return; return;
} }
if (!candidate_data.candidate) { if (!candidate_data.candidate) {
console.log('[W] candidate data null');
return; return;
} }
await current_rtc.addIceCandidate(candidate_data.candidate); await current_rtc.addIceCandidate(candidate_data.candidate);
@@ -250,8 +223,10 @@ export class RTCHandler {
sendMessage = (msg: string, files: File[], senderName: string) => { sendMessage = (msg: string, files: File[], senderName: string) => {
const ch = useLocalStore.getState().channel; const ch = useLocalStore.getState().channel;
console.log('[D] ready to send:', msg, files, ch);
const CHUNK_SIZE = 64 * 1024; // 64KB const CHUNK_SIZE = 64 * 1024; // 64KB
const BUFFERED_AMOUNT_THRESHOLD = 1 * 1024 * 1024; // 1MB const BUFFERED_AMOUNT_THRESHOLD = 1 * 1024 * 1024; // 1MB
if (ch && ch.readyState === 'open') { if (ch && ch.readyState === 'open') {
if (msg.trim()) { if (msg.trim()) {
// 发送文本消息 // 发送文本消息
@@ -279,28 +254,23 @@ export class RTCHandler {
}; };
ch.send(JSON.stringify(fileData)); ch.send(JSON.stringify(fileData));
ch.bufferedAmountLowThreshold = BUFFERED_AMOUNT_THRESHOLD;
let offset = 0; let offset = 0;
let chunkIndex = 0; let chunkIndex = 0;
const reader = new FileReader(); const reader = new FileReader();
const sendNextChunk = () => { function sendNextChunk() {
if (offset >= file.size) { if (offset >= file.size) return;
// 分块全部发送完毕,发送 file-end if (ch && ch.bufferedAmount && ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) {
ch.send(JSON.stringify({ // 等待缓冲区变低
type: 'file-end',
name: file.name,
timestamp,
}));
return;
}
if (ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) {
ch.addEventListener('bufferedamountlow', sendNextChunk, { once: true }); ch.addEventListener('bufferedamountlow', sendNextChunk, { once: true });
return; return;
} }
const slice = file.slice(offset, offset + CHUNK_SIZE); const slice = file.slice(offset, offset + CHUNK_SIZE);
reader.onload = (e) => { reader.onload = (e) => {
if (e.target?.result) { if (e.target?.result) {
ch.send(JSON.stringify({ ch?.send(JSON.stringify({
type: 'file-chunk', type: 'file-chunk',
name: file.name, name: file.name,
timestamp, timestamp,
@@ -314,7 +284,7 @@ export class RTCHandler {
} }
}; };
reader.readAsArrayBuffer(slice); reader.readAsArrayBuffer(slice);
}; }
sendNextChunk(); sendNextChunk();
}); });
} }

View File

@@ -6,12 +6,11 @@ interface SendDialogProps {
open: boolean; open: boolean;
onSend: (msg: string, files: File[]) => void; onSend: (msg: string, files: File[]) => void;
onClose: () => void; onClose: () => void;
name: string;
} }
export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose, name}) => { export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose}) => {
return ( return (
<Dialog open={open} title={`发送消息给${name}`} onClose={onClose}> <Dialog open={open} title="发送消息" onClose={onClose}>
<Sender onSend={onSend} /> <Sender onSend={onSend} />
</Dialog> </Dialog>
); );

View File

@@ -150,10 +150,17 @@ export const Sender: React.FC<SenderProps> = ({onSend}) => {
</div> </div>
<div className={classes.buttons}> <div className={classes.buttons}>
<input type="file" ref={fileInputRef} hidden onChange={handleFileSelect}/> <input type="file" ref={fileInputRef} hidden onChange={handleFileSelect}/>
<button className={classes.action_btn} onClick={() => { <button
className={classes.action_btn}
style={{ opacity: 0.5, cursor: 'not-allowed' }}
onClick={() => { message.warning('暂不支持文件发送') }}
>
📁
</button>
{/* <button className={classes.action_btn} onClick={() => {
fileInputRef.current && fileInputRef.current.click() fileInputRef.current && fileInputRef.current.click()
}}>📁 选择文件 }}>📁 选择文件
</button> </button> */}
<button className={classes.action_btn} onClick={handleSubmit}> </button> <button className={classes.action_btn} onClick={handleSubmit}> </button>
</div> </div>
</div> </div>

View File

@@ -33,6 +33,4 @@ export interface ReceivedMessage {
fileSize?: number; fileSize?: number;
fileBlobUrl?: string; fileBlobUrl?: string;
isFile?: boolean; isFile?: boolean;
progress?: number; // 0-1
receiving?: boolean; // 是否正在接收
} }

View File

@@ -55,15 +55,14 @@ const useClass = createUseStyles({
export const LocalSharing: React.FC = () => { export const LocalSharing: React.FC = () => {
const classes = useClass(); const classes = useClass();
const {id, name, set, } = useLocalStore(); const {id, name, set, setChannel} = useLocalStore();
const [_rtc, setRTC] = useState<RTCPeerConnection>(); const [rtc, setRTC] = useState<RTCPeerConnection>();
const rtcRef = useRef<RTCPeerConnection | null>(null); const rtcRef = useRef<RTCPeerConnection | null>(null);
const [clients, setClients] = useState<Client[]>([]); const [clients, setClients] = useState<Client[]>([]);
const {connect, close} = useWebsocket({}); const {connect, close} = useWebsocket({});
const [open, setOpen] = useState<{ send: boolean; receive: boolean }>({send: false, receive: false}); const [open, setOpen] = useState<{ send: boolean; receive: boolean }>({send: false, receive: false});
const [receivedMessage, setReceivedMessage] = useState<ReceivedMessage | null>(null); const [receivedMessage, setReceivedMessage] = useState<ReceivedMessage | null>(null);
const [showMessageDialog, setShowMessageDialog] = useState(false); const [showMessageDialog, setShowMessageDialog] = useState(false);
const [receivingFile, setReceivingFile] = useState<ReceivedMessage | null>(null);
// RTC处理器的回调函数 - 使用useCallback确保稳定性 // RTC处理器的回调函数 - 使用useCallback确保稳定性
const onChannelOpen = useCallback((type: 'sender' | 'receiver') => { const onChannelOpen = useCallback((type: 'sender' | 'receiver') => {
@@ -72,16 +71,9 @@ export const LocalSharing: React.FC = () => {
}, []); }, []);
const onMessageReceived = useCallback((message: ReceivedMessage) => { const onMessageReceived = useCallback((message: ReceivedMessage) => {
if (message.isFile && message.receiving) { console.log('[D] Message received:', message);
setReceivingFile(message);
} else if (message.isFile && !message.receiving) {
setReceivingFile(null);
setReceivedMessage(message); setReceivedMessage(message);
setShowMessageDialog(true); setShowMessageDialog(true);
} else {
setReceivedMessage(message);
setShowMessageDialog(true);
}
}, []); }, []);
const onChannelClose = useCallback(() => { const onChannelClose = useCallback(() => {
@@ -101,6 +93,7 @@ export const LocalSharing: React.FC = () => {
// 更新RTC处理器的回调函数 // 更新RTC处理器的回调函数
useEffect(() => { useEffect(() => {
if (rtcHandlerRef.current) { if (rtcHandlerRef.current) {
console.log('[D] Updating RTC handler callbacks');
rtcHandlerRef.current.updateCallbacks(rtcCallbacks); rtcHandlerRef.current.updateCallbacks(rtcCallbacks);
} }
}, [rtcCallbacks]); }, [rtcCallbacks]);
@@ -115,6 +108,7 @@ export const LocalSharing: React.FC = () => {
const handleWSEvent = async (e: MessageEvent) => { const handleWSEvent = async (e: MessageEvent) => {
const msgData = JSON.parse(e.data); const msgData = JSON.parse(e.data);
console.log('[D] ws event msg =', msgData);
if (msgData.type === "enter" || msgData.type === "leave") { if (msgData.type === "enter" || msgData.type === "leave") {
await updateClients(); await updateClients();
@@ -135,6 +129,7 @@ export const LocalSharing: React.FC = () => {
const handleBubbleClick = async (bubble: any) => { const handleBubbleClick = async (bubble: any) => {
setOpen({send: true, receive: false}); setOpen({send: true, receive: false});
console.log('[D] Bubble clicked:', bubble.id, 'Current RTC handler:', rtcHandlerRef.current);
if (rtcHandlerRef.current) { if (rtcHandlerRef.current) {
try { try {
await rtcHandlerRef.current.handleBubbleClick(bubble.id, id); await rtcHandlerRef.current.handleBubbleClick(bubble.id, id);
@@ -170,6 +165,7 @@ export const LocalSharing: React.FC = () => {
const response = await fetch('/api/ulocal/register', {method: 'POST'}); const response = await fetch('/api/ulocal/register', {method: 'POST'});
const data = ((await response.json()) as Resp<{ id: string; name: string }>).data; const data = ((await response.json()) as Resp<{ id: string; name: string }>).data;
set(data.id, data.name); set(data.id, data.name);
console.log(`[D] register id = ${data.id}`);
connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent}); connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent});
await updateClients(); await updateClients();
@@ -178,6 +174,7 @@ export const LocalSharing: React.FC = () => {
setRTC(_rtc); setRTC(_rtc);
// 在RTC连接创建后立即创建处理器实例 // 在RTC连接创建后立即创建处理器实例
console.log('[D] Creating RTC handler after connection setup');
rtcHandlerRef.current = new RTCHandler(rtcRef, rtcCallbacks); rtcHandlerRef.current = new RTCHandler(rtcRef, rtcCallbacks);
return () => { return () => {
@@ -192,14 +189,6 @@ export const LocalSharing: React.FC = () => {
const bubbles = generateBubbles(clients, id); const bubbles = generateBubbles(clients, id);
useEffect(() => {
if (receivingFile && receivingFile.isFile && !receivingFile.receiving) {
setReceivingFile(null);
setReceivedMessage(receivingFile);
setShowMessageDialog(true);
}
}, [receivingFile]);
return ( return (
<div className={classes.container}> <div className={classes.container}>
<CloudBackground/> <CloudBackground/>
@@ -220,17 +209,8 @@ export const LocalSharing: React.FC = () => {
open={open.send} open={open.send}
onSend={handleSend} onSend={handleSend}
onClose={() => setOpen({send: false, receive: false})} onClose={() => setOpen({send: false, receive: false})}
name={name}
/> />
{/* 文件接收进度弹窗 */}
{receivingFile && (
<MessageDialog
open={true}
message={receivingFile}
onClose={() => setReceivingFile(null)}
/>
)}
<MessageDialog <MessageDialog
open={showMessageDialog} open={showMessageDialog}

View File

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

View File

@@ -136,6 +136,7 @@ const PanelLeftUpload: React.FC<{ set_code: (code:string) => void }> = ({set_cod
} }
function onFileChange(e: React.ChangeEvent<HTMLInputElement>) { function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log('[D] onFileChange: e =', e)
setFile(e.currentTarget.files ? e.currentTarget.files[0] : null) setFile(e.currentTarget.files ? e.currentTarget.files[0] : null)
} }

View File

@@ -42,6 +42,7 @@ export const PanelRight = () => {
async function onFetchFile() { async function onFetchFile() {
const url = `/ushare/${code}` const url = `/ushare/${code}`
console.log('[D] onFetchFile: url =', url)
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
document.body.appendChild(link); document.body.appendChild(link);

View File

@@ -1,12 +1,13 @@
import {useState} from "react"; import {useState} from "react";
import {createUseStyles} from "react-jss"; import {createUseStyles} from "react-jss";
import {Dialog} from "../../component/dialog/dialog.tsx";
const useClass = createUseStyles({ const useClass = createUseStyles({
container: {} container: {}
}) })
export const TestPage = () => { export const TestPage = () => {
const classes = useClass() const classes = useClass()
const [_open, setOpen] = useState<boolean>(false) const [open, setOpen] = useState<boolean>(false)
const handleOpen = () => { const handleOpen = () => {
setOpen(true) setOpen(true)
@@ -14,5 +15,8 @@ export const TestPage = () => {
return <div className={classes.container}> return <div className={classes.container}>
<button onClick={handleOpen}>open</button> <button onClick={handleOpen}>open</button>
<Dialog open={open} title="hello world" >
<input />
</Dialog>
</div> </div>
} }

View File

@@ -8,7 +8,7 @@ export interface LocalStore {
setChannel: (chan?: RTCDataChannel) => void; setChannel: (chan?: RTCDataChannel) => void;
} }
export const useLocalStore = create<LocalStore>()((_set, _get) => ({ export const useLocalStore = create<LocalStore>()((_set, get) => ({
id: '', id: '',
name: '', name: '',
set: (id: string, name: string) => { set: (id: string, name: string) => {

View File

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

33
go.mod
View File

@@ -3,65 +3,50 @@ module github.com/loveuer/ushare
go 1.24.2 go 1.24.2
require ( require (
github.com/glebarez/sqlite v1.11.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/loveuer/nf v0.3.5 github.com/loveuer/nf v0.3.5
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/mileusna/useragent v1.3.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/spf13/cast v1.7.1
github.com/spf13/viper v1.20.1 github.com/spf13/viper v1.20.1
golang.org/x/crypto v0.32.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.17.0 // indirect github.com/fatih/color v1.17.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mileusna/useragent v1.3.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect github.com/skeema/knownhosts v1.2.2 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.11.0 // indirect
@@ -70,8 +55,4 @@ require (
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
) )

54
go.sum
View File

@@ -1,7 +1,5 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
@@ -20,8 +18,6 @@ github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxG
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -32,10 +28,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -46,36 +38,20 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -108,9 +84,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -137,7 +110,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -172,7 +144,8 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -188,6 +161,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -195,8 +170,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -204,6 +179,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -221,20 +198,5 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=

View File

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

View File

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

View File

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

View File

@@ -1,114 +0,0 @@
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,15 +4,13 @@ import (
"context" "context"
"github.com/loveuer/nf/nft/log" "github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/pkg/tool" "github.com/loveuer/ushare/internal/pkg/tool"
"os"
) )
type config struct { type config struct {
Debug bool Debug bool
Address string Address string
DataPath string DataPath string
Username string Auth string
Password string
CleanInterval int CleanInterval int
} }
@@ -21,22 +19,8 @@ var (
) )
func Init(_ context.Context) { func Init(_ context.Context) {
if Cfg.Username == "" { if Cfg.Auth != "" {
Cfg.Username = "admin" Cfg.Auth = tool.NewPassword(Cfg.Auth)
} log.Debug("opt.Init: encrypted password = %s", Cfg.Auth)
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
} }
} }

View File

@@ -1,11 +1,10 @@
package db package db
import ( import (
"au99999/internal/opt"
"au99999/pkg/tool"
"context" "context"
"github.com/loveuer/ushare/internal/opt"
"github.com/loveuer/ushare/internal/pkg/tool"
"gorm.io/gorm" "gorm.io/gorm"
) )

View File

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

49
make.sh
View File

@@ -1,49 +0,0 @@
#!/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 ..."

View File

@@ -167,6 +167,8 @@
const text = inputBox.value; const text = inputBox.value;
const files = fileInput.files; const files = fileInput.files;
console.log('文本内容:', text);
console.log('发送文件:', Array.from(files));
// 清空内容 // 清空内容
inputBox.value = ''; inputBox.value = '';