yunzer_go/server/controllers/file.go

1005 lines
23 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 controllers
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"server/models"
"strconv"
"strings"
"time"
beego "github.com/beego/beego/v2/server/web"
)
// FileController 处理文件相关请求
type FileController struct {
beego.Controller
}
// GetAllFiles 获取所有文件信息
func (c *FileController) GetAllFiles() {
files, err := models.GetAllFiles()
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件列表失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取文件列表成功",
"data": files,
}
}
c.ServeJSON()
}
// GetFileById 根据ID获取文件信息
func (c *FileController) GetFileById() {
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "参数错误",
}
c.ServeJSON()
return
}
file, err := models.GetFileById(id)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "文件不存在",
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取文件成功",
"data": file,
}
}
c.ServeJSON()
}
// GetFilesByTenant 根据租户ID获取文件信息
func (c *FileController) GetFilesByTenant() {
tenantID := c.GetString("tenant_id")
if tenantID == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "租户ID不能为空",
}
c.ServeJSON()
return
}
files, err := models.GetFilesByTenant(tenantID)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件列表失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取文件列表成功",
"data": files,
}
}
c.ServeJSON()
}
// GetFilesByCategory 根据分类获取文件信息
func (c *FileController) GetFilesByCategory() {
category := c.GetString("category")
if category == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "分类不能为空",
}
c.ServeJSON()
return
}
files, err := models.GetFilesByCategory(category)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件列表失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取文件列表成功",
"data": files,
}
}
c.ServeJSON()
}
// GetFilesByStatus 根据状态获取文件信息
func (c *FileController) GetFilesByStatus() {
statusStr := c.GetString("status")
status, err := strconv.Atoi(statusStr)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "状态参数错误",
}
c.ServeJSON()
return
}
files, err := models.GetFilesByStatus(int8(status))
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件列表失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取文件列表成功",
"data": files,
}
}
c.ServeJSON()
}
// CreateFile 创建新文件信息
func (c *FileController) CreateFile() {
var file models.FileInfo
// 解析请求体
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &file); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "请求参数错误",
"error": err.Error(),
}
c.ServeJSON()
return
}
// 从JWT中间件获取用户信息
if userID, ok := c.Ctx.Input.GetData("userId").(int); ok && userID > 0 {
file.UserID = userID
}
if username, ok := c.Ctx.Input.GetData("username").(string); ok && username != "" {
file.UploadBy = username
}
// 验证必填字段
if file.TenantID == "" || file.FileName == "" || file.OriginalName == "" ||
file.FilePath == "" || file.FileType == "" || file.FileExt == "" ||
file.Category == "" || file.UploadBy == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "必填字段不能为空",
}
c.ServeJSON()
return
}
// 添加文件信息
if _, err := models.AddFile(&file); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "创建文件失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "创建文件成功",
"data": file,
}
}
c.ServeJSON()
}
// UpdateFile 更新文件信息
func (c *FileController) UpdateFile() {
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "参数错误",
}
c.ServeJSON()
return
}
var file models.FileInfo
file.ID = id
// 解析请求体
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &file); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "请求参数错误",
"error": err.Error(),
}
c.ServeJSON()
return
}
// 更新文件信息
if err := models.UpdateFile(&file); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "更新文件失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "更新文件成功",
"data": file,
}
}
c.ServeJSON()
}
// DeleteFile 删除文件信息(软删除)
func (c *FileController) DeleteFile() {
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "参数错误",
}
c.ServeJSON()
return
}
if err := models.DeleteFile(id); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "删除文件失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "删除文件成功",
}
}
c.ServeJSON()
}
// HardDeleteFile 硬删除文件信息
func (c *FileController) HardDeleteFile() {
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "参数错误",
}
c.ServeJSON()
return
}
if err := models.HardDeleteFile(id); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "删除文件失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "删除文件成功",
}
}
c.ServeJSON()
}
// GetFileStatistics 获取文件统计信息
func (c *FileController) GetFileStatistics() {
tenantID := c.GetString("tenant_id")
if tenantID == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "租户ID不能为空",
}
c.ServeJSON()
return
}
stats, err := models.GetFileStatistics(tenantID)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取统计信息失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取统计信息成功",
"data": stats,
}
}
c.ServeJSON()
}
// SearchFiles 搜索文件
func (c *FileController) SearchFiles() {
keyword := c.GetString("keyword")
tenantID := c.GetString("tenant_id")
if keyword == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "搜索关键词不能为空",
}
c.ServeJSON()
return
}
if tenantID == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "租户ID不能为空",
}
c.ServeJSON()
return
}
files, err := models.SearchFiles(keyword, tenantID)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "搜索文件失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "搜索文件成功",
"data": files,
}
}
c.ServeJSON()
}
// GetMyFiles 获取当前用户的文件列表
func (c *FileController) GetMyFiles() {
// 从JWT中间件获取用户信息
userID, ok := c.Ctx.Input.GetData("userId").(int)
if !ok || userID <= 0 {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "用户未登录或登录已过期",
}
c.ServeJSON()
return
}
files, err := models.GetFilesByUserID(userID)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件列表失败: " + err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取成功",
"data": files,
}
}
c.ServeJSON()
}
// GetFilesPublic 不需要认证的获取文件信息接口
func (c *FileController) GetFilesPublic() {
files, err := models.GetAllFiles()
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件列表失败",
"error": err.Error(),
}
} else {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取文件列表成功",
"data": files,
}
}
c.ServeJSON()
}
// Post 处理文件上传
func (c *FileController) Post() {
// 从JWT中间件获取用户信息
userID, ok := c.Ctx.Input.GetData("userId").(int)
if !ok || userID <= 0 {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "用户未登录或登录已过期",
}
c.ServeJSON()
return
}
username, _ := c.Ctx.Input.GetData("username").(string)
if username == "" {
username = "unknown"
}
// 从JWT中间件获取租户ID
tenantID := "default"
if tid, ok := c.Ctx.Input.GetData("tenantId").(int); ok && tid > 0 {
tenantID = strconv.Itoa(tid)
}
// 获取上传的文件
file, header, err := c.GetFile("file")
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取上传文件失败: " + err.Error(),
}
c.ServeJSON()
return
}
defer file.Close()
// 获取文件基本信息
originalName := header.Filename
fileSize := header.Size
fileExt := strings.ToLower(filepath.Ext(originalName))
fileName := strings.TrimSuffix(originalName, fileExt)
// 读取文件内容到内存以计算MD5对于大文件可能需要优化
fileData := make([]byte, fileSize)
n, err := file.Read(fileData)
if err != nil && err != io.EOF {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "读取文件失败: " + err.Error(),
}
c.ServeJSON()
return
}
if int64(n) != fileSize {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "读取文件不完整",
}
c.ServeJSON()
return
}
// 计算文件MD5值
hash := md5.New()
hash.Write(fileData)
fileMD5 := hex.EncodeToString(hash.Sum(nil))
// 检查是否已存在相同MD5的文件
existingFile, err := models.GetFileByMD5AndTenant(fileMD5, tenantID)
if err == nil && existingFile != nil {
// 文件已存在,不保存文件,只创建数据库记录
// 生成日期路径(年/月/日)
now := time.Now()
// 创建文件信息记录(使用已存在的文件路径)
fileInfo := models.FileInfo{
TenantID: tenantID,
UserID: userID,
FileName: fileName,
OriginalName: originalName,
FilePath: existingFile.FilePath, // 使用已存在文件的路径
FileURL: existingFile.FileURL, // 使用已存在文件的URL
FileSize: fileSize,
FileType: getFileTypeByExt(fileExt),
FileExt: fileExt,
MD5: fileMD5,
Category: c.GetString("category"),
Status: 1,
UploadBy: username,
UploadTime: now,
}
// 如果分类为空,使用默认分类
if fileInfo.Category == "" {
fileInfo.Category = "未分类"
}
// 保存到数据库(只保存记录,不保存文件)
id, err := models.AddFile(&fileInfo)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "保存文件信息失败: " + err.Error(),
}
c.ServeJSON()
return
}
fileInfo.ID = id
// 返回成功响应
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "文件上传成功(重复文件,使用已有文件)",
"data": fileInfo,
}
c.ServeJSON()
return
}
// 文件不存在,正常上传流程
// 获取分类(可选)
category := c.GetString("category")
if category == "" {
category = "未分类"
}
// 生成日期路径(年/月/日)
now := time.Now()
datePath := now.Format("2006/01/02")
// 目录改成 server 文件夹目录下的 uploads
uploadDir := filepath.Join("uploads", datePath)
// 清理路径
uploadDir = filepath.Clean(uploadDir)
// 确保目录存在
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "创建上传目录失败: " + err.Error(),
}
c.ServeJSON()
return
}
// 生成唯一文件名(时间戳 + 原始文件名)
timestamp := now.Format("20060102150405")
uniqueFileName := timestamp + "_" + originalName
savePath := path.Join(uploadDir, uniqueFileName)
// 计算相对路径(用于存储到数据库)
relativePath := path.Join("uploads", datePath, uniqueFileName)
// 保存文件(将已读取的数据写入文件)
if err := os.WriteFile(savePath, fileData, 0644); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "保存文件失败: " + err.Error(),
}
c.ServeJSON()
return
}
// 获取文件类型
fileType := getFileTypeByExt(fileExt)
// 构造文件URL相对路径
fileURL := "/" + relativePath
// 创建文件信息记录
fileInfo := models.FileInfo{
TenantID: tenantID,
UserID: userID,
FileName: fileName,
OriginalName: originalName,
FilePath: relativePath,
FileURL: fileURL,
FileSize: fileSize,
FileType: fileType,
FileExt: fileExt,
MD5: fileMD5,
Category: category,
Status: 1, // 设置为正常状态
UploadBy: username,
UploadTime: now,
}
// 保存到数据库
id, err := models.AddFile(&fileInfo)
if err != nil {
// 如果数据库保存失败,删除已上传的文件
os.Remove(savePath)
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "保存文件信息失败: " + err.Error(),
}
c.ServeJSON()
return
}
fileInfo.ID = id
// 返回成功响应
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "文件上传成功",
"data": fileInfo,
}
c.ServeJSON()
}
// DownloadFile 下载文件
func (c *FileController) DownloadFile() {
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "参数错误",
}
c.ServeJSON()
return
}
// 获取文件信息
file, err := models.GetFileById(id)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "文件不存在",
}
c.ServeJSON()
return
}
// 获取实际的文件记录如果文件不存在通过MD5查找
actualFile, err := getActualFile(file)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件信息失败",
}
c.ServeJSON()
return
}
// 检查文件是否存在(尝试多个可能的路径)
var filePath string
possiblePaths := []string{
actualFile.FilePath, // 直接使用相对路径
filepath.Join("server", actualFile.FilePath), // server目录前缀
filepath.Join(".", actualFile.FilePath), // 当前目录
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
filePath = path
break
}
}
if filePath == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "文件不存在于服务器",
}
c.ServeJSON()
return
}
// 设置响应头
c.Ctx.Output.Header("Content-Description", "File Transfer")
c.Ctx.Output.Header("Content-Type", "application/octet-stream")
c.Ctx.Output.Header("Content-Disposition", "attachment; filename="+url.QueryEscape(actualFile.OriginalName))
c.Ctx.Output.Header("Content-Transfer-Encoding", "binary")
c.Ctx.Output.Header("Expires", "0")
c.Ctx.Output.Header("Cache-Control", "must-revalidate")
c.Ctx.Output.Header("Pragma", "public")
// 输出文件
http.ServeFile(c.Ctx.ResponseWriter, c.Ctx.Request, filePath)
}
// PreviewFile 预览文件
func (c *FileController) PreviewFile() {
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "参数错误",
}
c.ServeJSON()
return
}
// 获取文件信息
file, err := models.GetFileById(id)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "文件不存在",
}
c.ServeJSON()
return
}
// 获取实际的文件记录如果文件不存在通过MD5查找
actualFile, err := getActualFile(file)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件信息失败",
}
c.ServeJSON()
return
}
// 检查文件是否可预览
if !actualFile.CanPreview() {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "该文件类型不支持预览",
}
c.ServeJSON()
return
}
// 检查文件是否存在(尝试多个可能的路径)
var filePath string
possiblePaths := []string{
actualFile.FilePath, // 直接使用相对路径
filepath.Join("server", actualFile.FilePath), // server目录前缀
filepath.Join(".", actualFile.FilePath), // 当前目录
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
filePath = path
break
}
}
if filePath == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "文件不存在于服务器",
}
c.ServeJSON()
return
}
// 设置正确的 Content-Type
contentType := getContentType(actualFile.FileExt)
c.Ctx.Output.Header("Content-Type", contentType)
c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName))
// 打开文件
fileHandle, err := os.Open(filePath)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "打开文件失败: " + err.Error(),
}
c.ServeJSON()
return
}
defer fileHandle.Close()
// 复制文件内容到响应
io.Copy(c.Ctx.ResponseWriter, fileHandle)
}
// PublicPreviewFile 公开预览文件(用于 Office Online Viewer无需认证但仅用于预览
func (c *FileController) PublicPreviewFile() {
idStr := c.Ctx.Input.Param(":id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "参数错误",
}
c.ServeJSON()
return
}
// 获取文件信息
file, err := models.GetFileById(id)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "文件不存在",
}
c.ServeJSON()
return
}
// 获取实际的文件记录
actualFile, err := getActualFile(file)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取文件信息失败",
}
c.ServeJSON()
return
}
// 检查文件是否可预览
if !actualFile.CanPreview() {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "该文件类型不支持预览",
}
c.ServeJSON()
return
}
// 检查文件是否存在
var filePath string
possiblePaths := []string{
actualFile.FilePath,
filepath.Join("server", actualFile.FilePath),
filepath.Join(".", actualFile.FilePath),
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
filePath = path
break
}
}
if filePath == "" {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "文件不存在于服务器",
}
c.ServeJSON()
return
}
// 设置正确的 Content-Type
contentType := getContentType(actualFile.FileExt)
c.Ctx.Output.Header("Content-Type", contentType)
c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName))
// 允许跨域访问Office Online Viewer 需要)
c.Ctx.Output.Header("Access-Control-Allow-Origin", "*")
// 打开文件
fileHandle, err := os.Open(filePath)
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "打开文件失败: " + err.Error(),
}
c.ServeJSON()
return
}
defer fileHandle.Close()
// 复制文件内容到响应
io.Copy(c.Ctx.ResponseWriter, fileHandle)
}
// getFileTypeByExt 根据文件扩展名获取文件类型
func getFileTypeByExt(ext string) string {
ext = strings.ToLower(ext)
switch ext {
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
return "image"
case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt":
return "document"
case ".mp4", ".avi", ".mov", ".wmv":
return "video"
case ".mp3", ".wav", ".flac":
return "audio"
case ".zip", ".rar", ".7z":
return "archive"
default:
return "other"
}
}
// getContentType 根据文件扩展名获取 Content-Type
func getContentType(ext string) string {
ext = strings.ToLower(ext)
contentTypes := map[string]string{
// 图片
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".bmp": "image/bmp",
".webp": "image/webp",
".svg": "image/svg+xml",
// 文档
".pdf": "application/pdf",
".txt": "text/plain; charset=utf-8",
".doc": "application/msword",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".ppt": "application/vnd.ms-powerpoint",
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
// 视频
".mp4": "video/mp4",
".webm": "video/webm",
// 音频
".mp3": "audio/mpeg",
".wav": "audio/wav",
}
if contentType, ok := contentTypes[ext]; ok {
return contentType
}
return "application/octet-stream"
}
// getActualFile 获取实际的文件记录如果当前记录的文件不存在通过MD5查找唯一文件
func getActualFile(file *models.FileInfo) (*models.FileInfo, error) {
// 检查当前记录的文件是否存在(尝试多个可能的路径)
possiblePaths := []string{
file.FilePath, // 直接使用相对路径
filepath.Join("server", file.FilePath), // server目录前缀
filepath.Join(".", file.FilePath), // 当前目录
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
// 文件存在,返回当前记录
return file, nil
}
}
// 文件不存在通过MD5查找唯一文件
if file.MD5 == "" {
return file, nil
}
actualFile, err := models.GetFileByMD5(file.MD5)
if err == nil && actualFile != nil {
// 检查找到的文件是否存在
possiblePaths = []string{
actualFile.FilePath,
filepath.Join("server", actualFile.FilePath),
filepath.Join(".", actualFile.FilePath),
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
return actualFile, nil
}
}
}
// 如果找不到,返回原始记录
return file, nil
}