Fix button for uploading OPML

This commit is contained in:
Joakim Hellsén 2024-02-05 06:00:17 +01:00
commit 4cab55c211
6 changed files with 277 additions and 14 deletions

View file

@ -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("<h2>Results</h2><ul>")
for _, result := range ParseResult {
if len(h.ParseResult) > 0 {
errorBuilder.WriteString("<ul>")
for _, result := range h.ParseResult {
var listItemClass, statusMsg string
if result.IsError {
listItemClass = "error"

View file

@ -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") {

View file

@ -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/ <Ctrl-C> to stop")
http.ListenAndServe("127.0.0.1:8000", r)

94
opml.go Normal file
View file

@ -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
}

107
opml_test.go Normal file
View file

@ -0,0 +1,107 @@
package main
import "testing"
var opmlExample = `<?xml version="1.0" encoding="utf-8"?>
<opml version="1.0">
<head>
<title>My Feeds</title>
</head>
<body>
<outline text="24 ways" htmlUrl="http://24ways.org/" type="rss" xmlUrl="http://feeds.feedburner.com/24ways"/>
<outline text="Writing — by Jan" htmlUrl="http://writing.jan.io/" type="rss" xmlUrl="http://writing.jan.io/feed.xml"/>
</body>
</opml>
`
var secondOpmlExample = `<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
<head>
<title>Engineering Blogs</title>
</head>
<body>
<outline text="Engineering Blogs" title="Engineering Blogs">
<outline type="rss" text="8th Light" title="8th Light" xmlUrl="https://8thlight.com/blog/feed/atom.xml" htmlUrl="https://8thlight.com/blog/"/>
<outline type="rss" text="A" title="A" xmlUrl="http://www.vertabelo.com/_rss/blog.xml" htmlUrl="http://www.vertabelo.com/blog"/>
</outline>
</body>
</opml>
`
// 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])
}
}

View file

@ -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: "<p>Here be dragons.</p>",
}
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: "<p>Here be feeds.</p>",
}
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: "<p>Feeds added.</p>",
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: "<p>Feeds added.</p>",
ParseResult: parseResult,
}
html := fullHTML(htmlData)
w.Write([]byte(html))
}