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
15 changed files with 162 additions and 146 deletions

72
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '18 22 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

25
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Go
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

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 {

View File

@@ -2,7 +2,6 @@ package entity
import ( import (
"html/template" "html/template"
"strings"
"time" "time"
) )
@@ -20,13 +19,6 @@ func (bm Bookmark) Type() string {
return "bookmark" return "bookmark"
} }
func (bm Bookmark) DisplayTitle() string {
if strings.TrimSpace(bm.Info.Title) == "" {
return bm.URL
}
return bm.Info.Title
}
type PageInfo struct { type PageInfo struct {
Fetched time.Time Fetched time.Time
Title string Title string

View File

@@ -1,22 +0,0 @@
package entity
import (
"testing"
)
func TestTitle(t *testing.T) {
bm := Bookmark{
URL: "http://example.org",
Info: PageInfo{
Title: "",
},
}
if bm.DisplayTitle() != "http://example.org" {
t.Errorf("title incorrect - got %s", bm.DisplayTitle())
}
bm.Info.Title = "Example Site"
if bm.DisplayTitle() != "Example Site" {
t.Errorf("title incorrect - got %s", bm.DisplayTitle())
}
}

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,29 +24,23 @@ 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 { return false
log.Printf("version %s invalid", vi.Remote.Tag)
}
if !localValid || !remoteValid {
return false
}
return semver.Compare(vi.Local.Version, vi.Remote.Tag) < 0
} }
func (vi *Info) UpdateVersionInfo() { func (vi *Info) UpdateVersionInfo() {
@@ -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 }}

View File

@@ -1,7 +1,7 @@
<ul> <ul>
{{ range .results }} {{ range .results }}
<li> <li>
<a href="{{ .Bookmark.URL }}">{{ .Bookmark.DisplayTitle }}</a><br> <a href="{{ .Bookmark.URL }}">{{ .Bookmark.Info.Title }}</a><br>
{{ .Highlight }} {{ .Highlight }}
</li> </li>
{{ end }} {{ end }}