Files
reforger_crawler_main/controllers/indexerController.go
ilbinek 0d07148db6
Some checks failed
Cross Compile Go / build (push) Has been cancelled
dependencies added
2026-07-09 14:41:50 +02:00

262 lines
6.5 KiB
Go

package controllers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"gitea.tbdevent.eu/TBD/reforger_crawler_main/initializers"
"gitea.tbdevent.eu/TBD/reforger_crawler_main/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func GetNextToBeIndexed(c *gin.Context) {
checkIndexingTimeout()
maxFileSize := c.Query("maxFileSize")
maxSize := 10_000_000_000
if maxFileSize != "" {
if val, err := strconv.Atoi(maxFileSize); err == nil {
maxSize = val
}
}
addon, err := claimNextAddon(maxSize, true)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
if addon == nil {
addon, err = claimNextAddon(maxSize, false)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
}
if addon != nil {
c.JSON(200, gin.H{"guid": addon.ID, "currentVersion": addon.CurrentVersionNumber})
return
}
// nothing to index
c.JSON(200, gin.H{"guid": "", "currentVersion": ""})
}
// claimNextAddon picks the oldest indexable addon and claims it with an
// optimistic UPDATE, so concurrent indexers can never claim the same job.
// Returns nil when no candidate is available.
func claimNextAddon(maxSize int, priorityOnly bool) (*models.Addon, error) {
for range 5 {
query := initializers.DB.Where("to_be_indexed = ?", true).Where("is_being_indexed = ?", false).Where("blocked = ?", false).Where("current_version_size <= ?", maxSize)
if priorityOnly {
query = query.Where("priority_indexing = ?", true)
}
var addon models.Addon
ret := query.Order("updated_at asc").First(&addon)
if ret.Error == gorm.ErrRecordNotFound {
return nil, nil
}
if ret.Error != nil {
return nil, ret.Error
}
claim := initializers.DB.Model(&models.Addon{}).
Where("id = ?", addon.ID).
Where("is_being_indexed = ?", false).
Where("to_be_indexed = ?", true).
Updates(map[string]any{
"is_being_indexed": true,
"index_start_time": time.Now(),
})
if claim.Error != nil {
return nil, claim.Error
}
if claim.RowsAffected == 1 {
return &addon, nil
}
// lost the race to another indexer - retry with the next candidate
}
return nil, nil
}
func SaveIndexingResult(c *gin.Context) {
var result struct {
GUID string `json:"guid"`
CurrentVersion string `json:"currentVersion"`
Success bool `json:"success"`
Files []models.AddonFile `json:"files"`
}
err := c.ShouldBindJSON(&result)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
var addon models.Addon
ret := initializers.DB.Where("id = ?", result.GUID).First(&addon)
if ret.Error != nil {
c.JSON(404, gin.H{"error": "Addon not found"})
return
}
// get all files for this addon and delete them - old version
ret = initializers.DB.Where("addon_id = ?", addon.ID).Delete(&models.AddonFile{})
if ret.Error != nil {
c.JSON(500, gin.H{"error": ret.Error.Error()})
return
}
ret = initializers.DB.CreateInBatches(&result.Files, 100)
if ret.Error != nil {
c.JSON(500, gin.H{"error": ret.Error.Error()})
return
}
addon.CurrentVersionNumber = result.CurrentVersion
addon.IsBeingIndexed = false
addon.ToBeIndexed = false
ret = initializers.DB.Save(&addon)
if ret.Error != nil {
c.JSON(500, gin.H{"error": ret.Error.Error()})
return
}
if initializers.DiscordWebhookURL != "" {
text := "Indexing of addon " + addon.Name + " (" + addon.ID + ") was successful."
colour := 2228479
nbr := strconv.Itoa(len(result.Files))
size := fmt.Sprintf("%.2f MB", float64(addon.CurrentVersionSize)/1_000_000)
addonsToBeIndexed := int64(0)
addonsCount := int64(0)
initializers.DB.Model(&models.Addon{}).Where("to_be_indexed = ?", true).Count(&addonsToBeIndexed)
initializers.DB.Model(&models.Addon{}).Count(&addonsCount)
percentage := 0.0
if addonsCount > 0 {
percentage = (float64(addonsCount-addonsToBeIndexed) / float64(addonsCount)) * 100
}
txt := fmt.Sprintf("%d/%d (%.2f%%).", addonsCount-addonsToBeIndexed, addonsCount, percentage)
myEmbed := models.CustomEmbed{
Title: text,
Description: fmt.Sprintf("Files Indexed: %s\nOverall Progress: %s", nbr, txt),
Color: colour,
Timestamp: time.Now(),
Image: models.CustomImage{
URL: addon.Preview,
},
Fields: []models.CustomEmbedField{
{
Name: "Size",
Value: size,
Inline: true,
},
{
Name: "Current Version",
Value: addon.CurrentVersionNumber,
Inline: true,
},
// {
// Name: "Indexed by",
// Value: c.ClientIP(),
// Inline: false,
// },
},
}
myHook := models.CustomHook{
Username: "Reforger Crawler",
Embeds: []models.CustomEmbed{myEmbed},
}
err := SendCustomWebhook(initializers.DiscordWebhookURL, myHook)
if err != nil {
fmt.Println("Error sending webhook:", err)
}
}
c.JSON(200, gin.H{"status": "success"})
}
func DeleteAddon(c *gin.Context) {
guid := c.Query("guid")
if guid == "" {
c.JSON(400, gin.H{"error": "GUID is required"})
return
}
var addon models.Addon
ret := initializers.DB.Where("id = ?", guid).First(&addon)
if ret.Error != nil {
c.JSON(404, gin.H{"error": "Addon not found"})
return
}
// Delete associated files
ret = initializers.DB.Where("addon_id = ?", addon.ID).Delete(&models.AddonFile{})
if ret.Error != nil {
c.JSON(500, gin.H{"error": ret.Error.Error()})
return
}
// Delete outgoing dependency edges; incoming edges stay - other addons
// still declare this dependency even if the addon row is gone
ret = initializers.DB.Where("addon_id = ?", addon.ID).Delete(&models.AddonDependency{})
if ret.Error != nil {
c.JSON(500, gin.H{"error": ret.Error.Error()})
return
}
// Delete the addon
ret = initializers.DB.Delete(&addon)
if ret.Error != nil {
c.JSON(500, gin.H{"error": ret.Error.Error()})
return
}
c.JSON(200, gin.H{"status": "addon deleted"})
}
func checkIndexingTimeout() {
initializers.DB.Model(&models.Addon{}).
Where("is_being_indexed = ?", true).
Where("index_start_time < ?", time.Now().Add(-30*time.Minute)).
Update("is_being_indexed", false)
}
func SendCustomWebhook(webhookURL string, hook models.CustomHook) error {
payload, err := json.Marshal(hook)
if err != nil {
return err
}
req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return fmt.Errorf("webhook failed with status code %d", resp.StatusCode)
}
return nil
}