325 lines
6.8 KiB
Go
325 lines
6.8 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/artalkjs/artalk/v2/internal/cache"
|
|
"github.com/artalkjs/artalk/v2/internal/config"
|
|
"github.com/artalkjs/artalk/v2/internal/dao"
|
|
"github.com/artalkjs/artalk/v2/internal/db"
|
|
"github.com/artalkjs/artalk/v2/internal/hook"
|
|
"github.com/artalkjs/artalk/v2/internal/i18n"
|
|
"github.com/artalkjs/artalk/v2/internal/log"
|
|
"github.com/artalkjs/artalk/v2/internal/pkged"
|
|
)
|
|
|
|
type App struct {
|
|
conf *config.Config
|
|
dao *dao.Dao
|
|
cache *cache.Cache
|
|
service *map[string]Service
|
|
|
|
onTerminate *hook.Hook[*TerminateEvent]
|
|
onConfUpdated *hook.Hook[*ConfUpdatedEvent]
|
|
}
|
|
|
|
func NewApp(conf *config.Config) *App {
|
|
app := &App{
|
|
conf: conf,
|
|
service: &map[string]Service{},
|
|
onTerminate: &hook.Hook[*TerminateEvent]{},
|
|
onConfUpdated: &hook.Hook[*ConfUpdatedEvent]{},
|
|
}
|
|
|
|
app.injectDefaultServices()
|
|
app.registerDefaultHooks()
|
|
|
|
return app
|
|
}
|
|
|
|
func (app *App) injectDefaultServices() {
|
|
// Please do not depend on the order of dependency injection
|
|
AppInject(app, NewEmailService(app))
|
|
AppInject(app, NewIPRegionService(app))
|
|
AppInject(app, NewNotifyService(app))
|
|
AppInject(app, NewAntiSpamService(app))
|
|
}
|
|
|
|
func (app *App) registerDefaultHooks() {
|
|
app.OnTerminate().Add(func(e *TerminateEvent) error {
|
|
app.ResetBootstrapState()
|
|
return nil
|
|
})
|
|
}
|
|
|
|
var mutex = sync.Mutex{}
|
|
|
|
type timezoneInitializer struct {
|
|
once sync.Once
|
|
name string
|
|
err error
|
|
}
|
|
|
|
func (i *timezoneInitializer) init(timezone string, apply func(*time.Location)) error {
|
|
if timezone == "" {
|
|
timezone = "Local"
|
|
}
|
|
|
|
i.once.Do(func() {
|
|
i.name = timezone
|
|
if timezone == "Local" {
|
|
return
|
|
}
|
|
|
|
local, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
i.err = fmt.Errorf("timezone load error: %w (please check config or system env)", err)
|
|
return
|
|
}
|
|
apply(local)
|
|
})
|
|
|
|
if i.err != nil {
|
|
return i.err
|
|
}
|
|
if i.name != timezone {
|
|
return fmt.Errorf("timezone is already initialized as %q; restart the process to change it to %q", i.name, timezone)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var processTimezone timezoneInitializer
|
|
|
|
// Bootstrap implements App.
|
|
func (app *App) Bootstrap() error {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
if app.Conf() == nil {
|
|
return fmt.Errorf("app.conf cannot be nil while bootstrap")
|
|
}
|
|
|
|
// time.Local is process-global and must not be mutated after services start.
|
|
if err := processTimezone.init(app.Conf().TimeZone, func(local *time.Location) {
|
|
time.Local = local
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// i18n
|
|
app.initI18n()
|
|
|
|
// log
|
|
log.Init(log.Options{
|
|
IsDiscard: !app.Conf().Log.Enabled,
|
|
IsDebug: app.Conf().Debug,
|
|
LogFile: app.Conf().Log.Filename,
|
|
})
|
|
|
|
// DAO
|
|
if app.dao == nil {
|
|
if err := app.initDao(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// cache
|
|
if app.Conf().Cache.Enabled {
|
|
// cache
|
|
if err := app.initCache(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// load cache plugin on dao
|
|
app.dao.SetCache(dao.NewCacheAdaptor(app.Cache()))
|
|
|
|
// warm up cache
|
|
if app.Conf().Cache.WarmUp {
|
|
app.Dao().CacheWarmUp()
|
|
}
|
|
}
|
|
|
|
// keep config file and databases consistent
|
|
app.syncFromConf()
|
|
|
|
// init services (do not depend on the order of dependency injection)
|
|
for name, s := range *app.service {
|
|
if err := s.Init(); err != nil {
|
|
return fmt.Errorf("Service %s init error: %w", name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (app *App) ResetBootstrapState() error {
|
|
// close database
|
|
if app.Dao() != nil {
|
|
sqlDB, _ := app.Dao().DB().DB()
|
|
if err := sqlDB.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// close cache
|
|
if app.Cache() != nil {
|
|
app.Cache().Close()
|
|
}
|
|
|
|
app.dao = nil
|
|
app.cache = nil
|
|
|
|
// call service release funcs
|
|
if app.service != nil {
|
|
for name, s := range *app.service {
|
|
if err := s.Dispose(); err != nil {
|
|
return fmt.Errorf("service %s release error: %w", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// sync log
|
|
_ = log.Sync() // ignore error @see https://github.com/uber-go/zap/issues/991
|
|
|
|
return nil
|
|
}
|
|
|
|
func (app *App) Inject(name string, service Service) {
|
|
(*app.service)[name] = service
|
|
}
|
|
|
|
// @see https://github.com/golang/go/issues/55006
|
|
// @see https://github.com/golang/go/issues/49085
|
|
func AppInject[T Service](app *App, service T) {
|
|
app.Inject(genServiceName[T](), service)
|
|
}
|
|
|
|
func (app *App) Service(name string) (Service, error) {
|
|
if app.service == nil {
|
|
return nil, fmt.Errorf("services map is nil")
|
|
}
|
|
if _, isExits := (*app.service)[name]; !isExits {
|
|
return nil, fmt.Errorf("service %s not found", name)
|
|
}
|
|
return (*app.service)[name], nil
|
|
}
|
|
|
|
func AppService[T Service](app *App) (T, error) {
|
|
if app == nil {
|
|
var zero T
|
|
return zero, fmt.Errorf("app is nil")
|
|
}
|
|
service, err := app.Service(genServiceName[T]())
|
|
if err != nil {
|
|
var zero T
|
|
return zero, err
|
|
}
|
|
return service.(T), nil
|
|
}
|
|
|
|
func (app *App) Conf() *config.Config {
|
|
return app.conf
|
|
}
|
|
|
|
func (app *App) SetConf(conf *config.Config) {
|
|
app.conf = conf
|
|
app.onConfUpdated.Trigger(&ConfUpdatedEvent{App: app, Conf: conf})
|
|
}
|
|
|
|
func (app *App) Dao() *dao.Dao {
|
|
return app.dao
|
|
}
|
|
|
|
func (app *App) SetDao(dao *dao.Dao) {
|
|
app.dao = dao
|
|
}
|
|
|
|
func (app *App) Cache() *cache.Cache {
|
|
return app.cache
|
|
}
|
|
|
|
func (app *App) Restart() error {
|
|
// optimistically reset the app bootstrap state
|
|
if err := app.ResetBootstrapState(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// re-bootstrap
|
|
if err := app.Bootstrap(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (app *App) ConfTpl() string {
|
|
if app.Conf() == nil {
|
|
return config.Template("en")
|
|
}
|
|
return config.Template(strings.TrimSpace(app.Conf().Locale))
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Internal Initializations
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *App) initI18n() {
|
|
if pkged.FS() == nil {
|
|
log.Warn("i18n locales not load because the embed fs not found")
|
|
return
|
|
}
|
|
|
|
i18n.Load(app.Conf().Locale, func(locale string) ([]byte, error) {
|
|
file, err := pkged.FS().Open(fmt.Sprintf("i18n/%s.yml", locale))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return io.ReadAll(file)
|
|
})
|
|
}
|
|
|
|
func (app *App) initCache() error {
|
|
cache, err := cache.New(app.conf.Cache)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
app.cache = cache
|
|
|
|
return nil
|
|
}
|
|
|
|
func (app *App) initDao() error {
|
|
// create new db instance
|
|
dbInstance, err := db.NewDB(app.conf.DB)
|
|
if err != nil {
|
|
return fmt.Errorf("db init err: %w", err)
|
|
}
|
|
|
|
// create new dao instance
|
|
app.SetDao(dao.NewDao(dbInstance))
|
|
|
|
// patch: switch comment email hash algorithm by config
|
|
app.Dao().SetCommentEmailHashFunc(func(email string) string {
|
|
return config.GetHashFuncByFrontendConf(app.Conf())(strings.ToLower(email))
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Hooks
|
|
// -------------------------------------------------------------------
|
|
|
|
func (app *App) OnTerminate() *hook.Hook[*TerminateEvent] {
|
|
return app.onTerminate
|
|
}
|
|
|
|
func (app *App) OnConfUpdated() *hook.Hook[*ConfUpdatedEvent] {
|
|
return app.onConfUpdated
|
|
}
|