11 Commits

15 changed files with 188 additions and 19 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: tardisx
ko_fi: tardisx

View File

@@ -4,7 +4,9 @@
"colly",
"incpatch",
"linkwallet",
"nicetime",
"serialised",
"stopword"
"stopword",
"Upsert"
]
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Justin Hawkins
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -22,6 +22,44 @@ A self-hosted bookmark database with full-text page content search.
* ~24Mb database (600 bookmarks, full text content indexed)
* Easily export your bookmarks to a plain text file - your data is yours
# Installation
## Docker
* Copy the `docker-compose.yml-sample` to a directory somewhere
* Rename to `docker-compose.yml` and edit to your needs
* In most cases, you only need to change the path to the `/data`
mountpoint.
* Run `docker-compose up -d`
## Binary
* Download the appropriate binary from the releases page
* Install somewhere on your system
* Run `./linkwallet -db-path /some/path/xxxx.db` where `/some/path/xxxx.db`
is the location of your bookmarks database (will be created if it does not yet exist)
## Source
* Checkout the code
* `go build cmd/linkwallet/linkwallet.go`
## deb/rpm packages
Coming soon.
# Using
linkwallet is a 100% web-driven app. After running, hit the web interface
on port 8109 (docker using the sample docker-compose.yml) or 8080 (default
on binary).
Change the port number by setting the PORT environment variable.
If you put linkwallet on a separate machine, or behind a reverse proxy,
go into the config page and set the correct `BaseURL` parameter, or the bookmarklets
will not work.
# Roadmap
* More options when managing links

View File

