Skip to content

Commit 800bb06

Browse files
makew0rldlovetocode999
authored andcommitted
✨ Use XBEL for bookmarks - makew0rld#68
1 parent e7c59f1 commit 800bb06

File tree

6 files changed

+232
-67
lines changed

6 files changed

+232
-67
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Changed
9+
- Bookmarks are stored using XML in the XBEL format, old bookmarks are transferred (#68)
10+
811
### Fixed
912
- Help text is now the same color as `regular_text` in the theme config
1013
- Non-ASCII (multibyte) characters can now be used as keybindings (#198, #200)
14+
- Possible subscription update race condition on startup
1115

1216

1317
## [1.8.0] - 2021-02-17

amfora.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66

7+
"github.com/makeworld-the-better-one/amfora/bookmarks"
78
"github.com/makeworld-the-better-one/amfora/client"
89
"github.com/makeworld-the-better-one/amfora/config"
910
"github.com/makeworld-the-better-one/amfora/display"
@@ -44,13 +45,18 @@ func main() {
4445
fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
4546
os.Exit(1)
4647
}
48+
client.Init()
49+
4750
err = subscriptions.Init()
4851
if err != nil {
4952
fmt.Fprintf(os.Stderr, "subscriptions.json error: %v\n", err)
5053
os.Exit(1)
5154
}
52-
53-
client.Init()
55+
err = bookmarks.Init()
56+
if err != nil {
57+
fmt.Fprintf(os.Stderr, "bookmarks.xml error: %v\n", err)
58+
os.Exit(1)
59+
}
5460

5561
// Initialize lower-level cview app
5662
if err = display.App.Init(); err != nil {

bookmarks/bookmarks.go

Lines changed: 145 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,88 @@ package bookmarks
22

33
import (
44
"encoding/base32"
5+
"encoding/xml"
6+
"fmt"
7+
"io/ioutil"
8+
"os"
59
"sort"
610
"strings"
711

812
"github.com/makeworld-the-better-one/amfora/config"
913
)
1014

11-
var bkmkStore = config.BkmkStore
15+
func Init() error {
16+
f, err := os.Open(config.BkmkPath)
17+
if err == nil {
18+
// File exists and could be opened
1219

13-
// bkmkKey returns the viper key for the given bookmark URL.
14-
// Note that URLs are the keys, NOT the bookmark name.
15-
func bkmkKey(url string) string {
16-
// Keys are base32 encoded URLs to prevent any special chars like periods from being used
17-
return "bookmarks." + base32.StdEncoding.EncodeToString([]byte(url))
18-
}
20+
fi, err := f.Stat()
21+
if err == nil && fi.Size() > 0 {
22+
// File is not empty
1923

20-
func Set(url, name string) {
21-
bkmkStore.Set(bkmkKey(url), name)
22-
bkmkStore.WriteConfig() //nolint:errcheck
23-
}
24+
xbelBytes, err := ioutil.ReadAll(f)
25+
f.Close()
26+
if err != nil {
27+
return fmt.Errorf("read bookmarks.xml error: %w", err)
28+
}
29+
err = xml.Unmarshal(xbelBytes, &data)
30+
if err != nil {
31+
return fmt.Errorf("bookmarks.xml is corrupted: %w", err)
32+
}
33+
}
34+
f.Close()
35+
} else if !os.IsNotExist(err) {
36+
// There's an error opening the file, but it's not bc is doesn't exist
37+
return fmt.Errorf("open bookmarks.xml error: %w", err)
38+
}
2439

25-
// Get returns the NAME of the bookmark, given the URL.
26-
// It also returns a bool indicating whether it exists.
27-
func Get(url string) (string, bool) {
28-
name := bkmkStore.GetString(bkmkKey(url))
29-
return name, name != ""
30-
}
40+
if data.Bookmarks == nil {
41+
data.Bookmarks = make([]*xbelBookmark, 0)
42+
data.Version = xbelVersion
43+
}
3144

32-
func Remove(url string) {
33-
// XXX: Viper can't actually delete keys, which means the bookmarks file might get clouded
34-
// with non-entries over time.
35-
bkmkStore.Set(bkmkKey(url), "")
36-
bkmkStore.WriteConfig() //nolint:errcheck
37-
}
45+
if config.BkmkStore != nil {
46+
// There's still bookmarks stored in the old format
47+
// Add them and delete the file
3848

39-
// All returns all the bookmarks in a map of URLs to names.
40-
// It also returns a slice of map keys, sorted so that the map *values*
41-
// are in alphabetical order, with case ignored.
42-
func All() (map[string]string, []string) {
43-
bkmks := make(map[string]string)
49+
names, urls := oldBookmarks()
50+
for i := range names {
51+
data.Bookmarks = append(data.Bookmarks, &xbelBookmark{
52+
URL: urls[i],
53+
Name: names[i],
54+
})
55+
}
56+
57+
err := writeXbel()
58+
if err != nil {
59+
return fmt.Errorf("error saving old bookmarks into new format: %w", err)
60+
}
61+
62+
err = os.Remove(config.OldBkmkPath)
63+
if err != nil {
64+
return fmt.Errorf(
65+
"couldn't delete old bookmarks file (%s), you must delete it yourself to prevent duplicate bookmarks: %w",
66+
config.OldBkmkPath,
67+
err,
68+
)
69+
}
70+
config.BkmkStore = nil
71+
}
72+
73+
return nil
74+
}
4475

45-
bkmksMap, ok := bkmkStore.AllSettings()["bookmarks"].(map[string]interface{})
76+
// oldBookmarks returns a slice of names and a slice of URLs of the
77+
// bookmarks in config.BkmkStore.
78+
func oldBookmarks() ([]string, []string) {
79+
bkmksMap, ok := config.BkmkStore.AllSettings()["bookmarks"].(map[string]interface{})
4680
if !ok {
4781
// No bookmarks stored yet, return empty map
48-
return bkmks, []string{}
82+
return []string{}, []string{}
4983
}
5084

51-
inverted := make(map[string]string) // Holds inverted map, name->URL
52-
names := make([]string, 0, len(bkmksMap)) // Holds bookmark names, for sorting
53-
keys := make([]string, 0, len(bkmksMap)) // Final sorted keys (URLs), for returning at the end
85+
names := make([]string, 0, len(bkmksMap))
86+
urls := make([]string, 0, len(bkmksMap))
5487

5588
for b32Url, name := range bkmksMap {
5689
if n, ok := name.(string); n == "" || !ok {
@@ -63,15 +96,89 @@ func All() (map[string]string, []string) {
6396
// This would only happen if a user messed around with the bookmarks file
6497
continue
6598
}
66-
bkmks[string(url)] = name.(string)
67-
inverted[name.(string)] = string(url)
6899
names = append(names, name.(string))
100+
urls = append(urls, string(url))
101+
}
102+
return names, urls
103+
}
104+
105+
func writeXbel() error {
106+
xbelBytes, err := xml.MarshalIndent(&data, "", " ")
107+
if err != nil {
108+
return err
109+
}
110+
111+
xbelBytes = append(xbelHeader, xbelBytes...)
112+
err = ioutil.WriteFile(config.BkmkPath, xbelBytes, 0666)
113+
if err != nil {
114+
return err
115+
}
116+
return nil
117+
}
118+
119+
// Change the name of the bookmark at the provided URL.
120+
func Change(url, name string) {
121+
for _, bkmk := range data.Bookmarks {
122+
if bkmk.URL == url {
123+
bkmk.Name = name
124+
writeXbel() //nolint:errcheck
125+
return
126+
}
69127
}
128+
}
129+
130+
// Add will add a new bookmark.
131+
func Add(url, name string) {
132+
data.Bookmarks = append(data.Bookmarks, &xbelBookmark{
133+
URL: url,
134+
Name: name,
135+
})
136+
writeXbel() //nolint:errcheck
137+
}
138+
139+
// Get returns the NAME of the bookmark, given the URL.
140+
// It also returns a bool indicating whether it exists.
141+
func Get(url string) (string, bool) {
142+
for _, bkmk := range data.Bookmarks {
143+
if bkmk.URL == url {
144+
return bkmk.Name, true
145+
}
146+
}
147+
return "", false
148+
}
149+
150+
func Remove(url string) {
151+
for i, bkmk := range data.Bookmarks {
152+
if bkmk.URL == url {
153+
data.Bookmarks[i] = data.Bookmarks[len(data.Bookmarks)-1]
154+
data.Bookmarks = data.Bookmarks[:len(data.Bookmarks)-1]
155+
writeXbel() //nolint:errcheck
156+
return
157+
}
158+
}
159+
}
160+
161+
// All returns all the bookmarks in a map of URLs to names.
162+
// It also returns a slice of map keys, sorted so that the map *values*
163+
// are in alphabetical order, with case ignored.
164+
func All() (map[string]string, []string) {
165+
bkmksMap := make(map[string]string)
166+
167+
inverted := make(map[string]string) // Holds inverted map, name->URL
168+
names := make([]string, len(data.Bookmarks)) // Holds bookmark names, for sorting
169+
keys := make([]string, len(data.Bookmarks)) // Final sorted keys (URLs), for returning at the end
170+
171+
for i, bkmk := range data.Bookmarks {
172+
bkmksMap[bkmk.URL] = bkmk.Name
173+
inverted[bkmk.Name] = bkmk.URL
174+
names[i] = bkmk.Name
175+
}
176+
70177
// Sort, then turn back into URL keys
71178
sort.Strings(names)
72-
for _, name := range names {
73-
keys = append(keys, inverted[name])
179+
for i, name := range names {
180+
keys[i] = inverted[name]
74181
}
75182

76-
return bkmks, keys
183+
return bkmksMap, keys
77184
}

bookmarks/xbel.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package bookmarks
2+
3+
// Structs and code for the XBEL XML bookmark format.
4+
// https://github.com/makeworld-the-better-one/amfora/issues/68
5+
6+
import (
7+
"encoding/xml"
8+
)
9+
10+
var xbelHeader = []byte(xml.Header + `<!DOCTYPE xbel
11+
PUBLIC "+//IDN python.org//DTD XML Bookmark Exchange Language 1.1//EN//XML"
12+
"http://www.python.org/topics/xml/dtds/xbel-1.1.dtd">
13+
`)
14+
15+
const xbelVersion = "1.1"
16+
17+
type xbelBookmark struct {
18+
XMLName xml.Name `xml:"bookmark"`
19+
URL string `xml:"href,attr"`
20+
Name string `xml:"title"`
21+
}
22+
23+
// xbelFolder is unused as folders aren't supported by the UI yet.
24+
// Follow #56 for details.
25+
// https://github.com/makeworld-the-better-one/amfora/issues/56
26+
type xbelFolder struct {
27+
XMLName xml.Name `xml:"folder"`
28+
Version string `xml:"version,attr"`
29+
Folded string `xml:"folded,attr"` // Idk if this will be used or not
30+
Name string `xml:"title"`
31+
Bookmarks []*xbelBookmark `xml:"bookmark"`
32+
Folders []*xbelFolder `xml:"folder"`
33+
}
34+
35+
type xbel struct {
36+
XMLName xml.Name `xml:"xbel"`
37+
Version string `xml:"version,attr"`
38+
Bookmarks []*xbelBookmark `xml:"bookmark"`
39+
// Later: Folders []*xbelFolder
40+
}
41+
42+
// Instance of xbel - loaded from bookmarks file
43+
var data xbel

config/config.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ var tofuDBDir string
3232
var tofuDBPath string
3333

3434
// Bookmarks
35-
36-
var BkmkStore = viper.New()
35+
var BkmkStore = viper.New() // TOML API for old bookmarks file
3736
var bkmkDir string
38-
var bkmkPath string
37+
var OldBkmkPath string // Old bookmarks file that used TOML format
38+
var BkmkPath string // New XBEL (XML) bookmarks file, see #68
3939

4040
var DownloadsDir string
4141
var TempDownloadsDir string
@@ -111,7 +111,8 @@ func Init() error {
111111
// XDG data dir on POSIX systems
112112
bkmkDir = filepath.Join(basedir.DataHome, "amfora")
113113
}
114-
bkmkPath = filepath.Join(bkmkDir, "bookmarks.toml")
114+
OldBkmkPath = filepath.Join(bkmkDir, "bookmarks.toml")
115+
BkmkPath = filepath.Join(bkmkDir, "bookmarks.xml")
115116

116117
// Feeds dir and path
117118
if runtime.GOOS == "windows" {
@@ -160,10 +161,8 @@ func Init() error {
160161
if err != nil {
161162
return err
162163
}
163-
f, err = os.OpenFile(bkmkPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
164-
if err == nil {
165-
f.Close()
166-
}
164+
// OldBkmkPath isn't created because it shouldn't be there anyway
165+
167166
// Feeds
168167
err = os.MkdirAll(subscriptionDir, 0755)
169168
if err != nil {
@@ -179,16 +178,12 @@ func Init() error {
179178
return err
180179
}
181180

182-
BkmkStore.SetConfigFile(bkmkPath)
181+
BkmkStore.SetConfigFile(OldBkmkPath)
183182
BkmkStore.SetConfigType("toml")
184183
err = BkmkStore.ReadInConfig()
185184
if err != nil {
186-
return err
187-
}
188-
BkmkStore.Set("DO NOT TOUCH", true)
189-
err = BkmkStore.WriteConfig()
190-
if err != nil {
191-
return err
185+
// File doesn't exist, so remove the viper
186+
BkmkStore = nil
192187
}
193188

194189
// Setup main config

0 commit comments

Comments
 (0)