diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7857604..7537b90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,28 @@ # 第一阶段:构建前端 FROM node:20-alpine AS frontend-builder -WORKDIR /app -RUN npm install -g pnpm -COPY frontend/package.json frontend/pnpm-lock.yaml* ./ -RUN pnpm install -COPY frontend . +RUN npm install -g pnpm --registry=https://registry.npmmirror.com +COPY frontend /app/frontend +WORKDIR /app/frontend +RUN pnpm install --registry=https://registry.npmmirror.com RUN pnpm run build # 第二阶段:构建 Golang 后端 FROM golang:alpine AS backend-builder WORKDIR /app -COPY go.mod go.sum ./ +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOPROXY=https://goproxy.cn +COPY go.mod /app/go.mod +COPY go.sum /app/go.sum RUN go mod download -COPY main.go internal ./ -RUN CGO_ENABLED=0 GOOS=linux go build -o main . +COPY main.go /app/main.go +COPY internal /app/internal +RUN go build -ldflags '-s -w' -o ushare . # 第三阶段:生成最终镜像 FROM nginx:alpine -COPY --from=frontend-builder /app/dist /usr/share/nginx/html -COPY --from=backend-builder /app/main /app/main +COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html +COPY --from=backend-builder /app/ushare /usr/local/bin/ushare # 配置 Nginx RUN rm /etc/nginx/conf.d/default.conf @@ -28,4 +32,4 @@ COPY deployment/nginx.conf /etc/nginx/conf.d EXPOSE 80 # 启动服务 -CMD sh -c "/app/main & nginx -g 'daemon off;'" \ No newline at end of file +CMD sh -c "ushare & nginx -g 'daemon off;'" \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..8f7f31a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,23 @@ - - - - - Vite + React + TS - - -
- - - + + + + + + UShare + + +
+ + + \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 6177d70..5e072f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-jss": "^10.10.0", + "react-router-dom": "^7.5.3", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5258186..1dae18f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: react-jss: specifier: ^10.10.0 version: 10.10.0(react@19.1.0) + react-router-dom: + specifier: ^7.5.3 + version: 7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) zustand: specifier: ^5.0.3 version: 5.0.3(@types/react@19.1.2)(react@19.1.0) @@ -640,6 +643,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1120,6 +1127,23 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-router-dom@7.5.3: + resolution: {integrity: sha512-cK0jSaTyW4jV9SRKAItMIQfWZ/D6WEZafgHuuCb9g+SjhLolY78qc+De4w/Cz9ybjvLzShAmaIMEXt8iF1Cm+A==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.5.3: + resolution: {integrity: sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} @@ -1155,6 +1179,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + shallow-equal@1.2.1: resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} @@ -1205,6 +1232,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1833,6 +1863,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.0.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2344,6 +2376,21 @@ snapshots: react-refresh@0.17.0: {} + react-router-dom@7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-router: 7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + + react-router@7.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + cookie: 1.0.2 + react: 19.1.0 + set-cookie-parser: 2.7.1 + turbo-stream: 2.4.0 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + react@19.1.0: {} regenerator-runtime@0.14.1: {} @@ -2388,6 +2435,8 @@ snapshots: semver@7.7.1: {} + set-cookie-parser@2.7.1: {} + shallow-equal@1.2.1: {} shebang-command@2.0.0: @@ -2429,6 +2478,8 @@ snapshots: dependencies: typescript: 5.7.3 + turbo-stream@2.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 93e149d..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {FileSharing} from "./page/share.tsx"; - -export function App() { - return -} \ No newline at end of file diff --git a/frontend/src/api/upload.ts b/frontend/src/api/upload.ts index eac8f53..0bad589 100644 --- a/frontend/src/api/upload.ts +++ b/frontend/src/api/upload.ts @@ -1,5 +1,6 @@ import { useState } from 'react'; + interface UploadRes { code: string } @@ -25,6 +26,12 @@ export const useFileUpload = () => { }); if (!res1.ok) { + console.log(`[D] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`) + if (res1.status === 401) { + window.location.href = "/login?next=/share" + return "" + } + throw new Error("上传失败<1>"); } diff --git a/frontend/src/component/fluid/cloud.tsx b/frontend/src/component/fluid/cloud.tsx new file mode 100644 index 0000000..cad0814 --- /dev/null +++ b/frontend/src/component/fluid/cloud.tsx @@ -0,0 +1,159 @@ +import { useRef, useEffect } from 'react'; + +export const CloudBackground = () => { + const canvasRef = useRef(null); + + // 完整可配置参数 + const config = { + cloudNum: 8, // 云朵数量 + maxSpeed: 3.0, // 最大水平速度 + cloudSize: 100, // 基础云朵尺寸 (新增) + sizeVariation: 0.5, // 尺寸随机变化率 (0-1) + colorVariation: 20, // 色相变化范围 + verticalOscillation: 0.5, // 垂直浮动幅度 + shapeComplexity: 5, // 形状复杂度(组成圆形数量) + boundaryOffset: 3 // 边界偏移倍数 + }; + + type Cloud = { + x: number; + y: number; + speed: number; + circles: CloudCircle[]; + color: string; + maxRadius: number; // 记录云朵最大半径 + }; + + type CloudCircle = { + offsetX: number; + offsetY: number; + radius: number; + }; + + useEffect(() => { + const canvas = canvasRef.current!; + const ctx = canvas.getContext('2d')!; + + const resize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + resize(); + + // 生成云朵形状(基于配置参数) + const createCloudShape = () => { + const circles: CloudCircle[] = []; + const circleCount = 4 + Math.floor(Math.random() * config.shapeComplexity); + + for(let i = 0; i < circleCount; i++) { + circles.push({ + offsetX: (Math.random() - 0.5) * config.cloudSize * 1.5, + offsetY: (Math.random() - 0.5) * config.cloudSize * 0.8, + radius: config.cloudSize * (1 - config.sizeVariation + Math.random() * config.sizeVariation) + }); + } + return circles; + }; + + let clouds: Cloud[] = []; + const createClouds = () => { + clouds = Array.from({ length: config.cloudNum }).map(() => { + const shape = createCloudShape(); + return { + x: Math.random() * canvas.width, + y: canvas.height * (0.2 + Math.random() * 0.6), + speed: (Math.random() * 0.5 + 0.5) * config.maxSpeed, + circles: shape, + color: `hsla(210, 30%, 95%, ${0.8 + Math.random() * 0.2})`, + maxRadius: Math.max(...shape.map(c => c.radius)) // 计算最大半径 + }; + }); + }; + + const drawCloud = (cloud: Cloud) => { + ctx.save(); + ctx.beginPath(); + + cloud.circles.forEach(circle => { + ctx.moveTo(cloud.x + circle.offsetX, cloud.y + circle.offsetY); + ctx.arc( + cloud.x + circle.offsetX, + cloud.y + circle.offsetY, + circle.radius, + 0, + Math.PI * 2 + ); + }); + + const gradient = ctx.createRadialGradient( + cloud.x, cloud.y, 0, + cloud.x, cloud.y, config.cloudSize * 2 + ); + gradient.addColorStop(0, cloud.color); + gradient.addColorStop(1, `hsla(210, 50%, 98%, 0.3)`); + + ctx.fillStyle = gradient; + ctx.filter = `blur(${config.cloudSize * 0.2}px)`; // 模糊与尺寸关联 + ctx.fill(); + ctx.restore(); + }; + + let animationFrameId: number; + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 天空渐变背景 + const skyGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); + skyGradient.addColorStop(0, '#e6f3ff'); + skyGradient.addColorStop(1, '#d1e8ff'); + ctx.fillStyle = skyGradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + clouds.forEach(cloud => { + cloud.x += cloud.speed; + cloud.y += Math.sin(Date.now() / 1000 + cloud.x) * config.verticalOscillation; + + // 基于实际最大半径的边界检测 + const resetPosition = cloud.x > canvas.width + (cloud.maxRadius * config.boundaryOffset); + if (resetPosition) { + cloud.x = -cloud.maxRadius * config.boundaryOffset; + // 重置时重新生成形状 + const newShape = createCloudShape(); + cloud.circles = newShape; + cloud.maxRadius = Math.max(...newShape.map(c => c.radius)); + } + + drawCloud(cloud); + }); + + animationFrameId = requestAnimationFrame(animate); + }; + + createClouds(); + animate(); + + window.addEventListener('resize', () => { + resize(); + createClouds(); + }); + + return () => { + window.removeEventListener('resize', resize); + cancelAnimationFrame(animationFrameId); + }; + }, []); + + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/component/fluid/fluid.tsx b/frontend/src/component/fluid/fluid.tsx new file mode 100644 index 0000000..a9e8cab --- /dev/null +++ b/frontend/src/component/fluid/fluid.tsx @@ -0,0 +1,124 @@ +import { useRef, useEffect } from 'react'; + +export const AnimatedBackground = () => { + const canvasRef = useRef(null); + + // 粒子配置 + const config = { + particleNum: 100, + maxSpeed: 1.5, + particleRadius: 2, + lineWidth: 1.5, + lineDistance: 100 + }; + + type Particle = { + x: number; + y: number; + speedX: number; + speedY: number; + color: string; + radius: number; + }; + + useEffect(() => { + const canvas = canvasRef.current!; + const ctx = canvas.getContext('2d')!; + + // 设置canvas尺寸 + const resize = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + resize(); + + // 创建粒子数组 + let particles: Particle[] = []; + const createParticles = () => { + particles = Array.from({ length: config.particleNum }).map(() => ({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + speedX: (Math.random() - 0.5) * config.maxSpeed, + speedY: (Math.random() - 0.5) * config.maxSpeed, + color: `hsl(${Math.random() * 360}, 70%, 60%)`, + radius: Math.random() * config.particleRadius + 1 + })); + }; + + // 绘制连线 + const drawLine = (p1: Particle, p2: Particle) => { + const dx = p1.x - p2.x; + const dy = p1.y - p2.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < config.lineDistance) { + ctx.beginPath(); + ctx.strokeStyle = p1.color; + ctx.lineWidth = config.lineWidth * (1 - dist / config.lineDistance); + ctx.moveTo(p1.x, p1.y); + ctx.lineTo(p2.x, p2.y); + ctx.stroke(); + } + }; + + // 动画循环 + let animationFrameId: number; + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // 更新粒子位置 + particles.forEach(particle => { + particle.x += particle.speedX; + particle.y += particle.speedY; + + // 边界反弹 + if (particle.x < 0 || particle.x > canvas.width) particle.speedX *= -1; + if (particle.y < 0 || particle.y > canvas.height) particle.speedY *= -1; + + // 绘制粒子 + ctx.beginPath(); + ctx.fillStyle = particle.color; + ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); + ctx.fill(); + }); + + // 绘制粒子间的连线 + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + drawLine(particles[i], particles[j]); + } + } + + animationFrameId = requestAnimationFrame(animate); + }; + + // 初始化 + createParticles(); + animate(); + + // 窗口resize处理 + window.addEventListener('resize', () => { + resize(); + createParticles(); + }); + + // 清理 + return () => { + window.removeEventListener('resize', resize); + cancelAnimationFrame(animationFrameId); + }; + }, []); + + return ( + + ); +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index b356a57..966aef1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,19 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' -import {App} from './App.tsx' +import {createBrowserRouter, RouterProvider} from "react-router-dom"; +import {Login} from "./page/login.tsx"; +import {FileSharing} from "./page/share.tsx"; -createRoot(document.getElementById('root')!).render( - - - , +const container = document.getElementById('root') +const root = createRoot(container!) +const router = createBrowserRouter([ + {path: "/login", element: }, + {path: "*", element: }, +]) + +root.render( + + + , ) diff --git a/frontend/src/page/component/panel-mid.tsx b/frontend/src/page/component/panel-mid.tsx index e1be8a1..0e12ec1 100644 --- a/frontend/src/page/component/panel-mid.tsx +++ b/frontend/src/page/component/panel-mid.tsx @@ -2,6 +2,7 @@ import {createUseStyles} from "react-jss"; const useStyle = createUseStyles({ container: { + backgroundColor: 'lightgray', position: "relative", overflow: "hidden", }, diff --git a/frontend/src/page/login.tsx b/frontend/src/page/login.tsx new file mode 100644 index 0000000..7d07d0d --- /dev/null +++ b/frontend/src/page/login.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import {createUseStyles} from "react-jss"; +import {CloudBackground} from "../component/fluid/cloud.tsx"; + +const useClass = createUseStyles({ + container: { + overflow: 'hidden', + height: '100vh', + margin: 0, + padding: 0, + boxSizing: "border-box", + fontFamily: "'Segoe UI', Arial, sans-serif", + 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", + }, + form: { + height: '100%', + width: '100%', + display: "flex", + justifyContent: "center", + alignItems: "center", + flexDirection: 'column', + color: "#1a73e8", + padding: '40px', + }, + 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", + } + }, + }, + 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", + }, + }, + }, +}) + +export const Login: React.FC = () => { + const classes = useClass() + + return
+ +
+
+

UShare

+
+ +
+
+ +
+
+ +
+
+
+
+} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ffef60..e22e39a 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -3,5 +3,8 @@ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } - ] + ], + "compilerOptions": { + "allowJs": true + } } diff --git a/internal/controller/meta.go b/internal/controller/meta.go index 59ab22f..1ee708c 100644 --- a/internal/controller/meta.go +++ b/internal/controller/meta.go @@ -109,8 +109,12 @@ func (m *meta) Start(ctx context.Context) { for code, info := range m.m { if now.Sub(info.last) > 1*time.Minute { m.Lock() - info.f.Close() - os.RemoveAll(opt.FilePath(code)) + if err := info.f.Close(); err != nil { + log.Warn("handler.Meta: [timer] close file failed, file = %s, err = %s", opt.FilePath(code), err.Error()) + } + if err := os.RemoveAll(opt.FilePath(code)); err != nil { + log.Warn("handler.Meta: [timer] remove file failed, file = %s, err = %s", opt.FilePath(code), err.Error()) + } delete(m.m, code) m.Unlock() log.Warn("MetaController: code timeout removed, code = %s", code) diff --git a/internal/handler/share.go b/internal/handler/share.go index 06a41c1..ce06c0f 100644 --- a/internal/handler/share.go +++ b/internal/handler/share.go @@ -50,6 +50,10 @@ func Fetch() nf.HandlerFunc { func ShareNew() nf.HandlerFunc { return func(c *nf.Ctx) error { + if opt.Cfg.Auth { + return c.SendStatus(http.StatusUnauthorized) + } + filename := strings.TrimSpace(c.Param("filename")) if filename == "" { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "filename required"}) diff --git a/internal/opt/opt.go b/internal/opt/opt.go index 1cf532f..47daee7 100644 --- a/internal/opt/opt.go +++ b/internal/opt/opt.go @@ -4,6 +4,7 @@ type config struct { Debug bool Address string DataPath string + Auth bool } var ( diff --git a/main.go b/main.go index 24eec8f..73ec70e 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ 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.BoolVar(&opt.Cfg.Auth, "auth", false, "upload need login") flag.Parse() if opt.Cfg.Debug { diff --git a/page/login.html b/page/login.html new file mode 100644 index 0000000..f4c75ec --- /dev/null +++ b/page/login.html @@ -0,0 +1,166 @@ + + + + + + Login Page + + + + + + \ No newline at end of file