diff --git a/html.go b/html.go
index f5732da..b8abadc 100644
--- a/html.go
+++ b/html.go
@@ -17,6 +17,7 @@ type HTMLData struct {
Author string
CanonicalURL string
Content string
+ ParseResult []ParseResult
}
func minifyHTML(h string) string {
@@ -98,7 +99,7 @@ textarea {
}
`
-func fullHTML(h HTMLData, ParseResult []ParseResult) string {
+func fullHTML(h HTMLData) string {
var sb strings.Builder
var errorBuilder strings.Builder
@@ -106,9 +107,9 @@ func fullHTML(h HTMLData, ParseResult []ParseResult) string {
DatabaseSize := GetDBSize()
// This is the error message that will be displayed if there are any errors
- if len(ParseResult) > 0 {
- errorBuilder.WriteString("
Results
")
- for _, result := range ParseResult {
+ if len(h.ParseResult) > 0 {
+ errorBuilder.WriteString("")
+ for _, result := range h.ParseResult {
var listItemClass, statusMsg string
if result.IsError {
listItemClass = "error"
diff --git a/html_test.go b/html_test.go
index 78e5580..93a4293 100644
--- a/html_test.go
+++ b/html_test.go
@@ -34,14 +34,17 @@ func TestMinifyCSS(t *testing.T) {
// Displays error messages if there are any parse errors
func TestErrorMessages(t *testing.T) {
// Initialize test data
- h := HTMLData{}
parseResult := []ParseResult{
{IsError: true, Msg: "Error 1"},
{IsError: true, Msg: "Error 2"},
}
+ h := HTMLData{
+ ParseResult: parseResult,
+ }
+
// Invoke function under test
- result := fullHTML(h, parseResult)
+ result := fullHTML(h)
// Assert that the result contains the error messages
if !strings.Contains(result, "Error 1") || !strings.Contains(result, "Error 2") {
diff --git a/main.go b/main.go
index fa265b7..c3f7923 100644
--- a/main.go
+++ b/main.go
@@ -53,6 +53,7 @@ func main() {
r.Get("/api", ApiHandler)
r.Get("/feeds", FeedsHandler)
r.Post("/add", AddFeedHandler)
+ r.Post("/upload_opml", UploadOpmlHandler)
log.Println("Listening on http://localhost:8000/ to stop")
http.ListenAndServe("127.0.0.1:8000", r)
diff --git a/opml.go b/opml.go
new file mode 100644
index 0000000..501ab36
--- /dev/null
+++ b/opml.go
@@ -0,0 +1,94 @@
+package main
+
+import "encoding/xml"
+
+type opml struct {
+ Head struct {
+ Title string `xml:"title"`
+ } `xml:"head"`
+ Body Body `xml:"body"`
+}
+
+type Body struct {
+ Outlines []Outline `xml:"outline"`
+}
+
+type Outline struct {
+ Outlines []Outline `xml:"outline"`
+ XmlUrl string `xml:"xmlUrl,attr,omitempty"`
+ HtmlUrl string `xml:"htmlUrl,attr,omitempty"`
+}
+
+func (o *opml) ParseString(s string) error {
+ return xml.Unmarshal([]byte(s), o)
+}
+
+func (o *opml) String() (string, error) {
+ b, err := xml.Marshal(o)
+ if err != nil {
+ return "", err
+ }
+ return xml.Header + string(b), nil
+}
+
+type linksFromOpml struct {
+ XMLLinks []string `json:"xmlLinks"`
+ HTMLLinks []string `json:"htmlLinks"`
+}
+
+func removeDuplicates(s []string) []string {
+ seen := make(map[string]struct{}, len(s))
+ j := 0
+ for _, v := range s {
+ if _, ok := seen[v]; ok {
+ continue
+ }
+ seen[v] = struct{}{}
+ s[j] = v
+ j++
+ }
+ return s[:j]
+}
+
+func ParseOpml(s string) (linksFromOpml, error) {
+ // Get all the feeds from the OPML and return them as linksFromOpml
+ opml := &opml{}
+ err := opml.ParseString(s)
+ if err != nil {
+ return linksFromOpml{}, err
+ }
+
+ links := linksFromOpml{}
+ for _, outline := range opml.Body.Outlines {
+ links.XMLLinks = append(links.XMLLinks, outline.XmlUrl)
+ links.HTMLLinks = append(links.HTMLLinks, outline.HtmlUrl)
+ }
+
+ // Also check outlines for nested outlines
+ for _, outline := range opml.Body.Outlines {
+ for _, nestedOutline := range outline.Outlines {
+ links.XMLLinks = append(links.XMLLinks, nestedOutline.XmlUrl)
+ links.HTMLLinks = append(links.HTMLLinks, nestedOutline.HtmlUrl)
+ }
+ }
+
+ // Remove any empty strings
+ for i := 0; i < len(links.XMLLinks); i++ {
+ if links.XMLLinks[i] == "" {
+ links.XMLLinks = append(links.XMLLinks[:i], links.XMLLinks[i+1:]...)
+ i--
+ }
+ }
+ for i := 0; i < len(links.HTMLLinks); i++ {
+ if links.HTMLLinks[i] == "" {
+ links.HTMLLinks = append(links.HTMLLinks[:i], links.HTMLLinks[i+1:]...)
+ i--
+ }
+ }
+
+ // Remove any duplicates
+ links.XMLLinks = removeDuplicates(links.XMLLinks)
+ links.HTMLLinks = removeDuplicates(links.HTMLLinks)
+
+ return links, nil
+}
diff --git a/opml_test.go b/opml_test.go
new file mode 100644
index 0000000..180554a
--- /dev/null
+++ b/opml_test.go
@@ -0,0 +1,107 @@
+package main
+
+import "testing"
+
+var opmlExample = `
+
+
+ My Feeds
+
+
+
+
+
+
+`
+
+var secondOpmlExample = `
+
+
+ Engineering Blogs
+
+
+
+
+
+
+
+
+`
+
+// Test the opml parser
+func TestParseOpml(t *testing.T) {
+ links, err := ParseOpml(opmlExample)
+ if err != nil {
+ t.Error(err)
+ }
+ if len(links.XMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.XMLLinks))
+ }
+ if len(links.HTMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.HTMLLinks))
+ }
+
+ // Test that the links are unique
+ links.XMLLinks = removeDuplicates(links.XMLLinks)
+ links.HTMLLinks = removeDuplicates(links.HTMLLinks)
+ if len(links.XMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.XMLLinks))
+ }
+ if len(links.HTMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.HTMLLinks))
+ }
+
+ // Test that the links are correct
+ if links.XMLLinks[0] != "http://feeds.feedburner.com/24ways" {
+ t.Errorf("Expected http://feeds.feedburner.com/24ways, got %s", links.XMLLinks[0])
+ }
+ if links.XMLLinks[1] != "http://writing.jan.io/feed.xml" {
+ t.Errorf("Expected http://writing.jan.io/feed.xml, got %s", links.XMLLinks[1])
+ }
+ if links.HTMLLinks[0] != "http://24ways.org/" {
+ t.Errorf("Expected http://24ways.org/, got %s", links.HTMLLinks[0])
+ }
+ if links.HTMLLinks[1] != "http://writing.jan.io/" {
+ t.Errorf("Expected http://writing.jan.io/, got %s", links.HTMLLinks[1])
+ }
+
+}
+
+// Test the opml parser with nested outlines
+func TestParseOpmlNested(t *testing.T) {
+ links, err := ParseOpml(secondOpmlExample)
+ if err != nil {
+ t.Error(err)
+ }
+ if len(links.XMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.XMLLinks))
+ }
+ if len(links.HTMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.HTMLLinks))
+ }
+
+ // Test that the links are unique
+ links.XMLLinks = removeDuplicates(links.XMLLinks)
+ links.HTMLLinks = removeDuplicates(links.HTMLLinks)
+ if len(links.XMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.XMLLinks))
+ }
+ if len(links.HTMLLinks) != 2 {
+ t.Errorf("Expected 2 links, got %d", len(links.HTMLLinks))
+ }
+
+ // Test that the links are correct
+ if links.XMLLinks[0] != "https://8thlight.com/blog/feed/atom.xml" {
+ t.Errorf("Expected https://8thlight.com/blog/feed/atom.xml, got %s", links.XMLLinks[0])
+ }
+ if links.XMLLinks[1] != "http://www.vertabelo.com/_rss/blog.xml" {
+ t.Errorf("Expected http://www.vertabelo.com/_rss/blog.xml, got %s", links.XMLLinks[1])
+ }
+ if links.HTMLLinks[0] != "https://8thlight.com/blog/" {
+ t.Errorf("Expected https://8thlight.com/blog/, got %s", links.HTMLLinks[0])
+ }
+ if links.HTMLLinks[1] != "http://www.vertabelo.com/blog" {
+ t.Errorf("Expected http://www.vertabelo.com/blog, got %s", links.HTMLLinks[1])
+ }
+
+}
diff --git a/views.go b/views.go
index 247a7d7..4b1b88a 100644
--- a/views.go
+++ b/views.go
@@ -1,6 +1,7 @@
package main
import (
+ "io"
"log"
"net/http"
"strings"
@@ -74,7 +75,7 @@ func IndexHandler(w http.ResponseWriter, _ *http.Request) {
CanonicalURL: "http://localhost:8000/",
Content: content,
}
- html := fullHTML(htmlData, nil)
+ html := fullHTML(htmlData)
w.Write([]byte(html))
}
@@ -87,7 +88,7 @@ func ApiHandler(w http.ResponseWriter, _ *http.Request) {
CanonicalURL: "http://localhost:8000/api",
Content: "Here be dragons.
",
}
- html := fullHTML(htmlData, nil)
+ html := fullHTML(htmlData)
w.Write([]byte(html))
}
func FeedsHandler(w http.ResponseWriter, _ *http.Request) {
@@ -99,7 +100,7 @@ func FeedsHandler(w http.ResponseWriter, _ *http.Request) {
CanonicalURL: "http://localhost:8000/feeds",
Content: "Here be feeds.
",
}
- html := fullHTML(htmlData, nil)
+ html := fullHTML(htmlData)
w.Write([]byte(html))
}
@@ -126,17 +127,73 @@ func AddFeedHandler(w http.ResponseWriter, r *http.Request) {
// "Add" the feed to the database
log.Println("Adding feed:", feed_url)
- }
+ parseErrors = append(parseErrors, ParseResult{FeedURL: feed_url, Msg: "Added", IsError: false})
+ }
htmlData := HTMLData{
- Title: "FeedVault - Add Feeds",
- Description: "FeedVault - Add Feeds",
+ Title: "FeedVault",
+ Description: "FeedVault - A feed archive",
Keywords: "RSS, Atom, Feed, Archive",
Author: "TheLovinator",
- CanonicalURL: "http://localhost:8000/add",
+ CanonicalURL: "http://localhost:8000/",
Content: "Feeds added.
",
+ ParseResult: parseErrors,
}
- html := fullHTML(htmlData, parseErrors)
+ html := fullHTML(htmlData)
+ w.Write([]byte(html))
+}
+
+func UploadOpmlHandler(w http.ResponseWriter, r *http.Request) {
+ // Parse the form and get the file
+ r.ParseMultipartForm(10 << 20) // 10 MB
+ file, _, err := r.FormFile("file")
+ if err != nil {
+ http.Error(w, "No file provided", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ // Read the file
+ all, err := io.ReadAll(file)
+ if err != nil {
+ http.Error(w, "Failed to read file", http.StatusInternalServerError)
+ return
+ }
+ // Convert the file to a string
+ opml := string(all)
+
+ // Parse the OPML file
+ parseResult := []ParseResult{}
+ links, err := ParseOpml(opml)
+ if err != nil {
+ parseResult = append(parseResult, ParseResult{FeedURL: "/upload_opml", Msg: err.Error(), IsError: true})
+ } else {
+ // Add the feeds to the database
+ for _, feed_url := range links.XMLLinks {
+ log.Println("Adding feed:", feed_url)
+
+ // Validate the URL
+ err := validateURL(feed_url)
+ if err != nil {
+ parseResult = append(parseResult, ParseResult{FeedURL: feed_url, Msg: err.Error(), IsError: true})
+ continue
+ }
+
+ parseResult = append(parseResult, ParseResult{FeedURL: feed_url, Msg: "Added", IsError: false})
+ }
+ }
+
+ // Return the results
+ htmlData := HTMLData{
+ Title: "FeedVault",
+ Description: "FeedVault - A feed archive",
+ Keywords: "RSS, Atom, Feed, Archive",
+ Author: "TheLovinator",
+ CanonicalURL: "http://localhost:8000/",
+ Content: "Feeds added.
",
+ ParseResult: parseResult,
+ }
+ html := fullHTML(htmlData)
w.Write([]byte(html))
}