package repositories import ( "crypto/md5" "errors" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "whimsoc/vindn-backend/models" "gorm.io/gorm" "gorm.io/gorm/clause" ) type EventRepository interface { Save(event *models.Event) error FindAll() ([]models.Event, error) SoftDeleteEndedBefore(cutoffDate string) (int64, error) } type eventRepository struct { db *gorm.DB } func NewEventRepository(db *gorm.DB) EventRepository { return &eventRepository{db} } func (r *eventRepository) Save(event *models.Event) error { if event.Geo != nil && event.Geo.Main != nil { event.Latitude = event.Geo.Main.Latitude event.Longitude = event.Geo.Main.Longitude } var categories []models.Category for _, name := range event.CategoryNames { if name == "" { continue } var cat models.Category if err := r.db.FirstOrCreate(&cat, models.Category{Name: name}).Error; err != nil { return err } categories = append(categories, cat) } event.Categories = categories var mediaObjects []models.MediaObject for _, rawMediaObject := range event.RawMediaObjects { if rawMediaObject.Name == "" { continue } var mediaObject models.MediaObject h := md5.New() io.WriteString(h, rawMediaObject.Rel) io.WriteString(h, rawMediaObject.Name) io.WriteString(h, rawMediaObject.URL) if err := r.db.FirstOrCreate(&mediaObject, models.MediaObject{ Hash: fmt.Sprintf("%x", h.Sum(nil)), RefID: rawMediaObject.RefID, Name: rawMediaObject.Name, URL: rawMediaObject.URL, Type: rawMediaObject.Type, Rel: rawMediaObject.Rel, Alt: rawMediaObject.Alt, Source: rawMediaObject.Source, Description: rawMediaObject.Description, Prio: rawMediaObject.Prio, AiOptimized: rawMediaObject.AiOptimized, License: rawMediaObject.License, Ext: filepath.Ext(rawMediaObject.URL), }).Error; err != nil { return err } if mediaObject.Type != "" { mediaType, _, mediaTypeFound := strings.Cut(mediaObject.Type, "/") if err := r.saveMediaFile(mediaObject.Hash, mediaObject.URL, mediaType, mediaObject.Ext); mediaTypeFound && err != nil { log.Fatal(err) } } mediaObjects = append(mediaObjects, mediaObject) } event.MediaObjects = mediaObjects return r.db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "external_id"}}, UpdateAll: true, }).Create(event).Error } func (r *eventRepository) FindAll() ([]models.Event, error) { var events []models.Event result := r.db.Find(&events) return events, result.Error } // SoftDeleteEndedBefore marks events whose end_date is before cutoffDate // (an exclusive "YYYY-MM-DD" boundary, the same lexicographic string // comparison the frontend uses for its own date filtering) as deleted. // .Delete() on a gorm.Model sets deleted_at rather than removing the row, // consistent with how the frontend already treats soft-deleted events. // Events with a blank end_date are left alone — there's nothing to // compare, so leaving them is safer than guessing they're stale. func (r *eventRepository) SoftDeleteEndedBefore(cutoffDate string) (int64, error) { result := r.db.Where("end_date <> '' AND end_date < ?", cutoffDate).Delete(&models.Event{}) return result.RowsAffected, result.Error } func (r *eventRepository) saveMediaFile(filename string, url string, mediaType string, extension string) error { mediaFilePath := fmt.Sprintf("./public/storage/%s/%s%s", strings.ToLower(mediaType), filename, extension) log.Printf("Get file %s and save it to %s", url, mediaFilePath) // Check if the directory exists if _, err := os.Stat(filepath.Dir(mediaFilePath)); os.IsNotExist(err) { // Create the directory if it doesn't exist err := os.MkdirAll(filepath.Dir(mediaFilePath), 0755) // 0755 is a common permission for directories if err != nil { return fmt.Errorf("failed to create directory: %v", err) } } fileInfo, err := os.Stat(mediaFilePath) if errors.Is(err, os.ErrNotExist) || fileInfo.Size() == 0 { // Create the file out, err := os.Create(mediaFilePath) if err != nil { return err } defer out.Close() // Get the data resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // Write the body to file _, err = io.Copy(out, resp.Body) if err != nil { return err } } else { log.Printf("File already exists, do not download again. Stats: Name: %s, Size: %d", fileInfo.Name(), fileInfo.Size()) } return nil }