Files
opengist/internal/actions/actions.go
T
2026-06-20 01:28:48 +08:00

211 lines
5.4 KiB
Go

package actions
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync/atomic"
"time"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/index"
)
const (
SyncReposFromFS = iota
SyncReposFromDB
GitGcRepos
SyncGistPreviews
ResetHooks
IndexGists
SyncGistLanguages
DeleteExpiredGists
numActions // keep last — sizes the `running` array
)
// running tracks which actions are in progress in this instance, one slot per
// action type. It dedupes concurrent runs (e.g. a double-clicked admin button)
// and backs IsRunning for the admin panel; cross-instance single-flighting is
// handled separately by the DB action lock.
var running [numActions]atomic.Bool
const lockLease = time.Hour
type action struct {
run func()
spec string
}
var registry = map[int]action{
SyncReposFromFS: {run: syncReposFromFS},
SyncReposFromDB: {run: syncReposFromDB},
GitGcRepos: {run: gitGcRepos},
SyncGistPreviews: {run: syncGistPreviews},
ResetHooks: {run: resetHooks},
IndexGists: {run: indexGists},
SyncGistLanguages: {run: syncGistLanguages},
DeleteExpiredGists: {run: deleteExpiredGists, spec: "@every 1m"},
}
func IsRunning(actionType int) bool {
return actionType >= 0 && actionType < numActions && running[actionType].Load()
}
func RunOnce(actionType int) {
a, ok := registry[actionType]
if !ok {
log.Error().Msgf("Unknown action type %d", actionType)
return
}
if !running[actionType].CompareAndSwap(false, true) {
return // already running in this instance
}
defer running[actionType].Store(false)
// Single-flight the action across instances sharing the database so only
// one replica runs it at a time, whether triggered by the scheduler or
// manually.
acquired, err := db.AcquireLock(actionType, lockLease)
if err != nil {
log.Error().Err(err).Msgf("Could not acquire lock for action %d", actionType)
return
}
if !acquired {
return
}
defer func() {
if err := db.ReleaseLock(actionType); err != nil {
log.Error().Err(err).Msgf("Could not release lock for action %d", actionType)
}
}()
log.Info().Msgf("Starting running action %d", actionType)
a.run()
log.Info().Msgf("Finished running action %d", actionType)
}
func syncReposFromFS() {
log.Info().Msg("Syncing repositories from filesystem...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
// if repository does not exist, delete gist from database
if _, err := os.Stat(git.RepositoryPath(gist.User.Username, gist.Uuid)); err != nil && !os.IsExist(err) {
if err2 := gist.Delete(); err2 != nil {
log.Error().Err(err2).Msgf("Cannot delete gist %d", gist.ID)
}
}
}
}
func syncReposFromDB() {
log.Info().Msg("Syncing repositories from database...")
entries, err := filepath.Glob(filepath.Join(config.GetHomeDir(), "repos", "*", "*"))
if err != nil {
log.Error().Err(err).Msg("Cannot read repos directories")
return
}
for _, e := range entries {
path := strings.Split(e, string(os.PathSeparator))
gist, _ := db.GetGist(path[len(path)-2], path[len(path)-1])
if gist.ID == 0 {
if err := git.DeleteRepository(path[len(path)-2], path[len(path)-1]); err != nil {
log.Error().Err(err).Msgf("Cannot delete repository %s/%s", path[len(path)-2], path[len(path)-1])
}
}
}
}
func gitGcRepos() {
log.Info().Msg("Garbage collecting all repositories...")
if err := git.GcRepos(); err != nil {
log.Error().Err(err).Msg("Error garbage collecting repositories")
}
}
func syncGistPreviews() {
log.Info().Msg("Syncing all Gist previews...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
fmt.Println("Syncing preview for gist", gist.ID)
if err = gist.UpdatePreviewAndCount(false); err != nil {
log.Error().Err(err).Msgf("Cannot update preview and count for gist %d", gist.ID)
}
}
}
func resetHooks() {
log.Info().Msg("Resetting Git server hooks for all repositories...")
if err := git.ResetHooks(); err != nil {
log.Error().Err(err).Msg("Error resetting hooks for repositories")
}
}
func indexGists() {
log.Info().Msg("Rebuilding index from scratch...")
if err := index.ResetIndex(); err != nil {
log.Error().Err(err).Msg("Cannot reset index")
return
}
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
log.Info().Msgf("Indexing gist %d", gist.ID)
indexedGist, err := gist.ToIndexedGist()
if err != nil {
log.Error().Err(err).Msgf("Cannot convert gist %d to indexed gist", gist.ID)
continue
}
if err = index.AddInIndex(indexedGist); err != nil {
log.Error().Err(err).Msgf("Cannot index gist %d", gist.ID)
}
}
}
func syncGistLanguages() {
log.Info().Msg("Syncing all Gist languages...")
gists, err := db.GetAllGistsRows()
if err != nil {
log.Error().Err(err).Msg("Cannot get gists")
return
}
for _, gist := range gists {
log.Info().Msgf("Syncing languages for gist %d", gist.ID)
gist.UpdateLanguages()
}
}
func deleteExpiredGists() {
gists, err := db.DeleteExpiredGists()
if err != nil {
log.Error().Err(err).Msg("Cannot delete expired gists")
return
}
if len(gists) > 0 {
log.Info().Msgf("Deleted %d expired gist(s)", len(gists))
}
}