From c909339a730e609ed578b033f5098d1ec0b373f8 Mon Sep 17 00:00:00 2001 From: bytedream Date: Mon, 20 Dec 2021 23:10:04 +0100 Subject: [PATCH] Moved away from local files to complete database driven articles --- .env | 2 + api/api.go | 17 ------ api/error.go | 21 +++++++ api/recent.go | 7 +-- api/search.go | 11 ++-- api/upload.go | 60 ++++++++++++++++++++ article/article.go | 66 ---------------------- article/notify.go | 68 ----------------------- config/config.go | 6 +- database.sqlite3 | Bin 36864 -> 49152 bytes database/article.go | 34 ++++++++---- {schema => database}/schema.go | 21 ++++--- database/tag.go | 54 ++++++++++++++++++ go.mod | 4 +- go.sum | 47 +++++++++++++++- main.go | 98 ++++++--------------------------- parse/parse.go | 26 ++------- server/article.go | 17 ++---- server/error.go | 25 +++++++++ server/path.go | 17 ++++++ 20 files changed, 302 insertions(+), 299 deletions(-) delete mode 100644 api/api.go create mode 100644 api/error.go create mode 100644 api/upload.go delete mode 100644 article/article.go delete mode 100644 article/notify.go rename {schema => database}/schema.go (74%) create mode 100644 database/tag.go create mode 100644 server/error.go create mode 100644 server/path.go diff --git a/.env b/.env index b3140ba..673de2d 100644 --- a/.env +++ b/.env @@ -3,3 +3,5 @@ ARTICLE_ROOT=../articles/ SERVER_PORT=8080 DATABASE_FILE=database.sqlite3 +ARTICLE_TEMPLATE=../frontend/html/article.gohtml +FRONTEND_DIR=../frontend/ diff --git a/api/api.go b/api/api.go deleted file mode 100644 index 00dd8d4..0000000 --- a/api/api.go +++ /dev/null @@ -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) -} diff --git a/api/error.go b/api/error.go new file mode 100644 index 0000000..6036a61 --- /dev/null +++ b/api/error.go @@ -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) +} diff --git a/api/recent.go b/api/recent.go index 468d0c3..040c874 100644 --- a/api/recent.go +++ b/api/recent.go @@ -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}) } diff --git a/api/search.go b/api/search.go index b4befc9..09c062e 100644 --- a/api/search.go +++ b/api/search.go @@ -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}) } diff --git a/api/upload.go b/api/upload.go new file mode 100644 index 0000000..598f7a5 --- /dev/null +++ b/api/upload.go @@ -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) +} diff --git a/article/article.go b/article/article.go deleted file mode 100644 index 8511871..0000000 --- a/article/article.go +++ /dev/null @@ -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)) - } -} diff --git a/article/notify.go b/article/notify.go deleted file mode 100644 index d9888e3..0000000 --- a/article/notify.go +++ /dev/null @@ -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() - } - } -} diff --git a/config/config.go b/config/config.go index 140dad7..aa6afdb 100644 --- a/config/config.go +++ b/config/config.go @@ -10,7 +10,9 @@ var ( ServerPort string - DatabaseFile 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") } diff --git a/database.sqlite3 b/database.sqlite3 index 1101968b1bc28c0243f5eef3f9a4ab87de562faf..a015f289727d8601ac3592d4a6beec23faea34bd 100644 GIT binary patch delta 2018 zcmcgt&rcgi6rOd!YcF2b#&)*ILY)#hV1e>OM2T8es6gC^5=EpXB2^WcjdyTXUa!sW zSR^N7pm)&z1N{@)=9F7+t&n6ncc-UEsb)>%A@tXdGCGiee=!i z&Udk$oeKwrjDrwL!BAktV0?BsJ9o)JS5^*G zr;dN<-jW~fK1|E{%#5^OqsC*K7%sJ{HmMkG`lRW41Jl^jm*s`EGG1F)yj#YT1FMtD zw1VLY&6er^qF&w*nJTB&%4VJWF<VzPn@jMLfb3TMp+8xixda)?&wflIXMlB+R{T~vHbOtG5YiIeumP1L6xY%h@ z?6hq=s^Z-^02CkqCWH~$+^LZDe770=^yg57n%c^w+hCG$)!d7Hp#br+U+$ z7%dJ_i`sr#E%@JSi43OXDHRH}8b*y^e?!gR4`ZT@7aonKX>D4>43`6|;nvOOw!>2~ zz)&q>`3Y^<*w3iBj#5&8M<43rc(=i&d}>U=pzyO;@u8hh2S0@ zcdirANF=gq)A6WlfQf^>6;BXgIkjl#mC|YqTAPu+<~0Nx?2)ZD+?)QZ(N8+J^+I=5 zll6RF+Pe@I>Oona+*7Kk^j~kL?%0^D-ziGna!8lgduKD6;A9Cd!@K-6{i8A-Wc9zp zLyn^l2+K2xr^B?CIm?-{%#sSzVJ5zgpfrabmF_$6V)pXWCI*0S&~0XamHenf>$CG7}lM}YT}_W2D-LHYa~N}&9u tHR4e=zG!C9ctE^V+@dku9PhO|w}<)qsi4M`e1 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 Q;Of8hG+A0(G|qzle^~U|(EtDd diff --git a/database/article.go b/database/article.go index 806d8cb..0c42caa 100644 --- a/database/article.go +++ b/database/article.go @@ -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 { diff --git a/schema/schema.go b/database/schema.go similarity index 74% rename from schema/schema.go rename to database/schema.go index fb78d94..d754a20 100644 --- a/schema/schema.go +++ b/database/schema.go @@ -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 +} diff --git a/database/tag.go b/database/tag.go new file mode 100644 index 0000000..d564a3c --- /dev/null +++ b/database/tag.go @@ -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 +} diff --git a/go.mod b/go.mod index 47e6004..b836008 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index f7ec99b..3baa544 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 1bfb30c..3299d6a 100644 --- a/main.go +++ b/main.go @@ -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 -} diff --git a/parse/parse.go b/parse/parse.go index 982258f..f19499f 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -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()) } diff --git a/server/article.go b/server/article.go index 5bd1032..ab126fc 100644 --- a/server/article.go +++ b/server/article.go @@ -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) - } + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, article.Html) } diff --git a/server/error.go b/server/error.go new file mode 100644 index 0000000..c741a76 --- /dev/null +++ b/server/error.go @@ -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")) +} diff --git a/server/path.go b/server/path.go new file mode 100644 index 0000000..f0aaa04 --- /dev/null +++ b/server/path.go @@ -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) + } +}