Add API endpoint to import OPML file

This commit is contained in:
Frédéric Guillot 2018-04-29 18:56:40 -07:00
parent 7a1653a2e9
commit 5cacae6cf2
9 changed files with 96 additions and 23 deletions

2
Gopkg.lock generated
View file

@ -45,7 +45,7 @@
branch = "master"
name = "github.com/miniflux/miniflux-go"
packages = ["."]
revision = "887ba3b062946784f0e64edb1734f435beb204f9"
revision = "7939463a4e1a1c5392d026d8d28bf7732459abd7"
[[projects]]
name = "github.com/tdewolff/minify"

View file

@ -11,8 +11,6 @@ import (
"github.com/miniflux/miniflux/http/context"
"github.com/miniflux/miniflux/http/request"
"github.com/miniflux/miniflux/http/response/json"
"github.com/miniflux/miniflux/http/response/xml"
"github.com/miniflux/miniflux/reader/opml"
)
// CreateFeed is the API handler to create a new feed.
@ -143,17 +141,6 @@ func (c *Controller) GetFeeds(w http.ResponseWriter, r *http.Request) {
json.OK(w, feeds)
}
// Export is the API handler that incoves an OPML export.
func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
opmlHandler := opml.NewHandler(c.store)
opml, err := opmlHandler.Export(context.New(r).UserID())
if err != nil {
json.ServerError(w, errors.New("unable to export feeds to OPML"))
}
xml.OK(w, opml)
}
// GetFeed is the API handler to get a feed.
func (c *Controller) GetFeed(w http.ResponseWriter, r *http.Request) {
feedID, err := request.IntParam(r, "feedID")

39
api/opml.go Normal file
View file

@ -0,0 +1,39 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"net/http"
"github.com/miniflux/miniflux/http/context"
"github.com/miniflux/miniflux/http/response/json"
"github.com/miniflux/miniflux/http/response/xml"
"github.com/miniflux/miniflux/reader/opml"
)
// Export is the API handler that export feeds to OPML.
func (c *Controller) Export(w http.ResponseWriter, r *http.Request) {
opmlHandler := opml.NewHandler(c.store)
opml, err := opmlHandler.Export(context.New(r).UserID())
if err != nil {
json.ServerError(w, err)
return
}
xml.OK(w, opml)
}
// Import is the API handler that import an OPML file.
func (c *Controller) Import(w http.ResponseWriter, r *http.Request) {
opmlHandler := opml.NewHandler(c.store)
err := opmlHandler.Import(context.New(r).UserID(), r.Body)
defer r.Body.Close()
if err != nil {
json.ServerError(w, err)
return
}
json.Created(w, map[string]string{"message": "Feeds imported successfully"})
}

View file

@ -71,6 +71,7 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
apiRouter.HandleFunc("/feeds/{feedID}", apiController.RemoveFeed).Methods("DELETE")
apiRouter.HandleFunc("/feeds/{feedID}/icon", apiController.FeedIcon).Methods("GET")
apiRouter.HandleFunc("/export", apiController.Export).Methods("GET")
apiRouter.HandleFunc("/import", apiController.Import).Methods("POST")
apiRouter.HandleFunc("/feeds/{feedID}/entries", apiController.GetFeedEntries).Methods("GET")
apiRouter.HandleFunc("/feeds/{feedID}/entries/{entryID}", apiController.GetFeedEntry).Methods("GET")
apiRouter.HandleFunc("/entries", apiController.GetEntries).Methods("GET")

View file

