Files
appDistribute/background/main.go
sakuraiKiyoshi b733f81ed8 fix(并发控制): 修复apps数据保存时的并发安全问题
将saveApps函数的锁管理移到调用方,确保在文件保存操作期间保持锁
在uploadApp和deleteApp中调整锁的释放时机,防止数据竞争
2026-01-07 14:03:45 +08:00

375 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// App 结构体定义
type App struct {
ID string `json:"id"`
Name string `json:"name"`
FileName string `json:"fileName"`
Date string `json:"date"`
FilePath string `json:"filePath"` // 序列化到JSON中用于服务器重启后恢复
}
type Doc struct {
DocBody string `json:"docBody"`
}
// 全局变量
var (
docs []Doc
apps []App
appsMutex sync.RWMutex // 读写锁,提高并发性能
filesDir = "./files"
docsDir = "./docs"
jsonFile = "./apps.json"
port = ":6903" // 修改端口号为6903
)
// 生成唯一文件名
func generateUniqueFileName(originalName string) string {
// 获取文件扩展名
ext := filepath.Ext(originalName)
// 生成基于时间、随机数和原文件名的MD5哈希
timestamp := time.Now().UnixNano()
random := rand.Int63()
hash := md5.Sum([]byte(fmt.Sprintf("%d%d%s", timestamp, random, originalName)))
// 组合成新的唯一文件名
return fmt.Sprintf("%s%s", hex.EncodeToString(hash[:]), ext)
}
// 加载apps数据
func loadApps() {
appsMutex.Lock()
defer appsMutex.Unlock()
file, err := os.Open(jsonFile)
if err != nil {
// 如果文件不存在,初始化空切片
apps = []App{}
return
}
defer file.Close()
decoder := json.NewDecoder(file)
if err := decoder.Decode(&apps); err != nil {
apps = []App{}
}
}
// 保存apps数据调用者需要持有锁
func saveApps() error {
file, err := os.Create(jsonFile)
if err != nil {
return fmt.Errorf("failed to create apps.json: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(apps); err != nil {
return fmt.Errorf("failed to encode apps: %w", err)
}
return nil
}
func main() {
// 初始化随机数生成器
rand.Seed(time.Now().UnixNano())
// 创建files和docs目录
if err := os.MkdirAll(filesDir, 0755); err != nil {
panic(fmt.Sprintf("failed to create files directory: %v", err))
}
if err := os.MkdirAll(docsDir, 0755); err != nil {
panic(fmt.Sprintf("failed to create docs directory: %v", err))
}
// 加载apps数据
loadApps()
// 创建Gin引擎
// 生产环境中应该使用gin.ReleaseMode
// gin.SetMode(gin.ReleaseMode)
r := gin.Default()
// 添加CORS中间件
r.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(200)
return
}
c.Next()
})
// API路由
api := r.Group("/api")
{
api.GET("/apps", getApps)
api.POST("/apps", uploadApp)
api.DELETE("/apps/:id", deleteApp) // 添加删除应用的API端点
api.GET("/apps/:id", downloadApp)
api.GET("/apps/:id/", downloadApp) // 处理以/结尾的URL
api.GET("/docs", getDocs) // 新增获取文档的API端点
}
// 启动服务器
fmt.Printf("Server is running on http://localhost%s\n", port)
if err := r.Run(port); err != nil {
panic(fmt.Sprintf("failed to start server: %v", err))
}
}
// 获取App列表
func getApps(c *gin.Context) {
appsMutex.Lock()
defer appsMutex.Unlock()
// 验证文件是否存在,如果不存在则从切片中删除
appsToKeep := []App{}
appsChanged := false
for _, app := range apps {
filePath := filepath.Join(filesDir, app.FilePath)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
// 文件不存在跳过这个app即从逻辑上删除它
appsChanged = true
fmt.Printf("文件不存在,从列表中移除: %s, 文件路径: %s\n", app.Name, filePath)
} else {
// 文件存在保留这个app
appsToKeep = append(appsToKeep, app)
}
}
if appsChanged {
// 更新apps切片
apps = appsToKeep
// 保存到文件
if err := saveApps(); err != nil {
fmt.Printf("保存apps.json失败: %v\n", err)
}
}
c.JSON(http.StatusOK, apps)
}
// 上传App
func uploadApp(c *gin.Context) {
// 获取表单数据
name := c.PostForm("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "App名称不能为空"})
return
}
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "文件上传失败: " + err.Error()})
return
}
defer file.Close()
// 生成唯一ID和文件名
id := fmt.Sprintf("%d", time.Now().UnixNano())
originalFileName := header.Filename
uniqueFileName := generateUniqueFileName(originalFileName)
filePath := filepath.Join(filesDir, uniqueFileName)
// 创建目标文件
dst, err := os.Create(filePath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "文件保存失败: " + err.Error()})
return
}
defer dst.Close()
// 复制文件内容
if _, err = io.Copy(dst, file); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "文件复制失败: " + err.Error()})
return
}
// 添加到App列表
newApp := App{
ID: id,
Name: name,
FileName: originalFileName,
FilePath: uniqueFileName,
Date: time.Now().Format("2006-01-02 15:04:05"),
}
appsMutex.Lock()
apps = append(apps, newApp)
// 保存到文件
if err := saveApps(); err != nil {
appsMutex.Unlock()
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存数据失败: " + err.Error()})
return
}
appsMutex.Unlock()
c.JSON(http.StatusOK, gin.H{"success": true})
}
// 下载指定App
func downloadApp(c *gin.Context) {
// 获取ID
id := c.Param("id")
// 查找App
appsMutex.RLock()
var targetApp *App
for i := range apps {
if apps[i].ID == id {
targetApp = &apps[i]
break
}
}
appsMutex.RUnlock()
if targetApp == nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "App不存在"})
return
}
// 构建文件路径
filePath := filepath.Join(filesDir, targetApp.FilePath)
// 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "文件不存在"})
return
}
// 使用FileAttachment直接提供文件下载避免重定向
c.FileAttachment(filePath, targetApp.FileName)
}
// 删除指定App
func deleteApp(c *gin.Context) {
// 获取ID
id := c.Param("id")
// 查找App
appsMutex.Lock()
var targetApp *App
targetIndex := -1
for i := range apps {
if apps[i].ID == id {
targetApp = &apps[i]
targetIndex = i
break
}
}
appsMutex.Unlock()
if targetApp == nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "App不存在"})
return
}
// 构建文件路径
filePath := filepath.Join(filesDir, targetApp.FilePath)
// 删除文件
if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) {
// 文件不存在,可能已经被删除,继续执行
fmt.Printf("文件已经不存在: %s\n", filePath)
} else {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "删除文件失败: " + err.Error()})
return
}
}
// 从apps切片中删除该应用
appsMutex.Lock()
apps = append(apps[:targetIndex], apps[targetIndex+1:]...)
// 保存到文件
if err := saveApps(); err != nil {
appsMutex.Unlock()
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存数据失败: " + err.Error()})
return
}
appsMutex.Unlock()
c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"})
}
// 读取文档内容
func readDocFile(docName string) (string, error) {
filePath := filepath.Join(docsDir, docName+".txt")
content, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
return string(content), nil
}
// 获取文档内容
func getDocs(c *gin.Context) {
// 读取5个文档文件
doc1, err1 := readDocFile("doc1")
if err1 != nil {
doc1 = "" // 如果文件不存在或读取失败,返回空字符串
}
doc2, err2 := readDocFile("doc2")
if err2 != nil {
doc2 = ""
}
doc3, err3 := readDocFile("doc3")
if err3 != nil {
doc3 = ""
}
doc4, err4 := readDocFile("doc4")
if err4 != nil {
doc4 = ""
}
doc5, err5 := readDocFile("doc5")
if err5 != nil {
doc5 = ""
}
if len(docs) == 0 {
docs = append(docs, Doc{DocBody: doc1})
docs = append(docs, Doc{DocBody: doc2})
docs = append(docs, Doc{DocBody: doc3})
docs = append(docs, Doc{DocBody: doc4})
docs = append(docs, Doc{DocBody: doc5})
} else {
docs[0].DocBody = doc1
docs[1].DocBody = doc2
docs[2].DocBody = doc3
docs[3].DocBody = doc4
docs[4].DocBody = doc5
}
// 返回包含message1-5的JSON
c.JSON(http.StatusOK, docs)
}