Moved away from local files to complete database driven articles

This commit is contained in:
2021-12-20 23:10:04 +01:00
parent bc3ec64b3c
commit c909339a73
20 changed files with 302 additions and 299 deletions

2
.env
View File

@@ -3,3 +3,5 @@ ARTICLE_ROOT=../articles/
SERVER_PORT=8080
DATABASE_FILE=database.sqlite3
ARTICLE_TEMPLATE=../frontend/html/article.gohtml
FRONTEND_DIR=../frontend/

View File

@@ -1,17 +0,0 @@
package api
import (
"encoding/json"
"net/http"
)
type ApiError struct {
Message string `json:"message"`
OriginalError error
Code int
}
func (ae ApiError) Send(w http.ResponseWriter) error {
w.WriteHeader(ae.Code)
return json.NewEncoder(w).Encode(ae)
}

21
api/error.go Normal file
View File

@@ -0,0 +1,21 @@
package api
import (
"encoding/json"
"net/http"
)
var (
DatabaseError = ApiError{Message: "internal database error", Code: http.StatusInternalServerError}
InvalidJson = ApiError{Message: "invalid json", Code: http.StatusUnprocessableEntity}
)
type ApiError struct {
Message string `json:"message"`
Code int
}
func (ae ApiError) Send(w http.ResponseWriter) error {
w.WriteHeader(ae.Code)
return json.NewEncoder(w).Encode(ae)
}

View File

