Files
ufile/app.go
T

721 lines
22 KiB
Go

package main
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"io/fs"
"mime"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
const sessionCookie = "ufile_session"
var usernamePattern = regexp.MustCompile(`^[A-Za-z0-9_.-]{3,32}$`)
var pickupCodePattern = regexp.MustCompile(`^[0-9]{6}$`)
type App struct {
db *gorm.DB
filesDir string
web fs.FS
uploadMu sync.Mutex
downloadMu sync.Mutex
now func() time.Time
}
func newApp(dbPath, dataDir string, web fs.FS) (*App, string, error) {
filesDir := filepath.Join(dataDir, "files")
if err := os.MkdirAll(filesDir, 0o750); err != nil {
return nil, "", fmt.Errorf("create data directory: %w", err)
}
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return nil, "", fmt.Errorf("open database: %w", err)
}
if err := db.AutoMigrate(&User{}, &Session{}, &SharedFile{}); err != nil {
return nil, "", fmt.Errorf("migrate database: %w", err)
}
app := &App{db: db, filesDir: filesDir, web: web, now: time.Now}
password, err := app.ensureAdmin()
return app, password, err
}
func (a *App) ensureAdmin() (string, error) {
var count int64
if err := a.db.Model(&User{}).Count(&count).Error; err != nil {
return "", err
}
if count != 0 {
return "", nil
}
password, err := randomToken(18)
if err != nil {
return "", err
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
admin := User{ID: 1, Username: "admin", PasswordHash: string(hash), CanManageUsers: true, CanUpload: true, CanAdvancedUpload: true, MustChangePassword: true}
if err := a.db.Create(&admin).Error; err != nil {
return "", err
}
return password, nil
}
func (a *App) router() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
r.POST("/api/auth/login", a.login)
r.POST("/api/auth/logout", a.auth(false), a.logout)
r.GET("/api/auth/me", a.auth(false), a.me)
r.PUT("/api/auth/password", a.auth(false), a.changePassword)
r.POST("/api/files", a.auth(true), a.uploadFile)
r.GET("/api/users", a.auth(true), a.manageUsers, a.listUsers)
r.POST("/api/users", a.auth(true), a.manageUsers, a.createUser)
r.PUT("/api/users/:id", a.auth(true), a.manageUsers, a.updateUser)
r.POST("/api/users/:id/reset-password", a.auth(true), a.manageUsers, a.resetPassword)
r.DELETE("/api/users/:id", a.auth(true), a.manageUsers, a.deleteUser)
r.GET("/uf/:hash", a.downloadFile)
r.NoRoute(a.serveSPA)
return r
}
func (a *App) auth(requireChanged bool) gin.HandlerFunc {
return func(c *gin.Context) {
token, err := c.Cookie(sessionCookie)
if err != nil || token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
var session Session
if err := a.db.Where("token_hash = ? AND expires_at > ?", tokenHash(token), a.now()).First(&session).Error; err != nil {
clearSessionCookie(c)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "登录已过期"})
return
}
var user User
if err := a.db.First(&user, session.UserID).Error; err != nil {
clearSessionCookie(c)
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"})
return
}
if requireChanged && user.MustChangePassword {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "请先修改初始密码", "code": "PASSWORD_CHANGE_REQUIRED"})
return
}
c.Set("user", &user)
c.Set("session", &session)
c.Next()
}
}
func currentUser(c *gin.Context) *User { return c.MustGet("user").(*User) }
func (a *App) manageUsers(c *gin.Context) {
if !currentUser(c).CanManageUsers {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "没有用户管理权限"})
return
}
c.Next()
}
func permissionsWithin(actor *User, canManageUsers, canUpload, canAdvancedUpload bool) bool {
if canManageUsers && !actor.CanManageUsers {
return false
}
if canAdvancedUpload && !actor.CanAdvancedUpload {
return false
}
return !canUpload || actor.CanUpload || actor.CanAdvancedUpload
}
func canManageTarget(actor, target *User) bool {
return permissionsWithin(actor, target.CanManageUsers, target.CanUpload, target.CanAdvancedUpload)
}
func requireAssignablePermissions(c *gin.Context, canManageUsers, canUpload, canAdvancedUpload bool) bool {
if permissionsWithin(currentUser(c), canManageUsers, canUpload, canAdvancedUpload) {
return true
}
c.JSON(http.StatusForbidden, gin.H{"error": "不能授予自己不具备的权限"})
return false
}
func requireManageableTarget(c *gin.Context, target *User) bool {
if canManageTarget(currentUser(c), target) {
return true
}
c.JSON(http.StatusForbidden, gin.H{"error": "不能管理权限高于自己的用户"})
return false
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (a *App) login(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求格式不正确"})
return
}
var user User
if err := a.db.Where("username = ?", req.Username).First(&user).Error; err != nil || bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
token, err := randomToken(32)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建会话失败"})
return
}
session := Session{TokenHash: tokenHash(token), UserID: user.ID, ExpiresAt: a.now().Add(30 * 24 * time.Hour)}
if err := a.db.Create(&session).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建会话失败"})
return
}
setSessionCookie(c, token, session.ExpiresAt)
c.JSON(http.StatusOK, user)
}
func (a *App) logout(c *gin.Context) {
session := c.MustGet("session").(*Session)
a.db.Delete(session)
clearSessionCookie(c)
c.Status(http.StatusNoContent)
}
func (a *App) me(c *gin.Context) { c.JSON(http.StatusOK, currentUser(c)) }
func (a *App) changePassword(c *gin.Context) {
var req struct {
CurrentPassword string `json:"currentPassword"`
NewPassword string `json:"newPassword"`
}
if err := c.ShouldBindJSON(&req); err != nil || len(req.NewPassword) < 8 || len(req.NewPassword) > 128 {
c.JSON(http.StatusBadRequest, gin.H{"error": "新密码长度需要为 8 到 128 位"})
return
}
user := currentUser(c)
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)) != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "当前密码错误"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "修改密码失败"})
return
}
if err := a.db.Model(user).Updates(map[string]any{"password_hash": string(hash), "must_change_password": false}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "修改密码失败"})
return
}
user.PasswordHash = string(hash)
user.MustChangePassword = false
c.JSON(http.StatusOK, user)
}
func setSessionCookie(c *gin.Context, token string, expires time.Time) {
http.SetCookie(c.Writer, &http.Cookie{Name: sessionCookie, Value: token, Path: "/", Expires: expires, MaxAge: int(time.Until(expires).Seconds()), HttpOnly: true, SameSite: http.SameSiteLaxMode})
}
func clearSessionCookie(c *gin.Context) {
http.SetCookie(c.Writer, &http.Cookie{Name: sessionCookie, Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode})
}
func tokenHash(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
func randomToken(bytes int) (string, error) {
b := make([]byte, bytes)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func randomFileHash() (string, error) {
const alphabet = "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
b := make([]byte, 8)
raw := make([]byte, 8)
if _, err := rand.Read(raw); err != nil {
return "", err
}
for i := range b {
b[i] = alphabet[int(raw[i])%len(alphabet)]
}
return string(b), nil
}
func parseRetention(value string, now time.Time) (*time.Time, bool) {
var duration time.Duration
switch value {
case "1h":
duration = time.Hour
case "8h":
duration = 8 * time.Hour
case "1d":
duration = 24 * time.Hour
case "3d":
duration = 72 * time.Hour
case "forever":
return nil, true
default:
return nil, false
}
t := now.Add(duration)
return &t, true
}
func parseDownloads(value string) (*int, bool) {
if value == "unlimited" {
return nil, true
}
n, err := strconv.Atoi(value)
if err != nil || n < 1 || n > 99 {
return nil, false
}
return &n, true
}
func (a *App) uploadFile(c *gin.Context) {
user := currentUser(c)
if !user.CanUpload && !user.CanAdvancedUpload {
c.JSON(http.StatusForbidden, gin.H{"error": "没有上传权限"})
return
}
expiresAt, ok := parseRetention(c.PostForm("retention"), a.now())
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的保留时间"})
return
}
maxDownloads, ok := parseDownloads(c.PostForm("downloads"))
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "下载次数需要为 1 到 99"})
return
}
if (expiresAt == nil || maxDownloads == nil) && !user.CanAdvancedUpload {
c.JSON(http.StatusForbidden, gin.H{"error": "永久保留和无限下载需要高级上传权限"})
return
}
pickupCode := c.PostForm("pickupCode")
if pickupCode != "" && !pickupCodePattern.MatchString(pickupCode) {
c.JSON(http.StatusBadRequest, gin.H{"error": "取件码必须是 6 位数字"})
return
}
header, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"})
return
}
a.uploadMu.Lock()
defer a.uploadMu.Unlock()
if !user.CanAdvancedUpload {
var count int64
if err := a.db.Model(&SharedFile{}).Where("owner_id = ? AND (expires_at IS NULL OR expires_at > ?)", user.ID, a.now()).Count(&count).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "检查上传限额失败"})
return
}
if count >= 10 {
c.JSON(http.StatusForbidden, gin.H{"error": "普通上传用户最多保留 10 个有效文件"})
return
}
}
var codeHash string
if pickupCode != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(pickupCode), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "处理取件码失败"})
return
}
codeHash = string(hash)
}
file := SharedFile{OriginalName: filepath.Base(header.Filename), MimeType: header.Header.Get("Content-Type"), OwnerID: user.ID, ExpiresAt: expiresAt, MaxDownloads: maxDownloads, PickupCodeHash: codeHash}
if file.MimeType == "" {
file.MimeType = "application/octet-stream"
}
for attempts := 0; attempts < 10; attempts++ {
file.ID = 0
file.Hash, err = randomFileHash()
if err != nil {
break
}
err = a.db.Create(&file).Error
if err == nil {
break
}
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建文件记录失败"})
return
}
src, err := header.Open()
if err != nil {
a.db.Delete(&file)
c.JSON(http.StatusBadRequest, gin.H{"error": "读取上传文件失败"})
return
}
defer src.Close()
temp, err := os.CreateTemp(a.filesDir, ".upload-*")
if err != nil {
a.db.Delete(&file)
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
tempName := temp.Name()
defer os.Remove(tempName)
size, copyErr := io.Copy(temp, src)
closeErr := temp.Close()
finalPath := filepath.Join(a.filesDir, file.Hash)
if copyErr != nil || closeErr != nil || os.Rename(tempName, finalPath) != nil {
a.db.Delete(&file)
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
file.Size = size
file.Ready = true
if err := a.db.Model(&file).Updates(map[string]any{"size": size, "ready": true}).Error; err != nil {
os.Remove(finalPath)
a.db.Delete(&file)
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
c.JSON(http.StatusCreated, presentFile(file))
}
func (a *App) downloadFile(c *gin.Context) {
var file SharedFile
if err := a.db.Where("hash = ? AND ready = ?", c.Param("hash"), true).First(&file).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在或已失效"})
return
}
if file.ExpiresAt != nil && !file.ExpiresAt.After(a.now()) {
a.removeFile(file)
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在或已失效"})
return
}
if file.PickupCodeHash != "" && bcrypt.CompareHashAndPassword([]byte(file.PickupCodeHash), []byte(c.Query("code"))) != nil {
c.JSON(http.StatusForbidden, gin.H{"error": "取件码错误"})
return
}
handle, err := os.Open(filepath.Join(a.filesDir, file.Hash))
if err != nil {
a.db.Delete(&file)
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在或已失效"})
return
}
defer handle.Close()
removeAfter := false
if file.MaxDownloads != nil {
allowed, exhausted, err := a.consumeDownload(file)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"})
return
}
if !allowed {
handle.Close()
a.removeFile(file)
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在或已失效"})
return
}
removeAfter = exhausted
}
if removeAfter {
defer os.Remove(filepath.Join(a.filesDir, file.Hash))
}
c.Header("Content-Type", file.MimeType)
c.Header("Content-Length", strconv.FormatInt(file.Size, 10))
if c.Query("preview") != "true" {
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": file.OriginalName}))
} else {
c.Header("Content-Disposition", "inline")
c.Header("Content-Security-Policy", "sandbox; default-src 'none'; img-src 'self' data:; media-src 'self'; style-src 'unsafe-inline'")
}
c.Header("X-Content-Type-Options", "nosniff")
c.Status(http.StatusOK)
_, _ = io.Copy(c.Writer, handle)
}
func (a *App) consumeDownload(file SharedFile) (allowed, exhausted bool, err error) {
a.downloadMu.Lock()
defer a.downloadMu.Unlock()
result := a.db.Model(&SharedFile{}).Where("id = ? AND download_count < ?", file.ID, *file.MaxDownloads).UpdateColumn("download_count", gorm.Expr("download_count + 1"))
if result.Error != nil {
return false, false, result.Error
}
if result.RowsAffected == 0 {
return false, false, nil
}
var latest SharedFile
if err := a.db.First(&latest, file.ID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return true, false, nil
}
return false, false, err
}
if latest.DownloadCount < *file.MaxDownloads {
return true, false, nil
}
if err := a.db.Where("id = ? AND download_count >= ?", file.ID, *file.MaxDownloads).Delete(&SharedFile{}).Error; err != nil {
return false, false, err
}
return true, true, nil
}
func (a *App) removeFile(file SharedFile) {
a.db.Delete(&file)
_ = os.Remove(filepath.Join(a.filesDir, file.Hash))
}
func (a *App) cleanup() {
var files []SharedFile
now := a.now()
a.db.Where("(expires_at IS NOT NULL AND expires_at <= ?) OR (ready = ? AND created_at < ?)", now, false, now.Add(-time.Hour)).Find(&files)
for _, file := range files {
a.removeFile(file)
}
a.db.Where("expires_at <= ?", now).Delete(&Session{})
a.cleanupStorageOrphans(now)
}
func (a *App) cleanupStorageOrphans(now time.Time) {
entries, err := os.ReadDir(a.filesDir)
if err != nil {
return
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
if strings.HasPrefix(entry.Name(), ".upload-") {
if info, err := entry.Info(); err == nil && info.ModTime().Before(now.Add(-time.Hour)) {
_ = os.Remove(filepath.Join(a.filesDir, entry.Name()))
}
continue
}
if len(entry.Name()) != 8 {
continue
}
var count int64
if err := a.db.Model(&SharedFile{}).Where("hash = ?", entry.Name()).Count(&count).Error; err == nil && count == 0 {
_ = os.Remove(filepath.Join(a.filesDir, entry.Name()))
}
}
}
func (a *App) runCleanup(ctx context.Context) {
a.cleanup()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
a.cleanup()
case <-ctx.Done():
return
}
}
}
func (a *App) serveSPA(c *gin.Context) {
if a.web == nil || strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
return
}
path := strings.TrimPrefix(filepath.Clean(c.Request.URL.Path), "/")
if path != "." {
if data, err := fs.ReadFile(a.web, path); err == nil {
if kind := mime.TypeByExtension(filepath.Ext(path)); kind != "" {
c.Header("Content-Type", kind)
}
c.Data(http.StatusOK, c.Writer.Header().Get("Content-Type"), data)
return
}
}
data, err := fs.ReadFile(a.web, "index.html")
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
}
func parseUserID(c *gin.Context) (uint, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户 ID"})
return 0, false
}
return uint(id), true
}
func (a *App) listUsers(c *gin.Context) {
var users []User
if err := a.db.Order("id").Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取用户失败"})
return
}
c.JSON(http.StatusOK, users)
}
func (a *App) createUser(c *gin.Context) {
var req struct {
Username string `json:"username"`
CanManageUsers bool `json:"canManageUsers"`
CanUpload bool `json:"canUpload"`
CanAdvancedUpload bool `json:"canAdvancedUpload"`
}
if err := c.ShouldBindJSON(&req); err != nil || !usernamePattern.MatchString(req.Username) {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名需为 3 到 32 位字母、数字、点、横线或下划线"})
return
}
if !requireAssignablePermissions(c, req.CanManageUsers, req.CanUpload, req.CanAdvancedUpload) {
return
}
password, err := randomToken(12)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"})
return
}
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
user := User{Username: req.Username, PasswordHash: string(hash), CanManageUsers: req.CanManageUsers, CanUpload: req.CanUpload, CanAdvancedUpload: req.CanAdvancedUpload, MustChangePassword: true}
if err := a.db.Create(&user).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) || strings.Contains(err.Error(), "UNIQUE") {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"})
}
return
}
c.JSON(http.StatusCreated, gin.H{"user": user, "temporaryPassword": password})
}
func (a *App) updateUser(c *gin.Context) {
id, ok := parseUserID(c)
if !ok {
return
}
var req struct {
Username string `json:"username"`
CanManageUsers bool `json:"canManageUsers"`
CanUpload bool `json:"canUpload"`
CanAdvancedUpload bool `json:"canAdvancedUpload"`
}
if err := c.ShouldBindJSON(&req); err != nil || !usernamePattern.MatchString(req.Username) {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名需为 3 到 32 位字母、数字、点、横线或下划线"})
return
}
var user User
if err := a.db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if !requireManageableTarget(c, &user) || !requireAssignablePermissions(c, req.CanManageUsers, req.CanUpload, req.CanAdvancedUpload) {
return
}
err := a.db.Model(&user).Updates(map[string]any{"username": req.Username, "can_manage_users": req.CanManageUsers, "can_upload": req.CanUpload, "can_advanced_upload": req.CanAdvancedUpload}).Error
if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
return
}
a.db.First(&user, id)
c.JSON(http.StatusOK, user)
}
func (a *App) resetPassword(c *gin.Context) {
id, ok := parseUserID(c)
if !ok {
return
}
var user User
if err := a.db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if !requireManageableTarget(c, &user) {
return
}
password, err := randomToken(12)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置密码失败"})
return
}
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err := a.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&user).Updates(map[string]any{"password_hash": string(hash), "must_change_password": true}).Error; err != nil {
return err
}
return tx.Where("user_id = ?", id).Delete(&Session{}).Error
}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置密码失败"})
return
}
c.JSON(http.StatusOK, gin.H{"temporaryPassword": password})
}
func (a *App) deleteUser(c *gin.Context) {
a.uploadMu.Lock()
defer a.uploadMu.Unlock()
id, ok := parseUserID(c)
if !ok {
return
}
if id == 1 {
c.JSON(http.StatusForbidden, gin.H{"error": "ID 为 1 的用户不能删除"})
return
}
var user User
if err := a.db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if !requireManageableTarget(c, &user) {
return
}
var files []SharedFile
if err := a.db.Where("owner_id = ?", id).Find(&files).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败"})
return
}
err := a.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("user_id = ?", id).Delete(&Session{}).Error; err != nil {
return err
}
if err := tx.Where("owner_id = ?", id).Delete(&SharedFile{}).Error; err != nil {
return err
}
return tx.Delete(&user).Error
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败"})
return
}
for _, file := range files {
_ = os.Remove(filepath.Join(a.filesDir, file.Hash))
}
c.Status(http.StatusNoContent)
}