Skip to content

Commit

Permalink
feat: add import/export from/to file
Browse files Browse the repository at this point in the history
- Added `--import-from-file` flag to import feeds from a file
- Added `--export-to-file` flag to export feeds to a file
  • Loading branch information
radulucut committed Aug 28, 2024
1 parent 8f239a2 commit c8b1810
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ cleed list mylist --merge anotherlist

# Remove a list
cleed list mylist --remove

# Import feeds from a file
cleed list mylist --import-from-file feeds.txt

# Export feeds to a file
cleed list mylist --export-to-file feeds.txt
```

#### Configuration
Expand Down
16 changes: 16 additions & 0 deletions cmd/cleed/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ Examples:
# Remove a list
cleed list mylist --remove
# Import feeds from a file
cleed list mylist --import-from-file feeds.txt
# Export feeds to a file
cleed list mylist --export-to-file feeds.txt
`,

RunE: r.RunList,
Expand All @@ -35,6 +41,8 @@ Examples:
flags.String("rename", "", "rename a list")
flags.String("merge", "", "merge a list")
flags.Bool("remove", false, "remove a list")
flags.String("import-from-file", "", "import feeds from a file. Newline separated URLs")
flags.String("export-to-file", "", "export feeds to a file. Newline separated URLs")

r.Cmd.AddCommand(cmd)
}
Expand All @@ -54,5 +62,13 @@ func (r *Root) RunList(cmd *cobra.Command, args []string) error {
if cmd.Flag("remove").Changed {
return r.feed.RemoveList(args[0])
}
importFromFile := cmd.Flag("import-from-file").Value.String()
if importFromFile != "" {
return r.feed.ImportFromFile(importFromFile, args[0])
}
exportToFile := cmd.Flag("export-to-file").Value.String()
if exportToFile != "" {
return r.feed.ExportToFile(exportToFile, args[0])
}
return r.feed.ListFeeds(args[0])
}
123 changes: 123 additions & 0 deletions cmd/cleed/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,126 @@ func Test_List_Remove(t *testing.T) {
}
assert.Equal(t, "test", string(b))
}

func Test_List_ImportFromFile(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

timeMock := mocks.NewMockTime(ctrl)
timeMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()

out := new(bytes.Buffer)
printer := internal.NewPrinter(nil, out, out)
storage := _storage.NewLocalStorage("cleed_test", timeMock)
defer localStorageCleanup(t, storage)

configDir, err := os.UserConfigDir()
if err != nil {
t.Fatal(err)
}

listsDir := path.Join(configDir, "cleed_test", "lists")
err = os.MkdirAll(listsDir, 0700)
if err != nil {
t.Fatal(err)
}

err = os.WriteFile(path.Join(listsDir, "import"),
[]byte(fmt.Sprintf("%d %s\n%d %s\n",
defaultCurrentTime.Unix(), "https://example.com",
defaultCurrentTime.Unix()+300, "https://test.com",
),
), 0600)
if err != nil {
t.Fatal(err)
}

importFilePath := path.Join(listsDir, "test2")
err = os.WriteFile(importFilePath,
[]byte(fmt.Sprintf("%s\n%s\n%s\n%s\n",
"https://example0.com",
" https://test.com",
"# comment",
"https://example2.com",
),
), 0600)
if err != nil {
t.Fatal(err)
}

feed := internal.NewTerminalFeed(timeMock, printer, storage)
feed.SetAgent("cleed/test")

root, err := NewRoot("0.1.0", timeMock, printer, storage, feed)
assert.NoError(t, err)

os.Args = []string{"cleed", "list", "test", "--import-from-file", importFilePath}

err = root.Cmd.Execute()
assert.NoError(t, err)
assert.Equal(t, "added 3 feeds to list: test\n", out.String())

items, err := storage.GetFeedsFromList("test")
assert.NoError(t, err)
assert.Equal(t, []*_storage.ListItem{
{AddedAt: time.Unix(defaultCurrentTime.Unix(), 0), Address: "https://example.com"},
{AddedAt: time.Unix(defaultCurrentTime.Unix()+300, 0), Address: "https://test.com"},
{AddedAt: time.Unix(defaultCurrentTime.Unix(), 0), Address: "https://example0.com"},
{AddedAt: time.Unix(defaultCurrentTime.Unix(), 0), Address: "https://example2.com"},
}, items)
}

func Test_List_ExportToFile(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

timeMock := mocks.NewMockTime(ctrl)
timeMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()

out := new(bytes.Buffer)
printer := internal.NewPrinter(nil, out, out)
storage := _storage.NewLocalStorage("cleed_test", timeMock)
defer localStorageCleanup(t, storage)

configDir, err := os.UserConfigDir()
if err != nil {
t.Fatal(err)
}

listsDir := path.Join(configDir, "cleed_test", "lists")
err = os.MkdirAll(listsDir, 0700)
if err != nil {
t.Fatal(err)
}

err = os.WriteFile(path.Join(listsDir, "test"),
[]byte(fmt.Sprintf("%d %s\n%d %s\n",
defaultCurrentTime.Unix(), "https://example.com",
defaultCurrentTime.Unix()+300, "https://test.com",
),
), 0600)
if err != nil {
t.Fatal(err)
}

feed := internal.NewTerminalFeed(timeMock, printer, storage)
feed.SetAgent("cleed/test")

root, err := NewRoot("0.1.0", timeMock, printer, storage, feed)
assert.NoError(t, err)

exportPath := path.Join(listsDir, "export")
os.Args = []string{"cleed", "list", "test", "--export-to-file", exportPath}

err = root.Cmd.Execute()
assert.NoError(t, err)
assert.Equal(t, fmt.Sprintf("exported 2 feeds to %s\n", exportPath), out.String())

b, err := os.ReadFile(exportPath)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, `https://example.com
https://test.com
`, string(b))
}
52 changes: 52 additions & 0 deletions internal/feed.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package internal

import (
"bufio"
"compress/gzip"
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -244,6 +246,56 @@ func (f *TerminalFeed) RemoveList(list string) error {
return nil
}

func (f *TerminalFeed) ImportFromFile(path, list string) error {
fi, err := os.Open(path)
if err != nil {
return utils.NewInternalError("failed to open file: " + err.Error())
}
defer fi.Close()
urls := make([]string, 0)
scanner := bufio.NewScanner(fi)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if strings.HasPrefix(line, "#") {
continue
}
urls = append(urls, line)
}
err = f.storage.AddToList(urls, list)
if err != nil {
return utils.NewInternalError("failed to save feeds: " + err.Error())
}
f.printer.Printf("added %s to list: %s\n", utils.Pluralize(int64(len(urls)), "feed"), list)
return nil
}

func (f *TerminalFeed) ExportToFile(path, list string) error {
feeds, err := f.storage.GetFeedsFromList(list)
if err != nil {
return utils.NewInternalError("failed to list feeds: " + err.Error())
}
if len(feeds) == 0 {
f.printer.Println("no feeds to export")
return nil
}
fo, err := os.Create(path)
if err != nil {
return utils.NewInternalError("failed to create file: " + err.Error())
}
defer fo.Close()
for i := range feeds {
_, err = fo.WriteString(feeds[i].Address + "\n")
if err != nil {
return utils.NewInternalError("failed to write to file: " + err.Error())
}
}
f.printer.Printf("exported %s to %s\n", utils.Pluralize(int64(len(feeds)), "feed"), path)
return nil
}

type FeedItem struct {
Feed *gofeed.Feed
Item *gofeed.Item
Expand Down

0 comments on commit c8b1810

Please sign in to comment.