mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-02-02 04:52:50 +00:00
[feature] Use maintenance router to serve 503 while server is starting/migrating (#3705)
* [feature] Use maintenance router to serve 503 while server is starting/migrating * love you linter, kissies
This commit is contained in:
parent
61141ac232
commit
d16e4fa34d
|
@ -69,6 +69,36 @@
|
||||||
"go.uber.org/automaxprocs/maxprocs"
|
"go.uber.org/automaxprocs/maxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Maintenance starts and creates a GoToSocial server
|
||||||
|
// in maintenance mode (returns 503 for most requests).
|
||||||
|
var Maintenance action.GTSAction = func(ctx context.Context) error {
|
||||||
|
route, err := router.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating maintenance router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route maintenance handlers.
|
||||||
|
maintenance := web.NewMaintenance()
|
||||||
|
maintenance.Route(route)
|
||||||
|
|
||||||
|
// Start the maintenance router.
|
||||||
|
if err := route.Start(); err != nil {
|
||||||
|
return fmt.Errorf("error starting maintenance router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catch shutdown signals from the OS.
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
sig := <-sigs // block until signal received
|
||||||
|
log.Infof(ctx, "received signal %s, shutting down", sig)
|
||||||
|
|
||||||
|
if err := route.Stop(); err != nil {
|
||||||
|
log.Errorf(ctx, "error stopping router: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Start creates and starts a gotosocial server
|
// Start creates and starts a gotosocial server
|
||||||
var Start action.GTSAction = func(ctx context.Context) error {
|
var Start action.GTSAction = func(ctx context.Context) error {
|
||||||
// Set GOMAXPROCS / GOMEMLIMIT
|
// Set GOMAXPROCS / GOMEMLIMIT
|
||||||
|
@ -148,6 +178,23 @@
|
||||||
log.Info(ctx, "done! exiting...")
|
log.Info(ctx, "done! exiting...")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Create maintenance router.
|
||||||
|
var err error
|
||||||
|
route, err = router.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating maintenance router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route maintenance handlers.
|
||||||
|
maintenance := web.NewMaintenance()
|
||||||
|
maintenance.Route(route)
|
||||||
|
|
||||||
|
// Start the maintenance router to handle reqs
|
||||||
|
// while the instance is starting up / migrating.
|
||||||
|
if err := route.Start(); err != nil {
|
||||||
|
return fmt.Errorf("error starting maintenance router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize tracing (noop if not enabled).
|
// Initialize tracing (noop if not enabled).
|
||||||
if err := tracing.Initialize(); err != nil {
|
if err := tracing.Initialize(); err != nil {
|
||||||
return fmt.Errorf("error initializing tracing: %w", err)
|
return fmt.Errorf("error initializing tracing: %w", err)
|
||||||
|
@ -359,9 +406,15 @@ func(context.Context, time.Time) {
|
||||||
HTTP router initialization
|
HTTP router initialization
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Close down the maintenance router.
|
||||||
|
if err := route.Stop(); err != nil {
|
||||||
|
return fmt.Errorf("error stopping maintenance router: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate the main router.
|
||||||
route, err = router.New(ctx)
|
route, err = router.New(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating router: %s", err)
|
return fmt.Errorf("error creating main router: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start preparing middleware stack.
|
// Start preparing middleware stack.
|
||||||
|
|
|
@ -41,5 +41,19 @@ func serverCommands() *cobra.Command {
|
||||||
}
|
}
|
||||||
config.AddServerFlags(serverStartCmd)
|
config.AddServerFlags(serverStartCmd)
|
||||||
serverCmd.AddCommand(serverStartCmd)
|
serverCmd.AddCommand(serverStartCmd)
|
||||||
|
|
||||||
|
serverMaintenanceCmd := &cobra.Command{
|
||||||
|
Use: "maintenance",
|
||||||
|
Short: "start the gotosocial server in maintenance mode (returns 503 for almost all requests)",
|
||||||
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return preRun(preRunArgs{cmd: cmd})
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return run(cmd.Context(), server.Maintenance)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.AddServerFlags(serverMaintenanceCmd)
|
||||||
|
serverCmd.AddCommand(serverMaintenanceCmd)
|
||||||
|
|
||||||
return serverCmd
|
return serverCmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,13 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fileSystem struct {
|
type fileSystem struct {
|
||||||
|
@ -53,7 +56,11 @@ func (fs fileSystem) Open(path string) (http.File, error) {
|
||||||
// getAssetFileInfo tries to fetch the ETag for the given filePath from the module's
|
// getAssetFileInfo tries to fetch the ETag for the given filePath from the module's
|
||||||
// assetsETagCache. If it can't be found there, it uses the provided http.FileSystem
|
// assetsETagCache. If it can't be found there, it uses the provided http.FileSystem
|
||||||
// to generate a new ETag to go in the cache, which it then returns.
|
// to generate a new ETag to go in the cache, which it then returns.
|
||||||
func (m *Module) getAssetETag(filePath string, fs http.FileSystem) (string, error) {
|
func getAssetETag(
|
||||||
|
wet withETagCache,
|
||||||
|
filePath string,
|
||||||
|
fs http.FileSystem,
|
||||||
|
) (string, error) {
|
||||||
file, err := fs.Open(filePath)
|
file, err := fs.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error opening %s: %s", filePath, err)
|
return "", fmt.Errorf("error opening %s: %s", filePath, err)
|
||||||
|
@ -67,7 +74,8 @@ func (m *Module) getAssetETag(filePath string, fs http.FileSystem) (string, erro
|
||||||
|
|
||||||
fileLastModified := fileInfo.ModTime()
|
fileLastModified := fileInfo.ModTime()
|
||||||
|
|
||||||
if cachedETag, ok := m.eTagCache.Get(filePath); ok && !fileLastModified.After(cachedETag.lastModified) {
|
cache := wet.ETagCache()
|
||||||
|
if cachedETag, ok := cache.Get(filePath); ok && !fileLastModified.After(cachedETag.lastModified) {
|
||||||
// only return our cached etag if the file wasn't
|
// only return our cached etag if the file wasn't
|
||||||
// modified since last time, otherwise generate a
|
// modified since last time, otherwise generate a
|
||||||
// new one; eat fresh!
|
// new one; eat fresh!
|
||||||
|
@ -80,7 +88,7 @@ func (m *Module) getAssetETag(filePath string, fs http.FileSystem) (string, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// put new entry in cache before we return
|
// put new entry in cache before we return
|
||||||
m.eTagCache.Set(filePath, eTagCacheEntry{
|
cache.Set(filePath, eTagCacheEntry{
|
||||||
eTag: eTag,
|
eTag: eTag,
|
||||||
lastModified: fileLastModified,
|
lastModified: fileLastModified,
|
||||||
})
|
})
|
||||||
|
@ -99,7 +107,10 @@ func (m *Module) getAssetETag(filePath string, fs http.FileSystem) (string, erro
|
||||||
//
|
//
|
||||||
// todo: move this middleware out of the 'web' package and into the 'middleware'
|
// todo: move this middleware out of the 'web' package and into the 'middleware'
|
||||||
// package along with the other middlewares
|
// package along with the other middlewares
|
||||||
func (m *Module) assetsCacheControlMiddleware(fs http.FileSystem) gin.HandlerFunc {
|
func assetsCacheControlMiddleware(
|
||||||
|
wet withETagCache,
|
||||||
|
fs http.FileSystem,
|
||||||
|
) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Acquire context from gin request.
|
// Acquire context from gin request.
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
@ -118,7 +129,7 @@ func (m *Module) assetsCacheControlMiddleware(fs http.FileSystem) gin.HandlerFun
|
||||||
assetFilePath := strings.TrimPrefix(path.Clean(upath), assetsPathPrefix)
|
assetFilePath := strings.TrimPrefix(path.Clean(upath), assetsPathPrefix)
|
||||||
|
|
||||||
// either fetch etag from ttlcache or generate it
|
// either fetch etag from ttlcache or generate it
|
||||||
eTag, err := m.getAssetETag(assetFilePath, fs)
|
eTag, err := getAssetETag(wet, assetFilePath, fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error getting ETag for %s: %s", assetFilePath, err)
|
log.Errorf(ctx, "error getting ETag for %s: %s", assetFilePath, err)
|
||||||
return
|
return
|
||||||
|
@ -137,3 +148,23 @@ func (m *Module) assetsCacheControlMiddleware(fs http.FileSystem) gin.HandlerFun
|
||||||
// else let the rest of the request be processed normally
|
// else let the rest of the request be processed normally
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// routeAssets attaches *just* the
|
||||||
|
// assets filesystem to the given router.
|
||||||
|
func routeAssets(
|
||||||
|
wet withETagCache,
|
||||||
|
r *router.Router,
|
||||||
|
mi ...gin.HandlerFunc,
|
||||||
|
) {
|
||||||
|
// Group all static files from assets dir at /assets,
|
||||||
|
// so that they can use the same cache control middleware.
|
||||||
|
webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir())
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf(nil, "error getting absolute path of assets dir: %s", err)
|
||||||
|
}
|
||||||
|
fs := fileSystem{http.Dir(webAssetsAbsFilePath)}
|
||||||
|
assetsGroup := r.AttachGroup(assetsPathPrefix)
|
||||||
|
assetsGroup.Use(assetsCacheControlMiddleware(wet, fs))
|
||||||
|
assetsGroup.Use(mi...)
|
||||||
|
assetsGroup.StaticFS("/", fs)
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,10 @@
|
||||||
"codeberg.org/gruf/go-cache/v3"
|
"codeberg.org/gruf/go-cache/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type withETagCache interface {
|
||||||
|
ETagCache() cache.Cache[string, eTagCacheEntry]
|
||||||
|
}
|
||||||
|
|
||||||
func newETagCache() cache.TTLCache[string, eTagCacheEntry] {
|
func newETagCache() cache.TTLCache[string, eTagCacheEntry] {
|
||||||
eTagCache := cache.NewTTL[string, eTagCacheEntry](0, 1000, 0)
|
eTagCache := cache.NewTTL[string, eTagCacheEntry](0, 1000, 0)
|
||||||
eTagCache.SetTTL(time.Hour, false)
|
eTagCache.SetTTL(time.Hour, false)
|
||||||
|
|
70
internal/web/maintenance.go
Normal file
70
internal/web/maintenance.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/gruf/go-cache/v3"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/health"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MaintenanceModule struct {
|
||||||
|
eTagCache cache.Cache[string, eTagCacheEntry]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMaintenance returns a module that routes only
|
||||||
|
// static assets, and returns a code 503 maintenance
|
||||||
|
// message template to all other requests.
|
||||||
|
func NewMaintenance() *MaintenanceModule {
|
||||||
|
return &MaintenanceModule{
|
||||||
|
eTagCache: newETagCache(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ETagCache implements withETagCache.
|
||||||
|
func (m *MaintenanceModule) ETagCache() cache.Cache[string, eTagCacheEntry] {
|
||||||
|
return m.eTagCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MaintenanceModule) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
||||||
|
// Route static assets.
|
||||||
|
routeAssets(m, r, mi...)
|
||||||
|
|
||||||
|
// Serve OK in response to live
|
||||||
|
// requests, but not ready requests.
|
||||||
|
liveHandler := func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
r.AttachHandler(http.MethodGet, health.LivePath, liveHandler)
|
||||||
|
r.AttachHandler(http.MethodHead, health.LivePath, liveHandler)
|
||||||
|
|
||||||
|
// For everything else, serve maintenance template.
|
||||||
|
obj := map[string]string{"host": config.GetHost()}
|
||||||
|
r.AttachNoRouteHandler(func(c *gin.Context) {
|
||||||
|
retryAfter := time.Now().Add(120 * time.Second).UTC()
|
||||||
|
c.Writer.Header().Add("Retry-After", "120")
|
||||||
|
c.Writer.Header().Add("Retry-After", retryAfter.Format(http.TimeFormat))
|
||||||
|
c.Header("Cache-Control", "no-store")
|
||||||
|
c.HTML(http.StatusServiceUnavailable, "maintenance.tmpl", obj)
|
||||||
|
})
|
||||||
|
}
|
|
@ -21,14 +21,11 @@
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"codeberg.org/gruf/go-cache/v3"
|
"codeberg.org/gruf/go-cache/v3"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
|
@ -87,22 +84,22 @@ func New(db db.DB, processor *processing.Processor) *Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
// ETagCache implements withETagCache.
|
||||||
// Group all static files from assets dir at /assets,
|
func (m *Module) ETagCache() cache.Cache[string, eTagCacheEntry] {
|
||||||
// so that they can use the same cache control middleware.
|
return m.eTagCache
|
||||||
webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir())
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf(nil, "error getting absolute path of assets dir: %s", err)
|
|
||||||
}
|
}
|
||||||
fs := fileSystem{http.Dir(webAssetsAbsFilePath)}
|
|
||||||
assetsGroup := r.AttachGroup(assetsPathPrefix)
|
|
||||||
assetsGroup.Use(m.assetsCacheControlMiddleware(fs))
|
|
||||||
assetsGroup.Use(mi...)
|
|
||||||
assetsGroup.StaticFS("/", fs)
|
|
||||||
|
|
||||||
// handlers that serve profiles and statuses should use the SignatureCheck
|
// Route attaches the assets filesystem and profile,
|
||||||
// middleware, so that requests with content-type application/activity+json
|
// status, and other web handlers to the router.
|
||||||
// can still be served
|
func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
||||||
|
// Route static assets.
|
||||||
|
routeAssets(m, r, mi...)
|
||||||
|
|
||||||
|
// Route all other endpoints + handlers.
|
||||||
|
//
|
||||||
|
// Handlers that serve profiles and statuses should use
|
||||||
|
// the SignatureCheck middleware, so that requests with
|
||||||
|
// content-type application/activity+json can be served
|
||||||
profileGroup := r.AttachGroup(profileGroupPath)
|
profileGroup := r.AttachGroup(profileGroupPath)
|
||||||
profileGroup.Use(mi...)
|
profileGroup.Use(mi...)
|
||||||
profileGroup.Use(middleware.SignatureCheck(m.isURIBlocked), middleware.CacheControl(middleware.CacheControlConfig{
|
profileGroup.Use(middleware.SignatureCheck(m.isURIBlocked), middleware.CacheControl(middleware.CacheControlConfig{
|
||||||
|
@ -111,7 +108,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
||||||
profileGroup.Handle(http.MethodGet, "", m.profileGETHandler) // use empty path here since it's the base of the group
|
profileGroup.Handle(http.MethodGet, "", m.profileGETHandler) // use empty path here since it's the base of the group
|
||||||
profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler)
|
profileGroup.Handle(http.MethodGet, statusPath, m.threadGETHandler)
|
||||||
|
|
||||||
// Attach individual web handlers which require no specific middlewares
|
// Individual web handlers requiring no specific middlewares.
|
||||||
r.AttachHandler(http.MethodGet, "/", m.indexHandler) // front-page
|
r.AttachHandler(http.MethodGet, "/", m.indexHandler) // front-page
|
||||||
r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler)
|
r.AttachHandler(http.MethodGet, settingsPathPrefix, m.SettingsPanelHandler)
|
||||||
r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler)
|
r.AttachHandler(http.MethodGet, settingsPanelGlob, m.SettingsPanelHandler)
|
||||||
|
@ -128,7 +125,7 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
||||||
r.AttachHandler(http.MethodGet, signupPath, m.signupGETHandler)
|
r.AttachHandler(http.MethodGet, signupPath, m.signupGETHandler)
|
||||||
r.AttachHandler(http.MethodPost, signupPath, m.signupPOSTHandler)
|
r.AttachHandler(http.MethodPost, signupPath, m.signupPOSTHandler)
|
||||||
|
|
||||||
// Attach redirects from old endpoints to current ones for backwards compatibility
|
// Redirects from old endpoints to for back compat.
|
||||||
r.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) })
|
r.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) })
|
||||||
r.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) })
|
r.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) })
|
||||||
r.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, adminPanelPath) })
|
r.AttachHandler(http.MethodGet, "/admin", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, adminPanelPath) })
|
||||||
|
|
76
web/template/maintenance.tmpl
Normal file
76
web/template/maintenance.tmpl
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{{- /*
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/ -}}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<link rel="icon" href="/assets/logo.webp" type="image/webp">
|
||||||
|
<link rel="apple-touch-icon" href="/assets/logo.webp" type="image/webp">
|
||||||
|
<link rel="apple-touch-startup-image" href="/assets/logo.webp" type="image/webp">
|
||||||
|
<link rel="preload" href="/assets/dist/_colors.css" as="style">
|
||||||
|
<link rel="preload" href="/assets/dist/base.css" as="style">
|
||||||
|
<link rel="preload" href="/assets/dist/page.css" as="style">
|
||||||
|
<link rel="stylesheet" href="/assets/dist/_colors.css">
|
||||||
|
<link rel="stylesheet" href="/assets/dist/base.css">
|
||||||
|
<link rel="stylesheet" href="/assets/dist/page.css">
|
||||||
|
<title>{{- .host -}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<header class="page-header">
|
||||||
|
<a aria-label="{{- .host -}}. Go to instance homepage" href="/" class="nounderline">
|
||||||
|
<picture>
|
||||||
|
<img
|
||||||
|
src="/assets/logo.webp"
|
||||||
|
alt="A cartoon sloth smiling happily."
|
||||||
|
title="A cartoon sloth smiling happily."
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
<h1>{{- .host -}}</h1>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<div class="page-content">
|
||||||
|
<p>This GoToSocial instance is currently down for maintenance, starting up, or running database migrations. Please wait.</p>
|
||||||
|
<p>If you are the admin of this instance, check your GoToSocial logs for more details, and make sure to <strong>not interrupt any running database migrations</strong>!</p>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<nav>
|
||||||
|
<ul class="nodot">
|
||||||
|
<li id="version">
|
||||||
|
<a
|
||||||
|
href="https://github.com/superseriousbusiness/gotosocial"
|
||||||
|
class="nounderline"
|
||||||
|
rel="nofollow noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">🦥</span>
|
||||||
|
Source - GoToSocial
|
||||||
|
<span aria-hidden="true">🦥</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue