mirror of https://github.com/jlelse/GoBlog
Basic (experimental) plugin support with two plugin types (exec and middleware)
This commit is contained in:
parent
d813e9579c
commit
2158b156c5
|
@ -12,6 +12,7 @@ ADD leaflet/ /app/leaflet/
|
|||
ADD hlsjs/ /app/hlsjs/
|
||||
ADD dbmigrations/ /app/dbmigrations/
|
||||
ADD strings/ /app/strings/
|
||||
ADD plugins/ /app/plugins/
|
||||
|
||||
FROM buildbase as build
|
||||
|
||||
|
|
|
@ -291,7 +291,7 @@ func (a *goBlog) apGetRemoteActor(iri string) (*asPerson, int, error) {
|
|||
}
|
||||
|
||||
func (db *database) apGetAllInboxes(blog string) (inboxes []string, err error) {
|
||||
rows, err := db.query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
|
||||
rows, err := db.Query("select distinct inbox from activitypub_followers where blog = @blog", sql.Named("blog", blog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -307,17 +307,17 @@ func (db *database) apGetAllInboxes(blog string) (inboxes []string, err error) {
|
|||
}
|
||||
|
||||
func (db *database) apAddFollower(blog, follower, inbox string) error {
|
||||
_, err := db.exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
|
||||
_, err := db.Exec("insert or replace into activitypub_followers (blog, follower, inbox) values (@blog, @follower, @inbox)", sql.Named("blog", blog), sql.Named("follower", follower), sql.Named("inbox", inbox))
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *database) apRemoveFollower(blog, follower string) error {
|
||||
_, err := db.exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
|
||||
_, err := db.Exec("delete from activitypub_followers where blog = @blog and follower = @follower", sql.Named("blog", blog), sql.Named("follower", follower))
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *database) apRemoveInbox(inbox string) error {
|
||||
_, err := db.exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
|
||||
_, err := db.Exec("delete from activitypub_followers where inbox = @inbox", sql.Named("inbox", inbox))
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
3
app.go
3
app.go
|
@ -14,6 +14,7 @@ import (
|
|||
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
|
||||
"github.com/yuin/goldmark"
|
||||
"go.goblog.app/app/pkgs/minify"
|
||||
"go.goblog.app/app/pkgs/plugins"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"golang.org/x/sync/singleflight"
|
||||
"tailscale.com/tsnet"
|
||||
|
@ -75,6 +76,8 @@ type goBlog struct {
|
|||
mediaStorage mediaStorage
|
||||
// Minify
|
||||
min minify.Minifier
|
||||
// Plugins
|
||||
pluginHost *plugins.PluginHost
|
||||
// Reactions
|
||||
reactionsInit sync.Once
|
||||
reactionsCache *ristretto.Cache
|
||||
|
|
|
@ -154,7 +154,7 @@ func (db *database) getBlogStats(blog string) (data *blogStatsData, err error) {
|
|||
Months: map[string][]blogStatsRow{},
|
||||
}
|
||||
// Query and scan
|
||||
rows, err := db.query(blogStatsSql, sql.Named("status", statusPublished), sql.Named("blog", blog))
|
||||
rows, err := db.Query(blogStatsSql, sql.Named("status", statusPublished), sql.Named("blog", blog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
10
comments.go
10
comments.go
|
@ -28,7 +28,7 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
|
|||
a.serveError(w, r, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
row, err := a.db.queryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id))
|
||||
row, err := a.db.QueryRow("select id, target, name, website, comment from comments where id = @id", sql.Named("id", id))
|
||||
if err != nil {
|
||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -63,7 +63,7 @@ func (a *goBlog) createComment(w http.ResponseWriter, r *http.Request) {
|
|||
name := defaultIfEmpty(cleanHTMLText(r.FormValue("name")), "Anonymous")
|
||||
website := cleanHTMLText(r.FormValue("website"))
|
||||
// Insert
|
||||
result, err := a.db.exec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website))
|
||||
result, err := a.db.Exec("insert into comments (target, comment, name, website) values (@target, @comment, @name, @website)", sql.Named("target", target), sql.Named("comment", comment), sql.Named("name", name), sql.Named("website", website))
|
||||
if err != nil {
|
||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -116,7 +116,7 @@ func buildCommentsQuery(config *commentsRequestConfig) (query string, args []any
|
|||
func (db *database) getComments(config *commentsRequestConfig) ([]*comment, error) {
|
||||
comments := []*comment{}
|
||||
query, args := buildCommentsQuery(config)
|
||||
rows, err := db.query(query, args...)
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ func (db *database) getComments(config *commentsRequestConfig) ([]*comment, erro
|
|||
func (db *database) countComments(config *commentsRequestConfig) (count int, err error) {
|
||||
query, params := buildCommentsQuery(config)
|
||||
query = "select count(*) from (" + query + ")"
|
||||
row, err := db.queryRow(query, params...)
|
||||
row, err := db.QueryRow(query, params...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -143,6 +143,6 @@ func (db *database) countComments(config *commentsRequestConfig) (count int, err
|
|||
}
|
||||
|
||||
func (db *database) deleteComment(id int) error {
|
||||
_, err := db.exec("delete from comments where id = @id", sql.Named("id", id))
|
||||
_, err := db.Exec("delete from comments where id = @id", sql.Named("id", id))
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ type config struct {
|
|||
Blogs map[string]*configBlog `mapstructure:"blogs"`
|
||||
User *configUser `mapstructure:"user"`
|
||||
Hooks *configHooks `mapstructure:"hooks"`
|
||||
Plugins []*configPlugin `mapstructure:"plugins"`
|
||||
Micropub *configMicropub `mapstructure:"micropub"`
|
||||
PathRedirects []*configRegexRedirect `mapstructure:"pathRedirects"`
|
||||
ActivityPub *configActivityPub `mapstructure:"activityPub"`
|
||||
|
@ -322,6 +323,13 @@ type configPprof struct {
|
|||
Address string `mapstructure:"address"`
|
||||
}
|
||||
|
||||
type configPlugin struct {
|
||||
Path string `mapstructure:"path"`
|
||||
Type string `mapstructure:"type"`
|
||||
Import string `mapstructure:"import"`
|
||||
Config map[string]any `mapstructure:"config"`
|
||||
}
|
||||
|
||||
func (a *goBlog) loadConfigFile(file string) error {
|
||||
// Use viper to load the config file
|
||||
v := viper.New()
|
||||
|
|
20
database.go
20
database.go
|
@ -211,11 +211,11 @@ func (db *database) prepare(query string, args ...any) (*sql.Stmt, []any, error)
|
|||
|
||||
const dbNoCache = "nocache"
|
||||
|
||||
func (db *database) exec(query string, args ...any) (sql.Result, error) {
|
||||
return db.execContext(context.Background(), query, args...)
|
||||
func (db *database) Exec(query string, args ...any) (sql.Result, error) {
|
||||
return db.ExecContext(context.Background(), query, args...)
|
||||
}
|
||||
|
||||
func (db *database) execContext(c context.Context, query string, args ...any) (sql.Result, error) {
|
||||
func (db *database) ExecContext(c context.Context, query string, args ...any) (sql.Result, error) {
|
||||
if db == nil || db.db == nil {
|
||||
return nil, errors.New("database not initialized")
|
||||
}
|
||||
|
@ -234,11 +234,11 @@ func (db *database) execContext(c context.Context, query string, args ...any) (s
|
|||
return db.db.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (db *database) query(query string, args ...any) (*sql.Rows, error) {
|
||||
return db.queryContext(context.Background(), query, args...)
|
||||
func (db *database) Query(query string, args ...any) (*sql.Rows, error) {
|
||||
return db.QueryContext(context.Background(), query, args...)
|
||||
}
|
||||
|
||||
func (db *database) queryContext(c context.Context, query string, args ...any) (rows *sql.Rows, err error) {
|
||||
func (db *database) QueryContext(c context.Context, query string, args ...any) (rows *sql.Rows, err error) {
|
||||
if db == nil || db.db == nil {
|
||||
return nil, errors.New("database not initialized")
|
||||
}
|
||||
|
@ -257,11 +257,11 @@ func (db *database) queryContext(c context.Context, query string, args ...any) (
|
|||
return
|
||||
}
|
||||
|
||||
func (db *database) queryRow(query string, args ...any) (*sql.Row, error) {
|
||||
return db.queryRowContext(context.Background(), query, args...)
|
||||
func (db *database) QueryRow(query string, args ...any) (*sql.Row, error) {
|
||||
return db.QueryRowContext(context.Background(), query, args...)
|
||||
}
|
||||
|
||||
func (db *database) queryRowContext(c context.Context, query string, args ...any) (row *sql.Row, err error) {
|
||||
func (db *database) QueryRowContext(c context.Context, query string, args ...any) (row *sql.Row, err error) {
|
||||
if db == nil || db.db == nil {
|
||||
return nil, errors.New("database not initialized")
|
||||
}
|
||||
|
@ -283,5 +283,5 @@ func (db *database) queryRowContext(c context.Context, query string, args ...any
|
|||
// Other things
|
||||
|
||||
func (d *database) rebuildFTSIndex() {
|
||||
_, _ = d.exec("insert into posts_fts(posts_fts) values ('rebuild')")
|
||||
_, _ = d.Exec("insert into posts_fts(posts_fts) values ('rebuild')")
|
||||
}
|
||||
|
|
|
@ -15,17 +15,17 @@ func Test_database(t *testing.T) {
|
|||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
_, err = db.exec("create table test(test text);")
|
||||
_, err = db.Exec("create table test(test text);")
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
_, err = db.exec("insert into test (test) values ('Test')")
|
||||
_, err = db.Exec("insert into test (test) values ('Test')")
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
row, err := db.queryRow("select count(test) from test")
|
||||
row, err := db.QueryRow("select count(test) from test")
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ func Test_database(t *testing.T) {
|
|||
t.Error("Wrong result")
|
||||
}
|
||||
|
||||
rows, err := db.query("select count(test), test from test")
|
||||
rows, err := db.Query("select count(test), test from test")
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -53,6 +53,7 @@ require (
|
|||
// master
|
||||
github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||
github.com/traefik/yaegi v0.14.1
|
||||
github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2
|
||||
github.com/yuin/goldmark v1.4.13
|
||||
// master
|
||||
|
|
2
go.sum
2
go.sum
|
@ -481,6 +481,8 @@ github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec h1:o5aL1yX+/xzvK4
|
|||
github.com/tkrajina/gpxgo v1.2.2-0.20220217201249-321f19554eec/go.mod h1:795sjVRFo5wWyN6oOZp0RYienGGBJjpAlgOz2nCngA0=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
|
||||
github.com/traefik/yaegi v0.14.1 h1:t0ssyzeZCWTFGd/JnVuDxH/slMQfYg+2CDD4dLW/rU0=
|
||||
github.com/traefik/yaegi v0.14.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0=
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||
github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 h1:XMAtQHwKjWHIRwg+8Nj/rzUomQY1q6cM3ncA0wP8GU4=
|
||||
github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||
|
|
14
http.go
14
http.go
|
@ -7,6 +7,7 @@ import (
|
|||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"github.com/klauspost/compress/flate"
|
||||
"go.goblog.app/app/pkgs/httpcompress"
|
||||
"go.goblog.app/app/pkgs/maprouter"
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
|
@ -43,6 +45,16 @@ func (a *goBlog) startServer() (err error) {
|
|||
if a.httpsConfigured(false) {
|
||||
h = h.Append(a.securityHeaders)
|
||||
}
|
||||
// Add plugin middlewares
|
||||
middlewarePlugins := getPluginsForType[plugintypes.Middleware](a, "middleware")
|
||||
sort.Slice(middlewarePlugins, func(i, j int) bool {
|
||||
// Sort with descending prio
|
||||
return middlewarePlugins[i].Prio() > middlewarePlugins[j].Prio()
|
||||
})
|
||||
for _, plugin := range middlewarePlugins {
|
||||
h = h.Append(plugin.Handler)
|
||||
}
|
||||
// Finally...
|
||||
finalHandler := h.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
a.d.ServeHTTP(w, r)
|
||||
})
|
||||
|
@ -245,7 +257,7 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
|
|||
}
|
||||
// Check if post or alias
|
||||
path := r.URL.Path
|
||||
row, err := a.db.queryRow(`
|
||||
row, err := a.db.QueryRow(`
|
||||
-- normal posts
|
||||
select 'post', status, 200 from posts where path = @path
|
||||
union all
|
||||
|
|
|
@ -182,7 +182,7 @@ func (db *database) indieAuthSaveAuthRequest(data *indieauth.AuthenticationReque
|
|||
// Generate a code to identify the request
|
||||
code := uuid.NewString()
|
||||
// Save the request
|
||||
_, err := db.exec(
|
||||
_, err := db.Exec(
|
||||
"insert into indieauthauth (time, code, client, redirect, scope, challenge, challengemethod) values (?, ?, ?, ?, ?, ?, ?)",
|
||||
time.Now().UTC().Unix(), code, data.ClientID, data.RedirectURI, strings.Join(data.Scopes, " "), data.CodeChallenge, data.CodeChallengeMethod,
|
||||
)
|
||||
|
@ -194,7 +194,7 @@ func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.Authen
|
|||
// code valid for 10 minutes
|
||||
maxAge := time.Now().UTC().Add(-10 * time.Minute).Unix()
|
||||
// Query the database
|
||||
row, err := db.queryRow("select client, redirect, scope, challenge, challengemethod from indieauthauth where time >= ? and code = ?", maxAge, code)
|
||||
row, err := db.QueryRow("select client, redirect, scope, challenge, challengemethod from indieauthauth where time >= ? and code = ?", maxAge, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ func (db *database) indieAuthGetAuthRequest(code string) (data *indieauth.Authen
|
|||
data.Scopes = strings.Split(scope, " ")
|
||||
}
|
||||
// Delete the auth code and expired auth codes
|
||||
_, _ = db.exec("delete from indieauthauth where code = ? or time < ?", code, maxAge)
|
||||
_, _ = db.Exec("delete from indieauthauth where code = ? or time < ?", code, maxAge)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
|
@ -251,7 +251,7 @@ func (a *goBlog) indieAuthTokenVerification(w http.ResponseWriter, r *http.Reque
|
|||
func (db *database) indieAuthVerifyToken(token string) (data *indieauth.AuthenticationRequest, err error) {
|
||||
token = strings.ReplaceAll(token, "Bearer ", "")
|
||||
data = &indieauth.AuthenticationRequest{Scopes: []string{}}
|
||||
row, err := db.queryRow("select client, scope from indieauthtoken where token = @token", sql.Named("token", token))
|
||||
row, err := db.QueryRow("select client, scope from indieauthtoken where token = @token", sql.Named("token", token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -271,13 +271,13 @@ func (db *database) indieAuthVerifyToken(token string) (data *indieauth.Authenti
|
|||
// Save a new token to the database
|
||||
func (db *database) indieAuthSaveToken(data *indieauth.AuthenticationRequest) (string, error) {
|
||||
token := uuid.NewString()
|
||||
_, err := db.exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", time.Now().UTC().Unix(), token, data.ClientID, strings.Join(data.Scopes, " "))
|
||||
_, err := db.Exec("insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)", time.Now().UTC().Unix(), token, data.ClientID, strings.Join(data.Scopes, " "))
|
||||
return token, err
|
||||
}
|
||||
|
||||
// Revoke and delete the token from the database
|
||||
func (db *database) indieAuthRevokeToken(token string) {
|
||||
if token != "" {
|
||||
_, _ = db.exec("delete from indieauthtoken where token=?", token)
|
||||
_, _ = db.Exec("delete from indieauthtoken where token=?", token)
|
||||
}
|
||||
}
|
||||
|
|
6
main.go
6
main.go
|
@ -67,6 +67,12 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
// Initialize plugins
|
||||
if err = app.initPlugins(); err != nil {
|
||||
app.logErrAndQuit("Failed to init plugins:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Healthcheck tool
|
||||
if len(os.Args) >= 2 && os.Args[1] == "healthcheck" {
|
||||
// Connect to public address + "/ping" and exit with 0 when successful
|
||||
|
|
|
@ -41,19 +41,19 @@ func (a *goBlog) sendNotification(text string) {
|
|||
}
|
||||
|
||||
func (db *database) saveNotification(n *notification) error {
|
||||
if _, err := db.exec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil {
|
||||
if _, err := db.Exec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *database) deleteNotification(id int) error {
|
||||
_, err := db.exec("delete from notifications where id = @id", sql.Named("id", id))
|
||||
_, err := db.Exec("delete from notifications where id = @id", sql.Named("id", id))
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *database) deleteAllNotifications() error {
|
||||
_, err := db.exec("delete from notifications")
|
||||
_, err := db.Exec("delete from notifications")
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ func buildNotificationsQuery(config *notificationsRequestConfig) (query string,
|
|||
func (db *database) getNotifications(config *notificationsRequestConfig) ([]*notification, error) {
|
||||
notifications := []*notification{}
|
||||
query, args := buildNotificationsQuery(config)
|
||||
rows, err := db.query(query, args...)
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ func (db *database) getNotifications(config *notificationsRequestConfig) ([]*not
|
|||
func (db *database) countNotifications(config *notificationsRequestConfig) (count int, err error) {
|
||||
query, params := buildNotificationsQuery(config)
|
||||
query = "select count(*) from (" + query + ")"
|
||||
row, err := db.queryRow(query, params...)
|
||||
row, err := db.QueryRow(query, params...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ func (db *database) cachePersistentlyContext(ctx context.Context, key string, da
|
|||
if db == nil {
|
||||
return errors.New("database is nil")
|
||||
}
|
||||
_, err := db.execContext(ctx, "insert or replace into persistent_cache(key, data, date) values(@key, @data, @date)", sql.Named("key", key), sql.Named("data", data), sql.Named("date", utcNowString()))
|
||||
_, err := db.ExecContext(ctx, "insert or replace into persistent_cache(key, data, date) values(@key, @data, @date)", sql.Named("key", key), sql.Named("data", data), sql.Named("date", utcNowString()))
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ func (db *database) retrievePersistentCacheContext(c context.Context, key string
|
|||
return nil, errors.New("database is nil")
|
||||
}
|
||||
d, err, _ := db.pc.Do(key, func() (any, error) {
|
||||
if row, err := db.queryRowContext(c, "select data from persistent_cache where key = @key", sql.Named("key", key)); err != nil {
|
||||
if row, err := db.QueryRowContext(c, "select data from persistent_cache where key = @key", sql.Named("key", key)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
err = row.Scan(&data)
|
||||
|
@ -51,6 +51,6 @@ func (db *database) clearPersistentCache(pattern string) error {
|
|||
}
|
||||
|
||||
func (db *database) clearPersistentCacheContext(c context.Context, pattern string) error {
|
||||
_, err := db.execContext(c, "delete from persistent_cache where key like @pattern", sql.Named("pattern", pattern))
|
||||
_, err := db.ExecContext(c, "delete from persistent_cache where key like @pattern", sql.Named("pattern", pattern))
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/traefik/yaegi/interp"
|
||||
"github.com/traefik/yaegi/stdlib"
|
||||
)
|
||||
|
||||
type plugin struct {
|
||||
Config *PluginConfig
|
||||
plugin reflect.Value
|
||||
}
|
||||
|
||||
// PluginConfig is the configuration of the plugin.
|
||||
type PluginConfig struct {
|
||||
// Path is the storage path of the plugin.
|
||||
Path string
|
||||
// ImportPath is the module path i.e. "github.com/user/module".
|
||||
ImportPath string
|
||||
// PluginType is the type of plugin, this plugin is checked against that type.
|
||||
// The available types are specified by the implementor of this package.
|
||||
PluginType string
|
||||
}
|
||||
|
||||
func (p *plugin) initPlugin(host *PluginHost) error {
|
||||
const errText = "initPlugin: %w"
|
||||
|
||||
interpreter := interp.New(interp.Options{
|
||||
GoPath: p.Config.Path,
|
||||
})
|
||||
|
||||
if err := interpreter.Use(stdlib.Symbols); err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
if err := interpreter.Use(host.Symbols); err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
if _, err := interpreter.Eval(fmt.Sprintf(`import "%s"`, p.Config.ImportPath)); err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
v, err := interpreter.Eval(filepath.Base(p.Config.ImportPath) + ".GetPlugin")
|
||||
if err != nil {
|
||||
return fmt.Errorf(errText, err)
|
||||
}
|
||||
|
||||
result := v.Call([]reflect.Value{})
|
||||
if len(result) > 1 {
|
||||
return fmt.Errorf(errText+": function GetPlugin has more than one return value", ErrValidatingPlugin)
|
||||
}
|
||||
p.plugin = result[0]
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/traefik/yaegi/interp"
|
||||
)
|
||||
|
||||
// NewPluginHost initializes a PluginHost.
|
||||
func NewPluginHost(symbols interp.Exports) *PluginHost {
|
||||
return &PluginHost{
|
||||
Plugins: []*plugin{},
|
||||
PluginTypes: map[string]reflect.Type{},
|
||||
Symbols: symbols,
|
||||
}
|
||||
}
|
||||
|
||||
// AddPluginType adds a plugin type to the list.
|
||||
// The interface for the pluginType parameter should be a nil of the plugin type interface:
|
||||
//
|
||||
// (*PluginInterface)(nil)
|
||||
func (h *PluginHost) AddPluginType(name string, pluginType interface{}) {
|
||||
h.PluginTypes[name] = reflect.TypeOf(pluginType).Elem()
|
||||
}
|
||||
|
||||
// LoadPlugin loads a new plugin to the host.
|
||||
func (h *PluginHost) LoadPlugin(config *PluginConfig) (any, error) {
|
||||
p := &plugin{
|
||||
Config: config,
|
||||
}
|
||||
err := p.initPlugin(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = h.validatePlugin(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Plugins = append(h.Plugins, p)
|
||||
return p.plugin.Interface(), nil
|
||||
}
|
||||
|
||||
func (h *PluginHost) validatePlugin(p *plugin) error {
|
||||
pType := reflect.TypeOf(p.plugin.Interface())
|
||||
|
||||
if _, ok := h.PluginTypes[p.Config.PluginType]; !ok {
|
||||
return fmt.Errorf("validatePlugin: %v: %w", p.Config.PluginType, ErrInvalidType)
|
||||
}
|
||||
|
||||
if !pType.Implements(h.PluginTypes[p.Config.PluginType]) {
|
||||
return fmt.Errorf("validatePlugin:%v: %w %v", p, ErrValidatingPlugin, p.Config.PluginType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlugins returns a list of all plugins.
|
||||
func (h *PluginHost) GetPlugins() (list []any) {
|
||||
for _, p := range h.Plugins {
|
||||
list = append(list, p.plugin.Interface())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPluginsForType returns all the plugins that are of type pluginType or empty if the pluginType doesn't exist.
|
||||
func GetPluginsForType[T any](h *PluginHost, pluginType string) (list []T) {
|
||||
if _, ok := h.PluginTypes[pluginType]; !ok {
|
||||
return
|
||||
}
|
||||
for _, p := range h.Plugins {
|
||||
if p.Config.PluginType != pluginType {
|
||||
continue
|
||||
}
|
||||
if t, ok := p.plugin.Interface().(T); ok {
|
||||
list = append(list, t)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/traefik/yaegi/interp"
|
||||
)
|
||||
|
||||
// PluginHost manages the plugins.
|
||||
type PluginHost struct {
|
||||
// Plugins contains a list of the plugins.
|
||||
Plugins []*plugin
|
||||
// PluginTypes is a list of plugins types that plugins have to use at least one of.
|
||||
PluginTypes map[string]reflect.Type
|
||||
// Symbols is the map of symbols generated by yaegi extract.
|
||||
Symbols interp.Exports
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidType is returned when the plugin type specified by the plugin is invalid.
|
||||
ErrInvalidType = errors.New("invalid plugin type")
|
||||
// ErrValidatingPlugin is returned when the plugin fails to fully implement the interface of the plugin type.
|
||||
ErrValidatingPlugin = errors.New("plugin does not implement type")
|
||||
)
|
|
@ -0,0 +1,51 @@
|
|||
package plugintypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Interface to GoBlog
|
||||
|
||||
// App is used to access GoBlog's app instance.
|
||||
type App interface {
|
||||
GetDatabase() Database
|
||||
}
|
||||
|
||||
// Database is used to provide access to GoBlog's database.
|
||||
type Database interface {
|
||||
Exec(string, ...any) (sql.Result, error)
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
Query(string, ...any) (*sql.Rows, error)
|
||||
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
|
||||
QueryRow(string, ...any) (*sql.Row, error)
|
||||
QueryRowContext(context.Context, string, ...any) (*sql.Row, error)
|
||||
}
|
||||
|
||||
// Plugin types
|
||||
|
||||
// SetApp is used in all plugin types to allow
|
||||
// GoBlog set it's app instance to be accessible by the plugin.
|
||||
type SetApp interface {
|
||||
SetApp(App)
|
||||
}
|
||||
|
||||
// SetConfig is used in all plugin types to allow
|
||||
// GoBlog set plugin configuration.
|
||||
type SetConfig interface {
|
||||
SetConfig(map[string]any)
|
||||
}
|
||||
|
||||
type Exec interface {
|
||||
SetApp
|
||||
SetConfig
|
||||
Exec()
|
||||
}
|
||||
|
||||
type Middleware interface {
|
||||
SetApp
|
||||
SetConfig
|
||||
Handler(http.Handler) http.Handler
|
||||
Prio() int
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
// Code generated by 'yaegi extract go.goblog.app/app/pkgs/plugintypes'. DO NOT EDIT.
|
||||
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2020 - 2022 Jan-Lukas Else
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package yaegiwrappers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Symbols["go.goblog.app/app/pkgs/plugintypes/plugintypes"] = map[string]reflect.Value{
|
||||
// type definitions
|
||||
"App": reflect.ValueOf((*plugintypes.App)(nil)),
|
||||
"Database": reflect.ValueOf((*plugintypes.Database)(nil)),
|
||||
"Exec": reflect.ValueOf((*plugintypes.Exec)(nil)),
|
||||
"Middleware": reflect.ValueOf((*plugintypes.Middleware)(nil)),
|
||||
"SetApp": reflect.ValueOf((*plugintypes.SetApp)(nil)),
|
||||
"SetConfig": reflect.ValueOf((*plugintypes.SetConfig)(nil)),
|
||||
|
||||
// interface wrapper definitions
|
||||
"_App": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_App)(nil)),
|
||||
"_Database": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Database)(nil)),
|
||||
"_Exec": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Exec)(nil)),
|
||||
"_Middleware": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_Middleware)(nil)),
|
||||
"_SetApp": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetApp)(nil)),
|
||||
"_SetConfig": reflect.ValueOf((*_go_goblog_app_app_pkgs_plugintypes_SetConfig)(nil)),
|
||||
}
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_App is an interface wrapper for App type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_App struct {
|
||||
IValue interface{}
|
||||
WGetDatabase func() plugintypes.Database
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_App) GetDatabase() plugintypes.Database {
|
||||
return W.WGetDatabase()
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_Database is an interface wrapper for Database type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_Database struct {
|
||||
IValue interface{}
|
||||
WExec func(a0 string, a1 ...any) (sql.Result, error)
|
||||
WExecContext func(a0 context.Context, a1 string, a2 ...any) (sql.Result, error)
|
||||
WQuery func(a0 string, a1 ...any) (*sql.Rows, error)
|
||||
WQueryContext func(a0 context.Context, a1 string, a2 ...any) (*sql.Rows, error)
|
||||
WQueryRow func(a0 string, a1 ...any) (*sql.Row, error)
|
||||
WQueryRowContext func(a0 context.Context, a1 string, a2 ...any) (*sql.Row, error)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) Exec(a0 string, a1 ...any) (sql.Result, error) {
|
||||
return W.WExec(a0, a1...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) ExecContext(a0 context.Context, a1 string, a2 ...any) (sql.Result, error) {
|
||||
return W.WExecContext(a0, a1, a2...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) Query(a0 string, a1 ...any) (*sql.Rows, error) {
|
||||
return W.WQuery(a0, a1...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryContext(a0 context.Context, a1 string, a2 ...any) (*sql.Rows, error) {
|
||||
return W.WQueryContext(a0, a1, a2...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryRow(a0 string, a1 ...any) (*sql.Row, error) {
|
||||
return W.WQueryRow(a0, a1...)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Database) QueryRowContext(a0 context.Context, a1 string, a2 ...any) (*sql.Row, error) {
|
||||
return W.WQueryRowContext(a0, a1, a2...)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_Exec is an interface wrapper for Exec type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_Exec struct {
|
||||
IValue interface{}
|
||||
WExec func()
|
||||
WSetApp func(a0 plugintypes.App)
|
||||
WSetConfig func(a0 map[string]any)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) Exec() {
|
||||
W.WExec()
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetApp(a0 plugintypes.App) {
|
||||
W.WSetApp(a0)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Exec) SetConfig(a0 map[string]any) {
|
||||
W.WSetConfig(a0)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_Middleware is an interface wrapper for Middleware type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_Middleware struct {
|
||||
IValue interface{}
|
||||
WHandler func(a0 http.Handler) http.Handler
|
||||
WPrio func() int
|
||||
WSetApp func(a0 plugintypes.App)
|
||||
WSetConfig func(a0 map[string]any)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Handler(a0 http.Handler) http.Handler {
|
||||
return W.WHandler(a0)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) Prio() int {
|
||||
return W.WPrio()
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetApp(a0 plugintypes.App) {
|
||||
W.WSetApp(a0)
|
||||
}
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_Middleware) SetConfig(a0 map[string]any) {
|
||||
W.WSetConfig(a0)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_SetApp is an interface wrapper for SetApp type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_SetApp struct {
|
||||
IValue interface{}
|
||||
WSetApp func(a0 plugintypes.App)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_SetApp) SetApp(a0 plugintypes.App) {
|
||||
W.WSetApp(a0)
|
||||
}
|
||||
|
||||
// _go_goblog_app_app_pkgs_plugintypes_SetConfig is an interface wrapper for SetConfig type
|
||||
type _go_goblog_app_app_pkgs_plugintypes_SetConfig struct {
|
||||
IValue interface{}
|
||||
WSetConfig func(a0 map[string]any)
|
||||
}
|
||||
|
||||
func (W _go_goblog_app_app_pkgs_plugintypes_SetConfig) SetConfig(a0 map[string]any) {
|
||||
W.WSetConfig(a0)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package yaegiwrappers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var (
|
||||
Symbols = make(map[string]map[string]reflect.Value)
|
||||
)
|
||||
|
||||
//go:generate yaegi extract -license ../../LICENSE -name yaegiwrappers go.goblog.app/app/pkgs/plugintypes
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"go.goblog.app/app/pkgs/plugins"
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
"go.goblog.app/app/pkgs/yaegiwrappers"
|
||||
)
|
||||
|
||||
func (a *goBlog) initPlugins() error {
|
||||
a.pluginHost = plugins.NewPluginHost(yaegiwrappers.Symbols)
|
||||
|
||||
a.pluginHost.AddPluginType("exec", (*plugintypes.Exec)(nil))
|
||||
a.pluginHost.AddPluginType("middleware", (*plugintypes.Middleware)(nil))
|
||||
|
||||
for _, pc := range a.cfg.Plugins {
|
||||
if pluginInterface, err := a.pluginHost.LoadPlugin(&plugins.PluginConfig{
|
||||
Path: pc.Path,
|
||||
ImportPath: pc.Import,
|
||||
PluginType: pc.Type,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if pluginInterface != nil {
|
||||
if setAppPlugin, ok := pluginInterface.(plugintypes.SetApp); ok {
|
||||
setAppPlugin.SetApp(a)
|
||||
}
|
||||
if setConfigPlugin, ok := pluginInterface.(plugintypes.SetConfig); ok {
|
||||
setConfigPlugin.SetConfig(pc.Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execs := getPluginsForType[plugintypes.Exec](a, "exec")
|
||||
for _, p := range execs {
|
||||
go p.Exec()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPluginsForType[T any](a *goBlog, pluginType string) (list []T) {
|
||||
return plugins.GetPluginsForType[T](a.pluginHost, pluginType)
|
||||
}
|
||||
|
||||
// Implement all needed interfaces for goblog
|
||||
|
||||
var _ plugintypes.App = &goBlog{}
|
||||
|
||||
func (a *goBlog) GetDatabase() plugintypes.Database {
|
||||
return a.db
|
||||
}
|
||||
|
||||
var _ plugintypes.Database = &database{}
|
|
@ -0,0 +1,37 @@
|
|||
package demoexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
)
|
||||
|
||||
func GetPlugin() plugintypes.Exec {
|
||||
return &plugin{}
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
app plugintypes.App
|
||||
}
|
||||
|
||||
func (p *plugin) SetApp(app plugintypes.App) {
|
||||
p.app = app
|
||||
}
|
||||
|
||||
func (*plugin) SetConfig(_ map[string]any) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
func (p *plugin) Exec() {
|
||||
fmt.Println("Hello World from the demo plugin!")
|
||||
|
||||
row, _ := p.app.GetDatabase().QueryRow("select count (*) from posts")
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
fmt.Println(fmt.Errorf("failed to count posts: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Number of posts in database: %d", count)
|
||||
fmt.Println()
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package demomiddleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
)
|
||||
|
||||
func GetPlugin() plugintypes.Middleware {
|
||||
return &plugin{}
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
app plugintypes.App
|
||||
config map[string]any
|
||||
}
|
||||
|
||||
func (p *plugin) SetApp(app plugintypes.App) {
|
||||
p.app = app
|
||||
}
|
||||
|
||||
func (p *plugin) SetConfig(config map[string]any) {
|
||||
p.config = config
|
||||
}
|
||||
|
||||
func (p *plugin) Prio() int {
|
||||
if prioAny, ok := p.config["prio"]; ok {
|
||||
if prio, ok := prioAny.(int); ok {
|
||||
return prio
|
||||
}
|
||||
}
|
||||
return 100
|
||||
}
|
||||
|
||||
func (p *plugin) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Demo", fmt.Sprintf("This is from the demo middleware with prio %d", p.Prio()))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.goblog.app/app/pkgs/plugintypes"
|
||||
)
|
||||
|
||||
func TestExecPlugin(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
app.cfg.Plugins = []*configPlugin{
|
||||
{
|
||||
Path: "./plugins/demo",
|
||||
Type: "exec",
|
||||
Import: "demoexec",
|
||||
},
|
||||
}
|
||||
|
||||
err := app.initConfig(false)
|
||||
require.NoError(t, err)
|
||||
err = app.initPlugins()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMiddlewarePlugin(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
app.cfg.Plugins = []*configPlugin{
|
||||
{
|
||||
Path: "./plugins/demo",
|
||||
Type: "middleware",
|
||||
Import: "demomiddleware",
|
||||
Config: map[string]any{
|
||||
"prio": 99,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.initConfig(false)
|
||||
require.NoError(t, err)
|
||||
err = app.initPlugins()
|
||||
require.NoError(t, err)
|
||||
|
||||
middlewarePlugins := getPluginsForType[plugintypes.Middleware](app, "middleware")
|
||||
if assert.Len(t, middlewarePlugins, 1) {
|
||||
mdw := middlewarePlugins[0]
|
||||
assert.Equal(t, 99, mdw.Prio())
|
||||
}
|
||||
|
||||
}
|
22
postsDb.go
22
postsDb.go
|
@ -203,7 +203,7 @@ func (db *database) savePost(p *post, o *postCreationOptions) error {
|
|||
// Commit transaction
|
||||
sqlBuilder.WriteString("commit;")
|
||||
// Execute
|
||||
if _, err := db.exec(sqlBuilder.String(), sqlArgs...); err != nil {
|
||||
if _, err := db.Exec(sqlBuilder.String(), sqlArgs...); err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed: posts.path") {
|
||||
return errors.New("post already exists at given path")
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ func (a *goBlog) deletePost(path string) error {
|
|||
// Post exists, check if it's already marked as deleted
|
||||
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
|
||||
// Post is already marked as deleted, delete it from database
|
||||
if _, err = a.db.exec(
|
||||
if _, err = a.db.Exec(
|
||||
`begin; delete from posts where path = ?; insert or ignore into deleted (path) values (?); commit;`,
|
||||
dbNoCache, p.Path, p.Path, p.Path,
|
||||
); err != nil {
|
||||
|
@ -250,7 +250,7 @@ func (a *goBlog) deletePost(path string) error {
|
|||
}
|
||||
p.Parameters["deleted"] = []string{deletedTime}
|
||||
// Mark post as deleted
|
||||
if _, err = a.db.exec(
|
||||
if _, err = a.db.Exec(
|
||||
`begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; insert into post_parameters (path, parameter, value) values (?, 'deleted', ?); commit;`,
|
||||
dbNoCache, p.Status, p.Path, p.Path, p.Path, deletedTime,
|
||||
); err != nil {
|
||||
|
@ -283,7 +283,7 @@ func (a *goBlog) undeletePost(path string) error {
|
|||
// Remove parameter
|
||||
p.Parameters["deleted"] = nil
|
||||
// Update database
|
||||
if _, err = a.db.exec(
|
||||
if _, err = a.db.Exec(
|
||||
`begin; update posts set status = ? where path = ?; delete from post_parameters where path = ? and parameter = 'deleted'; commit;`,
|
||||
dbNoCache, p.Status, p.Path, p.Path,
|
||||
); err != nil {
|
||||
|
@ -320,7 +320,7 @@ func (db *database) replacePostParam(path, param string, values []string) error
|
|||
// Commit transaction
|
||||
sqlBuilder.WriteString("commit;")
|
||||
// Execute
|
||||
_, err := db.exec(sqlBuilder.String(), sqlArgs...)
|
||||
_, err := db.Exec(sqlBuilder.String(), sqlArgs...)
|
||||
bufferpool.Put(sqlBuilder)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -514,7 +514,7 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err
|
|||
// Order
|
||||
queryBuilder.WriteString(" order by id")
|
||||
// Query
|
||||
rows, err := d.query(queryBuilder.String(), sqlArgs...)
|
||||
rows, err := d.Query(queryBuilder.String(), sqlArgs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -542,7 +542,7 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err
|
|||
func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) {
|
||||
// Query posts
|
||||
query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, priority")
|
||||
rows, err := a.db.query(query, queryParams...)
|
||||
rows, err := a.db.Query(query, queryParams...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -595,7 +595,7 @@ func (a *goBlog) getPost(path string) (*post, error) {
|
|||
|
||||
func (d *database) countPosts(config *postsRequestConfig) (count int, err error) {
|
||||
query, params := buildPostsQuery(config, "path")
|
||||
row, err := d.queryRow("select count(distinct path) from ("+query+")", params...)
|
||||
row, err := d.QueryRow("select count(distinct path) from ("+query+")", params...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -606,7 +606,7 @@ func (d *database) countPosts(config *postsRequestConfig) (count int, err error)
|
|||
func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
|
||||
sections := lo.Keys(a.cfg.Blogs[blog].Sections)
|
||||
query, params := buildPostsQuery(&postsRequestConfig{randomOrder: true, limit: 1, blog: blog, sections: sections}, "path")
|
||||
row, err := a.db.queryRow(query, params...)
|
||||
row, err := a.db.QueryRow(query, params...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -621,7 +621,7 @@ func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
|
|||
|
||||
func (d *database) allTaxonomyValues(blog string, taxonomy string) ([]string, error) {
|
||||
// TODO: Query posts the normal way
|
||||
rows, err := d.query("select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status) order by value", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
|
||||
rows, err := d.Query("select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status) order by value", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -663,7 +663,7 @@ func (db *database) usesOfMediaFile(names ...string) (counts []int, err error) {
|
|||
nameValues.WriteByte(')')
|
||||
sqlArgs = append(sqlArgs, sql.Named(named, n))
|
||||
}
|
||||
rows, err := db.query(fmt.Sprintf(mediaUseSql, nameValues.String()), sqlArgs...)
|
||||
rows, err := db.Query(fmt.Sprintf(mediaUseSql, nameValues.String()), sqlArgs...)
|
||||
bufferpool.Put(nameValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -402,7 +402,7 @@ func Test_postDeletesParams(t *testing.T) {
|
|||
err = app.deletePost("/test/abc")
|
||||
require.NoError(t, err)
|
||||
|
||||
row, err := app.db.queryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test")
|
||||
row, err := app.db.QueryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test")
|
||||
require.NoError(t, err)
|
||||
|
||||
var count int
|
||||
|
@ -415,7 +415,7 @@ func Test_postDeletesParams(t *testing.T) {
|
|||
err = app.deletePost("/test/abc")
|
||||
require.NoError(t, err)
|
||||
|
||||
row, err = app.db.queryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test")
|
||||
row, err = app.db.QueryRow("select count(*) from post_parameters where path = ? and parameter = ?", "/test/abc", "test")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = row.Scan(&count)
|
||||
|
|
8
queue.go
8
queue.go
|
@ -22,7 +22,7 @@ func (a *goBlog) enqueue(name string, content []byte, schedule time.Time) error
|
|||
if len(content) == 0 {
|
||||
return errors.New("empty content")
|
||||
}
|
||||
_, err := a.db.exec(
|
||||
_, err := a.db.Exec(
|
||||
"insert into queue (name, content, schedule) values (@name, @content, @schedule)",
|
||||
sql.Named("name", name),
|
||||
sql.Named("content", content),
|
||||
|
@ -35,7 +35,7 @@ func (a *goBlog) enqueue(name string, content []byte, schedule time.Time) error
|
|||
}
|
||||
|
||||
func (a *goBlog) reschedule(qi *queueItem, dur time.Duration) error {
|
||||
_, err := a.db.exec(
|
||||
_, err := a.db.Exec(
|
||||
"update queue set schedule = @schedule, content = @content where id = @id",
|
||||
sql.Named("schedule", qi.schedule.Add(dur).UTC().Format(time.RFC3339Nano)),
|
||||
sql.Named("content", qi.content),
|
||||
|
@ -45,12 +45,12 @@ func (a *goBlog) reschedule(qi *queueItem, dur time.Duration) error {
|
|||
}
|
||||
|
||||
func (a *goBlog) dequeue(qi *queueItem) error {
|
||||
_, err := a.db.exec("delete from queue where id = @id", sql.Named("id", qi.id))
|
||||
_, err := a.db.Exec("delete from queue where id = @id", sql.Named("id", qi.id))
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *goBlog) peekQueue(ctx context.Context, name string) (*queueItem, error) {
|
||||
row, err := a.db.queryRowContext(
|
||||
row, err := a.db.QueryRowContext(
|
||||
ctx,
|
||||
"select id, name, content, schedule from queue where schedule <= @schedule and name = @name order by schedule asc limit 1",
|
||||
sql.Named("name", name),
|
||||
|
|
|
@ -77,7 +77,7 @@ func (a *goBlog) saveReaction(reaction, path string) error {
|
|||
defer a.reactionsSfg.Forget(path)
|
||||
defer a.reactionsCache.Del(path)
|
||||
// Insert reaction
|
||||
_, err := a.db.exec("insert into reactions (path, reaction, count) values (?, ?, 1) on conflict (path, reaction) do update set count=count+1", path, reaction)
|
||||
_, err := a.db.Exec("insert into reactions (path, reaction, count) values (?, ?, 1) on conflict (path, reaction) do update set count=count+1", path, reaction)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,7 @@ func (a *goBlog) getReactionsFromDatabase(path string) (map[string]int, error) {
|
|||
sqlBuf.WriteString(") and path not in (select path from post_parameters where parameter=? and value=?)")
|
||||
sqlArgs = append(sqlArgs, reactionsPostParam, "false")
|
||||
// Execute query
|
||||
rows, err := a.db.query(sqlBuf.String(), sqlArgs...)
|
||||
rows, err := a.db.Query(sqlBuf.String(), sqlArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
10
sessions.go
10
sessions.go
|
@ -23,7 +23,7 @@ const (
|
|||
|
||||
func (a *goBlog) initSessions() {
|
||||
deleteExpiredSessions := func() {
|
||||
if _, err := a.db.exec(
|
||||
if _, err := a.db.Exec(
|
||||
"delete from sessions where expires < @now",
|
||||
sql.Named("now", utcNowString()),
|
||||
); err != nil {
|
||||
|
@ -103,14 +103,14 @@ func (s *dbSessionStore) Delete(_ *http.Request, w http.ResponseWriter, session
|
|||
for k := range session.Values {
|
||||
delete(session.Values, k)
|
||||
}
|
||||
if _, err := s.db.exec("delete from sessions where id = @id", sql.Named("id", session.ID)); err != nil {
|
||||
if _, err := s.db.Exec("delete from sessions where id = @id", sql.Named("id", session.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dbSessionStore) load(session *sessions.Session) (err error) {
|
||||
row, err := s.db.queryRow(
|
||||
row, err := s.db.QueryRow(
|
||||
"select data, created, modified, expires from sessions where id = @id and expires > @now",
|
||||
sql.Named("id", session.ID),
|
||||
sql.Named("now", utcNowString()),
|
||||
|
@ -142,7 +142,7 @@ func (s *dbSessionStore) insert(session *sessions.Session) (err error) {
|
|||
session.ID = session.Name() + "-" + uuid.NewString()
|
||||
created, modified := utcNowString(), utcNowString()
|
||||
expires := time.Now().UTC().Add(time.Second * time.Duration(session.Options.MaxAge)).Format(time.RFC3339)
|
||||
_, err = s.db.exec(
|
||||
_, err = s.db.Exec(
|
||||
"insert or replace into sessions(id, data, created, modified, expires) values(@id, @data, @created, @modified, @expires)",
|
||||
sql.Named("id", session.ID),
|
||||
sql.Named("data", encoded.Bytes()),
|
||||
|
@ -163,7 +163,7 @@ func (s *dbSessionStore) save(session *sessions.Session) (err error) {
|
|||
if err = gob.NewEncoder(encoded).Encode(session.Values); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.exec(
|
||||
_, err = s.db.Exec(
|
||||
"update sessions set data = @data, modified = @modified where id = @id",
|
||||
sql.Named("data", encoded.Bytes()),
|
||||
sql.Named("modified", |