From 440aa96ad60d13a9d02a06033b9f0233a4924707 Mon Sep 17 00:00:00 2001 From: loveuer Date: Tue, 6 May 2025 18:04:20 +0800 Subject: [PATCH] =?UTF-8?q?init:=200.1.0=20feat:=20=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=9C=89=E4=BA=86=20=20=201.=20=E5=88=86?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=20=20=202.=20code=20=E5=9B=9E?= =?UTF-8?q?=E6=98=BE=20=20=203.=20=E4=B8=8B=E8=BD=BD=20todo:=20=E6=96=AD?= =?UTF-8?q?=E7=82=B9=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 31 +++ deployment/nginx.conf | 16 ++ frontend/src/api/upload.ts | 76 ++++++++ frontend/src/component/button/u-button.tsx | 91 +++++++-- frontend/src/component/message/u-message.tsx | 2 +- frontend/src/page/component/panel-left.tsx | 192 +++++++++++++------ internal/controller/meta.go | 4 +- internal/handler/share.go | 2 +- internal/opt/var.go | 1 + main.go | 3 +- 10 files changed, 345 insertions(+), 73 deletions(-) create mode 100644 Dockerfile create mode 100644 deployment/nginx.conf create mode 100644 frontend/src/api/upload.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7857604 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# 第一阶段:构建前端 +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 pnpm run build + +# 第二阶段:构建 Golang 后端 +FROM golang:alpine AS backend-builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY main.go internal ./ +RUN CGO_ENABLED=0 GOOS=linux go build -o main . + +# 第三阶段:生成最终镜像 +FROM nginx:alpine +COPY --from=frontend-builder /app/dist /usr/share/nginx/html +COPY --from=backend-builder /app/main /app/main + +# 配置 Nginx +RUN rm /etc/nginx/conf.d/default.conf +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 diff --git a/deployment/nginx.conf b/deployment/nginx.conf new file mode 100644 index 0000000..f4e47ac --- /dev/null +++ b/deployment/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + + location /api { + proxy_pass http://localhost:9119; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} \ No newline at end of file diff --git a/frontend/src/api/upload.ts b/frontend/src/api/upload.ts new file mode 100644 index 0000000..eac8f53 --- /dev/null +++ b/frontend/src/api/upload.ts @@ -0,0 +1,76 @@ +import { useState } from 'react'; + +interface UploadRes { + code: string +} + +export const useFileUpload = () => { + const [progress, setProgress] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const uploadFile = async (file: File): Promise => { + setLoading(true); + setError(null); + setProgress(0); + + try { + console.log(`[D] api.Upload: upload file = ${file.name}, size = ${file.size}`, file); + const url = `/api/share/${file.name}`; + + // 1. 初始化上传 + const res1 = await fetch(url, { + method: "PUT", + headers: {"X-File-Size": file.size.toString()} + }); + + if (!res1.ok) { + throw new Error("上传失败<1>"); + } + + const j1 = await res1.json() as UploadRes; + if (!j1.code) { + throw new Error("上传失败<2>"); + } + + // 2. 准备分片上传 + const CHUNK_SIZE = 1024 * 1024; + const totalChunks = Math.ceil(file.size / CHUNK_SIZE); + const code = j1.code; + + // 3. 上传分片 + for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { + const start = chunkIndex * CHUNK_SIZE; + const end = Math.min(start + CHUNK_SIZE, file.size); + const chunk = file.slice(start, end); + + const res = await fetch(`/api/share/${code}`, { + method: "POST", + headers: { + "Range": `bytes=${start}-${end - 1}`, + "Content-Type": "application/octet-stream" + }, + body: chunk + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`上传失败<3>: ${err}`); + } + + // 更新进度 + // const currentProgress = Number(((chunkIndex + 1) / totalChunks * 100).toFixed(2)); // 小数 + const currentProgress = Math.round(((chunkIndex + 1) / totalChunks) * 100); // 整数 0-100 + setProgress(currentProgress); + } + + return code; + } catch (err) { + throw err; // 将错误继续抛出以便组件处理 + } finally { + setLoading(false); + } + }; + + return { uploadFile, progress, loading, error }; +}; \ No newline at end of file diff --git a/frontend/src/component/button/u-button.tsx b/frontend/src/component/button/u-button.tsx index 1ea6371..014b545 100644 --- a/frontend/src/component/button/u-button.tsx +++ b/frontend/src/component/button/u-button.tsx @@ -1,5 +1,5 @@ import React, {ReactNode} from 'react'; -import {createUseStyles} from "react-jss"; +import {createUseStyles} from 'react-jss'; const useStyle = createUseStyles({ ubutton: { @@ -10,6 +10,8 @@ const useStyle = createUseStyles({ borderRadius: "5px", cursor: "pointer", transition: "background-color 0.3s", + position: 'relative', + overflow: 'hidden', "&:hover": { backgroundColor: "#45a049", }, @@ -17,17 +19,84 @@ const useStyle = createUseStyles({ backgroundColor: "#a5d6a7", cursor: "not-allowed", }, - } -}) + }, + loadingContent: { + display: 'flex', + alignItems: 'center', + gap: '8px', + }, + spinner: { + animation: '$spin 1s linear infinite', + border: '2px solid white', + borderTopColor: 'transparent', + borderRadius: '50%', + width: '16px', + height: '16px', + }, + '@keyframes spin': { + '0%': {transform: 'rotate(0deg)'}, + '100%': {transform: 'rotate(360deg)'}, + }, + progressBar: { + position: 'absolute', + bottom: 0, + left: 0, + height: '3px', + backgroundColor: 'rgba(255,255,255,0.5)', + width: '100%', + }, + progressFill: { + height: '100%', + backgroundColor: 'white', + transition: 'width 0.3s ease', + }, +}); + + type Props = { - onClick: () => void; + onClick?: () => void; children: ReactNode; disabled?: boolean; - style?: React.CSSProperties | undefined; + style?: React.CSSProperties; + loading?: boolean; + process?: number; }; -export const UButton: React.FC = ({onClick, children, style,disabled = false}) => { - const classes= useStyle() - return -} \ No newline at end of file + +export const UButton: React.FC = ({ + onClick, + children, + disabled = false, + style, + loading, + process + }) => { + const classes = useStyle(); + + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/component/message/u-message.tsx b/frontend/src/component/message/u-message.tsx index 0976f67..aa44007 100644 --- a/frontend/src/component/message/u-message.tsx +++ b/frontend/src/component/message/u-message.tsx @@ -1,5 +1,5 @@ import {createRoot} from "react-dom/client"; -import {useEffect, useState} from "react"; +import {useState} from "react"; import {createUseStyles} from "react-jss"; const useStyle = createUseStyles({ diff --git a/frontend/src/page/component/panel-left.tsx b/frontend/src/page/component/panel-left.tsx index 8653ae9..2b23240 100644 --- a/frontend/src/page/component/panel-left.tsx +++ b/frontend/src/page/component/panel-left.tsx @@ -1,10 +1,11 @@ import {createUseStyles} from "react-jss"; import {UButton} from "../../component/button/u-button.tsx"; -import React from "react"; +import React, {useState} from "react"; import {useStore} from "../../store/share.ts"; import {message} from "../../component/message/u-message.tsx"; +import {useFileUpload} from "../../api/upload.ts"; -const useStyle = createUseStyles({ +const useUploadStyle = createUseStyles({ container: { backgroundColor: "#e3f2fd", display: "flex", @@ -41,13 +42,93 @@ const useStyle = createUseStyles({ } }) -interface RespNew { - code: string -} +const useShowStyle = createUseStyles({ + container: { + backgroundColor: "#e3f2fd", + display: "flex", + justifyContent: "center", + alignItems: "center", + position: "relative", // 为关闭按钮提供定位基准 + }, + title: { + color: "#2c9678", + marginTop: 0, + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }, + form: { + backgroundColor: "#C8E6C9", + boxShadow: "inset 0 0 15px rgba(56, 142, 60, 0.15)", + padding: "30px", + borderRadius: "15px", + width: "70%", + margin: "20px 60px 20px 0", + position: "relative", + }, + closeButton: { + position: "absolute", + top: "10px", + right: "10px", + background: "transparent", + color: "white", + border: "none", + borderRadius: "50%", + width: "24px", + height: "24px", + cursor: "pointer", + "&:hover": { + // background: "#cc0000", + boxShadow: "20px 20px 60px #fff, -20px -20px 60px #fff", + // boxShadow: "20px 20px 60px #eee", + }, + }, + codeWrapper: { + backgroundColor: "rgba(255,255,255,0.8)", + padding: "0 15px", + borderRadius: "8px", + margin: "15px 0", + overflowX: "auto", + }, + pre: { + display: 'flex', + flexDirection: 'row', + color: 'black', + alignItems: 'center', + height: '24px', + "& > code": { + marginLeft: "0", + } + }, + copyButton: { + marginLeft: 'auto', + background: "#2c9678", + color: "white", + border: "none", + padding: "8px 16px", + borderRadius: "4px", + cursor: "pointer", + transition: "background 0.3s", + "&:hover": { + background: "#1f6d5a", + }, + }, +}); export const PanelLeft = () => { - const style = useStyle() + const [code, set_code] = useState("") + + if (code) { + return + } + + return +}; + +const PanelLeftUpload: React.FC<{ set_code: (code:string) => void }> = ({set_code}) => { + const style = useUploadStyle() const {file, setFile} = useStore() + const {uploadFile, progress, loading} = useFileUpload(); function onFileSelect() { // @ts-ignore @@ -64,55 +145,8 @@ export const PanelLeft = () => { return } - console.log(`[D] onFileUpload: upload file = ${file.name}, size = ${file.size}`, file) - - let res1 = await fetch(`/api/share/${file.name}`, { - method: "PUT", - headers: {"X-File-Size": file.size.toString()} - }) - let j1 = await res1.json() as RespNew - console.log('[D] onFileUpload: json 1 =', j1) - if (!j1.code) { - return - } - - - // todo: for 循环上传文件分片直到上传完成 - - // 2. 分片上传配置 - const CHUNK_SIZE = 1024 * 1024 // 1MB分片 - const totalChunks = Math.ceil(file.size / CHUNK_SIZE) - const code = j1.code - - // 3. 循环上传所有分片 - for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { - const start = chunkIndex * CHUNK_SIZE - const end = Math.min(start + CHUNK_SIZE, file.size) - const chunk = file.slice(start, end) - - // 4. 构造Range头(bytes=start-end) - const rangeHeader = `bytes=${start}-${end - 1}` // end-1因为Range是闭区间 - - // 5. 上传分片 - const res = await fetch(`/api/share/${code}`, { - method: "POST", - headers: { - "Range": rangeHeader, - "Content-Type": "application/octet-stream" // 二进制流类型 - }, - body: chunk - }) - - if (!res.ok) { - const err = await res.text() - throw new Error(`分片 ${chunkIndex} 上传失败: ${err}`) - } - - console.log(`[D] 分片进度: ${chunkIndex + 1}/${totalChunks}`, - `(${Math.round((chunkIndex + 1)/totalChunks*100)}%)`) - } - - console.log('[D] 所有分片上传完成') + const code = await uploadFile(file) + set_code(code) } function onFileClean() { @@ -123,13 +157,17 @@ export const PanelLeft = () => {

上传文件

{ - !file && + !file && !loading && 选择文件 } { - file && + file && !loading && 上传文件 } + { + loading && + 上传中 + } { file && @@ -140,4 +178,44 @@ export const PanelLeft = () => { }
+} + +const PanelLeftShow: React.FC<{ code: string; set_code: (code: string) => void }> = ({ code, set_code }) => { + const classes = useShowStyle(); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(code); + message.success("复制成功!"); + } catch (err) { + message.warning("复制失败,请手动选择文本复制"); + } + }; + + return ( +
+ +
+ +

+ 上传成功! +

+ +
+
+                        {code}
+                        
+                    
+
+
+
+ ); }; \ No newline at end of file diff --git a/internal/controller/meta.go b/internal/controller/meta.go index 8f3a70b..59ab22f 100644 --- a/internal/controller/meta.go +++ b/internal/controller/meta.go @@ -40,11 +40,11 @@ var ( MetaManager = &meta{m: make(map[string]*metaInfo)} ) -const letters = "1234567890abcdefghijklmnopqrstuvwxyz" +const letters = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" func (m *meta) New(size int64, filename, ip string) (string, error) { now := time.Now() - code, err := gonanoid.Generate(letters, 16) + code, err := gonanoid.Generate(letters, opt.CodeLength) if err != nil { return "", err } diff --git a/internal/handler/share.go b/internal/handler/share.go index 979297c..06a41c1 100644 --- a/internal/handler/share.go +++ b/internal/handler/share.go @@ -73,7 +73,7 @@ func ShareUpload() nf.HandlerFunc { rangeValidator := regexp.MustCompile(`^bytes=\d+-\d+$`) return func(c *nf.Ctx) error { code := strings.TrimSpace(c.Param("code")) - if len(code) != 16 { + if len(code) != opt.CodeLength { return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "invalid file code"}) } diff --git a/internal/opt/var.go b/internal/opt/var.go index eb2706e..78a0764 100644 --- a/internal/opt/var.go +++ b/internal/opt/var.go @@ -5,6 +5,7 @@ import "path/filepath" const ( Meta = ".meta." HeaderSize = "X-File-Size" + CodeLength = 8 ) func FilePath(code string) string { diff --git a/main.go b/main.go index fa0c325..24eec8f 100644 --- a/main.go +++ b/main.go @@ -13,12 +13,13 @@ import ( func init() { flag.BoolVar(&opt.Cfg.Debug, "debug", false, "debug mode") - flag.StringVar(&opt.Cfg.Address, "address", "0.0.0.0:80", "") + flag.StringVar(&opt.Cfg.Address, "address", "0.0.0.0:9119", "") flag.StringVar(&opt.Cfg.DataPath, "data", "/data", "") flag.Parse() if opt.Cfg.Debug { log.SetLogLevel(log.LogLevelDebug) + log.Debug("start server with debug mode") } }