6 Commits

Author SHA1 Message Date
e4edb08bd1 Deps 2025-05-01 23:42:59 +09:30
58b6692d1b Mostly done, first cut 2025-05-01 23:39:51 +09:30
badbe5e92f Remove unused code 2025-04-27 20:28:37 +09:30
903240dd18 Update deps 2025-04-27 20:26:19 +09:30
de90b9951a Keep on bleving 2025-04-27 20:21:33 +09:30
9b15528510 Start of blevification 2025-04-25 23:57:04 +09:30
10 changed files with 64 additions and 115 deletions

View File

@@ -6,32 +6,12 @@ before:
- go test ./... - go test ./...
builds: builds:
- main: ./cmd/linkwallet/ - env:
ldflags:
- -s -w -X github.com/tardisx/linkwallet/version.version={{.Version}} -X github.com/tardisx/linkwallet/version.commit={{.Commit}} -X github.com/tardisx/linkwallet/version.date={{.Date}}
env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:
- linux - linux
- windows - windows
- darwin - darwin
- freebsd
goarch:
- arm
- arm64
- amd64
goarm:
- 6
- 7
ignore:
- goos: darwin
goarch: arm
- goos: windows
goarch: arm
- goos: windows
goarch: arm64
- goos: freebsd
goarch: arm
archives: archives:
- formats: [tar.gz] - formats: [tar.gz]

View File

@@ -10,8 +10,5 @@
"serialised", "serialised",
"stopword", "stopword",
"Upsert" "Upsert"
],
"cSpell.ignoreWords": [
"rescrape"
] ]
} }

View File

@@ -5,16 +5,14 @@
A self-hosted bookmark database with full-text page content search. A self-hosted bookmark database with full-text page content search.
linkwallet uses the [Bleve](https://blevesearch.com) indexing library, providing Searching uses English stemming, providing matches against similar words, in both page
excellent support for free-text queries over the content of all your bookmarked titles and page content. Searches are lightning fast.
pages.
![Search][screenshot_search] ![Search][screenshot_search]
linkwallet indexes the page content, and automatically re-scrapes the pages Bookmark content is automatically re-scraped periodically. Tags can be applied (though with
periodically. Tags can be applied (though with the full-text search they are the full-text search they are often not needed). Bookmarks can be easily managed, and can be
often not needed). Bookmarks can be easily managed, and can be imported or imported or exported in bulk.
exported in bulk.
![Admin][screenshot_admin] ![Admin][screenshot_admin]
@@ -32,11 +30,14 @@ Bookmarks can be added with two clicks via the bookmarklet.
* Page content periodically refreshed automatically * Page content periodically refreshed automatically
* Interactively search across titles and content * Interactively search across titles and content
* Rippingly fast results, as you type * Rippingly fast results, as you type
* full text search ~30ms (over full text content of 600 bookmarks) * full text search ~60ms (over full text content of 600 bookmarks)
* No need to remember how you filed something, you just need a keyword * No need to remember how you filed something, you just need a keyword
or two to discover it again or two to discover it again
* Embedded database, no separate database required * Embedded database, no separate database required
* Extremely light on resources * Light on resources
* ~21Mb binary
* ~40Mb memory
* ~24Mb database (600 bookmarks, full text content indexed)
* Easily export your bookmarks to a plain text file - your data is yours * Easily export your bookmarks to a plain text file - your data is yours
# Installation # Installation
@@ -56,7 +57,11 @@ To upgrade:
## Packages (deb/rpm) ## Packages (deb/rpm)
[not yet migrated to new goreleaser - please message me if you need packages] * Download the .deb or .rpm from the releases
* Install using apt/dpkg/rpm
* Automatically creates a systemd service, enabled and started
* Runs as user `linkwallet`
* Database stored in `/var/lib/linkwallet`
## Binary ## Binary

View File

@@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/tardisx/linkwallet/db" "github.com/tardisx/linkwallet/db"
v "github.com/tardisx/linkwallet/version" "github.com/tardisx/linkwallet/version"
"github.com/tardisx/linkwallet/web" "github.com/tardisx/linkwallet/web"
) )
@@ -21,7 +21,7 @@ func main() {
} }
dbh := db.DB{} dbh := db.DB{}
rescrape, err := dbh.Open(dbPath) err := dbh.Open(dbPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -31,7 +31,7 @@ func main() {
go func() { go func() {
for { for {
v.VersionInfo.UpdateVersionInfo() version.VersionInfo.UpdateVersionInfo()
time.Sleep(time.Hour * 6) time.Sleep(time.Hour * 6)
} }
}() }()
@@ -47,24 +47,10 @@ func main() {
} }
}() }()
log.Printf("linkwallet version %s starting", v.VersionInfo.Local.Version) log.Printf("linkwallet version %s starting", version.VersionInfo.Local.Tag)
server := web.Create(bmm, cmm) server := web.Create(bmm, cmm)
go bmm.RunQueue() go bmm.RunQueue()
go bmm.UpdateContent() go bmm.UpdateContent()
if rescrape {
log.Printf("queueing all bookmarks for rescraping, as index was just created")
bookmarks, err := bmm.AllBookmarks()
if err != nil {
log.Printf("could not load all bookmarks: %s", err.Error())
} else {
for _, bm := range bookmarks {
bmm.QueueScrape(&bm)
}
}
log.Printf("queued %d bookmarks for scraping", len(bookmarks))
}
server.Start() server.Start()
} }