@@ -39,6 +39,19 @@ func (m *BookmarkManager) AddBookmark(bm *entity.Bookmark) error {
return nil
}
func (m *BookmarkManager) DeleteBookmark(bm *entity.Bookmark) error {
err := m.db.store.FindOne(bm, bolthold.Where("URL").Eq(bm.URL))
if err == bolthold.ErrNotFound {
return fmt.Errorf("bookmark does not exist")
}
// delete it
m.db.store.DeleteMatching(bm, bolthold.Where("ID").Eq(bm.ID))
// delete all the index entries
m.db.UpdateIndexForWordsByID([]string{}, bm.ID)
return nil
}
// ListBookmarks returns all bookmarks.
func (m *BookmarkManager) ListBookmarks() ([]entity.Bookmark, error) {
bookmarks := make([]entity.Bookmark, 0, 0)

View File

@@ -2,7 +2,6 @@ package db
import (
"log"
"time"
"github.com/tardisx/linkwallet/entity"
bolthold "github.com/timshannon/bolthold"
@@ -21,15 +20,12 @@ func (db *DB) UpdateIndexForWordsByID(words []string, id uint64) {
}
db.store.TxForEach(txn, &bolthold.Query{}, func(wi *entity.WordIndex) {
// log.Printf("considering this one: %s", wi.Word)
delete(wi.Bitmap, id)
})
// adding
var find, store time.Duration
for i, word := range words {
// log.Printf("indexing %s", word)
tF := time.Now()
thisWI := entity.WordIndex{Word: word}
err := db.store.TxGet(txn, "word_index_"+word, &thisWI)
if err == bolthold.ErrNotFound {
@@ -38,18 +34,13 @@ func (db *DB) UpdateIndexForWordsByID(words []string, id uint64) {
} else if err != nil {
panic(err)
}
findT := time.Since(tF)
tS := time.Now()
thisWI.Bitmap[id] = true
// log.Printf("BM: %v", thisWI.Bitmap)
err = db.store.TxUpsert(txn, "word_index_"+word, thisWI)
if err != nil {
panic(err)
}
findS := time.Since(tS)
find += findT
store += findS
if i > 0 && i%100 == 0 {
txn.Commit()
@@ -60,7 +51,6 @@ func (db *DB) UpdateIndexForWordsByID(words []string, id uint64) {
}
}
//log.Printf("find %s store %s", find, store)
txn.Commit()
}

View File

@@ -1,12 +1,12 @@
---
version: "2.1"
version: "3.0"
services:
linkwallet:
image: tardisx/linkwallet:latest
container_name: linkwallet
command: /app/linkwallet -db-path=/data/linkwallet.db
entrypoint: [ '/linkwallet', '-db-path', '/data/linkwallet.db' ]
volumes:
- /home/username/.linkwallet:/data
- /home/USERNAME/.linkwallet:/data
ports:
- 8109:8080
restart: unless-stopped

View File

@@ -8,7 +8,7 @@ import (
"golang.org/x/mod/semver"
)
const Tag = "v0.0.12"
const Tag = "v0.0.14"
var versionInfo struct {
Local struct {

View File

@@ -53,6 +53,8 @@
{{ template "manage.html" . }}
{{ else if eq .page "config" }}
{{ template "config.html" . }}
{{ else if eq .page "edit" }}
{{ template "edit.html" . }}
{{ end }}
{{/* template "foundation_sample.html" . */}}
</div>

7
web/templates/edit.html Normal file
View File

@@ -0,0 +1,7 @@
<div class="grid-x grid-padding-x">
<div class="large-12 cell">
<h5>Edit</h5>
{{ template "edit_form.html" . }}
</div>
</div>

View File

@@ -0,0 +1,32 @@
<form onsubmit="return false;" id="edit-form" hx-target="#edit-form">
<table>
<tr>
<th>Title</th>
<td>{{ .bookmark.Info.Title }}</td>
</tr>
<tr>
<th>URL</th>
<td>{{ .bookmark.URL }}</td>
</tr>
<tr>
<th>tags</th>
<td>
{{ template "tags_widget.html" .tw }}
</td>
</tr>
<tr>
<th>Created</th>
<td>{{ (nicetime .bookmark.TimestampCreated).HumanDuration }}</td>
</tr>
<tr>
<th>Last Scraped</th>
<td>{{ (nicetime .bookmark.TimestampLastScraped).HumanDuration }}</td>
</tr>
</table>
<p>
<button type="button" hx-confirm="Delete this bookmark permanently?" hx-delete="/edit/{{.bookmark.ID}}" class="alert button">delete</button>
<button type="button" class="button" hx-post="/edit/{{.bookmark.ID}}">save</button>
</p>
</form>

View File

@@ -0,0 +1,2 @@
<p>Bookmark deleted</p>

View File

@@ -5,7 +5,7 @@
<table>
<tr>
<th>id</th>
<th>&nbsp;</th>
<th>title/url</th>
<th>tags</th>
<th class="show-for-large">created</th>
@@ -13,7 +13,7 @@
</tr>
{{ range .bookmarks }}
<tr>
<th>{{ .ID }}</th>
<th><a class="button" href="/edit/{{ .ID }}">edit</a></th>
<td>
<a href="{{ .URL }}">{{ .Info.Title }}</a>
<br>

View File

@@ -5,7 +5,7 @@
hx-target="#label-widget"
hx-trigger="change">
<label for="tag-entry"
class="Xtext-right Xmiddle">Tags</label>
class="">Tags</label>
<input id="tag-entry" type="text" name="tag" placeholder="enter tags" />
</div>

View File

@@ -174,7 +174,7 @@ func Create(bmm *db.BookmarkManager, cmm *db.ConfigManager) *Server {
r.POST("/tags", func(c *gin.Context) {
newTag := c.PostForm("tag")
newTag := c.PostForm("tag") // new tag
oldTags := strings.Split(c.PostForm("tags_hidden"), "|")
remove := c.Query("remove")
@@ -235,6 +235,66 @@ func Create(bmm *db.BookmarkManager, cmm *db.ConfigManager) *Server {
)
})
r.GET("/edit/:id", func(c *gin.Context) {
bookmarkIDstring := c.Param("id")
bookmarkID, ok := strconv.ParseUint(bookmarkIDstring, 10, 64)
if ok != nil {
c.String(http.StatusBadRequest, "bad id")
return
}
bookmark := bmm.LoadBookmarkByID(bookmarkID)
meta := gin.H{"page": "edit", "bookmark": bookmark, "tw": gin.H{"tags": bookmark.Tags, "tags_hidden": strings.Join(bookmark.Tags, "|")}}
c.HTML(http.StatusOK,
"_layout.html", meta,
)
})
r.POST("/edit/:id", func(c *gin.Context) {
bookmarkIDstring := c.Param("id")
bookmarkID, ok := strconv.ParseUint(bookmarkIDstring, 10, 64)
if ok != nil {
c.String(http.StatusBadRequest, "bad id")
return
}
bookmark := bmm.LoadBookmarkByID(bookmarkID)
// freshen tags
if c.PostForm("tags_hidden") == "" {
// empty
bookmark.Tags = []string{}
} else {
bookmark.Tags = strings.Split(c.PostForm("tags_hidden"), "|")
}
bmm.SaveBookmark(&bookmark)
meta := gin.H{"page": "edit", "bookmark": bookmark, "tw": gin.H{"tags": bookmark.Tags, "tags_hidden": strings.Join(bookmark.Tags, "|")}}
c.HTML(http.StatusOK,
"edit_form.html", meta,
)
})
r.DELETE("/edit/:id", func(c *gin.Context) {
bookmarkIDstring := c.Param("id")
bookmarkID, ok := strconv.ParseUint(bookmarkIDstring, 10, 64)
if ok != nil {
c.String(http.StatusBadRequest, "bad id")
return
}
bookmark := bmm.LoadBookmarkByID(bookmarkID)
err := bmm.DeleteBookmark(&bookmark)
if err != nil {
panic(err)
}
c.HTML(http.StatusOK,
"edit_form_deleted.html", nil,
)
})
return server
}