2020-09-18 10:34:52 +00:00
package main
import (
2020-10-06 17:07:48 +00:00
"bytes"
2020-11-09 15:40:12 +00:00
"database/sql"
2020-09-18 10:34:52 +00:00
"errors"
2020-10-06 17:07:48 +00:00
"fmt"
2020-09-18 10:34:52 +00:00
"strings"
2020-10-06 17:07:48 +00:00
"text/template"
2020-09-18 10:34:52 +00:00
"time"
2020-10-06 17:07:48 +00:00
"github.com/araddon/dateparse"
2020-09-18 10:34:52 +00:00
)
2020-10-15 15:32:46 +00:00
func ( p * post ) checkPost ( ) error {
2020-09-18 10:34:52 +00:00
if p == nil {
return errors . New ( "no post" )
}
2020-10-06 17:07:48 +00:00
now := time . Now ( )
2020-09-18 10:34:52 +00:00
// Fix content
p . Content = strings . TrimSuffix ( strings . TrimPrefix ( p . Content , "\n" ) , "\n" )
// Fix date strings
if p . Published != "" {
d , err := dateparse . ParseIn ( p . Published , time . Local )
if err != nil {
return err
}
p . Published = d . String ( )
}
if p . Updated != "" {
d , err := dateparse . ParseIn ( p . Updated , time . Local )
if err != nil {
return err
}
p . Updated = d . String ( )
}
2020-10-14 16:31:05 +00:00
// Cleanup params
for key , value := range p . Parameters {
if value == nil {
delete ( p . Parameters , key )
continue
}
allValues := [ ] string { }
for _ , v := range value {
if v != "" {
allValues = append ( allValues , v )
}
}
if len ( allValues ) >= 1 {
p . Parameters [ key ] = allValues
} else {
delete ( p . Parameters , key )
}
}
2020-10-06 17:07:48 +00:00
// Check blog
if p . Blog == "" {
p . Blog = appConfig . DefaultBlog
}
// Check path
p . Path = strings . TrimSuffix ( p . Path , "/" )
if p . Path == "" {
if p . Section == "" {
p . Section = appConfig . Blogs [ p . Blog ] . DefaultSection
}
if p . Slug == "" {
2020-10-26 16:37:31 +00:00
random := generateRandomString ( 5 )
2020-10-06 17:07:48 +00:00
p . Slug = fmt . Sprintf ( "%v-%02d-%02d-%v" , now . Year ( ) , int ( now . Month ( ) ) , now . Day ( ) , random )
}
2020-10-06 18:04:07 +00:00
published , _ := dateparse . ParseIn ( p . Published , time . Local )
2020-10-06 17:07:48 +00:00
pathTmplString := appConfig . Blogs [ p . Blog ] . Sections [ p . Section ] . PathTemplate
if pathTmplString == "" {
return errors . New ( "path template empty" )
}
pathTmpl , err := template . New ( "location" ) . Parse ( pathTmplString )
if err != nil {
return errors . New ( "failed to parse location template" )
}
var pathBuffer bytes . Buffer
2020-11-01 17:37:21 +00:00
err = pathTmpl . Execute ( & pathBuffer , map [ string ] interface { } {
"BlogPath" : appConfig . Blogs [ p . Blog ] . Path ,
"Year" : published . Year ( ) ,
"Month" : int ( published . Month ( ) ) ,
"Day" : published . Day ( ) ,
"Slug" : p . Slug ,
"Section" : p . Section ,
} )
2020-10-06 17:07:48 +00:00
if err != nil {
return errors . New ( "failed to execute location template" )
}
p . Path = pathBuffer . String ( )
}
if p . Path != "" && ! strings . HasPrefix ( p . Path , "/" ) {
return errors . New ( "wrong path" )
}
2020-09-18 10:34:52 +00:00
return nil
}
2020-10-15 15:32:46 +00:00
func ( p * post ) create ( ) error {
2020-10-14 16:23:56 +00:00
return p . createOrReplace ( true )
}
2020-10-15 15:32:46 +00:00
func ( p * post ) replace ( ) error {
2020-10-14 16:23:56 +00:00
return p . createOrReplace ( false )
}
2020-10-15 15:32:46 +00:00
func ( p * post ) createOrReplace ( new bool ) error {
2020-10-14 16:23:56 +00:00
err := p . checkPost ( )
2020-09-18 10:34:52 +00:00
if err != nil {
return err
}
startWritingToDb ( )
2020-11-09 15:40:12 +00:00
postExists := postExists ( p . Path )
if postExists && new {
finishWritingToDb ( )
return errors . New ( "post already exists at given path" )
}
2020-09-18 10:34:52 +00:00
tx , err := appDb . Begin ( )
if err != nil {
2020-09-20 20:04:42 +00:00
finishWritingToDb ( )
2020-09-18 10:34:52 +00:00
return err
}
2020-11-09 15:40:12 +00:00
if postExists {
_ , err := tx . Exec ( "delete from posts where path = @path" , sql . Named ( "path" , p . Path ) )
if err != nil {
_ = tx . Rollback ( )
finishWritingToDb ( )
return err
}
2020-10-14 16:23:56 +00:00
}
2020-11-09 15:40:12 +00:00
_ , err = tx . Exec (
"insert into posts (path, content, published, updated, blog, section) values (@path, @content, @published, @updated, @blog, @section)" ,
sql . Named ( "path" , p . Path ) , sql . Named ( "content" , p . Content ) , sql . Named ( "published" , p . Published ) , sql . Named ( "updated" , p . Updated ) , sql . Named ( "blog" , p . Blog ) , sql . Named ( "section" , p . Section ) )
2020-09-18 10:34:52 +00:00
if err != nil {
_ = tx . Rollback ( )
2020-09-20 20:04:42 +00:00
finishWritingToDb ( )
2020-09-18 10:34:52 +00:00
return err
}
2020-11-09 15:40:12 +00:00
ppStmt , err := tx . Prepare ( "insert into post_parameters (path, parameter, value) values (@path, @parameter, @value)" )
2020-09-18 10:34:52 +00:00
if err != nil {
_ = tx . Rollback ( )
2020-09-20 20:04:42 +00:00
finishWritingToDb ( )
2020-09-18 10:34:52 +00:00
return err
}
for param , value := range p . Parameters {
for _ , value := range value {
if value != "" {
2020-11-09 15:40:12 +00:00
_ , err := ppStmt . Exec ( sql . Named ( "path" , p . Path ) , sql . Named ( "parameter" , param ) , sql . Named ( "value" , value ) )
2020-09-18 10:34:52 +00:00
if err != nil {
_ = tx . Rollback ( )
2020-09-20 20:04:42 +00:00
finishWritingToDb ( )
2020-09-18 10:34:52 +00:00
return err
}
}
}
}
err = tx . Commit ( )
if err != nil {
2020-09-20 20:04:42 +00:00
finishWritingToDb ( )
2020-09-18 10:34:52 +00:00
return err
}
finishWritingToDb ( )
2020-11-15 10:34:48 +00:00
rebuildFTSIndex ( )
2020-11-09 15:40:12 +00:00
if ! postExists {
defer p . postPostHooks ( )
} else {
defer p . postUpdateHooks ( )
}
2020-09-18 10:34:52 +00:00
return reloadRouter ( )
}
2020-10-14 16:23:56 +00:00
func deletePost ( path string ) error {
if path == "" {
return nil
2020-09-18 10:34:52 +00:00
}
2020-11-08 22:04:32 +00:00
p , err := getPost ( path )
if err != nil {
return err
}
2020-11-09 15:40:12 +00:00
_ , err = appDbExec ( "delete from posts where path = @path" , sql . Named ( "path" , p . Path ) )
2020-11-15 10:34:48 +00:00
if err != nil {
return err
}
rebuildFTSIndex ( )
2020-11-09 15:40:12 +00:00
defer p . postDeleteHooks ( )
return reloadRouter ( )
}
2020-11-15 10:34:48 +00:00
func rebuildFTSIndex ( ) {
_ , _ = appDbExec ( "insert into posts_fts(posts_fts) values ('rebuild')" )
return
}
2020-11-09 15:40:12 +00:00
func postExists ( path string ) bool {
result := 0
row , err := appDbQueryRow ( "select exists(select 1 from posts where path = @path)" , sql . Named ( "path" , path ) )
2020-09-18 10:34:52 +00:00
if err != nil {
2020-11-09 15:40:12 +00:00
return false
}
if err = row . Scan ( & result ) ; err != nil {
return false
2020-09-18 10:34:52 +00:00
}
2020-11-09 15:40:12 +00:00
return result == 1
}
func getPost ( path string ) ( * post , error ) {
posts , err := getPosts ( & postsRequestConfig { path : path } )
2020-09-18 10:34:52 +00:00
if err != nil {
2020-11-09 15:40:12 +00:00
return nil , err
} else if len ( posts ) == 0 {
return nil , errPostNotFound
2020-09-18 10:34:52 +00:00
}
2020-11-09 15:40:12 +00:00
return posts [ 0 ] , nil
}
type postsRequestConfig struct {
2020-11-15 10:34:48 +00:00
search string
2020-11-09 15:40:12 +00:00
blog string
path string
limit int
offset int
sections [ ] string
taxonomy * taxonomy
taxonomyValue string
parameter string
parameterValue string
}
func buildQuery ( config * postsRequestConfig ) ( query string , args [ ] interface { } ) {
args = [ ] interface { } { }
defaultSelection := "select p.path as path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), coalesce(blog, ''), coalesce(section, ''), coalesce(parameter, ''), coalesce(value, '') "
postsTable := "posts"
2020-11-15 10:34:48 +00:00
if config . search != "" {
postsTable = "posts_fts(@search)"
args = append ( args , sql . Named ( "search" , config . search ) )
}
2020-11-09 15:40:12 +00:00
if config . blog != "" {
postsTable = "(select * from " + postsTable + " where blog = @blog)"
args = append ( args , sql . Named ( "blog" , config . blog ) )
}
if config . parameter != "" {
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @param "
args = append ( args , sql . Named ( "param" , config . parameter ) )
if config . parameterValue != "" {
postsTable += "and pp.value = @paramval)"
args = append ( args , sql . Named ( "paramval" , config . parameterValue ) )
} else {
postsTable += "and length(coalesce(pp.value, '')) > 1)"
}
}
if config . taxonomy != nil && len ( config . taxonomyValue ) > 0 {
postsTable = "(select distinct p.* from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @taxname and lower(pp.value) = lower(@taxval))"
args = append ( args , sql . Named ( "taxname" , config . taxonomy . Name ) , sql . Named ( "taxval" , config . taxonomyValue ) )
}
if len ( config . sections ) > 0 {
postsTable = "(select * from " + postsTable + " where"
for i , section := range config . sections {
if i > 0 {
postsTable += " or"
}
named := fmt . Sprintf ( "section%v" , i )
postsTable += fmt . Sprintf ( " section = @%v" , named )
args = append ( args , sql . Named ( named , section ) )
}
postsTable += ")"
}
defaultTables := " from " + postsTable + " p left outer join post_parameters pp on p.path = pp.path "
defaultSorting := " order by p.published desc "
if config . path != "" {
query = defaultSelection + defaultTables + " where p.path = @path" + defaultSorting
args = append ( args , sql . Named ( "path" , config . path ) )
} else if config . limit != 0 || config . offset != 0 {
query = defaultSelection + " from (select * from " + postsTable + " p " + defaultSorting + " limit @limit offset @offset) p left outer join post_parameters pp on p.path = pp.path "
args = append ( args , sql . Named ( "limit" , config . limit ) , sql . Named ( "offset" , config . offset ) )
} else {
query = defaultSelection + defaultTables + defaultSorting
}
return
}
func getPosts ( config * postsRequestConfig ) ( posts [ ] * post , err error ) {
query , queryParams := buildQuery ( config )
rows , err := appDbQuery ( query , queryParams ... )
2020-09-18 10:34:52 +00:00
if err != nil {
2020-11-09 15:40:12 +00:00
return nil , err
2020-09-18 10:34:52 +00:00
}
2020-11-09 15:40:12 +00:00
defer func ( ) {
_ = rows . Close ( )
} ( )
paths := make ( map [ string ] int )
for rows . Next ( ) {
p := & post { }
var parameterName , parameterValue string
err = rows . Scan ( & p . Path , & p . Content , & p . Published , & p . Updated , & p . Blog , & p . Section , & parameterName , & parameterValue )
if err != nil {
return nil , err
}
if paths [ p . Path ] == 0 {
index := len ( posts )
paths [ p . Path ] = index + 1
p . Parameters = make ( map [ string ] [ ] string )
posts = append ( posts , p )
}
if parameterName != "" && posts != nil {
posts [ paths [ p . Path ] - 1 ] . Parameters [ parameterName ] = append ( posts [ paths [ p . Path ] - 1 ] . Parameters [ parameterName ] , parameterValue )
}
}
return posts , nil
}
func countPosts ( config * postsRequestConfig ) ( count int , err error ) {
query , params := buildQuery ( config )
query = "select count(distinct path) from (" + query + ")"
row , err := appDbQueryRow ( query , params ... )
2020-09-18 10:34:52 +00:00
if err != nil {
2020-11-09 15:40:12 +00:00
return
2020-09-18 10:34:52 +00:00
}
2020-11-09 15:40:12 +00:00
err = row . Scan ( & count )
return
}
func allPostPaths ( ) ( [ ] string , error ) {
var postPaths [ ] string
rows , err := appDbQuery ( "select path from posts" )
if err != nil {
return nil , err
}
for rows . Next ( ) {
var path string
_ = rows . Scan ( & path )
postPaths = append ( postPaths , path )
}
return postPaths , nil
}
func allTaxonomyValues ( blog string , taxonomy string ) ( [ ] string , error ) {
var values [ ] string
rows , err := appDbQuery ( "select distinct pp.value from posts p left outer join post_parameters pp on p.path = pp.path where pp.parameter = @tax and length(coalesce(pp.value, '')) > 1 and blog = @blog" , sql . Named ( "tax" , taxonomy ) , sql . Named ( "blog" , blog ) )
if err != nil {
return nil , err
}
for rows . Next ( ) {
var value string
_ = rows . Scan ( & value )
values = append ( values , value )
}
return values , nil
2020-09-18 10:34:52 +00:00
}