mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"fotbal-club/internal/models"
|
||||
"fotbal-club/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FilesController struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// FileInfo represents detailed file information with usage tracking
|
||||
type FileInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
FilePath string `json:"file_path"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
UploadedBy *models.User `json:"uploaded_by,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Usages []models.FileUsage `json:"usages,omitempty"`
|
||||
UsageCount int `json:"usage_count"`
|
||||
MD5Hash string `json:"md5_hash,omitempty"`
|
||||
}
|
||||
|
||||
// GetAllFiles returns all uploaded files with usage information
|
||||
func (fc *FilesController) GetAllFiles(c *gin.Context) {
|
||||
var files []models.UploadedFile
|
||||
|
||||
query := fc.DB.Preload("UploadedBy").Preload("Usages")
|
||||
|
||||
// Optional filtering
|
||||
if search := c.Query("search"); search != "" {
|
||||
query = query.Where("filename ILIKE ?", "%"+search+"%")
|
||||
}
|
||||
|
||||
if mimeType := c.Query("mime_type"); mimeType != "" {
|
||||
query = query.Where("mime_type LIKE ?", mimeType+"%")
|
||||
}
|
||||
|
||||
// Sorting
|
||||
sortBy := c.DefaultQuery("sort_by", "created_at")
|
||||
sortOrder := c.DefaultQuery("sort_order", "desc")
|
||||
query = query.Order(fmt.Sprintf("%s %s", sortBy, sortOrder))
|
||||
|
||||
if err := query.Find(&files).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch files"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to FileInfo format
|
||||
fileInfos := make([]FileInfo, len(files))
|
||||
for i, file := range files {
|
||||
fileInfos[i] = FileInfo{
|
||||
ID: file.ID,
|
||||
Filename: file.Filename,
|
||||
FilePath: file.FilePath,
|
||||
FileURL: file.FileURL,
|
||||
FileSize: file.FileSize,
|
||||
MimeType: file.MimeType,
|
||||
UploadedBy: file.UploadedBy,
|
||||
CreatedAt: file.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
Usages: file.Usages,
|
||||
UsageCount: len(file.Usages),
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, fileInfos)
|
||||
}
|
||||
|
||||
// GetUnusedFiles returns files that have no usage records
|
||||
func (fc *FilesController) GetUnusedFiles(c *gin.Context) {
|
||||
var files []models.UploadedFile
|
||||
|
||||
// Find files with no usages
|
||||
if err := fc.DB.Preload("UploadedBy").
|
||||
Preload("Usages").
|
||||
Joins("LEFT JOIN file_usages ON file_usages.file_id = uploaded_files.id").
|
||||
Where("file_usages.id IS NULL").
|
||||
Group("uploaded_files.id").
|
||||
Order("uploaded_files.created_at DESC").
|
||||
Find(&files).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch unused files"})
|
||||
return
|
||||
}
|
||||
|
||||
fileInfos := make([]FileInfo, len(files))
|
||||
for i, file := range files {
|
||||
fileInfos[i] = FileInfo{
|
||||
ID: file.ID,
|
||||
Filename: file.Filename,
|
||||
FilePath: file.FilePath,
|
||||
FileURL: file.FileURL,
|
||||
FileSize: file.FileSize,
|
||||
MimeType: file.MimeType,
|
||||
UploadedBy: file.UploadedBy,
|
||||
CreatedAt: file.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
Usages: file.Usages,
|
||||
UsageCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, fileInfos)
|
||||
}
|
||||
|
||||
// GetDuplicateFiles finds files with the same content (based on MD5 hash)
|
||||
func (fc *FilesController) GetDuplicateFiles(c *gin.Context) {
|
||||
var files []models.UploadedFile
|
||||
|
||||
if err := fc.DB.Preload("UploadedBy").Preload("Usages").Find(&files).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch files"})
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate MD5 hashes and group by hash
|
||||
hashMap := make(map[string][]FileInfo)
|
||||
|
||||
for _, file := range files {
|
||||
// Calculate MD5 hash of file content
|
||||
hash, err := calculateFileMD5(file.FilePath)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to calculate hash for %s: %v", file.FilePath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fileInfo := FileInfo{
|
||||
ID: file.ID,
|
||||
Filename: file.Filename,
|
||||
FilePath: file.FilePath,
|
||||
FileURL: file.FileURL,
|
||||
FileSize: file.FileSize,
|
||||
MimeType: file.MimeType,
|
||||
UploadedBy: file.UploadedBy,
|
||||
CreatedAt: file.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
Usages: file.Usages,
|
||||
UsageCount: len(file.Usages),
|
||||
MD5Hash: hash,
|
||||
}
|
||||
|
||||
hashMap[hash] = append(hashMap[hash], fileInfo)
|
||||
}
|
||||
|
||||
// Filter only duplicates (groups with more than one file)
|
||||
duplicates := make(map[string][]FileInfo)
|
||||
for hash, files := range hashMap {
|
||||
if len(files) > 1 {
|
||||
duplicates[hash] = files
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, duplicates)
|
||||
}
|
||||
|
||||
// DeleteFile deletes a file and its usage records
|
||||
func (fc *FilesController) DeleteFile(c *gin.Context) {
|
||||
fileID := c.Param("id")
|
||||
|
||||
var file models.UploadedFile
|
||||
if err := fc.DB.Preload("Usages").First(&file, fileID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch file"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Return usage information if file is being used and force flag is not set
|
||||
force := c.Query("force") == "true"
|
||||
if len(file.Usages) > 0 && !force {
|
||||
c.JSON(http.StatusConflict, gin.H{
|
||||
"error": "File is in use",
|
||||
"usages": file.Usages,
|
||||
"message": "This file is being used. Use force=true to delete anyway.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete physical file
|
||||
if err := os.Remove(file.FilePath); err != nil {
|
||||
logger.Warn("Failed to delete physical file %s: %v", file.FilePath, err)
|
||||
// Continue with database deletion even if physical file deletion fails
|
||||
}
|
||||
|
||||
// Delete database record (cascades to usages)
|
||||
if err := fc.DB.Delete(&file).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file record"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"})
|
||||
}
|
||||
|
||||
// GetFileUsages returns where a specific file is being used
|
||||
func (fc *FilesController) GetFileUsages(c *gin.Context) {
|
||||
fileID := c.Param("id")
|
||||
|
||||
var usages []models.FileUsage
|
||||
if err := fc.DB.Where("file_id = ?", fileID).Find(&usages).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch file usages"})
|
||||
return
|
||||
}
|
||||
|
||||
// Enrich usage data with entity information
|
||||
enrichedUsages := make([]map[string]interface{}, len(usages))
|
||||
for i, usage := range usages {
|
||||
entityInfo := getEntityInfo(fc.DB, usage.EntityType, usage.EntityID)
|
||||
enrichedUsages[i] = map[string]interface{}{
|
||||
"id": usage.ID,
|
||||
"entity_type": usage.EntityType,
|
||||
"entity_id": usage.EntityID,
|
||||
"field_name": usage.FieldName,
|
||||
"entity_info": entityInfo,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, enrichedUsages)
|
||||
}
|
||||
|
||||
// ScanAndSyncFiles scans the uploads directory and syncs with database
|
||||
func (fc *FilesController) ScanAndSyncFiles(c *gin.Context) {
|
||||
uploadsDir := "uploads"
|
||||
|
||||
var filesInDB []models.UploadedFile
|
||||
if err := fc.DB.Find(&filesInDB).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch database files"})
|
||||
return
|
||||
}
|
||||
|
||||
dbFileMap := make(map[string]models.UploadedFile)
|
||||
for _, file := range filesInDB {
|
||||
dbFileMap[file.FilePath] = file
|
||||
}
|
||||
|
||||
foundFiles := 0
|
||||
newFiles := 0
|
||||
orphanedFiles := 0
|
||||
skippedFiles := 0
|
||||
newFilesList := []string{}
|
||||
orphanedFilesList := []string{}
|
||||
|
||||
// Walk through uploads directory
|
||||
err := filepath.Walk(uploadsDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
logger.Warn("Error accessing path %s: %v", path, err)
|
||||
return nil // Continue with other files
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip .gitkeep and hidden files
|
||||
filename := filepath.Base(path)
|
||||
if filename == ".gitkeep" || strings.HasPrefix(filename, ".") {
|
||||
skippedFiles++
|
||||
return nil
|
||||
}
|
||||
|
||||
foundFiles++
|
||||
|
||||
// Normalize path separators for cross-platform compatibility
|
||||
normalizedPath := filepath.ToSlash(path)
|
||||
if filepath.Separator == '\\' {
|
||||
// On Windows, convert backslashes to forward slashes
|
||||
normalizedPath = strings.ReplaceAll(path, "\\", "/")
|
||||
}
|
||||
|
||||
// Check both original path and normalized path
|
||||
_, existsOriginal := dbFileMap[path]
|
||||
_, existsNormalized := dbFileMap[normalizedPath]
|
||||
|
||||
if !existsOriginal && !existsNormalized {
|
||||
// File exists on disk but not in database - add it
|
||||
mimeType := detectMimeType(path)
|
||||
fileURL := "/" + filepath.ToSlash(path)
|
||||
|
||||
newFile := models.UploadedFile{
|
||||
Filename: filename,
|
||||
FilePath: normalizedPath,
|
||||
FileURL: fileURL,
|
||||
FileSize: info.Size(),
|
||||
MimeType: mimeType,
|
||||
}
|
||||
|
||||
if err := fc.DB.Create(&newFile).Error; err != nil {
|
||||
logger.Warn("Failed to create database record for %s: %v", path, err)
|
||||
} else {
|
||||
newFiles++
|
||||
newFilesList = append(newFilesList, filename)
|
||||
logger.Info("Added new file to database: %s", filename)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Failed to scan directory: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan directory"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for orphaned database records (files in DB but not on disk)
|
||||
for _, dbFile := range filesInDB {
|
||||
// Try both original path and with backslashes (for Windows)
|
||||
paths := []string{dbFile.FilePath, strings.ReplaceAll(dbFile.FilePath, "/", "\\")}
|
||||
found := false
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
orphanedFiles++
|
||||
orphanedFilesList = append(orphanedFilesList, dbFile.Filename)
|
||||
// Optionally delete orphaned records
|
||||
// fc.DB.Delete(&dbFile)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Scan completed: %d found, %d new, %d orphaned, %d skipped", foundFiles, newFiles, orphanedFiles, skippedFiles)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Scan completed",
|
||||
"found_files": foundFiles,
|
||||
"new_files": newFiles,
|
||||
"orphaned_files": orphanedFiles,
|
||||
"skipped_files": skippedFiles,
|
||||
"new_files_list": newFilesList,
|
||||
"orphaned_list": orphanedFilesList,
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to calculate MD5 hash of a file
|
||||
func calculateFileMD5(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := md5.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// Helper function to detect MIME type from file extension
|
||||
func detectMimeType(filePath string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
mimeTypes := map[string]string{
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".pdf": "application/pdf",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
|
||||
if mime, ok := mimeTypes[ext]; ok {
|
||||
return mime
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// Helper function to get entity information
|
||||
func getEntityInfo(db *gorm.DB, entityType string, entityID uint) map[string]interface{} {
|
||||
info := map[string]interface{}{
|
||||
"type": entityType,
|
||||
"id": entityID,
|
||||
}
|
||||
|
||||
switch entityType {
|
||||
case "article":
|
||||
var article models.Article
|
||||
if err := db.Select("id", "title", "slug").First(&article, entityID).Error; err == nil {
|
||||
info["title"] = article.Title
|
||||
info["slug"] = article.Slug
|
||||
info["url"] = fmt.Sprintf("/news/%s", article.Slug)
|
||||
}
|
||||
case "player":
|
||||
var player models.Player
|
||||
if err := db.Select("id", "first_name", "last_name").First(&player, entityID).Error; err == nil {
|
||||
info["name"] = fmt.Sprintf("%s %s", player.FirstName, player.LastName)
|
||||
info["url"] = fmt.Sprintf("/hraci/%d", player.ID)
|
||||
}
|
||||
case "sponsor":
|
||||
var sponsor models.Sponsor
|
||||
if err := db.Select("id", "name").First(&sponsor, entityID).Error; err == nil {
|
||||
info["name"] = sponsor.Name
|
||||
}
|
||||
case "event":
|
||||
var event models.Event
|
||||
if err := db.Select("id", "title").First(&event, entityID).Error; err == nil {
|
||||
info["title"] = event.Title
|
||||
info["url"] = fmt.Sprintf("/aktivita/%d", event.ID)
|
||||
}
|
||||
case "contact":
|
||||
var contact models.Contact
|
||||
if err := db.Select("id", "name", "position").First(&contact, entityID).Error; err == nil {
|
||||
info["name"] = contact.Name
|
||||
info["position"] = contact.Position
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
Reference in New Issue
Block a user