@@ -2,7 +2,6 @@ package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"encoding/json"
"net/http"
"strconv"
@@ -16,7 +15,7 @@ func Recent(w http.ResponseWriter, r *http.Request) {
if l := query.Get("limit"); l != "" {
limit, err = strconv.ParseInt(l, 10, 64)
if err != nil {
ApiError{"invalid 'limit' parameter", err, http.StatusUnprocessableEntity}.Send(w)
ApiError{"invalid 'limit' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
} else {
@@ -27,13 +26,13 @@ func Recent(w http.ResponseWriter, r *http.Request) {
Limit: int(limit),
})
var articleSummaries []schema.ArticleSummary
var articleSummaries []database.ArticleSummary
for _, article := range articles {
articleSummaries = append(articleSummaries, article.ToArticleSummary())
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(struct {
Articles []schema.ArticleSummary
Articles []database.ArticleSummary
}{articleSummaries})
}

View File

@@ -2,7 +2,6 @@ package api
import (
"TheAdversary/database"
"TheAdversary/schema"
"encoding/json"
"net/http"
"strconv"
@@ -20,21 +19,21 @@ func Search(w http.ResponseWriter, r *http.Request) {
if f := query.Get("from"); f != "" {
from, err = time.Parse(time.RFC3339, f)
if err != nil {
ApiError{"could not parse 'from' parameter as RFC3339 time", err, http.StatusUnprocessableEntity}.Send(w)
ApiError{"could not parse 'from' parameter as RFC3339 time", http.StatusUnprocessableEntity}.Send(w)
return
}
}
if t := query.Get("to"); t != "" {
to, err = time.Parse(time.RFC3339, t)
if err != nil {
ApiError{"could not parse 'to' parameter as RFC3339 time", err, http.StatusUnprocessableEntity}.Send(w)
ApiError{"could not parse 'to' parameter as RFC3339 time", http.StatusUnprocessableEntity}.Send(w)
return
}
}
if l := query.Get("limit"); l != "" {
limit, err = strconv.ParseInt(l, 10, 64)
if err != nil {
ApiError{"invalid 'limit' parameter", err, http.StatusUnprocessableEntity}.Send(w)
ApiError{"invalid 'limit' parameter", http.StatusUnprocessableEntity}.Send(w)
return
}
} else {
@@ -49,13 +48,13 @@ func Search(w http.ResponseWriter, r *http.Request) {
Limit: int(limit),
})
var articleSummaries []schema.ArticleSummary
var articleSummaries []database.ArticleSummary
for _, article := range articles {
articleSummaries = append(articleSummaries, article.ToArticleSummary())
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(struct {
Articles []schema.ArticleSummary
Articles []database.ArticleSummary `json:"articles"`
}{articleSummaries})
}

60
api/upload.go Normal file
View File

@@ -0,0 +1,60 @@
package api
import (
"TheAdversary/database"
"TheAdversary/parse"
"encoding/base64"
"encoding/json"
"go.uber.org/zap"
"net/http"
"time"
)
type uploadRequest struct {
Name string
Author string
Title string
Summary string
Image string
Tags []string
Content string
}
func Upload(w http.ResponseWriter, r *http.Request) {
var request uploadRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
InvalidJson.Send(w)
return
}
rawMarkdown, err := base64.StdEncoding.DecodeString(request.Content)
if err != nil {
zap.S().Warnf("Cannot decode base64")
ApiError{Message: "invalid base64 content", Code: http.StatusUnprocessableEntity}.Send(w)
return
}
db := database.GetDB()
tags, err := db.AddOrGetTags(request.Tags)
if err != nil {
zap.S().Error("Failed to add or get tag to / from database: %v", err)
DatabaseError.Send(w)
return
}
if err = db.AddArticle(database.Article{
Name: request.Name,
Title: request.Title,
Summary: request.Summary,
Image: request.Image,
Added: time.Now().Unix(),
Markdown: request.Content,
Html: string(parse.ParseToHtml(rawMarkdown)),
}, tags); err != nil {
zap.S().Errorf("Failed to add article to database: %v", err)
DatabaseError.Send(w)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@@ -1,66 +0,0 @@
package article
import (
"TheAdversary/config"
"TheAdversary/parse"
"TheAdversary/schema"
"github.com/gomarkdown/markdown/ast"
"path/filepath"
"strings"
"time"
)
func LoadArticle(path string) (*schema.Article, error) {
node, err := parse.Parse(path)
if err != nil {
return nil, err
}
article := &schema.Article{
Name: ArticleName(path),
Added: time.Now().Unix(),
}
children := node.GetChildren()
to := 3
if len(children) < to {
to = len(children)
}
for _, child := range children[0:to] {
switch child.(type) {
case *ast.Heading:
if article.Title != "" {
article.Summary = extractText(child.(*ast.Heading).Container)
} else {
article.Title = extractText(child.(*ast.Heading).Container)
}
case *ast.Paragraph:
if article.Summary == "" {
article.Summary = extractText(child.(*ast.Paragraph).Container)
}
case *ast.BlockQuote:
if article.Summary == "" {
article.Summary = extractText(child.(*ast.BlockQuote).Container)
}
case *ast.Image:
article.Image = string(child.(*ast.Image).Destination)
}
}
if article.Title == "" {
article.Title = strings.ReplaceAll(strings.ReplaceAll(ArticleName(path), "-", " "), "_", " ")
}
return article, err
}
func extractText(container ast.Container) string {
return string(container.GetChildren()[0].(*ast.Text).Literal)
}
func ArticleName(path string) string {
ext := filepath.Ext(path)
if ext != ".md" {
return strings.TrimPrefix(path, config.ArticleRoot)
} else {
return strings.TrimSuffix(strings.TrimPrefix(path, config.ArticleRoot), filepath.Ext(path))
}
}

View File

@@ -1,68 +0,0 @@
package article
import (
"TheAdversary/config"
"TheAdversary/database"
"database/sql"
"fmt"
"github.com/fsnotify/fsnotify"
"sync"
"time"
)
func Notify() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
err = watcher.Add(config.ArticleRoot)
if err != nil {
return err
}
db := database.GetDB()
var lock sync.Mutex
for {
select {
case event, ok := <-watcher.Events:
lock.Lock()
if !ok {
return fmt.Errorf("failed to catch event")
}
if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write {
_, err := db.GetArticleByName(ArticleName(event.Name))
if err != nil && err == sql.ErrNoRows {
article, err := LoadArticle(event.Name)
if err != nil {
return err
}
err = db.AddArticle(article)
if err != nil {
return err
}
} else if err != nil {
return err
} else {
article, err := LoadArticle(event.Name)
if err != nil {
return err
}
article.Modified = time.Now().Unix()
if err = db.UpdateArticle(ArticleName(event.Name), article); err != nil {
return err
}
}
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
if err = db.DeleteArticlesByNames(ArticleName(event.Name)); err != nil {
return err
}
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
if err = db.DeleteArticlesByNames(ArticleName(event.Name)); err != nil {
return err
}
}
lock.Unlock()
}
}
}

View File

@@ -11,6 +11,8 @@ var (
ServerPort string
DatabaseFile string
ArticleTemplate string
FrontendDir string
)
func init() {
@@ -21,4 +23,6 @@ func init() {
ServerPort = os.Getenv("SERVER_PORT")
DatabaseFile = os.Getenv("DATABASE_FILE")
ArticleTemplate = os.Getenv("ARTICLE_TEMPLATE")
FrontendDir = os.Getenv("FRONTEND_DIR")
}

Binary file not shown.

View File

@@ -1,9 +1,9 @@
package database
import (
"TheAdversary/schema"
"database/sql"
"fmt"
"gorm.io/gorm/clause"
)
type ArticleQueryOptions struct {
@@ -15,16 +15,15 @@ type ArticleQueryOptions struct {
Limit int
}
func (db *Database) GetArticleByName(name string) (*schema.Article, error) {
article := &schema.Article{}
err := db.gormDB.Table("article").Where("name = ?", name).Scan(article).Error
if article.Added == 0 {
func (db *Database) GetArticleByName(name string) (article *Article, err error) {
err = db.gormDB.Table("article").Where("name = ?", name).Scan(&article).Error
if article == nil {
return nil, sql.ErrNoRows
}
return article, err
}
func (db *Database) GetArticles(query string, options ArticleQueryOptions) ([]*schema.Article, error) {
func (db *Database) GetArticles(query string, options ArticleQueryOptions) ([]*Article, error) {
request := db.gormDB.Table("article")
var where bool
if options.Name {
@@ -69,9 +68,9 @@ func (db *Database) GetArticles(query string, options ArticleQueryOptions) ([]*s
return nil, err
}
var articles []*schema.Article
var articles []*Article
for rows.Next() {
article := &schema.Article{}
article := &Article{}
if err = db.gormDB.ScanRows(rows, article); err != nil {
return nil, err
}
@@ -81,12 +80,23 @@ func (db *Database) GetArticles(query string, options ArticleQueryOptions) ([]*s
return articles, nil
}
func (db *Database) AddArticle(article *schema.Article) error {
return db.gormDB.Table("article").Create(article).Error
func (db *Database) AddArticle(article Article, tags []Tag) error {
if err := db.gormDB.Table("article").Create(&article).Select("id", &article.ID).Error; err != nil {
return err
}
return db.gormDB.Table("article_tags").Create(ArticleTagsFromTagSlice(article, tags)).Error
}
func (db *Database) UpdateArticle(name string, article *schema.Article) error {
return db.gormDB.Table("article").Where("name = ?", name).Save(article).Error
func (db *Database) UpdateArticle(article Article, tags []Tag) error {
if err := db.gormDB.Table("article").Where("id = ?", article.ID).Save(article).Error; err != nil {
return err
}
return db.gormDB.Table("article_tags").Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "article_id"}, {Name: "tag_id"}},
DoNothing: true,
}).Create(ArticleTagsFromTagSlice(article, tags)).Error
}
func (db *Database) DeleteArticlesByNames(names ...string) error {

View File

@@ -1,21 +1,17 @@
package schema
package database
import (
"fmt"
"path/filepath"
)
import "fmt"
type Article struct {
ID int
Name string
Title string
Summary string
Image string
Added int64
Modified int64
}
func (a Article) FilePath(articleDir string) string {
return filepath.Join(articleDir, a.Name) + ".md"
Markdown string
Html string
}
func (a Article) ToArticleSummary() ArticleSummary {
@@ -33,3 +29,10 @@ type ArticleSummary struct {
Image string `json:"image"`
Link string `json:"link"`
}
type Author struct {
ID int
Name string
Email string
Password string
}

54
database/tag.go Normal file
View File

@@ -0,0 +1,54 @@
package database
import (
"gorm.io/gorm/clause"
"strings"
)
type Tag struct {
ID int
Name string
}
func ArticleTagsFromTagSlice(article Article, tags []Tag) (tagsTable []ArticleTags) {
for _, tag := range tags {
tagsTable = append(tagsTable, ArticleTags{
ArticleID: article.ID,
TagID: tag.ID,
})
}
return
}
type ArticleTags struct {
ArticleID int
TagID int
}
func (db *Database) AddOrGetTags(names []string) (tags []Tag, err error) {
for i, name := range names {
names[i] = strings.ToLower(name)
}
err = db.gormDB.Table("tag").Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).Select("name").Create(&names).Error
if err != nil {
return nil, err
}
err = db.gormDB.Table("tag").Find(&tags).Where("name in (?)", &names).Error
return
}
func (db *Database) SetTags(article Article, tags []Tag) error {
return db.gormDB.Table("tags").Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "article_id"}, {Name: "tag_id"}},
DoNothing: true,
}).Create(ArticleTagsFromTagSlice(article, tags)).Error
}
func (db *Database) RemoveTags(article Article, tags []Tag) error {
return db.gormDB.Table("tags").Delete(ArticleTagsFromTagSlice(article, tags)).Error
}

4
go.mod
View File

@@ -3,10 +3,10 @@ module TheAdversary
go 1.17
require (
github.com/fsnotify/fsnotify v1.4.9
github.com/gomarkdown/markdown v0.0.0-20211207152620-5d6539fd8bfc
github.com/gorilla/mux v1.8.0
github.com/joho/godotenv v1.4.0
go.uber.org/zap v1.19.1
gorm.io/driver/sqlite v1.2.6
gorm.io/gorm v1.22.4
)
@@ -15,5 +15,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.3 // indirect
github.com/mattn/go-sqlite3 v1.14.9 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
)

47
go.sum
View File

@@ -1,3 +1,5 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -14,19 +16,62 @@ github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI=
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=

98
main.go
View File

@@ -2,112 +2,46 @@ package main
import (
"TheAdversary/api"
"TheAdversary/article"
"TheAdversary/config"
"TheAdversary/database"
"TheAdversary/schema"
"TheAdversary/server"
"fmt"
"github.com/gorilla/mux"
"io/fs"
"net/http"
"path/filepath"
)
func main() {
r := mux.NewRouter()
r.StrictSlash(true)
r.HandleFunc("/api/upload", api.Upload).Methods(http.MethodPost)
r.HandleFunc("/api/recent", api.Recent).Methods(http.MethodGet)
r.HandleFunc("/api/search", api.Search).Methods(http.MethodGet)
r.HandleFunc("/article/{article}", server.Article).Methods(http.MethodGet)
r.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, filepath.Join(config.FrontendDir, "img", "logodark.svg"))
})
r.PathPrefix("/sass/").HandlerFunc(server.ServePath)
r.PathPrefix("/img/").HandlerFunc(server.ServePath)
r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
http.ServeFile(w, r, filepath.Join(config.FrontendDir, "html", "landingpage.html"))
} else {
server.Error404(w, r)
}
})
db, err := database.NewSqlite3Connection(config.DatabaseFile)
if err != nil {
panic(err)
}
database.SetGlobDB(db)
if err = checkArticles(); err != nil {
panic(err)
}
go func() {
if err := article.Notify(); err != nil {
panic(err)
}
}()
if err := http.ListenAndServe(fmt.Sprintf(":%s", config.ServerPort), r); err != nil {
panic(err)
}
}
func checkArticles() error {
var files []string
err := filepath.Walk(config.ArticleRoot, func(path string, info fs.FileInfo, err error) error {
if !info.IsDir() {
files = append(files, filepath.Join(config.ArticleRoot, path))
}
return err
})
if err != nil {
return err
}
articles, err := database.GetDB().GetArticles("", database.ArticleQueryOptions{})
if err != nil {
return err
}
toAdd, toRemove := checkFiles(files, articles)
db := database.GetDB()
for _, addFile := range toAdd {
a, err := article.LoadArticle(addFile)
if err != nil {
return err
}
err = db.AddArticle(a)
if err != nil {
return err
}
}
if len(toRemove) > 0 {
if err = db.DeleteArticlesByNames(toRemove...); err != nil {
return err
}
}
return nil
}
func checkFiles(files []string, articles []*schema.Article) ([]string, []string) {
toAdd := files
for i, file := range files {
articleName := article.ArticleName(file)
for _, a := range articles {
if articleName == a.Name {
toAdd = append(toAdd[:i], toAdd[i+1:]...)
}
}
}
var toRemove []string
for _, a := range articles {
var found bool
for _, file := range files {
if a.Name == article.ArticleName(file) {
found = true
break
}
}
if !found {
toRemove = append(toRemove, a.Name)
}
}
return toAdd, toRemove
}

View File

@@ -1,13 +1,9 @@
package parse
import (
"TheAdversary/config"
"TheAdversary/schema"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"os"
)
func newParser() *parser.Parser {
@@ -16,26 +12,14 @@ func newParser() *parser.Parser {
return parser.NewWithExtensions(extensions)
}
func newHtmlRenderer(title string) *html.Renderer {
func newHtmlRenderer() *html.Renderer {
renderOpts := html.RendererOptions{
Title: title,
Flags: html.CommonFlags | html.CompletePage,
Flags: html.CommonFlags | html.LazyLoadImages,
}
return html.NewRenderer(renderOpts)
}
func Parse(path string) (ast.Node, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return markdown.Parse(raw, newParser()), nil
}
func ParseToHtml(article *schema.Article) ([]byte, error) {
node, err := Parse(article.FilePath(config.ArticleRoot))
if err != nil {
return nil, err
}
return markdown.Render(node, newHtmlRenderer(article.Title)), nil
func ParseToHtml(rawMarkdown []byte) []byte {
node := markdown.Parse(rawMarkdown, newParser())
return markdown.Render(node, newHtmlRenderer())
}

View File

@@ -2,33 +2,28 @@ package server
import (
"TheAdversary/database"
"TheAdversary/parse"
"database/sql"
"fmt"
"github.com/gorilla/mux"
"net/http"
)
// var tmpl = template.Must(template.ParseFiles(config.ArticleTemplate))
func Article(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
articleName := mux.Vars(r)["article"]
article, err := database.GetDB().GetArticleByName(articleName)
if err != nil {
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusNotFound)
Error404(w, r)
} else {
w.WriteHeader(http.StatusInternalServerError)
Error500(w, r)
}
return
}
parsed, _ := parse.ParseToHtml(article)
if len(parsed) > 0 {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(parsed))
} else {
w.WriteHeader(http.StatusNotFound)
}
fmt.Fprint(w, article.Html)
}

25
server/error.go Normal file
View File

@@ -0,0 +1,25 @@
package server
import (
"TheAdversary/config"
"io"
"log"
"net/http"
"path/filepath"
)
func init() {
// disable default log output because http.ServeFile prints
// a message if a header is written 2 times or more
log.Default().SetOutput(io.Discard)
}
func Error404(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
http.ServeFile(w, r, filepath.Join(config.FrontendDir, "error", "404.html"))
}
func Error500(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
http.ServeFile(w, r, filepath.Join(config.FrontendDir, "error", "500.html"))
}

17
server/path.go Normal file
View File

@@ -0,0 +1,17 @@
package server
import (
"TheAdversary/config"
"net/http"
"os"
"path/filepath"
)
func ServePath(w http.ResponseWriter, r *http.Request) {
path := filepath.Join(config.FrontendDir, r.URL.Path)
if _, err := os.Stat(path); os.IsNotExist(err) {
Error404(w, r)
} else {
http.ServeFile(w, r, path)
}
}