@ -7,6 +7,8 @@
package main
import (
"bytes"
"io/ioutil"
"math/rand"
"strconv"
"strings"
@ -653,6 +655,32 @@ func TestExport(t *testing.T) {
}
}
func TestImport(t *testing.T) {
username := getRandomUsername()
client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)
_, err := client.CreateUser(username, testStandardPassword, false)
if err != nil {
t.Fatal(err)
}
client = miniflux.NewClient(testBaseURL, username, testStandardPassword)
data := `<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<body>
<outline text="Test Category">
<outline title="Test" text="Test" xmlUrl="` + testFeedURL + `" htmlUrl="` + testWebsiteURL + `"></outline>
</outline>
</body>
</opml>`
b := bytes.NewReader([]byte(data))
err = client.Import(ioutil.NopCloser(b))
if err != nil {
t.Fatal(err)
}
}
func TestUpdateFeed(t *testing.T) {
username := getRandomUsername()
client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)

View file

@ -23,8 +23,7 @@ type Handler struct {
func (h *Handler) Export(userID int64) (string, error) {
feeds, err := h.store.Feeds(userID)
if err != nil {
logger.Error("[OPML:Export] %v", err)
return "", errors.New("unable to fetch feeds")
return "", err
}
var subscriptions SubcriptionList
@ -74,7 +73,7 @@ func (h *Handler) Import(userID int64, data io.Reader) error {
err := h.store.CreateCategory(category)
if err != nil {
logger.Error("[OPML:Import] %v", err)
return fmt.Errorf(`unable to create this category: "%s"`, subscription.CategoryName)
return fmt.Errorf(`unable to create this category: %q`, subscription.CategoryName)
}
}
}

View file

@ -26,7 +26,7 @@ package main
import (
"fmt"
"io/ioutil"
"github.com/miniflux/miniflux-go"
)
@ -41,7 +41,7 @@ func main() {
}
fmt.Println(feeds)
// Backup to opml file.
// Backup your feeds to an OPML file.
opml, err := client.Export()
if err != nil {
fmt.Println(err)
@ -53,8 +53,8 @@ func main() {
fmt.Println(err)
return
}
fmt.Println("backup done!")
fmt.Println("backup done!")
}
```

View file

@ -7,6 +7,7 @@ package miniflux
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"strconv"
@ -230,6 +231,12 @@ func (c *Client) Export() ([]byte, error) {
return opml, nil
}
// Import imports an OPML file.
func (c *Client) Import(f io.ReadCloser) error {
_, err := c.request.PostFile("/v1/import", f)
return err
}
// Feed gets a feed.
func (c *Client) Feed(feedID int64) (*Feed, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d", feedID))

View file

@ -26,6 +26,7 @@ var (
errNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
errForbidden = errors.New("miniflux: access forbidden")
errServerError = errors.New("miniflux: internal server error")
errNotFound = errors.New("miniflux: resource not found")
)
type errorResponse struct {
@ -46,6 +47,10 @@ func (r *request) Post(path string, data interface{}) (io.ReadCloser, error) {
return r.execute(http.MethodPost, path, data)
}
func (r *request) PostFile(path string, f io.ReadCloser) (io.ReadCloser, error) {
return r.execute(http.MethodPost, path, f)
}
func (r *request) Put(path string, data interface{}) (io.ReadCloser, error) {
return r.execute(http.MethodPut, path, data)
}
@ -72,7 +77,12 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
request.SetBasicAuth(r.username, r.password)
if data != nil {
request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
switch data.(type) {
case io.ReadCloser:
request.Body = data.(io.ReadCloser)
default:
request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
}
}
client := r.buildClient()
@ -88,6 +98,8 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
return nil, errForbidden
case http.StatusInternalServerError:
return nil, errServerError
case http.StatusNotFound:
return nil, errNotFound
case http.StatusBadRequest:
defer response.Body.Close()
@ -100,8 +112,8 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
return nil, fmt.Errorf("miniflux: bad request (%s)", resp.ErrorMessage)
}
if response.StatusCode >= 400 {
return nil, fmt.Errorf("miniflux: server error (statusCode=%d)", response.StatusCode)
if response.StatusCode > 400 {
return nil, fmt.Errorf("miniflux: status code=%d", response.StatusCode)
}
return response.Body, nil