View File

@@ -14,7 +14,6 @@ import (
"time" "time"
"github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/lang/en"
"github.com/blevesearch/bleve/v2/search/query" "github.com/blevesearch/bleve/v2/search/query"
"github.com/tardisx/linkwallet/content" "github.com/tardisx/linkwallet/content"
"github.com/tardisx/linkwallet/entity" "github.com/tardisx/linkwallet/entity"
@@ -125,11 +124,11 @@ func (m *BookmarkManager) Search(opts SearchOptions) ([]entity.BookmarkSearchRes
if opts.All { if opts.All {
q = bleve.NewMatchAllQuery() q = bleve.NewMatchAllQuery()
} else { } else {
mq := bleve.NewMatchQuery(opts.Query)
mq.Analyzer = en.AnalyzerName
tq := bleve.NewTermQuery(opts.Query)
q = bleve.NewDisjunctionQuery(mq, tq) q = bleve.NewDisjunctionQuery(
bleve.NewMatchQuery(opts.Query),
bleve.NewTermQuery(opts.Query),
)
} }
req := bleve.NewSearchRequest(q) req := bleve.NewSearchRequest(q)
@@ -189,6 +188,7 @@ func (m *BookmarkManager) UpdateIndexForBookmark(bm *entity.Bookmark) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
log.Printf("done bleving")
} }
func (m *BookmarkManager) QueueScrape(bm *entity.Bookmark) { func (m *BookmarkManager) QueueScrape(bm *entity.Bookmark) {
@@ -262,17 +262,6 @@ func (m *BookmarkManager) UpdateContent() {
} }
} }
// AllBookmarks returns all bookmarks. It does not use the index for this
// operation.
func (m *BookmarkManager) AllBookmarks() ([]entity.Bookmark, error) {
bookmarks := make([]entity.Bookmark, 0)
err := m.db.store.Find(&bookmarks, &bolthold.Query{})
if err != nil {
panic(err)
}
return bookmarks, nil
}
func (m *BookmarkManager) Stats() (entity.DBStats, error) { func (m *BookmarkManager) Stats() (entity.DBStats, error) {
stats := entity.DBStats{} stats := entity.DBStats{}
err := m.db.store.Get("stats", &stats) err := m.db.store.Get("stats", &stats)

View File

@@ -19,17 +19,14 @@ type DB struct {
bleve bleve.Index bleve bleve.Index
} }
// Open opens the bookmark boltdb, and the bleve index. It returns // Open opens the bookmark boltdb, and the bleve index.
// true if the index was newly created, so the caller knows all bookmarks func (db *DB) Open(path string) error {
// need to be re-scraped
func (db *DB) Open(path string) (bool, error) {
// options := bolthold.DefaultOptions // options := bolthold.DefaultOptions
// options.Dir = dir // options.Dir = dir
// options.ValueDir = dir // options.ValueDir = dir
rescrapeNeeded := false
store, err := bolthold.Open(path, 0666, nil) store, err := bolthold.Open(path, 0666, nil)
if err != nil { if err != nil {
return false, fmt.Errorf("cannot open '%s' - %s", path, err) return fmt.Errorf("cannot open '%s' - %s", path, err)
} }
blevePath := path + ".bleve" blevePath := path + ".bleve"
@@ -40,21 +37,17 @@ func (db *DB) Open(path string) (bool, error) {
if err == bleve.ErrorIndexPathExists { if err == bleve.ErrorIndexPathExists {
index, err = bleve.Open(blevePath) index, err = bleve.Open(blevePath)
if err != nil { if err != nil {
return false, fmt.Errorf("cannot open bleve '%s' - %s", path, err) return fmt.Errorf("cannot open bleve '%s' - %s", path, err)
} }
} else { } else {
return false, fmt.Errorf("cannot open bleve '%s' - %s", path, err) return fmt.Errorf("cannot open bleve '%s' - %s", path, err)
} }
} else {
// we just created an index, one didn't exist, so we need to queue
// all bookmarks to be scraped
rescrapeNeeded = true
} }
db.store = store db.store = store
db.file = path db.file = path
db.bleve = index db.bleve = index
return rescrapeNeeded, nil return nil
} }
func createIndexMapping() mapping.IndexMapping { func createIndexMapping() mapping.IndexMapping {

15
release_tag.pl Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/perl
open my $fh, "<", "version/version.go" || die "oops";
while (my $l = <$fh>) {
if ($l =~ m/const Tag = "(.+)"/) {
$tag = $1;
system ('git', 'tag', '-a', $tag, '-m', "version $tag for release") ;
die "could not tag?\n" if $? != 0;
system ('git', 'push', 'origin', $tag);
die "could not push tag?\n" if $? != 0;
exit 0;
}
}
die "no version in version/version.go?\n";

View File

@@ -3,7 +3,6 @@ package version
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"strings" "strings"
"sync" "sync"
@@ -11,20 +10,11 @@ import (
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
) )
var version string // populated by goreleaser, without leading 'v' const Tag = "v0.0.36"
var commit string
var date string
var VersionInfo Info
func init() {
VersionInfo.Remote.Valid = false
VersionInfo.Local.Version = "v" + version
}
type Info struct { type Info struct {
Local struct { Local struct {
Version string Tag string
} }
Remote struct { Remote struct {
Valid bool Valid bool
@@ -34,31 +24,25 @@ type Info struct {
m sync.Mutex m sync.Mutex
} }
var VersionInfo Info
func init() {
VersionInfo.Remote.Valid = false
VersionInfo.Local.Tag = Tag
}
func (vi *Info) UpgradeAvailable() bool { func (vi *Info) UpgradeAvailable() bool {
vi.m.Lock() vi.m.Lock()
defer vi.m.Unlock() defer vi.m.Unlock()
if !vi.Remote.Valid { if !vi.Remote.Valid {
return false return false
} }
if semver.Compare(vi.Local.Tag, vi.Remote.Tag) < 0 {
log.Printf("checking if upgrade available - local %s remote %s", vi.Local.Version, vi.Remote.Tag) return true
localValid := semver.IsValid(vi.Local.Version)
remoteValid := semver.IsValid(vi.Remote.Tag)
if !localValid {
log.Printf("version %s invalid", vi.Local.Version)
} }
if !remoteValid {
log.Printf("version %s invalid", vi.Remote.Tag)
}
if !localValid || !remoteValid {
return false return false
} }
return semver.Compare(vi.Local.Version, vi.Remote.Tag) < 0
}
func (vi *Info) UpdateVersionInfo() { func (vi *Info) UpdateVersionInfo() {
client := github.NewClient(nil) client := github.NewClient(nil)
@@ -75,7 +59,7 @@ func (vi *Info) UpdateVersionInfo() {
vi.Remote.Valid = true vi.Remote.Valid = true
vi.UpgradeReleaseNotes = "" vi.UpgradeReleaseNotes = ""
for _, r := range rels { for _, r := range rels {
if semver.Compare(VersionInfo.Local.Version, *r.TagName) < 0 { if semver.Compare(VersionInfo.Local.Tag, *r.TagName) < 0 {
vi.UpgradeReleaseNotes += fmt.Sprintf("*Version %s*\n\n", *r.TagName) vi.UpgradeReleaseNotes += fmt.Sprintf("*Version %s*\n\n", *r.TagName)
bodyLines := strings.Split(*r.Body, "\n") bodyLines := strings.Split(*r.Body, "\n")
for _, l := range bodyLines { for _, l := range bodyLines {

View File

@@ -23,7 +23,6 @@
<li> <li>
<a href="#">Admin</a> <a href="#">Admin</a>
<ul class="menu vertical"> <ul class="menu vertical">
<li><a href="/info">System Info</a></li>
<li><a href="/config">Configuration</a></li> <li><a href="/config">Configuration</a></li>
<li><a href="/manage">Manage links</a></li> <li><a href="/manage">Manage links</a></li>
<li><a href="/export">Export all URLs</a></li> <li><a href="/export">Export all URLs</a></li>
@@ -35,11 +34,12 @@
</div> </div>
<div class="top-bar-right"> <div class="top-bar-right">
<ul class="menu"> <ul class="menu">
<li class="menu-text"> <li>
{{ version.Local.Version }} <a href="/info">{{ version.Local.Tag }}
{{ if version.UpgradeAvailable }} {{ if version.UpgradeAvailable }}
<a href="/info"></a>
{{ end }} {{ end }}
</a>
</li> </li>
<li> <li>
<a href="https://github.com/tardisx/linkwallet"> <a href="https://github.com/tardisx/linkwallet">

View File

@@ -26,7 +26,7 @@
<a href="https://github.com/tardisx/linkwallet/releases/tag/{{ version.Remote.Tag }}"> <a href="https://github.com/tardisx/linkwallet/releases/tag/{{ version.Remote.Tag }}">
{{ version.Remote.Tag }} {{ version.Remote.Tag }}
</a> </a>
(you have {{ version.Local.Version }}). (you have {{ version.Local.Tag }}).
</p> </p>
{{ markdown version.UpgradeReleaseNotes }} {{ markdown version.UpgradeReleaseNotes }}