9 Commits

Author SHA1 Message Date
1900
b6f7f903d0 Merge 14d1bd1fc3 into 9d5b8d99f7 2023-12-30 13:24:32 +00:00
rebron1900
14d1bd1fc3 build docker v2.5 2023-12-30 21:24:11 +08:00
rebron1900
01cadf644c build docker v2.5 2023-12-30 21:02:03 +08:00
rebron1900
dd79d9404d build docker v2.5 2023-12-30 20:49:51 +08:00
rebron1900
d45b6c0e79 build docker v2.5 2023-12-30 20:35:27 +08:00
rebron1900
63a357330c build docker v2.5 2023-12-30 20:02:54 +08:00
rebron1900
773f8fbd63 change readme.md and app.css 2023-12-30 12:45:46 +08:00
rebron1900
de7e8eb7fc remove test code 2023-12-30 12:31:44 +08:00
rebron1900
2cc9488ef9 add auto set Feed Url,adapter Rsshub 2023-12-30 12:28:31 +08:00
149 changed files with 21113 additions and 47415 deletions

View File

@@ -1,25 +0,0 @@
name: Build & Upload
inputs:
id:
description: artifact name
required: true
cmd:
description: command to run
required: true
out:
description: path to output file
required: true
runs:
using: composite
steps:
- name: compile
run: ${{ inputs.cmd }}
shell: bash
- name: archive
run: tar -cvf ${{ inputs.out }}.tar ${{ inputs.out }}
shell: bash
- name: upload
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.id }}
path: ${{ inputs.out }}.tar

47
.github/workflows/build-docker-image vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Build Yarr Docker image
on:
workflow_dispatch:
push:
paths:
- 'yarr-version.txt'
jobs:
main:
runs-on: ubuntu-latest
steps:
-
name: Read latest release tag
id: read-tag
run: |
echo ::set-output name=tag::$(curl -sL https://raw.githubusercontent.com/rebron1900/yarr/main/yarr-version.txt)
-
name: Checkout
uses: actions/checkout@v2
with:
repository: 'rebron1900/yarr'
ref: ${{ steps.read-tag.outputs.tag }}
-
name: Remove GOARCH
run: |
sed -i -e 's/GOARCH=amd64//g' makefile
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to registry
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Push to Docker
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/yarr:latest
${{ secrets.DOCKER_USERNAME }}/yarr:${{ steps.read-tag.outputs.tag }}

View File

@@ -1,41 +0,0 @@
name: Publish Docker Image
on:
push:
tags:
- v*
env:
REGISTRY: ghcr.io
IMAGE_NAME: nkanaev/yarr
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./etc/dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64

View File

@@ -1,143 +1,179 @@
name: Build name: build
on: on:
push: push:
tags: tags: ['v*', 'test*']
- v*
workflow_dispatch:
jobs: jobs:
build_macos: build_macos:
name: Build for MacOS name: Build for MacOS
runs-on: macos-13 runs-on: macos-13
steps: steps:
- name: Checkout - name: "Checkout"
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v4
with: with:
go-version: '^1.23' submodules: 'recursive'
- name: Build arm64 gui - name: "Setup Go"
uses: ./.github/actions/prepare uses: actions/setup-go@v2
with: with:
id: darwin_arm64_gui go-version: '^1.17'
cmd: make darwin_arm64_gui - name: Cache Go Modules
out: out/darwin_arm64_gui/yarr.app uses: actions/cache@v2
- name: Build amd64 gui
uses: ./.github/actions/prepare
with: with:
id: darwin_amd64_gui path: ~/go/pkg/mod
cmd: make darwin_amd64_gui key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
out: out/darwin_amd64_gui/yarr.app restore-keys: |
- name: Build arm64 cli ${{ runner.os }}-go-
uses: ./.github/actions/prepare - name: "Build"
run: make build_macos
- name: Upload
uses: actions/upload-artifact@v2
with: with:
id: darwin_arm64 name: macos
cmd: make darwin_arm64 path: _output/macos/yarr.app
out: out/darwin_arm64/yarr
- name: Build amd64 cli
uses: ./.github/actions/prepare
with:
id: darwin_amd64
cmd: make darwin_amd64
out: out/darwin_amd64/yarr
build_windows: build_windows:
name: Build for Windows name: Build for Windows
runs-on: windows-2022 runs-on: windows-2022
steps: steps:
- name: Checkout - name: "Checkout"
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v4
with: with:
go-version: '^1.23' submodules: 'recursive'
- name: Build amd64 gui - name: "Setup Go"
uses: ./.github/actions/prepare uses: actions/setup-go@v2
with: with:
id: windows_amd64_gui go-version: '^1.17'
cmd: make windows_amd64_gui - name: Cache Go Modules
out: out/windows_amd64_gui/yarr.exe uses: actions/cache@v2
- name: Build arm64 gui
if: false
uses: ./.github/actions/prepare
with: with:
id: windows_arm64_gui path: ~/go/pkg/mod
cmd: make windows_arm64_gui key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
out: out/windows_arm64_gui/yarr.exe restore-keys: |
${{ runner.os }}-go-
- name: "Build"
run: make build_windows
- name: Upload
uses: actions/upload-artifact@v2
with:
name: windows
path: _output/windows/yarr.exe
build_multi_cli: build_linux:
name: Build for Windows/MacOS/Linux CLI name: Build for Linux
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: "Checkout"
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v4
with: with:
go-version: '^1.23' submodules: 'recursive'
- name: Setup Zig - name: "Setup Go"
uses: mlugg/setup-zig@v1 uses: actions/setup-go@v2
with: with:
version: 0.14.0 go-version: '^1.17'
- name: Build linux/amd64 - name: Cache Go Modules
uses: ./.github/actions/prepare uses: actions/cache@v2
with: with:
id: linux_amd64 path: ~/go/pkg/mod
cmd: make linux_amd64 key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
out: out/linux_amd64/yarr restore-keys: |
- name: Build linux/arm64 ${{ runner.os }}-go-
uses: ./.github/actions/prepare - name: "Build"
run: make build_linux
- name: Upload
uses: actions/upload-artifact@v2
with: with:
id: linux_arm64 name: linux
cmd: make linux_arm64 path: _output/linux/yarr
out: out/linux_arm64/yarr
- name: Build linux/armv7
uses: ./.github/actions/prepare
with:
id: linux_armv7
cmd: make linux_armv7
out: out/linux_armv7/yarr
- name: Build windows/amd64
uses: ./.github/actions/prepare
with:
id: windows_amd64
cmd: make windows_amd64
out: out/windows_amd64/yarr
- name: Build windows/arm64
uses: ./.github/actions/prepare
with:
id: windows_arm64
cmd: make windows_arm64
out: out/windows_arm64/yarr
create_release: create_release:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_macos, build_windows, build_multi_cli] if: ${{ !contains(github.ref, 'test') }}
needs: [build_macos, build_windows, build_linux]
steps: steps:
- name: Create Release
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: true
- name: Download Artifacts - name: Download Artifacts
uses: actions/download-artifact@v4.1.7 uses: actions/download-artifact@v2
with: with:
path: . path: .
- name: Preparation - name: Preparation
run: | run: |
set -ex
ls -R ls -R
for tarfile in `ls **/*.tar`; do chmod u+x macos/Contents/MacOS/yarr
tar -xvf $tarfile chmod u+x linux/yarr
done
for dir in out/*; do mv macos yarr.app && zip -r yarr-macos.zip yarr.app
echo "Compressing: $dir" mv windows/yarr.exe . && zip yarr-windows.zip yarr.exe
(test -d "$dir" && cd $dir && zip -r ../yarr_`basename $dir`.zip *) mv linux/yarr . && zip yarr-linux.zip yarr
done - name: Upload MacOS
ls out uses: actions/upload-release-asset@v1
- name: Upload Release
uses: softprops/action-gh-release@v2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
draft: true upload_url: ${{ steps.create_release.outputs.upload_url }}
prerelease: true asset_path: ./yarr-macos.zip
files: | asset_name: yarr-${{ github.ref }}-macos64.zip
out/*.zip asset_content_type: application/zip
- name: Upload Windows
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./yarr-windows.zip
asset_name: yarr-${{ github.ref }}-windows64.zip
asset_content_type: application/zip
- name: Upload Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./yarr-linux.zip
asset_name: yarr-${{ github.ref }}-linux64.zip
asset_content_type: application/zip
build_docker:
runs-on: ubuntu-latest
needs: [create_release]
steps:
- name: Read latest release tag
id: read-tag
run: |
echo ::set-output name=tag::$(curl -sL https://raw.githubusercontent.com/rebron1900/yarr/master/yarr-version.txt)
- name: Checkout
uses: actions/checkout@v2
with:
repository: 'rebron1900/yarr'
ref: ${{ steps.read-tag.outputs.tag }}
- name: Remove GOARCH
run: |
sed -i -e 's/GOARCH=amd64//g' makefile
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to registry
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push to Docker
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/yarr:latest
${{ secrets.DOCKER_USERNAME }}/yarr:${{ steps.read-tag.outputs.tag }}

View File

@@ -1,19 +0,0 @@
name: Test
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '^1.18'
- name: Run tests
run: make test

4
.gitignore vendored
View File

@@ -1,9 +1,5 @@
/_output /_output
/out
/yarr /yarr
*.db *.db
*.db-shm
*.db-wal
*.syso *.syso
versioninfo.rc versioninfo.rc
.DS_Store

View File

@@ -0,0 +1,48 @@
package main
import (
"flag"
"io/ioutil"
"strings"
)
var rsrc = `1 VERSIONINFO
FILEVERSION {VERSION_COMMA},0,0
PRODUCTVERSION {VERSION_COMMA},0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "080904E4"
BEGIN
VALUE "CompanyName", "Old MacDonald's Farm"
VALUE "FileDescription", "Yet another RSS reader"
VALUE "FileVersion", "{VERSION}"
VALUE "InternalName", "yarr"
VALUE "LegalCopyright", "nkanaev"
VALUE "OriginalFilename", "yarr.exe"
VALUE "ProductName", "yarr"
VALUE "ProductVersion", "{VERSION}"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x809, 1252
END
END
1 ICON "icon.ico"
`
func main() {
var version, outfile string
flag.StringVar(&version, "version", "0.0", "")
flag.StringVar(&outfile, "outfile", "versioninfo.rc", "")
flag.Parse()
version_comma := strings.ReplaceAll(version, ".", ",")
out := strings.ReplaceAll(rsrc, "{VERSION}", version)
out = strings.ReplaceAll(out, "{VERSION_COMMA}", version_comma)
ioutil.WriteFile(outfile, []byte(out), 0644)
}

99
cmd/package_macos/main.go Normal file
View File

@@ -0,0 +1,99 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"strconv"
"strings"
)
var plist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>yarr</string>
<key>CFBundleDisplayName</key>
<string>yarr</string>
<key>CFBundleIdentifier</key>
<string>nkanaev.yarr</string>
<key>CFBundleVersion</key>
<string>VERSION</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>yarr</string>
<key>CFBundleIconFile</key>
<string>icon</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.news</string>
<key>NSHighResolutionCapable</key>
<string>True</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020 nkanaev. All rights reserved.</string>
</dict>
</plist>
`
func run(cmd ...string) {
fmt.Println(cmd)
err := exec.Command(cmd[0], cmd[1:]...).Run()
if err != nil {
log.Fatal(err)
}
}
func main() {
var version, outdir string
flag.StringVar(&version, "version", "0.0", "")
flag.StringVar(&outdir, "outdir", "", "")
flag.Parse()
outfile := "yarr"
binDir := path.Join(outdir, "yarr.app", "Contents/MacOS")
resDir := path.Join(outdir, "yarr.app", "Contents/Resources")
plistFile := path.Join(outdir, "yarr.app", "Contents/Info.plist")
pkginfoFile := path.Join(outdir, "yarr.app", "Contents/PkgInfo")
os.MkdirAll(binDir, 0700)
os.MkdirAll(resDir, 0700)
f, _ := ioutil.ReadFile(path.Join(outdir, outfile))
ioutil.WriteFile(path.Join(binDir, outfile), f, 0755)
ioutil.WriteFile(plistFile, []byte(strings.Replace(plist, "VERSION", version, 1)), 0644)
ioutil.WriteFile(pkginfoFile, []byte("APPL????"), 0644)
iconFile := path.Join(outdir, "icon.png")
iconsetDir := path.Join(outdir, "icon.iconset")
os.Mkdir(iconsetDir, 0700)
for _, res := range []int{1024, 512, 256, 128, 64, 32, 16} {
outfile := fmt.Sprintf("icon_%dx%d.png", res, res)
if res == 1024 || res == 64 {
outfile = fmt.Sprintf("icon_%dx%d@2x.png", res/2, res/2)
}
cmd := []string{
"sips", "-s", "format", "png", "--resampleWidth", strconv.Itoa(res),
iconFile, "--out", path.Join(iconsetDir, outfile),
}
run(cmd...)
}
icnsFile := path.Join(resDir, "icon.icns")
run("iconutil", "-c", "icns", iconsetDir, "-o", icnsFile)
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/nkanaev/yarr/src/platform" "github.com/nkanaev/yarr/src/platform"
"github.com/nkanaev/yarr/src/server" "github.com/nkanaev/yarr/src/server"
"github.com/nkanaev/yarr/src/storage" "github.com/nkanaev/yarr/src/storage"
"github.com/nkanaev/yarr/src/worker"
) )
var Version string = "0.0" var Version string = "0.0"
@@ -90,16 +89,12 @@ func main() {
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
} }
if open && strings.HasPrefix(addr, "unix:") {
log.Fatal("Cannot open ", addr, " in browser")
}
if db == "" {
configPath, err := os.UserConfigDir() configPath, err := os.UserConfigDir()
if err != nil { if err != nil {
log.Fatal("Failed to get config dir: ", err) log.Fatal("Failed to get config dir: ", err)
} }
if db == "" {
storagePath := filepath.Join(configPath, "yarr") storagePath := filepath.Join(configPath, "yarr")
if err := os.MkdirAll(storagePath, 0755); err != nil { if err := os.MkdirAll(storagePath, 0755); err != nil {
log.Fatal("Failed to create app config dir: ", err) log.Fatal("Failed to create app config dir: ", err)
@@ -110,7 +105,6 @@ func main() {
log.Printf("using db file %s", db) log.Printf("using db file %s", db)
var username, password string var username, password string
var err error
if authfile != "" { if authfile != "" {
f, err := os.Open(authfile) f, err := os.Open(authfile)
if err != nil { if err != nil {
@@ -137,7 +131,6 @@ func main() {
log.Fatal("Failed to initialise database: ", err) log.Fatal("Failed to initialise database: ", err)
} }
worker.SetVersion(Version)
srv := server.NewServer(store, addr) srv := server.NewServer(store, addr)
if basepath != "" { if basepath != "" {

View File

@@ -1,33 +1,21 @@
## Compilation ## Compilation
Prerequisies: Install `Go >= 1.17` and `GCC`. Get the source code:
* Go >= 1.23
* C Compiler (GCC / Clang / ...)
* Zig >= 0.14.0 (optional, for cross-compiling CLI versions)
* binutils (optional, for building Windows GUI version)
Get the source code:
git clone https://github.com/nkanaev/yarr.git git clone https://github.com/nkanaev/yarr.git
Compile: Then run one of the corresponding commands:
# create cli for the host OS/architecture # create an executable for the host os
make host # out/yarr make build_macos # -> _output/macos/yarr.app
make build_linux # -> _output/linux/yarr
make build_windows # -> _output/windows/yarr.exe
# create GUI, works only in the target OS # host-specific cli version (no gui)
make windows_amd64_gui # out/windows_amd64_gui/yarr.exe make build_default # -> _output/yarr
make windows_arm64_gui # out/windows_arm64_gui/yarr.exe
make darwin_arm64_gui # out/darwin_arm64_gui/yarr.app
make darwin_amd64_gui # out/darwin_amd64_gui/yarr.app
# create cli, cross-compiles within any OS/architecture # ... or start a dev server locally
make linux_amd64 make serve # starts a server at http://localhost:7070
make linux_arm64
make linux_armv7
make windows_amd64
make windows_arm64
# ... or build a docker image # ... or build a docker image
docker build -t yarr -f etc/dockerfile . docker build -t yarr -f etc/dockerfile .

View File

@@ -1,34 +1,11 @@
# upcoming # upcoming
- (new) serve on unix socket (thanks to @rvighne)
- (new) more auto-refresh options: 12h & 24h (thanks to @aswerkljh for suggestion)
- (fix) smooth scrolling on iOS (thanks to gatheraled)
- (fix) displaying youtube shorts in "Read Here" (thanks to @Dean-Corso for the report)
- (etc) theme-color support (thanks to @asimpson)
- (etc) cookie security measures (thanks to Tom Fitzhenry)
- (etc) restrict access to internal IPs for page crawler (thanks to Omar Kurt)
# v2.5 (2025-03-26)
- (new) Fever API support (thanks to @icefed) - (new) Fever API support (thanks to @icefed)
- (new) editable feed link (thanks to @adaszko)
- (new) switch to feed by clicking the title in the article page (thanks to @tarasglek for suggestion)
- (new) support multiple media links
- (new) next/prev article navigation buttons (thanks to @tillcash)
- (fix) duplicate articles caused by the same feed addition (thanks to @adaszko) - (fix) duplicate articles caused by the same feed addition (thanks to @adaszko)
- (fix) relative article links (thanks to @adazsko for the report) - (fix) relative article links (thanks to @adazsko for the report)
- (fix) atom article links stored in id element (thanks to @adazsko for the report) - (fix) atom article links stored in id element (thanks to @adazsko for the report)
- (fix) parsing atom feed titles (thanks to @wnh) - (fix) parsing atom feed titles (thanks to @wnh)
- (fix) sorting same-day batch articles (thanks to @lamescholar for the report) - (fix) sorting same-day batch articles (thanks to @lamescholar for the report)
- (fix) showing login page in the selected theme (thanks to @feddiriko for the report)
- (fix) parsing atom feeds with html elements (thanks to @tillcash & @toBeOfUse for the report, @krkk for the fix)
- (fix) parsing feeds with missing guids (thanks to @hoyii for the report)
- (fix) sending actual client version to servers (thanks to @aidanholm)
- (fix) error caused by missing config dir (thanks to @timster)
- (etc) load external images with no-referrer policy (thanks to @tillcash for the report)
- (etc) open external links with no-referrer policy (thanks to @donovanglover)
- (etc) show article content in the list if title is missing (thanks to @asimpson for suggestion)
- (etc) accessibility improvements (thanks to @tseykovets)
# v2.4 (2023-08-15) # v2.4 (2023-08-15)

View File

@@ -1,68 +0,0 @@
- site: https://vimeo.com/channels/staffpicks/videos
feed: https://vimeo.com/channels/staffpicks/videos/rss
tags: [vimeo, image]
- site: https://www.youtube.com/@everyframeapainting/videos
feed: https://www.youtube.com/feeds/videos.xml?channel_id=UCjFqcJQXGZ6T6sxyFB-5i6A"
tags: [youtube, image]
- site: https://iwdrm.tumblr.com/
feed: https://iwdrm.tumblr.com/rss
tags: [tumblr, image]
- site: https://falseknees.tumblr.com/
feed: https://falseknees.tumblr.com/rss
tags: [tumblr, image]
- site: https://accidentallyquadratic.tumblr.com/
feed: https://accidentallyquadratic.tumblr.com/rss
info: text blog with code sections
tags: [tumblr, text, code]
- site: https://www.flickr.com/photos/maratsafin/
feed: https://www.flickr.com/services/feeds/photos_public.gne?id=59021497@N07&lang=en-us&format=atom
tags: [flickr, image]
- site: https://www.reddit.com/r/comics
feed: https://www.reddit.com/r/comics.rss
tags: [reddit, image]
- site: https://www.reddit.com/r/AITAH
feed: https://www.reddit.com/r/AITAH.rss
tags: [reddit, text]
- site: https://idothei.wordpress.com/
feed: https://idothei.wordpress.com/feed/
tags: [wordpress, text]
- site: https://www.vidarholen.net/contents/blog/
feed: https://www.vidarholen.net/contents/blog/?feed=rss2
tags: [wordpress, text]
- site: https://blog.posthaven.com/
feed: https://blog.posthaven.com/posts.atom
tags: [posthaven, text]
- site: https://medium.com/@dailynewsletter
feed: https://medium.com/feed/@dailynewsletter
tags: [medium, text]
- site: https://thereveal.substack.com/
feed: https://thereveal.substack.com/feed
tags: [substack, text]
- site: https://tema.livejournal.com/
feed: https://tema.livejournal.com/data/rss
tags: [livejournal, text]
- site: https://mametter.hatenablog.com/
feed: https://mametter.hatenablog.com/feed
tags: [hatena, text]
- site: https://juliepowell.blogspot.com/
feed: https://juliepowell.blogspot.com/feeds/posts/default
tags: [blogger, text]
- site: https://micro.blog/val
feed: https://micro.blog/posts/val
tags: [json, microblog]

View File

@@ -1,14 +1,12 @@
FROM golang:alpine3.21 AS build FROM golang:alpine AS build
RUN apk add build-base git RUN apk add build-base git
WORKDIR /src WORKDIR /src
COPY . . COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \ RUN make build_linux
--mount=type=cache,target=/root/go/pkg \
make host
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache ca-certificates && update-ca-certificates RUN apk add --no-cache ca-certificates && \
COPY --from=build /src/out/yarr /usr/local/bin/yarr update-ca-certificates
COPY --from=build /src/_output/linux/yarr /usr/local/bin/yarr
EXPOSE 7070 EXPOSE 7070
ENTRYPOINT ["/usr/local/bin/yarr"] CMD ["/usr/local/bin/yarr", "-addr", "0.0.0.0:7070", "-db", "/data/yarr.db"]
CMD ["-addr", "0.0.0.0:7070", "-db", "/data/yarr.db"]

View File

@@ -12,9 +12,9 @@ RUN env DEBIAN_FRONTEND=noninteractive \
apt install -y qemu-user qemu-user-static apt install -y qemu-user qemu-user-static
# Install Golang # Install Golang
RUN wget --quiet https://go.dev/dl/go1.24.1.linux-amd64.tar.gz && \ RUN wget --quiet https://go.dev/dl/go1.18.2.linux-amd64.tar.gz && \
rm -rf /usr/local/go && \ rm -rf /usr/local/go && \
tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz tar -C /usr/local -xzf go1.18.2.linux-amd64.tar.gz
ENV PATH=$PATH:/usr/local/go/bin ENV PATH=$PATH:/usr/local/go/bin
# Copy source code # Copy source code
@@ -27,12 +27,18 @@ RUN env \
CC=aarch64-linux-gnu-gcc \ CC=aarch64-linux-gnu-gcc \
CGO_ENABLED=1 \ CGO_ENABLED=1 \
GOOS=linux GOARCH=arm64 \ GOOS=linux GOARCH=arm64 \
make host && mv out/yarr /root/out/yarr.arm64 go build \
-tags "sqlite_foreign_keys linux" \
-ldflags="-s -w" \
-o /root/out/yarr.arm64 ./cmd/yarr
RUN env \ RUN env \
CC=arm-linux-gnueabihf-gcc \ CC=arm-linux-gnueabihf-gcc \
CGO_ENABLED=1 \ CGO_ENABLED=1 \
GOOS=linux GOARCH=arm GOARM=7 \ GOOS=linux GOARCH=arm GOARM=7 \
make host && mv out/yarr /root/out/yarr.armv7 go build \
-tags "sqlite_foreign_keys linux" \
-ldflags="-s -w" \
-o /root/out/yarr.arm7 ./cmd/yarr
CMD ["/bin/bash"] CMD ["/bin/bash"]

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,22 +1,14 @@
#!/bin/bash #!/bin/bash
if [[ ! -d "$HOME/.local/share/applications" ]]; then
mkdir -p "$HOME/.local/share/applications"
fi
cat >"$HOME/.local/share/applications/yarr.desktop" <<END cat >"$HOME/.local/share/applications/yarr.desktop" <<END
[Desktop Entry] [Desktop Entry]
Name=yarr Name=yarr
Exec=$HOME/.local/bin/yarr -open Exec=$HOME/.local/bin/yarr -open
Icon=yarr Icon=yarr
Type=Application Type=Application
Categories=Internet;Network;News;Feed; Categories=Internet;
END END
if [[ ! -d "$HOME/.local/share/icons" ]]; then
mkdir -p "$HOME/.local/share/icons"
fi
cat >"$HOME/.local/share/icons/yarr.svg" <<END cat >"$HOME/.local/share/icons/yarr.svg" <<END
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor-favicon"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor-favicon">

View File

@@ -1,62 +0,0 @@
#/bin/sh
set -e
usage() {
echo "usage: $0 VERSION path/to/icon.icns path/to/binary output/dir"
}
if [ $# -eq 0 ]; then
usage
exit
fi
VERSION=$1
ICNFILE=$2
BINFILE=$3
OUTPATH=$4
mkdir -p $OUTPATH/yarr.app/Contents/MacOS
mkdir -p $OUTPATH/yarr.app/Contents/Resources
mv $BINFILE $OUTPATH/yarr.app/Contents/MacOS/yarr
cp $ICNFILE $OUTPATH/yarr.app/Contents/Resources/icon.icns
chmod u+x $OUTPATH/yarr.app/Contents/MacOS/yarr
echo -n 'APPL????' >$OUTPATH/yarr.app/Contents/PkgInfo
cat <<EOF >$OUTPATH/yarr.app/Contents/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>yarr</string>
<key>CFBundleDisplayName</key>
<string>yarr</string>
<key>CFBundleIdentifier</key>
<string>nkanaev.yarr</string>
<key>CFBundleVersion</key>
<string>$VERSION</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>yarr</string>
<key>CFBundleIconFile</key>
<string>icon</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.news</string>
<key>NSHighResolutionCapable</key>
<string>True</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020 nkanaev. All rights reserved.</string>
</dict>
</plist>
EOF

View File

@@ -1,89 +0,0 @@
#!/bin/bash
set -e
# Function to display usage information
usage() {
echo "Usage: $0 [-version VERSION] [-outfile FILENAME]"
echo " -version VERSION Set the version number (default: 0.0)"
echo " -outfile FILENAME Set the output file name (default: versioninfo.rc)"
echo ""
echo "This script generates a Windows resource file with version information."
exit 1
}
# Default values
version="0.0"
outfile="versioninfo.rc"
# Check if help is requested
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
fi
if [ $# -eq 0 ]; then
usage
fi
# Parse command-line options
while [[ $# -gt 0 ]]; do
case $1 in
-version)
if [[ -z "$2" || "$2" == -* ]]; then
echo "Error: Missing value for -version parameter"
usage
fi
version="$2"
shift 2
;;
-outfile)
if [[ -z "$2" || "$2" == -* ]]; then
echo "Error: Missing value for -outfile parameter"
usage
fi
outfile="$2"
shift 2
;;
*)
echo "Error: Unknown parameter: $1"
usage
;;
esac
done
# Replace dots with commas for version_comma
version_comma="${version//./,}"
# Use a here document for the template with ENDFILE delimiter
cat <<ENDFILE > "$outfile"
1 VERSIONINFO
FILEVERSION $version_comma,0,0
PRODUCTVERSION $version_comma,0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "080904E4"
BEGIN
VALUE "CompanyName", "Old MacDonald's Farm"
VALUE "FileDescription", "Yet another RSS reader"
VALUE "FileVersion", "$version"
VALUE "InternalName", "yarr"
VALUE "LegalCopyright", "nkanaev"
VALUE "OriginalFilename", "yarr.exe"
VALUE "ProductName", "yarr"
VALUE "ProductVersion", "$version"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x809, 1252
END
END
1 ICON "icon.ico"
ENDFILE
# Set the correct permissions
chmod 644 "$outfile"
echo "Generated $outfile with version $version"

12
go.mod
View File

@@ -1,13 +1,11 @@
module github.com/nkanaev/yarr module github.com/nkanaev/yarr
go 1.23.0 go 1.17
toolchain go1.23.5
require ( require (
github.com/mattn/go-sqlite3 v1.14.24 github.com/mattn/go-sqlite3 v1.14.7
golang.org/x/net v0.38.0 golang.org/x/net v0.17.0
golang.org/x/sys v0.31.0 golang.org/x/sys v0.13.0
) )
require golang.org/x/text v0.23.0 // indirect require golang.org/x/text v0.13.0 // indirect

53
go.sum
View File

@@ -1,8 +1,45 @@
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -1,90 +1,33 @@
VERSION=2.5 VERSION=2.5
GITHASH=$(shell git rev-parse --short=8 HEAD) GITHASH=$(shell git rev-parse --short=8 HEAD)
GO_TAGS = sqlite_foreign_keys sqlite_json CGO_ENABLED=1
GO_LDFLAGS = -s -w -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
GO_FLAGS = -tags "$(GO_TAGS)" -ldflags="$(GO_LDFLAGS)" GO_LDFLAGS = -s -w
GO_FLAGS_DEBUG = -tags "$(GO_TAGS) debug" GO_LDFLAGS := $(GO_LDFLAGS) -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
GO_FLAGS_GUI = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS)"
GO_FLAGS_GUI_WIN = -tags "$(GO_TAGS) gui" -ldflags="$(GO_LDFLAGS) -H windowsgui"
export CGO_ENABLED=1 build_default:
mkdir -p _output
go build -tags "sqlite_foreign_keys" -ldflags="$(GO_LDFLAGS)" -o _output/yarr ./cmd/yarr
default: test host build_macos:
mkdir -p _output/macos
GOOS=darwin GOARCH=amd64 go build -tags "sqlite_foreign_keys macos" -ldflags="$(GO_LDFLAGS)" -o _output/macos/yarr ./cmd/yarr
cp src/platform/icon.png _output/macos/icon.png
go run ./cmd/package_macos -outdir _output/macos -version "$(VERSION)"
# platform-specific files build_linux:
mkdir -p _output/linux
GOOS=linux GOARCH=amd64 go build -tags "sqlite_foreign_keys linux" -ldflags="$(GO_LDFLAGS)" -o _output/linux/yarr ./cmd/yarr
etc/icon.icns: etc/icon_macos.png build_windows:
mkdir -p etc/icon.iconset mkdir -p _output/windows
sips -s format png --resampleWidth 1024 etc/icon_macos.png --out etc/icon.iconset/icon_512x512@2x.png go run ./cmd/generate_versioninfo -version "$(VERSION)" -outfile src/platform/versioninfo.rc
sips -s format png --resampleWidth 512 etc/icon_macos.png --out etc/icon.iconset/icon_512x512.png
sips -s format png --resampleWidth 256 etc/icon_macos.png --out etc/icon.iconset/icon_256x256.png
sips -s format png --resampleWidth 128 etc/icon_macos.png --out etc/icon.iconset/icon_128x128.png
sips -s format png --resampleWidth 64 etc/icon_macos.png --out etc/icon.iconset/icon_32x32@2x.png
sips -s format png --resampleWidth 32 etc/icon_macos.png --out etc/icon.iconset/icon_32x32.png
sips -s format png --resampleWidth 16 etc/icon_macos.png --out etc/icon.iconset/icon_16x16.png
iconutil -c icns etc/icon.iconset -o etc/icon.icns
src/platform/versioninfo.rc:
./etc/windows_versioninfo.sh -version "$(VERSION)" -outfile src/platform/versioninfo.rc
windres -i src/platform/versioninfo.rc -O coff -o src/platform/versioninfo.syso windres -i src/platform/versioninfo.rc -O coff -o src/platform/versioninfo.syso
GOOS=windows GOARCH=amd64 go build -tags "sqlite_foreign_keys windows" -ldflags="$(GO_LDFLAGS) -H windowsgui" -o _output/windows/yarr.exe ./cmd/yarr
# build targets
host:
go build $(GO_FLAGS) -o out/yarr ./cmd/yarr
darwin_amd64:
# cross-compilation not supported: CC="zig cc -target x86_64-macos-none"
GOOS=darwin GOARCH=arm64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
darwin_arm64:
# cross-compilation not supported: CC="zig cc -target aarch64-macos-none"
GOOS=darwin GOARCH=arm64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
linux_amd64:
CC="zig cc -target x86_64-linux-musl -O2 -g0" CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux GOARCH=amd64 \
go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
linux_arm64:
CC="zig cc -target aarch64-linux-musl -O2 -g0" CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux GOARCH=arm64 \
go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
linux_armv7:
CC="zig cc -target arm-linux-musleabihf -O2 -g0" CGO_CFLAGS="-D_LARGEFILE64_SOURCE" GOOS=linux GOARCH=arm GOARM=7 \
go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
windows_amd64:
CC="zig cc -target x86_64-windows-gnu" GOOS=windows GOARCH=amd64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
windows_arm64:
CC="zig cc -target aarch64-windows-gnu" GOOS=windows GOARCH=arm64 go build $(GO_FLAGS) -o out/$@/yarr ./cmd/yarr
darwin_arm64_gui: etc/icon.icns
GOOS=darwin GOARCH=arm64 go build $(GO_FLAGS_GUI) -o out/$@/yarr ./cmd/yarr
./etc/macos_package.sh $(VERSION) etc/icon.icns out/$@/yarr out/$@
darwin_amd64_gui: etc/icon.icns
GOOS=darwin GOARCH=amd64 go build $(GO_FLAGS_GUI) -o out/$@/yarr ./cmd/yarr
./etc/macos_package.sh $(VERSION) etc/icon.icns out/$@/yarr out/$@
windows_amd64_gui: src/platform/versioninfo.rc
GOOS=windows GOARCH=amd64 go build $(GO_FLAGS_GUI_WIN) -o out/$@/yarr.exe ./cmd/yarr
windows_arm64_gui: src/platform/versioninfo.rc
GOOS=windows GOARCH=arm64 go build $(GO_FLAGS_GUI_WIN) -o out/$@/yarr.exe ./cmd/yarr
serve: serve:
go run $(GO_FLAGS_DEBUG) ./cmd/yarr -db local.db go run -tags "sqlite_foreign_keys" ./cmd/yarr -db local.db
test: test:
go test $(GO_FLAGS) ./... go test -tags "sqlite_foreign_keys" ./...
.PHONY: \
host \
darwin_amd64 darwin_amd64_gui \
darwin_arm64 darwin_arm64_gui \
windows_amd64 windows_amd64_gui \
windows_arm64 windows_arm64_gui \
serve test

View File

@@ -9,30 +9,94 @@ The app is a single binary with an embedded database (SQLite).
## usage ## usage
The latest prebuilt binaries for Linux/MacOS/Windows are available The latest prebuilt binaries for Linux/MacOS/Windows AMD64 are available
[here](https://github.com/nkanaev/yarr/releases/latest). [here](https://github.com/nkanaev/yarr/releases/latest). Installation instructions:
The archives follow the naming convention `yarr_{OS}_{ARCH}[_gui].zip`, where:
* `OS` is the target operating system * Command Arges
* `ARCH` is the CPU architecture (`arm64` for AArch64, `amd64` for X86-64)
* `-gui` indicates that the binary ships with the GUI (tray icon), and is a command line application if omitted
Usage instructions: ```
-addr string
address to run server on (default "127.0.0.1:7070")
-auth-file path
path to a file containing username:password
-base string
base path of the service url
-cert-file path
path to cert file for https
-db path
storage file path
-key-file path
path to key file for https
-log-file path
path to log file to use instead of stdout
-open
open the server in browser
-version
print application version
```
* MacOS: place `yarr.app` in `/Applications` folder, [open the app][macos-open], click the anchor menu bar icon, select "Open". * MacOS
* Windows: open `yarr.exe`, click the anchor system tray icon, select "Open". Download `yarr-*-macos64.zip`, unzip it, place `yarr.app` in `/Applications` folder, [open the app][macos-open], click the anchor menu bar icon, select "Open".
* Linux: place `yarr` in `$HOME/.local/bin` and run [the script](etc/install-linux.sh). * Windows
Download `yarr-*-windows64.zip`, unzip it, open `yarr.exe`, click the anchor system tray icon, select "Open".
* Linux
Download `yarr-*-linux64.zip`, unzip it, place `yarr` in `$HOME/.local/bin`
and run [the script](etc/install-linux.sh).
[macos-open]: https://support.apple.com/en-gb/guide/mac-help/mh40616/mac [macos-open]: https://support.apple.com/en-gb/guide/mac-help/mh40616/mac
For self-hosting, see `yarr -h` for auth, tls & server configuration flags. * Docker environment
See more: You can use docker or docker-compose to run yarr, and you can also use environment variables to configure startup parameters.
* [Building from source code](doc/build.md) - `YARR_ADDR` address to run server on (default "127.0.0.1:7070")
* [Fever API support](doc/fever.md) - `YARR_BASE` base path of the service url
- `YARR_AUTHFILE` path to a file containing username:password
- `YARR_CERTFILE` path to cert file for https
- `YARR_KEYFILE` path to key file for https
- `YARR_DB` storage file path
- `YARR_LOGFILE` path to log file to use instead of stdout
* Docker run
```
docker run -d \
--name yarr \
-p 25255:7070 \
-e YARR_AUTHFILE="/data/.auth.list" \
-v /data/yarr-data:/data \
--restart always \
arsfeld/yarr:latest
```
* Docker-Compose Run
Create a file named `.auth.list` under the `/data/` directory, and the content format should be: `username:password`.
Then start by running docker-compose up -d and enjoy!
```yaml
version: '3.3'
services:
yarr:
container_name: yarr
image: 'arsfeld/yarr:latest'
restart: always
ports:
- '25255:7070'
environment:
YARR_AUTHFILE: "/data/.auth.list"
volumes:
- '/data/yarr-data:/data'
```
* See more:
* [Building from source code](doc/build.md)
* [Fever API support](doc/fever.md)
## credits ## credits

View File

@@ -1,5 +1,3 @@
//go:build !debug
package assets package assets
import "embed" import "embed"

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

Before

Width:  |  Height:  |  Size: 269 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>

Before

Width:  |  Height:  |  Size: 268 B

View File

@@ -8,7 +8,6 @@
<link rel="icon" href="./static/graphicarts/favicon.svg" type="image/svg+xml"> <link rel="icon" href="./static/graphicarts/favicon.svg" type="image/svg+xml">
<link rel="alternate icon" href="./static/graphicarts/favicon.png" type="image/png"> <link rel="alternate icon" href="./static/graphicarts/favicon.png" type="image/png">
<link rel="manifest" href="./manifest.json" /> <link rel="manifest" href="./manifest.json" />
<meta name="theme-color" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script> <script>
window.app = window.app || {} window.app = window.app || {}
@@ -24,23 +23,20 @@
<div class="p-2 toolbar d-flex align-items-center"> <div class="p-2 toolbar d-flex align-items-center">
<div class="icon mx-2">{% inline "anchor.svg" %}</div> <div class="icon mx-2">{% inline "anchor.svg" %}</div>
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<button class="toolbar-item ml-1" <button class="toolbar-item"
:class="{active: filterSelected == 'unread'}" :class="{active: filterSelected == 'unread'}"
:aria-pressed="filterSelected == 'unread'"
title="Unread" title="Unread"
@click="filterSelected = 'unread'"> @click="filterSelected = 'unread'">
<span class="icon">{% inline "circle-full.svg" %}</span> <span class="icon">{% inline "circle-full.svg" %}</span>
</button> </button>
<button class="toolbar-item mx-1" <button class="toolbar-item"
:class="{active: filterSelected == 'starred'}" :class="{active: filterSelected == 'starred'}"
:aria-pressed="filterSelected == 'starred'"
title="Starred" title="Starred"
@click="filterSelected = 'starred'"> @click="filterSelected = 'starred'">
<span class="icon">{% inline "star-full.svg" %}</span> <span class="icon">{% inline "star-full.svg" %}</span>
</button> </button>
<button class="toolbar-item mr-1" <button class="toolbar-item"
:class="{active: filterSelected == ''}" :class="{active: filterSelected == ''}"
:aria-pressed="filterSelected == ''"
title="All" title="All"
@click="filterSelected = ''"> @click="filterSelected = ''">
<span class="icon">{% inline "assorted.svg" %}</span> <span class="icon">{% inline "assorted.svg" %}</span>
@@ -63,12 +59,10 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<header class="dropdown-header" role="heading" aria-level="2">Theme</header> <header class="dropdown-header">Theme</header>
<div class="row text-center m-0"> <div class="row text-center m-0">
<button class="btn btn-link col-4 px-0 rounded-0" <button class="btn btn-link col-4 px-0 rounded-0"
:class="'theme-'+t" :class="'theme-'+t"
:aria-label="t"
:aria-pressed="theme.name == t"
@click.stop="theme.name = t" @click.stop="theme.name = t"
v-for="t in ['light', 'sepia', 'night']"> v-for="t in ['light', 'sepia', 'night']">
<span class="icon" v-if="theme.name == t">{% inline "check.svg" %}</span> <span class="icon" v-if="theme.name == t">{% inline "check.svg" %}</span>
@@ -77,33 +71,25 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<header class="dropdown-header" role="heading" aria-level="2">Auto Refresh</header> <header class="dropdown-header">Auto Refresh</header>
<div class="row text-center m-0"> <div class="row text-center m-0">
<button class="dropdown-item col-4 px-0" <button class="dropdown-item col-4 px-0" :class="{active: !refreshRate}" @click.stop="refreshRate = 0">0</button>
@click.stop="changeRefreshRate(-1)" <button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 10}" @click.stop="refreshRate = 10">10m</button>
:disabled="!refreshRate"> <button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 30}" @click.stop="refreshRate = 30">30m</button>
<span class="icon"> <button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 60}" @click.stop="refreshRate = 60">1h</button>
{% inline "chevron-down.svg" %} <button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 120}" @click.stop="refreshRate = 120">2h</button>
</span> <button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 240}" @click.stop="refreshRate = 240">4h</button>
</button>
<div class="col-4 d-flex align-items-center justify-content-center">{{ refreshRateTitle }}</div>
<button class="dropdown-item col-4 px-0"
@click.stop="changeRefreshRate(1)" :disabled="refreshRate === refreshRateOptions.at(-1).value">
<span class="icon">
{% inline "chevron-up.svg" %}
</span>
</button>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<header class="dropdown-header" role="heading" aria-level="2">Show first</header> <header class="dropdown-header">Show first</header>
<div class="d-flex text-center"> <div class="d-flex text-center">
<button class="dropdown-item px-0" :aria-pressed="itemSortNewestFirst" :class="{active: itemSortNewestFirst}" @click.stop="itemSortNewestFirst=true">New</button> <button class="dropdown-item px-0" :class="{active: itemSortNewestFirst}" @click.stop="itemSortNewestFirst=true">New</button>
<button class="dropdown-item px-0" :aria-pressed="!itemSortNewestFirst" :class="{active: !itemSortNewestFirst}" @click.stop="itemSortNewestFirst=false">Old</button> <button class="dropdown-item px-0" :class="{active: !itemSortNewestFirst}" @click.stop="itemSortNewestFirst=false">Old</button>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<header class="dropdown-header" role="heading" aria-level="2">Subscriptions</header> <header class="dropdown-header">Subscriptions</header>
<form id="opml-import-form" enctype="multipart/form-data" tabindex="-1"> <form id="opml-import-form" enctype="multipart/form-data" tabindex="-1">
<input type="file" <input type="file"
id="opml-import" id="opml-import"
@@ -131,7 +117,7 @@
</button> </button>
</dropdown> </dropdown>
</div> </div>
<div id="feed-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1"> <div id="feed-list-scroll" class="p-2 overflow-auto border-top flex-grow-1">
<label class="selectgroup"> <label class="selectgroup">
<input type="radio" name="feed" value="" v-model="feedSelected"> <input type="radio" name="feed" value="" v-model="feedSelected">
<div class="selectgroup-label d-flex align-items-center w-100"> <div class="selectgroup-label d-flex align-items-center w-100">
@@ -144,8 +130,10 @@
</label> </label>
<div v-for="folder in foldersWithFeeds"> <div v-for="folder in foldersWithFeeds">
<label class="selectgroup mt-1" <label class="selectgroup mt-1"
:class="{'d-none': mustHideFolder(folder)}" :class="{'d-none': filterSelected
v-if="folder.id"> && !(current.folder.id == folder.id || current.feed.folder_id == folder.id)
&& !filteredFolderStats[folder.id]
&& (!itemSelectedDetails || (feedsById[itemSelectedDetails.feed_id] || {}).folder_id != folder.id)}">
<input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected" v-if="folder.id"> <input type="radio" name="feed" :value="'folder:'+folder.id" v-model="feedSelected" v-if="folder.id">
<div class="selectgroup-label d-flex align-items-center w-100" v-if="folder.id"> <div class="selectgroup-label d-flex align-items-center w-100" v-if="folder.id">
<span class="icon mr-2" <span class="icon mr-2"
@@ -159,7 +147,10 @@
</label> </label>
<div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}"> <div v-show="!folder.id || folder.is_expanded" class="mt-1" :class="{'pl-3': folder.id}">
<label class="selectgroup" <label class="selectgroup"
:class="{'d-none': mustHideFeed(feed)}" :class="{'d-none': filterSelected
&& !(current.feed.id == feed.id)
&& !filteredFeedStats[feed.id]
&& (!itemSelectedDetails || itemSelectedDetails.feed_id != feed.id)}"
v-for="feed in folder.feeds"> v-for="feed in folder.feeds">
<input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected"> <input type="radio" name="feed" :value="'feed:'+feed.id" v-model="feedSelected">
<div class="selectgroup-label d-flex align-items-center w-100"> <div class="selectgroup-label d-flex align-items-center w-100">
@@ -215,12 +206,12 @@
<template v-slot:button> <template v-slot:button>
<span class="icon">{% inline "more-horizontal.svg" %}</span> <span class="icon">{% inline "more-horizontal.svg" %}</span>
</template> </template>
<header class="dropdown-header" role="heading" aria-level="2">{{ current.feed.title }}</header> <header class="dropdown-header">{{ current.feed.title }}</header>
<a class="dropdown-item" :href="current.feed.link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" v-if="current.feed.link"> <a class="dropdown-item" :href="current.feed.link" target="_blank" v-if="current.feed.link">
<span class="icon mr-1">{% inline "globe.svg" %}</span> <span class="icon mr-1">{% inline "globe.svg" %}</span>
Website Website
</a> </a>
<a class="dropdown-item" :href="current.feed.feed_link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" v-if="current.feed.feed_link"> <a class="dropdown-item" :href="current.feed.feed_link" target="_blank" v-if="current.feed.feed_link">
<span class="icon mr-1">{% inline "rss.svg" %}</span> <span class="icon mr-1">{% inline "rss.svg" %}</span>
Feed Link Feed Link
</a> </a>
@@ -229,12 +220,8 @@
<span class="icon mr-1">{% inline "edit.svg" %}</span> <span class="icon mr-1">{% inline "edit.svg" %}</span>
Rename Rename
</button> </button>
<button class="dropdown-item" @click="updateFeedLink(current.feed)" v-if="current.feed.feed_link">
<span class="icon mr-1">{% inline "edit.svg" %}</span>
Change Link
</button>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<header class="dropdown-header" role="heading" aria-level="2">Move to...</header> <header class="dropdown-header">Move to...</header>
<button class="dropdown-item" <button class="dropdown-item"
v-if="folder.id != current.feed.folder_id" v-if="folder.id != current.feed.folder_id"
v-for="folder in folders" v-for="folder in folders"
@@ -264,7 +251,7 @@
<template v-slot:button> <template v-slot:button>
<span class="icon">{% inline "more-horizontal.svg" %}</span> <span class="icon">{% inline "more-horizontal.svg" %}</span>
</template> </template>
<header class="dropdown-header" role="heading" aria-level="2">{{ current.folder.title }}</header> <header class="dropdown-header">{{ current.folder.title }}</header>
<button class="dropdown-item" @click="renameFolder(current.folder)"> <button class="dropdown-item" @click="renameFolder(current.folder)">
<span class="icon mr-1">{% inline "edit.svg" %}</span> <span class="icon mr-1">{% inline "edit.svg" %}</span>
Rename Rename
@@ -276,7 +263,7 @@
</button> </button>
</dropdown> </dropdown>
</div> </div>
<div id="item-list-scroll" class="p-2 overflow-auto scroll-touch border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist"> <div id="item-list-scroll" class="p-2 overflow-auto border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist">
<label v-for="item in items" :key="item.id" <label v-for="item in items" :key="item.id"
class="selectgroup"> class="selectgroup">
<input type="radio" name="item" :value="item.id" v-model="itemSelected"> <input type="radio" name="item" :value="item.id" v-model="itemSelected">
@@ -335,45 +322,29 @@
title="Read Here"> title="Read Here">
<span class="icon" :class="{'icon-loading': loading.readability}">{% inline "book-open.svg" %}</span> <span class="icon" :class="{'icon-loading': loading.readability}">{% inline "book-open.svg" %}</span>
</button> </button>
<a class="toolbar-item" :href="itemSelectedDetails.link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" title="Open Link"> <a class="toolbar-item" :href="itemSelectedDetails.link" target="_blank" title="Open Link">
<span class="icon">{% inline "external-link.svg" %}</span> <span class="icon">{% inline "external-link.svg" %}</span>
</a> </a>
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<button class="toolbar-item" @click="navigateToItem(-1)" title="Previous Article" :disabled="itemSelected == items[0].id">
<span class="icon">{% inline "chevron-left.svg" %}</span>
</button>
<button class="toolbar-item" @click="navigateToItem(+1)" title="Next Article" :disabled="itemSelected == items[items.length - 1].id">
<span class="icon">{% inline "chevron-right.svg" %}</span>
</button>
<button class="toolbar-item" @click="itemSelected=null" title="Close Article"> <button class="toolbar-item" @click="itemSelected=null" title="Close Article">
<span class="icon">{% inline "x.svg" %}</span> <span class="icon">{% inline "x.svg" %}</span>
</button> </button>
</div> </div>
<div v-if="itemSelectedDetails" <div v-if="itemSelectedDetails"
ref="content" ref="content"
class="content px-4 pt-3 pb-5 border-top overflow-auto scroll-touch" class="content px-4 pt-3 pb-5 border-top overflow-auto"
:class="{'font-serif': theme.font == 'serif', 'font-monospace': theme.font == 'monospace'}" :class="{'font-serif': theme.font == 'serif', 'font-monospace': theme.font == 'monospace'}"
:style="{'font-size': theme.size + 'rem'}"> :style="{'font-size': theme.size + 'rem'}">
<div class="content-wrapper"> <div class="content-wrapper">
<h1><b>{{ itemSelectedDetails.title || 'untitled' }}</b></h1> <h1><b>{{ itemSelectedDetails.title || 'untitled' }}</b></h1>
<div class="text-muted"> <div class="text-muted">
<div> <div>{{ (feedsById[itemSelectedDetails.feed_id] || {}).title }}</div>
<span class="cursor-pointer" @click="feedSelected = 'feed:'+(feedsById[itemSelectedDetails.feed_id] || {}).id">
{{ (feedsById[itemSelectedDetails.feed_id] || {}).title }}
</span>
</div>
<time>{{ formatDate(itemSelectedDetails.date) }}</time> <time>{{ formatDate(itemSelectedDetails.date) }}</time>
</div> </div>
<hr> <hr>
<div v-if="!itemSelectedReadability"> <div v-if="!itemSelectedReadability">
<div v-if="contentImages.length"> <img :src="itemSelectedDetails.image" v-if="itemSelectedDetails.image" class="mb-3">
<figure v-for="media in contentImages"> <audio class="w-100" controls v-if="itemSelectedDetails.podcast_url" :src="itemSelectedDetails.podcast_url"></audio>
<img :src="media.url" loading="lazy">
<figcaption v-if="media.description">{{ media.description }}</figcaption>
</figure>
</div>
<audio class="w-100" controls v-for="media in contentAudios" :src="media.url"></audio>
<video class="w-100" controls v-for="media in contentVideos" :src="media.url"></video>
</div> </div>
<div v-html="itemSelectedContent"></div> <div v-html="itemSelectedContent"></div>
</div> </div>
@@ -387,7 +358,7 @@
<p class="cursor-default"><b>New Feed</b></p> <p class="cursor-default"><b>New Feed</b></p>
<form action="" @submit.prevent="createFeed(event)" class="mt-4"> <form action="" @submit.prevent="createFeed(event)" class="mt-4">
<label for="feed-url">URL</label> <label for="feed-url">URL</label>
<input id="feed-url" name="url" type="url" class="form-control" required autocomplete="off" :readonly="feedNewChoice.length > 0" placeholder="https://example.com/feed" v-focus> <input id="feed-url" name="url" type="url" class="form-control" required autocomplete="off" :readonly="feedNewChoice.length > 0" placeholder="https://example.com/feed" v-focus V-model="autoFeedUrl">
<label for="feed-folder" class="mt-3 d-block"> <label for="feed-folder" class="mt-3 d-block">
Folder Folder
<a href="#" class="float-right text-decoration-none" @click.prevent="createNewFeedFolder()">new folder</a> <a href="#" class="float-right text-decoration-none" @click.prevent="createNewFeedFolder()">new folder</a>
@@ -423,7 +394,6 @@
<tr><td colspan=2>&nbsp;</td></tr> <tr><td colspan=2>&nbsp;</td></tr>
<tr><td><kbd>j</kbd> <kbd>k</kbd></td> <td>next / prev article</td></tr> <tr><td><kbd>j</kbd> <kbd>k</kbd></td> <td>next / prev article</td></tr>
<tr><td><kbd>l</kbd> <kbd>h</kbd></td> <td>next / prev feed</td></tr> <tr><td><kbd>l</kbd> <kbd>h</kbd></td> <td>next / prev feed</td></tr>
<tr><td><kbd>q</kbd></td> <td>close article</td></tr>
<tr><td colspan=2>&nbsp;</td></tr> <tr><td colspan=2>&nbsp;</td></tr>
<tr><td><kbd>R</kbd></td> <td>mark all read</td></tr> <tr><td><kbd>R</kbd></td> <td>mark all read</td></tr>

View File

@@ -2,26 +2,6 @@
var TITLE = document.title var TITLE = document.title
function scrollto(target, scroll) {
var padding = 10
var targetRect = target.getBoundingClientRect()
var scrollRect = scroll.getBoundingClientRect()
// target
var relativeOffset = targetRect.y - scrollRect.y
var absoluteOffset = relativeOffset + scroll.scrollTop
if (padding <= relativeOffset && relativeOffset + targetRect.height <= scrollRect.height - padding) return
var newPos = scroll.scrollTop
if (relativeOffset < padding) {
newPos = absoluteOffset - padding
} else {
newPos = absoluteOffset - scrollRect.height + targetRect.height + padding
}
scroll.scrollTop = Math.round(newPos)
}
var debounce = function(callback, wait) { var debounce = function(callback, wait) {
var timeout var timeout
return function() { return function() {
@@ -203,6 +183,14 @@ Vue.component('relative-time', {
}) })
var vm = new Vue({ var vm = new Vue({
mounted:function(){
const subscribe_to = new URLSearchParams(window.location.search).get('subscribe_to');
if(subscribe_to){
vm.settings = 'create'
//document.getElementById("feed-url").value = subscribe_to;
this.autoFeedUrl = subscribe_to;
}
},
created: function() { created: function() {
this.refreshStats() this.refreshStats()
.then(this.refreshFeeds.bind(this)) .then(this.refreshFeeds.bind(this))
@@ -211,7 +199,6 @@ var vm = new Vue({
api.feeds.list_errors().then(function(errors) { api.feeds.list_errors().then(function(errors) {
vm.feed_errors = errors vm.feed_errors = errors
}) })
this.updateMetaTheme(app.settings.theme_name)
}, },
data: function() { data: function() {
var s = app.settings var s = app.settings
@@ -250,25 +237,9 @@ var vm = new Vue({
'font': s.theme_font, 'font': s.theme_font,
'size': s.theme_size, 'size': s.theme_size,
}, },
'themeColors': {
'night': '#0e0e0e',
'sepia': '#f4f0e5',
'light': '#fff',
},
'refreshRate': s.refresh_rate, 'refreshRate': s.refresh_rate,
'authenticated': app.authenticated, 'authenticated': app.authenticated,
'feed_errors': {}, 'feed_errors': {},
'refreshRateOptions': [
{ title: "0", value: 0 },
{ title: "10m", value: 10 },
{ title: "30m", value: 30 },
{ title: "1h", value: 60 },
{ title: "2h", value: 120 },
{ title: "4h", value: 240 },
{ title: "12h", value: 720 },
{ title: "24h", value: 1440 },
],
} }
}, },
computed: { computed: {
@@ -315,28 +286,11 @@ var vm = new Vue({
return this.itemSelectedDetails.content || '' return this.itemSelectedDetails.content || ''
}, },
contentImages: function() {
if (!this.itemSelectedDetails) return []
return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'image')
},
contentAudios: function() {
if (!this.itemSelectedDetails) return []
return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'audio')
},
contentVideos: function() {
if (!this.itemSelectedDetails) return []
return (this.itemSelectedDetails.media_links || []).filter(l => l.type === 'video')
},
refreshRateTitle: function () {
const entry = this.refreshRateOptions.find(o => o.value === this.refreshRate)
return entry ? entry.title : '0'
},
}, },
watch: { watch: {
'theme': { 'theme': {
deep: true, deep: true,
handler: function(theme) { handler: function(theme) {
this.updateMetaTheme(theme.name)
document.body.classList.value = 'theme-' + theme.name document.body.classList.value = 'theme-' + theme.name
api.settings.update({ api.settings.update({
theme_name: theme.name, theme_name: theme.name,
@@ -412,9 +366,6 @@ var vm = new Vue({
}, },
}, },
methods: { methods: {
updateMetaTheme: function(theme) {
document.querySelector("meta[name='theme-color']").content = this.themeColors[theme]
},
refreshStats: function(loopMode) { refreshStats: function(loopMode) {
return api.status().then(function(data) { return api.status().then(function(data) {
if (loopMode && !vm.itemSelected) vm.refreshItems() if (loopMode && !vm.itemSelected) vm.refreshItems()
@@ -464,7 +415,7 @@ var vm = new Vue({
vm.feeds = values[1] vm.feeds = values[1]
}) })
}, },
refreshItems: function(loadMore = false) { refreshItems: function(loadMore) {
if (this.feedSelected === null) { if (this.feedSelected === null) {
vm.items = [] vm.items = []
vm.itemsHasMore = false vm.itemsHasMore = false
@@ -477,7 +428,7 @@ var vm = new Vue({
} }
this.loading.items = true this.loading.items = true
return api.items.list(query).then(function(data) { api.items.list(query).then(function(data) {
if (loadMore) { if (loadMore) {
vm.items = vm.items.concat(data.list) vm.items = vm.items.concat(data.list)
} else { } else {
@@ -500,17 +451,13 @@ var vm = new Vue({
var scale = (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16) / 16 var scale = (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16) / 16
var el = this.$refs.itemlist var el = this.$refs.itemlist
if (el.scrollHeight === 0) return false // element is invisible (responsive design)
var closeToBottom = (el.scrollHeight - el.scrollTop - el.offsetHeight) < bottomSpace * scale var closeToBottom = (el.scrollHeight - el.scrollTop - el.offsetHeight) < bottomSpace * scale
return closeToBottom return closeToBottom
}, },
loadMoreItems: function(event, el) { loadMoreItems: function(event, el) {
if (!this.itemsHasMore) return if (!this.itemsHasMore) return
if (this.loading.items) return if (this.loading.items) return
if (this.itemListCloseToBottom()) return this.refreshItems(true) if (this.itemListCloseToBottom()) this.refreshItems(true)
if (this.itemSelected && this.itemSelected === this.items[this.items.length - 1].id) return this.refreshItems(true)
}, },
markItemsRead: function() { markItemsRead: function() {
var query = this.getItemsQuery() var query = this.getItemsQuery()
@@ -584,14 +531,6 @@ var vm = new Vue({
}) })
} }
}, },
updateFeedLink: function(feed) {
var newLink = prompt('Enter feed link', feed.feed_link)
if (newLink) {
api.feeds.update(feed.id, {feed_link: newLink}).then(function() {
feed.feed_link = newLink
})
}
},
renameFeed: function(feed) { renameFeed: function(feed) {
var newTitle = prompt('Enter new title', feed.title) var newTitle = prompt('Enter new title', feed.title)
if (newTitle) { if (newTitle) {
@@ -744,91 +683,8 @@ var vm = new Vue({
this.filteredFolderStats = statsFolders this.filteredFolderStats = statsFolders
this.filteredTotalStats = statsTotal this.filteredTotalStats = statsTotal
}, },
// navigation helper, navigate relative to selected item
navigateToItem: function(relativePosition) {
let vm = this
if (vm.itemSelected == null) {
// if no item is selected, select first
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
return
} }
var itemPosition = vm.items.findIndex(function(x) { return x.id === vm.itemSelected })
if (itemPosition === -1) {
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
return
}
var newPosition = itemPosition + relativePosition
if (newPosition < 0 || newPosition >= vm.items.length) return
vm.itemSelected = vm.items[newPosition].id
vm.$nextTick(function() {
var scroll = document.querySelector('#item-list-scroll')
var handle = scroll.querySelector('input[type=radio]:checked')
var target = handle && handle.parentElement
if (target && scroll) scrollto(target, scroll)
vm.loadMoreItems()
})
},
// navigation helper, navigate relative to selected feed
navigateToFeed: function(relativePosition) {
let vm = this
const navigationList = this.foldersWithFeeds
.filter(folder => !folder.id || !vm.mustHideFolder(folder))
.map((folder) => {
if (this.mustHideFolder(folder)) return []
const folds = folder.id ? [`folder:${folder.id}`] : []
const feeds = (folder.is_expanded || !folder.id) ? folder.feeds.filter(f => !vm.mustHideFeed(f)).map(f => `feed:${f.id}`) : []
return folds.concat(feeds)
})
.flat()
navigationList.unshift('')
var currentFeedPosition = navigationList.indexOf(vm.feedSelected)
if (currentFeedPosition == -1) {
vm.feedSelected = ''
return
}
var newPosition = currentFeedPosition+relativePosition
if (newPosition < 0 || newPosition >= navigationList.length) return
vm.feedSelected = navigationList[newPosition]
vm.$nextTick(function() {
var scroll = document.querySelector('#feed-list-scroll')
var handle = scroll.querySelector('input[type=radio]:checked')
var target = handle && handle.parentElement
if (target && scroll) scrollto(target, scroll)
})
},
changeRefreshRate: function(offset) {
const curIdx = this.refreshRateOptions.findIndex(o => o.value === this.refreshRate)
if (curIdx <= 0 && offset < 0) return
if (curIdx >= (this.refreshRateOptions.length - 1) && offset > 0) return
this.refreshRate = this.refreshRateOptions[curIdx + offset].value
},
mustHideFolder: function (folder) {
return this.filterSelected
&& !(this.current.folder.id == folder.id || this.current.feed.folder_id == folder.id)
&& !this.filteredFolderStats[folder.id]
&& (!this.itemSelectedDetails || (this.feedsById[this.itemSelectedDetails.feed_id] || {}).folder_id != folder.id)
},
mustHideFeed: function (feed) {
return this.filterSelected
&& !(this.current.feed.id == feed.id)
&& !this.filteredFeedStats[feed.id]
&& (!this.itemSelectedDetails || this.itemSelectedDetails.feed_id != feed.id)
},
}
}) })
vm.$mount('#app') vm.$mount('#app')

View File

@@ -1,4 +1,79 @@
function scrollto(target, scroll) {
var padding = 10
var targetRect = target.getBoundingClientRect()
var scrollRect = scroll.getBoundingClientRect()
// target
var relativeOffset = targetRect.y - scrollRect.y
var absoluteOffset = relativeOffset + scroll.scrollTop
if (padding <= relativeOffset && relativeOffset + targetRect.height <= scrollRect.height - padding) return
var newPos = scroll.scrollTop
if (relativeOffset < padding) {
newPos = absoluteOffset - padding
} else {
newPos = absoluteOffset - scrollRect.height + targetRect.height + padding
}
scroll.scrollTop = Math.round(newPos)
}
var helperFunctions = { var helperFunctions = {
// navigation helper, navigate relative to selected item
navigateToItem: function(relativePosition) {
if (vm.itemSelected == null) {
// if no item is selected, select first
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
return
}
var itemPosition = vm.items.findIndex(function(x) { return x.id === vm.itemSelected })
if (itemPosition === -1) {
if (vm.items.length !== 0) vm.itemSelected = vm.items[0].id
return
}
var newPosition = itemPosition + relativePosition
if (newPosition < 0 || newPosition >= vm.items.length) return
vm.itemSelected = vm.items[newPosition].id
vm.$nextTick(function() {
var scroll = document.querySelector('#item-list-scroll')
var handle = scroll.querySelector('input[type=radio]:checked')
var target = handle && handle.parentElement
if (target && scroll) scrollto(target, scroll)
})
},
// navigation helper, navigate relative to selected feed
navigateToFeed: function(relativePosition) {
var navigationList = Array.from(document.querySelectorAll('#col-feed-list input[name=feed]'))
.filter(function(r) { return r.offsetParent !== null && r.value !== 'folder:null' })
.map(function(r) { return r.value })
var currentFeedPosition = navigationList.indexOf(vm.feedSelected)
if (currentFeedPosition == -1) {
vm.feedSelected = ''
return
}
var newPosition = currentFeedPosition+relativePosition
if (newPosition < 0 || newPosition >= navigationList.length) return
vm.feedSelected = navigationList[newPosition]
vm.$nextTick(function() {
var scroll = document.querySelector('#feed-list-scroll')
var handle = scroll.querySelector('input[type=radio]:checked')
var target = handle && handle.parentElement
if (target && scroll) scrollto(target, scroll)
})
},
scrollContent: function(direction) { scrollContent: function(direction) {
var padding = 40 var padding = 40
var scroll = document.querySelector('.content') var scroll = document.querySelector('.content')
@@ -17,7 +92,7 @@ var helperFunctions = {
var shortcutFunctions = { var shortcutFunctions = {
openItemLink: function() { openItemLink: function() {
if (vm.itemSelectedDetails && vm.itemSelectedDetails.link) { if (vm.itemSelectedDetails && vm.itemSelectedDetails.link) {
window.open(vm.itemSelectedDetails.link, '_blank', 'noopener,noreferrer') window.open(vm.itemSelectedDetails.link, '_blank')
} }
}, },
toggleReadability: function() { toggleReadability: function() {
@@ -43,16 +118,16 @@ var shortcutFunctions = {
document.getElementById("searchbar").focus() document.getElementById("searchbar").focus()
}, },
nextItem(){ nextItem(){
vm.navigateToItem(+1) helperFunctions.navigateToItem(+1)
}, },
previousItem() { previousItem() {
vm.navigateToItem(-1) helperFunctions.navigateToItem(-1)
}, },
nextFeed(){ nextFeed(){
vm.navigateToFeed(+1) helperFunctions.navigateToFeed(+1)
}, },
previousFeed() { previousFeed() {
vm.navigateToFeed(-1) helperFunctions.navigateToFeed(-1)
}, },
scrollForward: function() { scrollForward: function() {
helperFunctions.scrollContent(+1) helperFunctions.scrollContent(+1)
@@ -60,9 +135,6 @@ var shortcutFunctions = {
scrollBackward: function() { scrollBackward: function() {
helperFunctions.scrollContent(-1) helperFunctions.scrollContent(-1)
}, },
closeItem: function () {
vm.itemSelected = null
},
showAll() { showAll() {
vm.filterSelected = '' vm.filterSelected = ''
}, },
@@ -88,7 +160,6 @@ var keybindings = {
"h": shortcutFunctions.previousFeed, "h": shortcutFunctions.previousFeed,
"f": shortcutFunctions.scrollForward, "f": shortcutFunctions.scrollForward,
"b": shortcutFunctions.scrollBackward, "b": shortcutFunctions.scrollBackward,
"q": shortcutFunctions.closeItem,
"1": shortcutFunctions.showUnread, "1": shortcutFunctions.showUnread,
"2": shortcutFunctions.showStarred, "2": shortcutFunctions.showStarred,
"3": shortcutFunctions.showAll, "3": shortcutFunctions.showAll,
@@ -107,7 +178,6 @@ var codebindings = {
"KeyH": shortcutFunctions.previousFeed, "KeyH": shortcutFunctions.previousFeed,
"KeyF": shortcutFunctions.scrollForward, "KeyF": shortcutFunctions.scrollForward,
"KeyB": shortcutFunctions.scrollBackward, "KeyB": shortcutFunctions.scrollBackward,
"KeyQ": shortcutFunctions.closeItem,
"Digit1": shortcutFunctions.showUnread, "Digit1": shortcutFunctions.showUnread,
"Digit2": shortcutFunctions.showStarred, "Digit2": shortcutFunctions.showStarred,
"Digit3": shortcutFunctions.showAll, "Digit3": shortcutFunctions.showAll,

View File

@@ -22,7 +22,7 @@
} }
</style> </style>
</head> </head>
<body class="theme-{% .settings.theme_name %}"> <body>
<form action="" method="post"> <form action="" method="post">
<img src="./static/graphicarts/anchor.svg" alt=""> <img src="./static/graphicarts/anchor.svg" alt="">
{% if .error %} {% if .error %}

View File

@@ -100,10 +100,6 @@ select.form-control:not([multiple]):not([size]) {
padding-right: 0; padding-right: 0;
} }
.scroll-touch {
-webkit-overflow-scrolling: touch;
}
/* custom elements */ /* custom elements */
.font-serif { .font-serif {
@@ -529,6 +525,46 @@ a,
margin: 0 !important; margin: 0 !important;
} }
/* Beautify the scroll bar. */
:root {
--custom-thumb-color: #6c757d;
--custom-track-color: rgba(0, 0, 0, 0);
--custom-width: thin;
--custom-thumb-color-hover: #ef4c4c;
--custom-track-color-hover: rgba(0, 0, 0, 0);
--webkit-scrollbar-width-height: 7px;
--webkit-scrollbar-border-radius: 6px;
--workaround-gh-scrollbars: 0;
}
*:not(select) {
scrollbar-color: var(--custom-thumb-color) var(--custom-track-color) !important;
scrollbar-width: var(--custom-width) !important;
}
/* Chrome and derivatives*/
::-webkit-scrollbar {
max-width: var(--webkit-scrollbar-width-height) !important;
max-height: var(--webkit-scrollbar-width-height) !important;
background: var(--custom-track-color) !important;
}
::-webkit-scrollbar-corner,
::-webkit-scrollbar-track,
::-webkit-scrollbar-track-piece {
background: var(--custom-track-color) !important;
}
::-webkit-scrollbar-thumb {
background: var(--custom-thumb-color) !important;
border-radius: var(--webkit-scrollbar-border-radius) !important;
}
::-webkit-scrollbar-corner:hover,
::-webkit-scrollbar-track:hover,
::-webkit-scrollbar-track-piece:hover {
background: var(--custom-track-color-hover) !important;
}
::-webkit-scrollbar-thumb:hover {
background: var(--custom-thumb-color-hover) !important;
}
/* responsive layout /* responsive layout
tablet: tablet:

View File

@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"regexp" "regexp"
"strings" "strings"
"unicode"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
@@ -62,16 +61,3 @@ func ExtractText(content string) string {
text = whitespaceRegex.ReplaceAllLiteralString(text, " ") text = whitespaceRegex.ReplaceAllLiteralString(text, " ")
return text return text
} }
func TruncateText(input string, size int) string {
runes := []rune(input)
if len(runes) <= size {
return input
}
for i := size - 1; i > 0; i-- {
if unicode.IsSpace(runes[i]) {
return string(runes[:i]) + " ..."
}
}
return input
}

View File

@@ -24,21 +24,3 @@ func TestExtractText(t *testing.T) {
} }
} }
} }
func TestTruncateText(t *testing.T) {
input := "Lorem ipsum — классический текст-«рыба»"
size := 30
want := "Lorem ipsum — классический ..."
have := TruncateText(input, size)
if want != have {
t.Errorf("\nsize: %d\nwant: %#v\nhave: %#v", size, want, have)
}
size = 1000
want = input
have = TruncateText(input, size)
if want != have {
t.Errorf("\nsize: %d\nwant: %#v\nhave: %#v", size, want, have)
}
}

View File

@@ -167,7 +167,7 @@ func getExtraAttributes(tagName string) ([]string, []string) {
case "iframe": case "iframe":
return []string{"sandbox", "loading"}, []string{`sandbox="allow-scripts allow-same-origin allow-popups"`, `loading="lazy"`} return []string{"sandbox", "loading"}, []string{`sandbox="allow-scripts allow-same-origin allow-popups"`, `loading="lazy"`}
case "img": case "img":
return []string{"loading"}, []string{`loading="lazy"`, `referrerpolicy="no-referrer"`} return []string{"loading"}, []string{`loading="lazy"`}
default: default:
return nil, nil return nil, nil
} }

View File

@@ -8,11 +8,10 @@ import "testing"
func TestValidInput(t *testing.T) { func TestValidInput(t *testing.T) {
input := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy">.</p>` input := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy">.</p>`
want := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy" referrerpolicy="no-referrer">.</p>` output := Sanitize("http://example.org/", input)
have := Sanitize("http://example.org/", input)
if have != want { if input != output {
t.Errorf("Wrong output: \nwant: %#v\nhave: %#v", want, have) t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
} }
} }
@@ -28,31 +27,31 @@ func TestImgWithTextDataURL(t *testing.T) {
func TestImgWithDataURL(t *testing.T) { func TestImgWithDataURL(t *testing.T) {
input := `<img src="data:image/gif;base64,test" alt="Example">` input := `<img src="data:image/gif;base64,test" alt="Example">`
want := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy" referrerpolicy="no-referrer">` expected := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy">`
have := Sanitize("http://example.org/", input) output := Sanitize("http://example.org/", input)
if have != want { if output != expected {
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have) t.Errorf(`Wrong output: %s`, output)
} }
} }
func TestImgWithSrcset(t *testing.T) { func TestImgWithSrcset(t *testing.T) {
input := `<img srcset="example-320w.jpg, example-480w.jpg 1.5x, example-640w.jpg 2x, example-640w.jpg 640w" src="example-640w.jpg" alt="Example">` input := `<img srcset="example-320w.jpg, example-480w.jpg 1.5x, example-640w.jpg 2x, example-640w.jpg 640w" src="example-640w.jpg" alt="Example">`
want := `<img srcset="http://example.org/example-320w.jpg, http://example.org/example-480w.jpg 1.5x, http://example.org/example-640w.jpg 2x, http://example.org/example-640w.jpg 640w" src="http://example.org/example-640w.jpg" alt="Example" loading="lazy" referrerpolicy="no-referrer">` expected := `<img srcset="http://example.org/example-320w.jpg, http://example.org/example-480w.jpg 1.5x, http://example.org/example-640w.jpg 2x, http://example.org/example-640w.jpg 640w" src="http://example.org/example-640w.jpg" alt="Example" loading="lazy">`
have := Sanitize("http://example.org/", input) output := Sanitize("http://example.org/", input)
if have != want { if output != expected {
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have) t.Errorf(`Wrong output: %s`, output)
} }
} }
func TestImgWithSrcsetAndDataURL(t *testing.T) { func TestImgWithSrcsetAndDataURL(t *testing.T) {
input := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example">` input := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example">`
want := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example" loading="lazy" referrerpolicy="no-referrer">` expected := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example" loading="lazy">`
have := Sanitize("http://example.org/", input) output := Sanitize("http://example.org/", input)
if have != want { if output != expected {
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have) t.Errorf(`Wrong output: %s`, output)
} }
} }
@@ -68,16 +67,16 @@ func TestSourceWithSrcsetAndMedia(t *testing.T) {
func TestMediumImgWithSrcset(t *testing.T) { func TestMediumImgWithSrcset(t *testing.T) {
input := `<img alt="Image for post" class="t u v ef aj" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" width="2730" height="3407">` input := `<img alt="Image for post" class="t u v ef aj" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" width="2730" height="3407">`
want := `<img alt="Image for post" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" loading="lazy" referrerpolicy="no-referrer">` expected := `<img alt="Image for post" src="https://miro.medium.com/max/5460/1*aJ9JibWDqO81qMfNtqgqrw.jpeg" srcset="https://miro.medium.com/max/552/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 276w, https://miro.medium.com/max/1000/1*aJ9JibWDqO81qMfNtqgqrw.jpeg 500w" sizes="500px" loading="lazy">`
have := Sanitize("http://example.org/", input) output := Sanitize("http://example.org/", input)
if have != want { if output != expected {
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have) t.Errorf(`Wrong output: %s`, output)
} }
} }
func TestSelfClosingTags(t *testing.T) { func TestSelfClosingTags(t *testing.T) {
input := `<p>This <br> is a <strong>text</strong><br/>.</p>` input := `<p>This <br> is a <strong>text</strong> <br/>with an image: <img src="http://example.org/" alt="Test" loading="lazy"/>.</p>`
output := Sanitize("http://example.org/", input) output := Sanitize("http://example.org/", input)
if input != output { if input != output {
@@ -96,11 +95,11 @@ func TestTable(t *testing.T) {
func TestRelativeURL(t *testing.T) { func TestRelativeURL(t *testing.T) {
input := `This <a href="/test.html">link is relative</a> and this image: <img src="../folder/image.png"/>` input := `This <a href="/test.html">link is relative</a> and this image: <img src="../folder/image.png"/>`
want := `This <a href="http://example.org/test.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a> and this image: <img src="http://example.org/folder/image.png" loading="lazy" referrerpolicy="no-referrer"/>` expected := `This <a href="http://example.org/test.html" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer">link is relative</a> and this image: <img src="http://example.org/folder/image.png" loading="lazy"/>`
have := Sanitize("http://example.org/", input) output := Sanitize("http://example.org/", input)
if want != have { if expected != output {
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have) t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
} }
} }
@@ -166,11 +165,11 @@ func TestInvalidNestedTag(t *testing.T) {
func TestValidIFrame(t *testing.T) { func TestValidIFrame(t *testing.T) {
input := `<iframe src="http://example.org/"></iframe>` input := `<iframe src="http://example.org/"></iframe>`
want := `<iframe src="http://example.org/" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>` expected := `<iframe src="http://example.org/" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
have := Sanitize("http://example.org/", input) output := Sanitize("http://example.org/", input)
if want != have { if expected != output {
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have) t.Errorf("Wrong output:\nwant: %s\nhave: %s", expected, output)
} }
} }

View File

@@ -1,7 +1,6 @@
package scraper package scraper
import ( import (
"net/url"
"strings" "strings"
"github.com/nkanaev/yarr/src/content/htmlutil" "github.com/nkanaev/yarr/src/content/htmlutil"
@@ -36,18 +35,6 @@ func FindFeeds(body string, base string) map[string]string {
link := htmlutil.AbsoluteUrl(href, base) link := htmlutil.AbsoluteUrl(href, base)
if link != "" { if link != "" {
candidates[link] = name candidates[link] = name
l, err := url.Parse(link)
if err == nil && l.Host == "www.youtube.com" && l.Path == "/feeds/videos.xml" {
// https://wiki.archiveteam.org/index.php/YouTube/Technical_details#Playlists
channelID, found := strings.CutPrefix(l.Query().Get("channel_id"), "UC")
if found {
const url string = "https://www.youtube.com/feeds/videos.xml?playlist_id="
candidates[url+"UULF"+channelID] = name + " - Videos"
candidates[url+"UULV"+channelID] = name + " - Live Streams"
candidates[url+"UUSH"+channelID] = name + " - Short videos"
}
}
} }
} }

View File

@@ -22,8 +22,6 @@ func VideoIFrame(link string) string {
youtubeID := "" youtubeID := ""
if l.Host == "www.youtube.com" && l.Path == "/watch" { if l.Host == "www.youtube.com" && l.Path == "/watch" {
youtubeID = l.Query().Get("v") youtubeID = l.Query().Get("v")
} else if l.Host == "www.youtube.com" && strings.HasPrefix(l.Path, "/shorts/") {
youtubeID = strings.TrimPrefix(l.Path, "/shorts/")
} else if l.Host == "youtu.be" { } else if l.Host == "youtu.be" {
youtubeID = strings.TrimLeft(l.Path, "/") youtubeID = strings.TrimLeft(l.Path, "/")
} }

View File

@@ -3,6 +3,7 @@ package parser
import ( import (
"encoding/xml" "encoding/xml"
"html"
"io" "io"
"strings" "strings"
@@ -57,7 +58,7 @@ func (a *atomText) String() string {
if a.Type == "xhtml" { if a.Type == "xhtml" {
data = a.XML data = a.XML
} }
return strings.TrimSpace(data) return html.UnescapeString(strings.TrimSpace(data))
} }
func (links atomLinks) First(rel string) string { func (links atomLinks) First(rel string) string {
@@ -89,8 +90,6 @@ func ParseAtom(r io.Reader) (*Feed, error) {
guidFromID = srcitem.ID + "::" + srcitem.Updated guidFromID = srcitem.ID + "::" + srcitem.Updated
} }
mediaLinks := srcitem.mediaLinks()
link := firstNonEmpty(srcitem.OrigLink, srcitem.Links.First("alternate"), srcitem.Links.First(""), linkFromID) link := firstNonEmpty(srcitem.OrigLink, srcitem.Links.First("alternate"), srcitem.Links.First(""), linkFromID)
dstfeed.Items = append(dstfeed.Items, Item{ dstfeed.Items = append(dstfeed.Items, Item{
GUID: firstNonEmpty(guidFromID, srcitem.ID, link), GUID: firstNonEmpty(guidFromID, srcitem.ID, link),
@@ -98,7 +97,8 @@ func ParseAtom(r io.Reader) (*Feed, error) {
URL: link, URL: link,
Title: srcitem.Title.Text(), Title: srcitem.Title.Text(),
Content: firstNonEmpty(srcitem.Content.String(), srcitem.Summary.String(), srcitem.firstMediaDescription()), Content: firstNonEmpty(srcitem.Content.String(), srcitem.Summary.String(), srcitem.firstMediaDescription()),
MediaLinks: mediaLinks, ImageURL: srcitem.firstMediaThumbnail(),
AudioURL: "",
}) })
} }
return dstfeed, nil return dstfeed, nil

View File

@@ -45,6 +45,8 @@ func TestAtom(t *testing.T) {
URL: "http://example.org/2003/12/13/atom03.html", URL: "http://example.org/2003/12/13/atom03.html",
Title: "Atom-Powered Robots Run Amok", Title: "Atom-Powered Robots Run Amok",
Content: `<div xmlns="http://www.w3.org/1999/xhtml"><p>This is the entry content.</p></div>`, Content: `<div xmlns="http://www.w3.org/1999/xhtml"><p>This is the entry content.</p></div>`,
ImageURL: "",
AudioURL: "",
}, },
}, },
} }
@@ -139,15 +141,9 @@ func TestAtomImageLink(t *testing.T) {
</entry> </entry>
</feed> </feed>
`)) `))
if len(feed.Items[0].MediaLinks) != 1 { have := feed.Items[0].ImageURL
t.Fatalf("Expected 1 media link, got: %#v", feed.Items[0].MediaLinks) want := `https://example.com/image.png?width=100&height=100`
} if want != have {
have := feed.Items[0].MediaLinks[0]
want := MediaLink{
URL: `https://example.com/image.png?width=100&height=100`,
Type: "image",
}
if !reflect.DeepEqual(want, have) {
t.Fatalf("item.image_url doesn't match\nwant: %#v\nhave: %#v\n", want, have) t.Fatalf("item.image_url doesn't match\nwant: %#v\nhave: %#v\n", want, have)
} }
} }
@@ -169,8 +165,8 @@ func TestAtomImageLinkDuplicated(t *testing.T) {
if want != have { if want != have {
t.Fatalf("want: %#v\nhave: %#v\n", want, have) t.Fatalf("want: %#v\nhave: %#v\n", want, have)
} }
if len(feed.Items[0].MediaLinks) != 0 { if feed.Items[0].ImageURL != "" {
t.Fatal("item media link must be excluded if present in the content") t.Fatal("item.image_url must be unset if present in the content")
} }
} }
@@ -218,19 +214,3 @@ func TestAtomLinkInID(t *testing.T) {
t.Fatalf("\nwant: %#v\nhave: %#v\n", want, have) t.Fatalf("\nwant: %#v\nhave: %#v\n", want, have)
} }
} }
func TestAtomDoesntEscapeHTMLTags(t *testing.T) {
feed, _ := Parse(strings.NewReader(`
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<entry><summary type="html">&amp;lt;script&amp;gt;alert(1);&amp;lt;/script&amp;gt;</summary></entry>
</feed>
`))
have := feed.Items[0].Content
want := "&lt;script&gt;alert(1);&lt;/script&gt;"
if !reflect.DeepEqual(want, have) {
t.Logf("want: %#v", want)
t.Logf("have: %#v", have)
t.FailNow()
}
}

View File

@@ -2,7 +2,6 @@ package parser
import ( import (
"bytes" "bytes"
"crypto/sha256"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
@@ -120,7 +119,6 @@ func ParseAndFix(r io.Reader, baseURL, fallbackEncoding string) (*Feed, error) {
} }
feed.TranslateURLs(baseURL) feed.TranslateURLs(baseURL)
feed.SetMissingDatesTo(time.Now()) feed.SetMissingDatesTo(time.Now())
feed.SetMissingGUIDs()
return feed, nil return feed, nil
} }
@@ -134,14 +132,11 @@ func (feed *Feed) cleanup() {
feed.Items[i].Title = strings.TrimSpace(htmlutil.ExtractText(item.Title)) feed.Items[i].Title = strings.TrimSpace(htmlutil.ExtractText(item.Title))
feed.Items[i].Content = strings.TrimSpace(item.Content) feed.Items[i].Content = strings.TrimSpace(item.Content)
if len(feed.Items[i].MediaLinks) > 0 { if item.ImageURL != "" && strings.Contains(item.Content, item.ImageURL) {
mediaLinks := make([]MediaLink, 0) feed.Items[i].ImageURL = ""
for _, link := range item.MediaLinks {
if !strings.Contains(item.Content, link.URL) {
mediaLinks = append(mediaLinks, link)
} }
} if item.AudioURL != "" && strings.Contains(item.Content, item.AudioURL) {
feed.Items[i].MediaLinks = mediaLinks feed.Items[i].AudioURL = ""
} }
} }
} }
@@ -173,12 +168,3 @@ func (feed *Feed) TranslateURLs(base string) error {
} }
return nil return nil
} }
func (feed *Feed) SetMissingGUIDs() {
for i, item := range feed.Items {
if item.GUID == "" {
id := strings.Join([]string{item.Title, item.Date.Format(time.RFC3339), item.URL}, ";;")
feed.Items[i].GUID = fmt.Sprintf("%x", sha256.Sum256([]byte(id)))
}
}
}

View File

@@ -150,32 +150,3 @@ func TestParseCleanIllegalCharsInNonUTF8(t *testing.T) {
t.Fatalf("invalid feed, got: %v", feed) t.Fatalf("invalid feed, got: %v", feed)
} }
} }
func TestParseMissingGUID(t *testing.T) {
data := `
<?xml version="1.0" encoding="windows-1251"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<item>
<title>foo</title>
</item>
<item>
<title>bar</title>
</item>
</channel>
</rss>
`
feed, err := ParseAndFix(strings.NewReader(data), "", "")
if err != nil {
t.Fatal(err)
}
if len(feed.Items) != 2 {
t.Fatalf("expected 2 items, got %d", len(feed.Items))
}
if feed.Items[0].GUID == "" || feed.Items[1].GUID == "" {
t.Fatalf("item GUIDs are missing, got %#v", feed.Items)
}
if feed.Items[0].GUID == feed.Items[1].GUID {
t.Fatalf("item GUIDs are not unique, got %#v", feed.Items)
}
}

View File

@@ -1,9 +1,5 @@
package parser package parser
import (
"strings"
)
type media struct { type media struct {
MediaGroups []mediaGroup `xml:"http://search.yahoo.com/mrss/ group"` MediaGroups []mediaGroup `xml:"http://search.yahoo.com/mrss/ group"`
MediaContents []mediaContent `xml:"http://search.yahoo.com/mrss/ content"` MediaContents []mediaContent `xml:"http://search.yahoo.com/mrss/ content"`
@@ -12,17 +8,12 @@ type media struct {
} }
type mediaGroup struct { type mediaGroup struct {
MediaContent []mediaContent `xml:"http://search.yahoo.com/mrss/ content"`
MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"` MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"`
MediaDescriptions []mediaDescription `xml:"http://search.yahoo.com/mrss/ description"` MediaDescriptions []mediaDescription `xml:"http://search.yahoo.com/mrss/ description"`
} }
type mediaContent struct { type mediaContent struct {
MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"` MediaThumbnails []mediaThumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"`
MediaType string `xml:"type,attr"`
MediaMedium string `xml:"medium,attr"`
MediaURL string `xml:"url,attr"`
MediaDescription mediaDescription `xml:"http://search.yahoo.com/mrss/ description"`
} }
type mediaThumbnail struct { type mediaThumbnail struct {
@@ -31,7 +22,7 @@ type mediaThumbnail struct {
type mediaDescription struct { type mediaDescription struct {
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
Text string `xml:",chardata"` Description string `xml:",chardata"`
} }
func (m *media) firstMediaThumbnail() string { func (m *media) firstMediaThumbnail() string {
@@ -53,59 +44,12 @@ func (m *media) firstMediaThumbnail() string {
func (m *media) firstMediaDescription() string { func (m *media) firstMediaDescription() string {
for _, d := range m.MediaDescriptions { for _, d := range m.MediaDescriptions {
return plain2html(d.Text) return plain2html(d.Description)
} }
for _, g := range m.MediaGroups { for _, g := range m.MediaGroups {
for _, d := range g.MediaDescriptions { for _, d := range g.MediaDescriptions {
return plain2html(d.Text) return plain2html(d.Description)
} }
} }
return "" return ""
} }
func (m *media) mediaLinks() []MediaLink {
links := make([]MediaLink, 0)
for _, thumbnail := range m.MediaThumbnails {
links = append(links, MediaLink{URL: thumbnail.URL, Type: "image"})
}
for _, group := range m.MediaGroups {
for _, thumbnail := range group.MediaThumbnails {
links = append(links, MediaLink{
URL: thumbnail.URL,
Type: "image",
})
}
}
for _, content := range m.MediaContents {
if content.MediaURL != "" {
url := content.MediaURL
description := content.MediaDescription.Text
if strings.HasPrefix(content.MediaType, "image/") {
links = append(links, MediaLink{URL: url, Type: "image", Description: description})
} else if strings.HasPrefix(content.MediaType, "audio/") {
links = append(links, MediaLink{URL: url, Type: "audio", Description: description})
} else if strings.HasPrefix(content.MediaType, "video/") {
links = append(links, MediaLink{URL: url, Type: "video", Description: description})
} else if content.MediaMedium == "image" || content.MediaMedium == "audio" || content.MediaMedium == "video" {
links = append(links, MediaLink{URL: url, Type: content.MediaMedium, Description: description})
} else {
if len(content.MediaThumbnails) > 0 {
links = append(links, MediaLink{
URL: content.MediaThumbnails[0].URL,
Type: "image",
})
}
}
}
for _, thumbnail := range content.MediaThumbnails {
links = append(links, MediaLink{
URL: thumbnail.URL,
Type: "image",
})
}
}
if len(links) == 0 {
return nil
}
return links
}

View File

@@ -15,11 +15,6 @@ type Item struct {
Title string Title string
Content string Content string
MediaLinks []MediaLink ImageURL string
} AudioURL string
type MediaLink struct {
URL string
Type string
Description string
} }

View File

@@ -74,14 +74,14 @@ func ParseRSS(r io.Reader) (*Feed, error) {
SiteURL: srcfeed.Link, SiteURL: srcfeed.Link,
} }
for _, srcitem := range srcfeed.Items { for _, srcitem := range srcfeed.Items {
mediaLinks := srcitem.mediaLinks() podcastURL := ""
for _, e := range srcitem.Enclosures { for _, e := range srcitem.Enclosures {
if strings.HasPrefix(e.Type, "audio/") { if strings.HasPrefix(e.Type, "audio/") {
podcastURL := e.URL podcastURL = e.URL
if srcitem.OrigEnclosureLink != "" && strings.Contains(podcastURL, path.Base(srcitem.OrigEnclosureLink)) { if srcitem.OrigEnclosureLink != "" && strings.Contains(podcastURL, path.Base(srcitem.OrigEnclosureLink)) {
podcastURL = srcitem.OrigEnclosureLink podcastURL = srcitem.OrigEnclosureLink
} }
mediaLinks = append(mediaLinks, MediaLink{URL: podcastURL, Type: "audio"})
break break
} }
} }
@@ -96,8 +96,9 @@ func ParseRSS(r io.Reader) (*Feed, error) {
Date: dateParse(firstNonEmpty(srcitem.DublinCoreDate, srcitem.PubDate)), Date: dateParse(firstNonEmpty(srcitem.DublinCoreDate, srcitem.PubDate)),
URL: firstNonEmpty(srcitem.OrigLink, srcitem.Link, permalink), URL: firstNonEmpty(srcitem.OrigLink, srcitem.Link, permalink),
Title: srcitem.Title, Title: srcitem.Title,
Content: firstNonEmpty(srcitem.ContentEncoded, srcitem.Description, srcitem.firstMediaDescription()), Content: firstNonEmpty(srcitem.ContentEncoded, srcitem.Description),
MediaLinks: mediaLinks, AudioURL: podcastURL,
ImageURL: srcitem.firstMediaThumbnail(),
}) })
} }
return dstfeed, nil return dstfeed, nil

View File

@@ -75,15 +75,9 @@ func TestRSSMediaContentThumbnail(t *testing.T) {
</channel> </channel>
</rss> </rss>
`)) `))
if len(feed.Items[0].MediaLinks) != 1 { have := feed.Items[0].ImageURL
t.Fatalf("Expected 1 media link, got %#v", feed.Items[0].MediaLinks) want := "https://i.vimeocdn.com/video/1092705247_960.jpg"
} if have != want {
have := feed.Items[0].MediaLinks[0]
want := MediaLink{
URL: "https://i.vimeocdn.com/video/1092705247_960.jpg",
Type: "image",
}
if !reflect.DeepEqual(want, have) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
t.Logf("have: %#v", have) t.Logf("have: %#v", have)
t.FailNow() t.FailNow()
@@ -133,15 +127,9 @@ func TestRSSPodcast(t *testing.T) {
</channel> </channel>
</rss> </rss>
`)) `))
if len(feed.Items[0].MediaLinks) != 1 { have := feed.Items[0].AudioURL
t.Fatal("Invalid media links") want := "http://example.com/audio.ext"
} if want != have {
have := feed.Items[0].MediaLinks[0]
want := MediaLink{
URL: "http://example.com/audio.ext",
Type: "audio",
}
if !reflect.DeepEqual(want, have) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
t.Logf("have: %#v", have) t.Logf("have: %#v", have)
t.FailNow() t.FailNow()
@@ -159,15 +147,9 @@ func TestRSSOpusPodcast(t *testing.T) {
</channel> </channel>
</rss> </rss>
`)) `))
if len(feed.Items[0].MediaLinks) != 1 { have := feed.Items[0].AudioURL
t.Fatal("Invalid media links") want := "http://example.com/audio.ext"
} if want != have {
have := feed.Items[0].MediaLinks[0]
want := MediaLink{
URL: "http://example.com/audio.ext",
Type: "audio",
}
if !reflect.DeepEqual(want, have) {
t.Logf("want: %#v", want) t.Logf("want: %#v", want)
t.Logf("have: %#v", have) t.Logf("have: %#v", have)
t.FailNow() t.FailNow()
@@ -194,9 +176,8 @@ func TestRSSPodcastDuplicated(t *testing.T) {
if want != have { if want != have {
t.Fatalf("content doesn't match\nwant: %#v\nhave: %#v\n", want, have) t.Fatalf("content doesn't match\nwant: %#v\nhave: %#v\n", want, have)
} }
if feed.Items[0].AudioURL != "" {
if len(feed.Items[0].MediaLinks) != 0 { t.Fatal("item.audio_url must be unset if present in the content")
t.Fatal("item media must be excluded if present in the content")
} }
} }
@@ -242,47 +223,8 @@ func TestRSSIsPermalink(t *testing.T) {
}, },
} }
for i := 0; i < len(want); i++ { for i := 0; i < len(want); i++ {
if !reflect.DeepEqual(want, have) { if want[i] != have[i] {
t.Errorf("Failed to handle isPermalink\nwant: %#v\nhave: %#v\n", want[i], have[i]) t.Errorf("Failed to handle isPermalink\nwant: %#v\nhave: %#v\n", want[i], have[i])
} }
} }
} }
func TestRSSMultipleMedia(t *testing.T) {
feed, _ := Parse(strings.NewReader(`
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<item>
<guid isPermaLink="true">http://example.com/posts/1</guid>
<media:content url="https://example.com/path/to/image1.png" type="image/png" fileSize="1000" medium="image">
<media:description type="plain">description 1</media:description>
</media:content>
<media:content url="https://example.com/path/to/image2.png" type="image/png" fileSize="2000" medium="image">
<media:description type="plain">description 2</media:description>
</media:content>
<media:content url="https://example.com/path/to/video1.mp4" type="video/mp4" fileSize="2000" medium="image">
<media:description type="plain">video description</media:description>
</media:content>
</item>
</channel>
</rss>
`))
have := feed.Items
want := []Item{
{
GUID: "http://example.com/posts/1",
URL: "http://example.com/posts/1",
MediaLinks: []MediaLink{
{URL: "https://example.com/path/to/image1.png", Type: "image", Description: "description 1"},
{URL: "https://example.com/path/to/image2.png", Type: "image", Description: "description 2"},
{URL: "https://example.com/path/to/video1.mp4", Type: "video", Description: "video description"},
},
},
}
if !reflect.DeepEqual(want, have) {
t.Logf("want: %#v", want)
t.Logf("have: %#v", have)
t.Fatal("invalid rss")
}
}

View File

@@ -1,4 +1,5 @@
//go:build !windows //go:build !windows
// +build !windows
package platform package platform

View File

@@ -1,5 +1,3 @@
//go:build windows
package platform package platform
import ( import (

View File

@@ -1,4 +1,5 @@
//go:build (darwin || windows) && gui //go:build macos || windows
// +build macos windows
package platform package platform
@@ -10,7 +11,6 @@ import (
func Start(s *server.Server) { func Start(s *server.Server) {
systrayOnReady := func() { systrayOnReady := func() {
systray.SetIcon(Icon) systray.SetIcon(Icon)
systray.SetTooltip("yarr")
menuOpen := systray.AddMenuItem("Open", "") menuOpen := systray.AddMenuItem("Open", "")
systray.AddSeparator() systray.AddSeparator()

View File

@@ -1,4 +1,5 @@
//go:build !gui //go:build !windows && !macos
// +build !windows,!macos
package platform package platform

View File

@@ -1,4 +1,5 @@
//go:build darwin && gui //go:build macos
// +build macos
package platform package platform

View File

@@ -1,4 +1,5 @@
//go:build windows && gui //go:build windows
// +build windows
package platform package platform

View File

@@ -1,4 +1,5 @@
//go:build linux || freebsd || openbsd //go:build !windows && !darwin
// +build !windows,!darwin
package platform package platform

View File

@@ -1,4 +1,5 @@
//go:build darwin //go:build darwin
// +build darwin
package platform package platform

View File

@@ -1,4 +1,5 @@
//go:build windows //go:build windows
// +build windows
package platform package platform

View File

@@ -7,6 +7,7 @@ import (
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"strings" "strings"
"time"
) )
func IsAuthenticated(req *http.Request, username, password string) bool { func IsAuthenticated(req *http.Request, username, password string) bool {
@@ -25,10 +26,8 @@ func Authenticate(rw http.ResponseWriter, username, password, basepath string) {
http.SetCookie(rw, &http.Cookie{ http.SetCookie(rw, &http.Cookie{
Name: "auth", Name: "auth",
Value: username + ":" + secret(username, password), Value: username + ":" + secret(username, password),
MaxAge: 604800, // 1 week Expires: time.Now().Add(time.Hour * 24 * 7), // 1 week,
Path: basepath, Path: basepath,
Secure: true,
SameSite: http.SameSiteLaxMode,
}) })
} }

View File

@@ -6,7 +6,6 @@ import (
"github.com/nkanaev/yarr/src/assets" "github.com/nkanaev/yarr/src/assets"
"github.com/nkanaev/yarr/src/server/router" "github.com/nkanaev/yarr/src/server/router"
"github.com/nkanaev/yarr/src/storage"
) )
type Middleware struct { type Middleware struct {
@@ -14,7 +13,6 @@ type Middleware struct {
Password string Password string
BasePath string BasePath string
Public []string Public []string
DB *storage.Storage
} }
func unsafeMethod(method string) bool { func unsafeMethod(method string) bool {
@@ -48,15 +46,12 @@ func (m *Middleware) Handler(c *router.Context) {
c.Redirect(rootUrl) c.Redirect(rootUrl)
return return
} else { } else {
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]interface{}{ c.HTML(http.StatusOK, assets.Template("login.html"), map[string]string{
"username": username, "username": username,
"error": "Invalid username/password", "error": "Invalid username/password",
"settings": m.DB.GetSettings(),
}) })
return return
} }
} }
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]interface{}{ c.HTML(http.StatusOK, assets.Template("login.html"), nil)
"settings": m.DB.GetSettings(),
})
} }

View File

@@ -34,8 +34,7 @@ func (s *Server) handler() http.Handler {
BasePath: s.BasePath, BasePath: s.BasePath,
Username: s.Username, Username: s.Username,
Password: s.Password, Password: s.Password,
Public: []string{"/static", "/fever", "/manifest.json"}, Public: []string{"/static", "/fever"},
DB: s.db,
} }
r.Use(a.Handler) r.Use(a.Handler)
} }
@@ -87,7 +86,7 @@ func (s *Server) handleManifest(c *router.Context) {
"short_name": "yarr", "short_name": "yarr",
"description": "yet another rss reader", "description": "yet another rss reader",
"display": "standalone", "display": "standalone",
"start_url": "/" + strings.TrimPrefix(s.BasePath, "/"), "start_url": s.BasePath,
"icons": []map[string]interface{}{ "icons": []map[string]interface{}{
{ {
"src": s.BasePath + "/static/graphicarts/favicon.png", "src": s.BasePath + "/static/graphicarts/favicon.png",
@@ -294,11 +293,6 @@ func (s *Server) handleFeed(c *router.Context) {
s.db.UpdateFeedFolder(id, &folderId) s.db.UpdateFeedFolder(id, &folderId)
} }
} }
if link, ok := body["feed_link"]; ok {
if reflect.TypeOf(link).Kind() == reflect.String {
s.db.UpdateFeedLink(id, link.(string))
}
}
c.Out.WriteHeader(http.StatusOK) c.Out.WriteHeader(http.StatusOK)
} else if c.Req.Method == "DELETE" { } else if c.Req.Method == "DELETE" {
s.db.DeleteFeed(id) s.db.DeleteFeed(id)
@@ -329,9 +323,6 @@ func (s *Server) handleItem(c *router.Context) {
} }
item.Content = sanitizer.Sanitize(item.Link, item.Content) item.Content = sanitizer.Sanitize(item.Link, item.Content)
for i, link := range item.MediaLinks {
item.MediaLinks[i].Description = sanitizer.Sanitize(item.Link, link.Description)
}
c.JSON(http.StatusOK, item) c.JSON(http.StatusOK, item)
} else if c.Req.Method == "PUT" { } else if c.Req.Method == "PUT" {
@@ -374,19 +365,12 @@ func (s *Server) handleItemList(c *router.Context) {
} }
newestFirst := query.Get("oldest_first") != "true" newestFirst := query.Get("oldest_first") != "true"
items := s.db.ListItems(filter, perPage+1, newestFirst, true) items := s.db.ListItems(filter, perPage+1, newestFirst, false)
hasMore := false hasMore := false
if len(items) == perPage+1 { if len(items) == perPage+1 {
hasMore = true hasMore = true
items = items[:perPage] items = items[:perPage]
} }
for i, item := range items {
if item.Title == "" {
text := htmlutil.ExtractText(item.Content)
items[i].Title = htmlutil.TruncateText(text, 140)
}
}
c.JSON(http.StatusOK, map[string]interface{}{ c.JSON(http.StatusOK, map[string]interface{}{
"list": items, "list": items,
"has_more": hasMore, "has_more": hasMore,
@@ -513,10 +497,6 @@ func (s *Server) handlePageCrawl(c *router.Context) {
}) })
return return
} }
if isInternalFromURL(url) {
log.Printf("attempt to access internal IP %s from %s", url, c.Req.RemoteAddr)
return
}
body, err := worker.GetBody(url) body, err := worker.GetBody(url)
if err != nil { if err != nil {

View File

@@ -2,10 +2,7 @@ package server
import ( import (
"log" "log"
"net"
"net/http" "net/http"
"os"
"strings"
"sync" "sync"
"github.com/nkanaev/yarr/src/storage" "github.com/nkanaev/yarr/src/storage"
@@ -56,31 +53,14 @@ func (s *Server) Start() {
s.worker.RefreshFeeds() s.worker.RefreshFeeds()
} }
var ln net.Listener httpserver := &http.Server{Addr: s.Addr, Handler: s.handler()}
var err error var err error
if path, isUnix := strings.CutPrefix(s.Addr, "unix:"); isUnix {
err = os.Remove(path)
if err != nil {
log.Print(err)
}
ln, err = net.Listen("unix", path)
} else {
ln, err = net.Listen("tcp", s.Addr)
}
if err != nil {
log.Fatal(err)
}
httpserver := &http.Server{Handler: s.handler()}
if s.CertFile != "" && s.KeyFile != "" { if s.CertFile != "" && s.KeyFile != "" {
err = httpserver.ServeTLS(ln, s.CertFile, s.KeyFile) err = httpserver.ListenAndServeTLS(s.CertFile, s.KeyFile)
ln.Close()
} else { } else {
err = httpserver.Serve(ln) err = httpserver.ListenAndServe()
} }
if err != http.ErrServerClosed { if err != http.ErrServerClosed {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -1,35 +0,0 @@
package server
import (
"net"
"net/url"
"strings"
)
func isInternalFromURL(urlStr string) bool {
parsedURL, err := url.Parse(urlStr)
if err != nil {
return false
}
host := parsedURL.Host
// Handle "host:port" format
if strings.Contains(host, ":") {
host, _, err = net.SplitHostPort(host)
if err != nil {
return false
}
}
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()
}

View File

@@ -1,31 +0,0 @@
package server
import "testing"
func TestIsInternalFromURL(t *testing.T) {
tests := []struct {
url string
expected bool
}{
{"http://192.168.1.1:8080", true},
{"http://10.0.0.5", true},
{"http://172.16.0.1", true},
{"http://172.31.255.255", true},
{"http://172.32.0.1", false}, // outside private range
{"http://127.0.0.1", true},
{"http://127.0.0.1:7000", true},
{"http://127.0.0.1:7000/secret", true},
{"http://169.254.0.5", true},
{"http://localhost", true}, // resolves to 127.0.0.1
{"http://8.8.8.8", false},
{"http://google.com", false}, // resolves to public IPs
{"invalid-url", false}, // invalid format
{"", false}, // empty string
}
for _, test := range tests {
result := isInternalFromURL(test.url)
if result != test.expected {
t.Errorf("isInternalFromURL(%q) = %v; want %v", test.url, result, test.expected)
}
}
}

View File

@@ -71,11 +71,6 @@ func (s *Storage) UpdateFeedFolder(feedId int64, newFolderId *int64) bool {
return err == nil return err == nil
} }
func (s *Storage) UpdateFeedLink(feedId int64, newLink string) bool {
_, err := s.db.Exec(`update feeds set feed_link = ? where id = ?`, newLink, feedId)
return err == nil
}
func (s *Storage) UpdateFeedIcon(feedId int64, icon *[]byte) bool { func (s *Storage) UpdateFeedIcon(feedId int64, icon *[]byte) bool {
_, err := s.db.Exec(`update feeds set icon = ? where id = ?`, icon, feedId) _, err := s.db.Exec(`update feeds set icon = ? where id = ?`, icon, feedId)
return err == nil return err == nil

View File

@@ -1,7 +1,6 @@
package storage package storage
import ( import (
"database/sql/driver"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
@@ -45,29 +44,6 @@ func (s *ItemStatus) UnmarshalJSON(b []byte) error {
return nil return nil
} }
type MediaLink struct {
URL string `json:"url"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
}
type MediaLinks []MediaLink
func (m *MediaLinks) Scan(src any) error {
switch data := src.(type) {
case []byte:
return json.Unmarshal(data, m)
case string:
return json.Unmarshal([]byte(data), m)
default:
return nil
}
}
func (m MediaLinks) Value() (driver.Value, error) {
return json.Marshal(m)
}
type Item struct { type Item struct {
Id int64 `json:"id"` Id int64 `json:"id"`
GUID string `json:"guid"` GUID string `json:"guid"`
@@ -77,7 +53,8 @@ type Item struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Date time.Time `json:"date"` Date time.Time `json:"date"`
Status ItemStatus `json:"status"` Status ItemStatus `json:"status"`
MediaLinks MediaLinks `json:"media_links"` ImageURL *string `json:"image"`
AudioURL *string `json:"podcast_url"`
} }
type ItemFilter struct { type ItemFilter struct {
@@ -117,6 +94,7 @@ func (list ItemList) Swap(i, j int) {
list[i], list[j] = list[j], list[i] list[i], list[j] = list[j], list[i]
} }
func (s *Storage) CreateItems(items []Item) bool { func (s *Storage) CreateItems(items []Item) bool {
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { if err != nil {
@@ -133,17 +111,13 @@ func (s *Storage) CreateItems(items []Item) bool {
_, err = tx.Exec(` _, err = tx.Exec(`
insert into items ( insert into items (
guid, feed_id, title, link, date, guid, feed_id, title, link, date,
content, media_links, content, image, podcast_url,
date_arrived, status date_arrived, status
) )
values ( values (?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', ?), ?, ?, ?, ?, ?)
?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', ?),
?, ?,
?, ?
)
on conflict (feed_id, guid) do nothing`, on conflict (feed_id, guid) do nothing`,
item.GUID, item.FeedId, item.Title, item.Link, item.Date, item.GUID, item.FeedId, item.Title, item.Link, item.Date,
item.Content, item.MediaLinks, item.Content, item.ImageURL, item.AudioURL,
now, UNREAD, now, UNREAD,
) )
if err != nil { if err != nil {
@@ -258,7 +232,7 @@ func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool, with
order = "i.id desc" order = "i.id desc"
} }
selectCols := "i.id, i.guid, i.feed_id, i.title, i.link, i.date, i.status, i.media_links" selectCols := "i.id, i.guid, i.feed_id, i.title, i.link, i.date, i.status, i.image, i.podcast_url"
if withContent { if withContent {
selectCols += ", i.content" selectCols += ", i.content"
} else { } else {
@@ -281,7 +255,7 @@ func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool, with
err = rows.Scan( err = rows.Scan(
&x.Id, &x.GUID, &x.FeedId, &x.Id, &x.GUID, &x.FeedId,
&x.Title, &x.Link, &x.Date, &x.Title, &x.Link, &x.Date,
&x.Status, &x.MediaLinks, &x.Content, &x.Status, &x.ImageURL, &x.AudioURL, &x.Content,
) )
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@@ -297,12 +271,12 @@ func (s *Storage) GetItem(id int64) *Item {
err := s.db.QueryRow(` err := s.db.QueryRow(`
select select
i.id, i.guid, i.feed_id, i.title, i.link, i.content, i.id, i.guid, i.feed_id, i.title, i.link, i.content,
i.date, i.status, i.media_links i.date, i.status, i.image, i.podcast_url
from items i from items i
where i.id = ? where i.id = ?
`, id).Scan( `, id).Scan(
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content, &i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
&i.Date, &i.Status, &i.MediaLinks, &i.Date, &i.Status, &i.ImageURL, &i.AudioURL,
) )
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@@ -423,6 +397,7 @@ func (s *Storage) DeleteOldItems() {
where status != ? where status != ?
group by i.feed_id group by i.feed_id
`, itemsKeepSize, STARRED) `, itemsKeepSize, STARRED)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return return

View File

@@ -77,12 +77,12 @@ func getItem(db *Storage, guid string) *Item {
err := db.db.QueryRow(` err := db.db.QueryRow(`
select select
i.id, i.guid, i.feed_id, i.title, i.link, i.content, i.id, i.guid, i.feed_id, i.title, i.link, i.content,
i.date, i.status, i.media_links i.date, i.status, i.image, i.podcast_url
from items i from items i
where i.guid = ? where i.guid = ?
`, guid).Scan( `, guid).Scan(
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content, &i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
&i.Date, &i.Status, &i.MediaLinks, &i.Date, &i.Status, &i.ImageURL, &i.AudioURL,
) )
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@@ -16,17 +16,13 @@ var migrations = []func(*sql.Tx) error{
m06_fill_missing_dates, m06_fill_missing_dates,
m07_add_feed_size, m07_add_feed_size,
m08_normalize_datetime, m08_normalize_datetime,
m09_change_item_index,
m10_add_item_medialinks,
} }
var maxVersion = int64(len(migrations)) var maxVersion = int64(len(migrations))
func migrate(db *sql.DB) error { func migrate(db *sql.DB) error {
var version int64 var version int64
if err := db.QueryRow("pragma user_version").Scan(&version); err != nil { db.QueryRow("pragma user_version").Scan(&version)
return err
}
if version >= maxVersion { if version >= maxVersion {
return nil return nil
@@ -298,34 +294,3 @@ func m08_normalize_datetime(tx *sql.Tx) error {
_, err = tx.Exec(`update items set date = strftime('%Y-%m-%d %H:%M:%f', date);`) _, err = tx.Exec(`update items set date = strftime('%Y-%m-%d %H:%M:%f', date);`)
return err return err
} }
func m09_change_item_index(tx *sql.Tx) error {
sql := `
drop index if exists idx_item_status;
create index if not exists idx_item__date_id_status on items(date,id,status);
`
_, err := tx.Exec(sql)
return err
}
func m10_add_item_medialinks(tx *sql.Tx) error {
sql := `
alter table items add column media_links json;
update items set media_links = case
when coalesce(image, '') != '' and coalesce(podcast_url, '') != ''
then json_array(json_object('type', 'image', 'url', image), json_object('type', 'audio', 'url', podcast_url))
when coalesce(image, '') != ''
then json_array(json_object('type', 'image', 'url', image))
when coalesce(podcast_url, '') != ''
then json_array(json_object('type', 'audio', 'url', podcast_url))
else null
end;
alter table items drop column image;
alter table items drop column podcast_url;
`
_, err := tx.Exec(sql)
return err
}

View File

@@ -2,9 +2,6 @@ package storage
import ( import (
"database/sql" "database/sql"
"log"
"strings"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
@@ -13,17 +10,14 @@ type Storage struct {
} }
func New(path string) (*Storage, error) { func New(path string) (*Storage, error) {
if pos := strings.IndexRune(path, '?'); pos == -1 {
params := "_journal=WAL&_sync=NORMAL&_busy_timeout=5000&cache=shared"
log.Printf("opening db with params: %s", params)
path = path + "?" + params
}
db, err := sql.Open("sqlite3", path) db, err := sql.Open("sqlite3", path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO: https://foxcpp.dev/articles/the-right-way-to-use-go-sqlite3
db.SetMaxOpenConns(1)
if err = migrate(db); err != nil { if err = migrate(db); err != nil {
return nil, err return nil, err
} }

View File

@@ -1,4 +1,3 @@
//go:build darwin || windows
// +build darwin windows // +build darwin windows
/* /*

View File

@@ -1,4 +1,3 @@
//go:build never
// +build never // +build never
package systray package systray

View File

@@ -1,4 +1,3 @@
//go:build darwin
// +build darwin // +build darwin
package systray package systray

View File

@@ -1,4 +1,3 @@
//go:build windows
// +build windows // +build windows
package systray package systray

View File

@@ -1,4 +1,3 @@
//go:build windows
// +build windows // +build windows
package systray package systray

View File

@@ -32,10 +32,6 @@ func (c *Client) getConditional(url, lastModified, etag string) (*http.Response,
var client *Client var client *Client
func SetVersion(num string) {
client.userAgent = "Yarr/" + num
}
func init() { func init() {
transport := &http.Transport{ transport := &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,

View File

@@ -143,9 +143,13 @@ func ConvertItems(items []parser.Item, feed storage.Feed) []storage.Item {
result := make([]storage.Item, len(items)) result := make([]storage.Item, len(items))
for i, item := range items { for i, item := range items {
item := item item := item
mediaLinks := make(storage.MediaLinks, 0) var audioURL *string = nil
for _, link := range item.MediaLinks { if item.AudioURL != "" {
mediaLinks = append(mediaLinks, storage.MediaLink(link)) audioURL = &item.AudioURL
}
var imageURL *string = nil
if item.ImageURL != "" {
imageURL = &item.ImageURL
} }
result[i] = storage.Item{ result[i] = storage.Item{
GUID: item.GUID, GUID: item.GUID,
@@ -155,7 +159,8 @@ func ConvertItems(items []parser.Item, feed storage.Feed) []storage.Item {
Content: item.Content, Content: item.Content,
Date: item.Date, Date: item.Date,
Status: storage.UNREAD, Status: storage.UNREAD,
MediaLinks: mediaLinks, ImageURL: imageURL,
AudioURL: audioURL,
} }
} }
return result return result

View File

@@ -1,23 +1,23 @@
go-sqlite3 go-sqlite3
========== ==========
[![Go Reference](https://pkg.go.dev/badge/github.com/mattn/go-sqlite3.svg)](https://pkg.go.dev/github.com/mattn/go-sqlite3) [![GoDoc Reference](https://godoc.org/github.com/mattn/go-sqlite3?status.svg)](http://godoc.org/github.com/mattn/go-sqlite3)
[![GitHub Actions](https://github.com/mattn/go-sqlite3/workflows/Go/badge.svg)](https://github.com/mattn/go-sqlite3/actions?query=workflow%3AGo) [![GitHub Actions](https://github.com/mattn/go-sqlite3/workflows/Go/badge.svg)](https://github.com/mattn/go-sqlite3/actions?query=workflow%3AGo)
[![Financial Contributors on Open Collective](https://opencollective.com/mattn-go-sqlite3/all/badge.svg?label=financial+contributors)](https://opencollective.com/mattn-go-sqlite3) [![Financial Contributors on Open Collective](https://opencollective.com/mattn-go-sqlite3/all/badge.svg?label=financial+contributors)](https://opencollective.com/mattn-go-sqlite3)
[![codecov](https://codecov.io/gh/mattn/go-sqlite3/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-sqlite3) [![codecov](https://codecov.io/gh/mattn/go-sqlite3/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-sqlite3)
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-sqlite3)](https://goreportcard.com/report/github.com/mattn/go-sqlite3) [![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-sqlite3)](https://goreportcard.com/report/github.com/mattn/go-sqlite3)
Latest stable version is v1.14 or later, not v2. Latest stable version is v1.14 or later not v2.
~~**NOTE:** The increase to v2 was an accident. There were no major changes or features.~~ ~~**NOTE:** The increase to v2 was an accident. There were no major changes or features.~~
# Description # Description
A sqlite3 driver that conforms to the built-in database/sql interface. sqlite3 driver conforming to the built-in database/sql interface
Supported Golang version: See [.github/workflows/go.yaml](./.github/workflows/go.yaml). Supported Golang version: See [.github/workflows/go.yaml](./.github/workflows/go.yaml)
This package follows the official [Golang Release Policy](https://golang.org/doc/devel/release.html#policy). [This package follows the official Golang Release Policy.](https://golang.org/doc/devel/release.html#policy)
### Overview ### Overview
@@ -40,7 +40,7 @@ This package follows the official [Golang Release Policy](https://golang.org/doc
- [Alpine](#alpine) - [Alpine](#alpine)
- [Fedora](#fedora) - [Fedora](#fedora)
- [Ubuntu](#ubuntu) - [Ubuntu](#ubuntu)
- [macOS](#mac-osx) - [Mac OSX](#mac-osx)
- [Windows](#windows) - [Windows](#windows)
- [Errors](#errors) - [Errors](#errors)
- [User Authentication](#user-authentication) - [User Authentication](#user-authentication)
@@ -64,7 +64,7 @@ This package follows the official [Golang Release Policy](https://golang.org/doc
# Installation # Installation
This package can be installed with the `go get` command: This package can be installed with the go get command:
go get github.com/mattn/go-sqlite3 go get github.com/mattn/go-sqlite3
@@ -72,28 +72,28 @@ _go-sqlite3_ is *cgo* package.
If you want to build your app using go-sqlite3, you need gcc. If you want to build your app using go-sqlite3, you need gcc.
However, after you have built and installed _go-sqlite3_ with `go install github.com/mattn/go-sqlite3` (which requires gcc), you can build your app without relying on gcc in future. However, after you have built and installed _go-sqlite3_ with `go install github.com/mattn/go-sqlite3` (which requires gcc), you can build your app without relying on gcc in future.
***Important: because this is a `CGO` enabled package, you are required to set the environment variable `CGO_ENABLED=1` and have a `gcc` compiler present within your path.*** ***Important: because this is a `CGO` enabled package you are required to set the environment variable `CGO_ENABLED=1` and have a `gcc` compile present within your path.***
# API Reference # API Reference
API documentation can be found [here](http://godoc.org/github.com/mattn/go-sqlite3). API documentation can be found here: http://godoc.org/github.com/mattn/go-sqlite3
Examples can be found under the [examples](./_example) directory. Examples can be found under the [examples](./_example) directory
# Connection String # Connection String
When creating a new SQLite database or connection to an existing one, with the file name additional options can be given. When creating a new SQLite database or connection to an existing one, with the file name additional options can be given.
This is also known as a DSN (Data Source Name) string. This is also known as a DSN string. (Data Source Name).
Options are append after the filename of the SQLite database. Options are append after the filename of the SQLite database.
The database filename and options are separated by an `?` (Question Mark). The database filename and options are seperated by an `?` (Question Mark).
Options should be URL-encoded (see [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)). Options should be URL-encoded (see [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)).
This also applies when using an in-memory database instead of a file. This also applies when using an in-memory database instead of a file.
Options can be given using the following format: `KEYWORD=VALUE` and multiple options can be combined with the `&` ampersand. Options can be given using the following format: `KEYWORD=VALUE` and multiple options can be combined with the `&` ampersand.
This library supports DSN options of SQLite itself and provides additional options. This library supports dsn options of SQLite itself and provides additional options.
Boolean values can be one of: Boolean values can be one of:
* `0` `no` `false` `off` * `0` `no` `false` `off`
@@ -138,23 +138,24 @@ file:test.db?cache=shared&mode=memory
This package allows additional configuration of features available within SQLite3 to be enabled or disabled by golang build constraints also known as build `tags`. This package allows additional configuration of features available within SQLite3 to be enabled or disabled by golang build constraints also known as build `tags`.
Click [here](https://golang.org/pkg/go/build/#hdr-Build_Constraints) for more information about build tags / constraints. [Click here for more information about build tags / constraints.](https://golang.org/pkg/go/build/#hdr-Build_Constraints)
### Usage ### Usage
If you wish to build this library with additional extensions / features, use the following command: If you wish to build this library with additional extensions / features.
Use the following command.
```bash ```bash
go build -tags "<FEATURE>" go build --tags "<FEATURE>"
``` ```
For available features, see the extension list. For available features see the extension list.
When using multiple build tags, all the different tags should be space delimited. When using multiple build tags, all the different tags should be space delimted.
Example: Example:
```bash ```bash
go build -tags "icu json1 fts5 secure_delete" go build --tags "icu json1 fts5 secure_delete"
``` ```
### Feature / Extension List ### Feature / Extension List
@@ -165,7 +166,6 @@ go build -tags "icu json1 fts5 secure_delete"
| Allow URI Authority | sqlite_allow_uri_authority | URI filenames normally throws an error if the authority section is not either empty or "localhost".<br><br>However, if SQLite is compiled with the SQLITE_ALLOW_URI_AUTHORITY compile-time option, then the URI is converted into a Uniform Naming Convention (UNC) filename and passed down to the underlying operating system that way | | Allow URI Authority | sqlite_allow_uri_authority | URI filenames normally throws an error if the authority section is not either empty or "localhost".<br><br>However, if SQLite is compiled with the SQLITE_ALLOW_URI_AUTHORITY compile-time option, then the URI is converted into a Uniform Naming Convention (UNC) filename and passed down to the underlying operating system that way |
| App Armor | sqlite_app_armor | When defined, this C-preprocessor macro activates extra code that attempts to detect misuse of the SQLite API, such as passing in NULL pointers to required parameters or using objects after they have been destroyed. <br><br>App Armor is not available under `Windows`. | | App Armor | sqlite_app_armor | When defined, this C-preprocessor macro activates extra code that attempts to detect misuse of the SQLite API, such as passing in NULL pointers to required parameters or using objects after they have been destroyed. <br><br>App Armor is not available under `Windows`. |
| Disable Load Extensions | sqlite_omit_load_extension | Loading of external extensions is enabled by default.<br><br>To disable extension loading add the build tag `sqlite_omit_load_extension`. | | Disable Load Extensions | sqlite_omit_load_extension | Loading of external extensions is enabled by default.<br><br>To disable extension loading add the build tag `sqlite_omit_load_extension`. |
| Enable Serialization with `libsqlite3` | sqlite_serialize | Serialization and deserialization of a SQLite database is available by default, unless the build tag `libsqlite3` is set.<br><br>To enable this functionality even if `libsqlite3` is set, add the build tag `sqlite_serialize`. |
| Foreign Keys | sqlite_foreign_keys | This macro determines whether enforcement of foreign key constraints is enabled or disabled by default for new database connections.<br><br>Each database connection can always turn enforcement of foreign key constraints on and off and run-time using the foreign_keys pragma.<br><br>Enforcement of foreign key constraints is normally off by default, but if this compile-time parameter is set to 1, enforcement of foreign key constraints will be on by default | | Foreign Keys | sqlite_foreign_keys | This macro determines whether enforcement of foreign key constraints is enabled or disabled by default for new database connections.<br><br>Each database connection can always turn enforcement of foreign key constraints on and off and run-time using the foreign_keys pragma.<br><br>Enforcement of foreign key constraints is normally off by default, but if this compile-time parameter is set to 1, enforcement of foreign key constraints will be on by default |
| Full Auto Vacuum | sqlite_vacuum_full | Set the default auto vacuum to full | | Full Auto Vacuum | sqlite_vacuum_full | Set the default auto vacuum to full |
| Incremental Auto Vacuum | sqlite_vacuum_incr | Set the default auto vacuum to incremental | | Incremental Auto Vacuum | sqlite_vacuum_incr | Set the default auto vacuum to incremental |
@@ -173,20 +173,17 @@ go build -tags "icu json1 fts5 secure_delete"
| International Components for Unicode | sqlite_icu | This option causes the International Components for Unicode or "ICU" extension to SQLite to be added to the build | | International Components for Unicode | sqlite_icu | This option causes the International Components for Unicode or "ICU" extension to SQLite to be added to the build |
| Introspect PRAGMAS | sqlite_introspect | This option adds some extra PRAGMA statements. <ul><li>PRAGMA function_list</li><li>PRAGMA module_list</li><li>PRAGMA pragma_list</li></ul> | | Introspect PRAGMAS | sqlite_introspect | This option adds some extra PRAGMA statements. <ul><li>PRAGMA function_list</li><li>PRAGMA module_list</li><li>PRAGMA pragma_list</li></ul> |
| JSON SQL Functions | sqlite_json | When this option is defined in the amalgamation, the JSON SQL functions are added to the build automatically | | JSON SQL Functions | sqlite_json | When this option is defined in the amalgamation, the JSON SQL functions are added to the build automatically |
| Math Functions | sqlite_math_functions | This compile-time option enables built-in scalar math functions. For more information see [Built-In Mathematical SQL Functions](https://www.sqlite.org/lang_mathfunc.html) |
| OS Trace | sqlite_os_trace | This option enables OSTRACE() debug logging. This can be verbose and should not be used in production. |
| Pre Update Hook | sqlite_preupdate_hook | Registers a callback function that is invoked prior to each INSERT, UPDATE, and DELETE operation on a database table. | | Pre Update Hook | sqlite_preupdate_hook | Registers a callback function that is invoked prior to each INSERT, UPDATE, and DELETE operation on a database table. |
| Secure Delete | sqlite_secure_delete | This compile-time option changes the default setting of the secure_delete pragma.<br><br>When this option is not used, secure_delete defaults to off. When this option is present, secure_delete defaults to on.<br><br>The secure_delete setting causes deleted content to be overwritten with zeros. There is a small performance penalty since additional I/O must occur.<br><br>On the other hand, secure_delete can prevent fragments of sensitive information from lingering in unused parts of the database file after it has been deleted. See the documentation on the secure_delete pragma for additional information | | Secure Delete | sqlite_secure_delete | This compile-time option changes the default setting of the secure_delete pragma.<br><br>When this option is not used, secure_delete defaults to off. When this option is present, secure_delete defaults to on.<br><br>The secure_delete setting causes deleted content to be overwritten with zeros. There is a small performance penalty since additional I/O must occur.<br><br>On the other hand, secure_delete can prevent fragments of sensitive information from lingering in unused parts of the database file after it has been deleted. See the documentation on the secure_delete pragma for additional information |
| Secure Delete (FAST) | sqlite_secure_delete_fast | For more information see [PRAGMA secure_delete](https://www.sqlite.org/pragma.html#pragma_secure_delete) | | Secure Delete (FAST) | sqlite_secure_delete_fast | For more information see [PRAGMA secure_delete](https://www.sqlite.org/pragma.html#pragma_secure_delete) |
| Tracing / Debug | sqlite_trace | Activate trace functions | | Tracing / Debug | sqlite_trace | Activate trace functions |
| User Authentication | sqlite_userauth | SQLite User Authentication see [User Authentication](#user-authentication) for more information. | | User Authentication | sqlite_userauth | SQLite User Authentication see [User Authentication](#user-authentication) for more information. |
| Virtual Tables | sqlite_vtable | SQLite Virtual Tables see [SQLite Official VTABLE Documentation](https://www.sqlite.org/vtab.html) for more information, and a [full example here](https://github.com/mattn/go-sqlite3/tree/master/_example/vtable) |
# Compilation # Compilation
This package requires the `CGO_ENABLED=1` environment variable if not set by default, and the presence of the `gcc` compiler. This package requires `CGO_ENABLED=1` ennvironment variable if not set by default, and the presence of the `gcc` compiler.
If you need to add additional CFLAGS or LDFLAGS to the build command, and do not want to modify this package, then this can be achieved by using the `CGO_CFLAGS` and `CGO_LDFLAGS` environment variables. If you need to add additional CFLAGS or LDFLAGS to the build command, and do not want to modify this package. Then this can be achieved by using the `CGO_CFLAGS` and `CGO_LDFLAGS` environment variables.
## Android ## Android
@@ -194,14 +191,14 @@ This package can be compiled for android.
Compile with: Compile with:
```bash ```bash
go build -tags "android" go build --tags "android"
``` ```
For more information see [#201](https://github.com/mattn/go-sqlite3/issues/201) For more information see [#201](https://github.com/mattn/go-sqlite3/issues/201)
# ARM # ARM
To compile for `ARM` use the following environment: To compile for `ARM` use the following environment.
```bash ```bash
env CC=arm-linux-gnueabihf-gcc CXX=arm-linux-gnueabihf-g++ \ env CC=arm-linux-gnueabihf-gcc CXX=arm-linux-gnueabihf-g++ \
@@ -219,14 +216,15 @@ This library can be cross-compiled.
In some cases you are required to the `CC` environment variable with the cross compiler. In some cases you are required to the `CC` environment variable with the cross compiler.
## Cross Compiling from macOS ## Cross Compiling from MAC OSX
The simplest way to cross compile from macOS is to use [xgo](https://github.com/karalabe/xgo). The simplest way to cross compile from OSX is to use [xgo](https://github.com/karalabe/xgo).
Steps: Steps:
- Install [musl-cross](https://github.com/FiloSottile/homebrew-musl-cross) (`brew install FiloSottile/musl-cross/musl-cross`). - Install [xgo](https://github.com/karalabe/xgo) (`go get github.com/karalabe/xgo`).
- Run `CC=x86_64-linux-musl-gcc CXX=x86_64-linux-musl-g++ GOARCH=amd64 GOOS=linux CGO_ENABLED=1 go build -ldflags "-linkmode external -extldflags -static"`. - Ensure that your project is within your `GOPATH`.
- Run `xgo local/path/to/project`.
Please refer to the project's [README](https://github.com/FiloSottile/homebrew-musl-cross#readme) for further information. Please refer to the project's [README](https://github.com/karalabe/xgo/blob/master/README.md) for further information.
# Google Cloud Platform # Google Cloud Platform
@@ -236,23 +234,23 @@ Please work only with compiled final binaries.
## Linux ## Linux
To compile this package on Linux, you must install the development tools for your linux distribution. To compile this package on Linux you must install the development tools for your linux distribution.
To compile under linux use the build tag `linux`. To compile under linux use the build tag `linux`.
```bash ```bash
go build -tags "linux" go build --tags "linux"
``` ```
If you wish to link directly to libsqlite3 then you can use the `libsqlite3` build tag. If you wish to link directly to libsqlite3 then you can use the `libsqlite3` build tag.
``` ```
go build -tags "libsqlite3 linux" go build --tags "libsqlite3 linux"
``` ```
### Alpine ### Alpine
When building in an `alpine` container run the following command before building: When building in an `alpine` container run the following command before building.
``` ```
apk add --update gcc musl-dev apk add --update gcc musl-dev
@@ -270,43 +268,34 @@ sudo yum groupinstall "Development Tools" "Development Libraries"
sudo apt-get install build-essential sudo apt-get install build-essential
``` ```
## macOS ## Mac OSX
macOS should have all the tools present to compile this package. If not, install XCode to add all the developers tools. OSX should have all the tools present to compile this package, if not install XCode this will add all the developers tools.
Required dependency: Required dependency
```bash ```bash
brew install sqlite3 brew install sqlite3
``` ```
For macOS, there is an additional package to install which is required if you wish to build the `icu` extension. For OSX there is an additional package install which is required if you wish to build the `icu` extension.
This additional package can be installed with `homebrew`: This additional package can be installed with `homebrew`.
```bash ```bash
brew upgrade icu4c brew upgrade icu4c
``` ```
To compile for macOS on x86: To compile for Mac OSX.
```bash ```bash
go build -tags "darwin amd64" go build --tags "darwin"
``` ```
To compile for macOS on ARM chips: If you wish to link directly to libsqlite3 then you can use the `libsqlite3` build tag.
```bash
go build -tags "darwin arm64"
```
If you wish to link directly to libsqlite3, use the `libsqlite3` build tag:
``` ```
# x86 go build --tags "libsqlite3 darwin"
go build -tags "libsqlite3 darwin amd64"
# ARM
go build -tags "libsqlite3 darwin arm64"
``` ```
Additional information: Additional information:
@@ -315,14 +304,14 @@ Additional information:
## Windows ## Windows
To compile this package on Windows, you must have the `gcc` compiler installed. To compile this package on Windows OS you must have the `gcc` compiler installed.
1) Install a Windows `gcc` toolchain. 1) Install a Windows `gcc` toolchain.
2) Add the `bin` folder to the Windows path, if the installer did not do this by default. 2) Add the `bin` folders to the Windows path if the installer did not do this by default.
3) Open a terminal for the TDM-GCC toolchain, which can be found in the Windows Start menu. 3) Open a terminal for the TDM-GCC toolchain, can be found in the Windows Start menu.
4) Navigate to your project folder and run the `go build ...` command for this package. 4) Navigate to your project folder and run the `go build ...` command for this package.
For example the TDM-GCC Toolchain can be found [here](https://jmeubank.github.io/tdm-gcc/). For example the TDM-GCC Toolchain can be found [here](https://sourceforge.net/projects/tdm-gcc/).
## Errors ## Errors
@@ -360,28 +349,28 @@ This package supports the SQLite User Authentication module.
## Compile ## Compile
To use the User authentication module, the package has to be compiled with the tag `sqlite_userauth`. See [Features](#features). To use the User authentication module the package has to be compiled with the tag `sqlite_userauth`. See [Features](#features).
## Usage ## Usage
### Create protected database ### Create protected database
To create a database protected by user authentication, provide the following argument to the connection string `_auth`. To create a database protected by user authentication provide the following argument to the connection string `_auth`.
This will enable user authentication within the database. This option however requires two additional arguments: This will enable user authentication within the database. This option however requires two additional arguments:
- `_auth_user` - `_auth_user`
- `_auth_pass` - `_auth_pass`
When `_auth` is present in the connection string user authentication will be enabled and the provided user will be created When `_auth` is present on the connection string user authentication will be enabled and the provided user will be created
as an `admin` user. After initial creation, the parameter `_auth` has no effect anymore and can be omitted from the connection string. as an `admin` user. After initial creation, the parameter `_auth` has no effect anymore and can be omitted from the connection string.
Example connection strings: Example connection string:
Create an user authentication database with user `admin` and password `admin`: Create an user authentication database with user `admin` and password `admin`.
`file:test.s3db?_auth&_auth_user=admin&_auth_pass=admin` `file:test.s3db?_auth&_auth_user=admin&_auth_pass=admin`
Create an user authentication database with user `admin` and password `admin` and use `SHA1` for the password encoding: Create an user authentication database with user `admin` and password `admin` and use `SHA1` for the password encoding.
`file:test.s3db?_auth&_auth_user=admin&_auth_pass=admin&_auth_crypt=sha1` `file:test.s3db?_auth&_auth_user=admin&_auth_pass=admin&_auth_crypt=sha1`
@@ -407,11 +396,11 @@ salt this can be configured with `_auth_salt`.
### Restrictions ### Restrictions
Operations on the database regarding user management can only be preformed by an administrator user. Operations on the database regarding to user management can only be preformed by an administrator user.
### Support ### Support
The user authentication supports two kinds of users: The user authentication supports two kinds of users
- administrators - administrators
- regular users - regular users
@@ -422,7 +411,7 @@ User management can be done by directly using the `*SQLiteConn` or by SQL.
#### SQL #### SQL
The following sql functions are available for user management: The following sql functions are available for user management.
| Function | Arguments | Description | | Function | Arguments | Description |
|----------|-----------|-------------| |----------|-----------|-------------|
@@ -431,7 +420,7 @@ The following sql functions are available for user management:
| `auth_user_change` | username `string`, password `string`, admin `int` | Function to modify an user. Users can change their own password, but only an administrator can change the administrator flag. | | `auth_user_change` | username `string`, password `string`, admin `int` | Function to modify an user. Users can change their own password, but only an administrator can change the administrator flag. |
| `authUserDelete` | username `string` | Delete an user from the database. Can only be used by an administrator. The current logged in administrator cannot be deleted. This is to make sure their is always an administrator remaining. | | `authUserDelete` | username `string` | Delete an user from the database. Can only be used by an administrator. The current logged in administrator cannot be deleted. This is to make sure their is always an administrator remaining. |
These functions will return an integer: These functions will return an integer.
- 0 (SQLITE_OK) - 0 (SQLITE_OK)
- 23 (SQLITE_AUTH) Failed to perform due to authentication or insufficient privileges - 23 (SQLITE_AUTH) Failed to perform due to authentication or insufficient privileges
@@ -452,7 +441,7 @@ SELECT user_delete('user');
#### *SQLiteConn #### *SQLiteConn
The following functions are available for User authentication from the `*SQLiteConn`: The following functions are available for User authentication from the `*SQLiteConn`.
| Function | Description | | Function | Description |
|----------|-------------| |----------|-------------|
@@ -463,16 +452,16 @@ The following functions are available for User authentication from the `*SQLiteC
### Attached database ### Attached database
When using attached databases, SQLite will use the authentication from the `main` database for the attached database(s). When using attached databases. SQLite will use the authentication from the `main` database for the attached database(s).
# Extensions # Extensions
If you want your own extension to be listed here, or you want to add a reference to an extension; please submit an Issue for this. If you want your own extension to be listed here or you want to add a reference to an extension; please submit an Issue for this.
## Spatialite ## Spatialite
Spatialite is available as an extension to SQLite, and can be used in combination with this repository. Spatialite is available as an extension to SQLite, and can be used in combination with this repository.
For an example, see [shaxbee/go-spatialite](https://github.com/shaxbee/go-spatialite). For an example see [shaxbee/go-spatialite](https://github.com/shaxbee/go-spatialite).
## extension-functions.c from SQLite3 Contrib ## extension-functions.c from SQLite3 Contrib
@@ -482,7 +471,7 @@ extension-functions.c is available as an extension to SQLite, and provides the f
- String: replicate, charindex, leftstr, rightstr, ltrim, rtrim, trim, replace, reverse, proper, padl, padr, padc, strfilter. - String: replicate, charindex, leftstr, rightstr, ltrim, rtrim, trim, replace, reverse, proper, padl, padr, padc, strfilter.
- Aggregate: stdev, variance, mode, median, lower_quartile, upper_quartile - Aggregate: stdev, variance, mode, median, lower_quartile, upper_quartile
For an example, see [dinedal/go-sqlite3-extension-functions](https://github.com/dinedal/go-sqlite3-extension-functions). For an example see [dinedal/go-sqlite3-extension-functions](https://github.com/dinedal/go-sqlite3-extension-functions).
# FAQ # FAQ
@@ -502,7 +491,7 @@ For an example, see [dinedal/go-sqlite3-extension-functions](https://github.com/
- Can I use this in multiple routines concurrently? - Can I use this in multiple routines concurrently?
Yes for readonly. But not for writable. See [#50](https://github.com/mattn/go-sqlite3/issues/50), [#51](https://github.com/mattn/go-sqlite3/issues/51), [#209](https://github.com/mattn/go-sqlite3/issues/209), [#274](https://github.com/mattn/go-sqlite3/issues/274). Yes for readonly. But, No for writable. See [#50](https://github.com/mattn/go-sqlite3/issues/50), [#51](https://github.com/mattn/go-sqlite3/issues/51), [#209](https://github.com/mattn/go-sqlite3/issues/209), [#274](https://github.com/mattn/go-sqlite3/issues/274).
- Why I'm getting `no such table` error? - Why I'm getting `no such table` error?
@@ -516,7 +505,7 @@ For an example, see [dinedal/go-sqlite3-extension-functions](https://github.com/
Note that if the last database connection in the pool closes, the in-memory database is deleted. Make sure the [max idle connection limit](https://golang.org/pkg/database/sql/#DB.SetMaxIdleConns) is > 0, and the [connection lifetime](https://golang.org/pkg/database/sql/#DB.SetConnMaxLifetime) is infinite. Note that if the last database connection in the pool closes, the in-memory database is deleted. Make sure the [max idle connection limit](https://golang.org/pkg/database/sql/#DB.SetMaxIdleConns) is > 0, and the [connection lifetime](https://golang.org/pkg/database/sql/#DB.SetConnMaxLifetime) is infinite.
For more information see: For more information see
* [#204](https://github.com/mattn/go-sqlite3/issues/204) * [#204](https://github.com/mattn/go-sqlite3/issues/204)
* [#511](https://github.com/mattn/go-sqlite3/issues/511) * [#511](https://github.com/mattn/go-sqlite3/issues/511)
* https://www.sqlite.org/sharedcache.html#shared_cache_and_in_memory_databases * https://www.sqlite.org/sharedcache.html#shared_cache_and_in_memory_databases
@@ -526,20 +515,20 @@ For an example, see [dinedal/go-sqlite3-extension-functions](https://github.com/
OS X limits OS-wide to not have more than 1000 files open simultaneously by default. OS X limits OS-wide to not have more than 1000 files open simultaneously by default.
For more information, see [#289](https://github.com/mattn/go-sqlite3/issues/289) For more information see [#289](https://github.com/mattn/go-sqlite3/issues/289)
- Trying to execute a `.` (dot) command throws an error. - Trying to execute a `.` (dot) command throws an error.
Error: `Error: near ".": syntax error` Error: `Error: near ".": syntax error`
Dot command are part of SQLite3 CLI, not of this library. Dot command are part of SQLite3 CLI not of this library.
You need to implement the feature or call the sqlite3 cli. You need to implement the feature or call the sqlite3 cli.
More information see [#305](https://github.com/mattn/go-sqlite3/issues/305). More information see [#305](https://github.com/mattn/go-sqlite3/issues/305)
- Error: `database is locked` - Error: `database is locked`
When you get a database is locked, please use the following options. When you get a database is locked. Please use the following options.
Add to DSN: `cache=shared` Add to DSN: `cache=shared`
@@ -548,24 +537,24 @@ For an example, see [dinedal/go-sqlite3-extension-functions](https://github.com/
db, err := sql.Open("sqlite3", "file:locked.sqlite?cache=shared") db, err := sql.Open("sqlite3", "file:locked.sqlite?cache=shared")
``` ```
Next, please set the database connections of the SQL package to 1: Second please set the database connections of the SQL package to 1.
```go ```go
db.SetMaxOpenConns(1) db.SetMaxOpenConns(1)
``` ```
For more information, see [#209](https://github.com/mattn/go-sqlite3/issues/209). More information see [#209](https://github.com/mattn/go-sqlite3/issues/209)
## Contributors ## Contributors
### Code Contributors ### Code Contributors
This project exists thanks to all the people who [[contribute](CONTRIBUTING.md)]. This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/mattn/go-sqlite3/graphs/contributors"><img src="https://opencollective.com/mattn-go-sqlite3/contributors.svg?width=890&button=false" /></a> <a href="https://github.com/mattn/go-sqlite3/graphs/contributors"><img src="https://opencollective.com/mattn-go-sqlite3/contributors.svg?width=890&button=false" /></a>
### Financial Contributors ### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute here](https://opencollective.com/mattn-go-sqlite3/contribute)]. Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/mattn-go-sqlite3/contribute)]
#### Individuals #### Individuals

View File

@@ -7,7 +7,7 @@ package sqlite3
/* /*
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h" #include <sqlite3-binding.h>
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif

View File

@@ -12,7 +12,7 @@ package sqlite3
/* /*
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h" #include <sqlite3-binding.h>
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif
@@ -100,13 +100,13 @@ func preUpdateHookTrampoline(handle unsafe.Pointer, dbHandle uintptr, op int, db
// Use handles to avoid passing Go pointers to C. // Use handles to avoid passing Go pointers to C.
type handleVal struct { type handleVal struct {
db *SQLiteConn db *SQLiteConn
val any val interface{}
} }
var handleLock sync.Mutex var handleLock sync.Mutex
var handleVals = make(map[unsafe.Pointer]handleVal) var handleVals = make(map[unsafe.Pointer]handleVal)
func newHandle(db *SQLiteConn, v any) unsafe.Pointer { func newHandle(db *SQLiteConn, v interface{}) unsafe.Pointer {
handleLock.Lock() handleLock.Lock()
defer handleLock.Unlock() defer handleLock.Unlock()
val := handleVal{db: db, val: v} val := handleVal{db: db, val: v}
@@ -124,7 +124,7 @@ func lookupHandleVal(handle unsafe.Pointer) handleVal {
return handleVals[handle] return handleVals[handle]
} }
func lookupHandle(handle unsafe.Pointer) any { func lookupHandle(handle unsafe.Pointer) interface{} {
return lookupHandleVal(handle).val return lookupHandleVal(handle).val
} }
@@ -238,7 +238,7 @@ func callbackArg(typ reflect.Type) (callbackArgConverter, error) {
switch typ.Kind() { switch typ.Kind() {
case reflect.Interface: case reflect.Interface:
if typ.NumMethod() != 0 { if typ.NumMethod() != 0 {
return nil, errors.New("the only supported interface type is any") return nil, errors.New("the only supported interface type is interface{}")
} }
return callbackArgGeneric, nil return callbackArgGeneric, nil
case reflect.Slice: case reflect.Slice:
@@ -353,20 +353,6 @@ func callbackRetNil(ctx *C.sqlite3_context, v reflect.Value) error {
return nil return nil
} }
func callbackRetGeneric(ctx *C.sqlite3_context, v reflect.Value) error {
if v.IsNil() {
C.sqlite3_result_null(ctx)
return nil
}
cb, err := callbackRet(v.Elem().Type())
if err != nil {
return err
}
return cb(ctx, v.Elem())
}
func callbackRet(typ reflect.Type) (callbackRetConverter, error) { func callbackRet(typ reflect.Type) (callbackRetConverter, error) {
switch typ.Kind() { switch typ.Kind() {
case reflect.Interface: case reflect.Interface:
@@ -374,11 +360,6 @@ func callbackRet(typ reflect.Type) (callbackRetConverter, error) {
if typ.Implements(errorInterface) { if typ.Implements(errorInterface) {
return callbackRetNil, nil return callbackRetNil, nil
} }
if typ.NumMethod() == 0 {
return callbackRetGeneric, nil
}
fallthrough fallthrough
case reflect.Slice: case reflect.Slice:
if typ.Elem().Kind() != reflect.Uint8 { if typ.Elem().Kind() != reflect.Uint8 {

View File

@@ -23,7 +23,7 @@ var errNilPtr = errors.New("destination pointer is nil") // embedded in descript
// convertAssign copies to dest the value in src, converting it if possible. // convertAssign copies to dest the value in src, converting it if possible.
// An error is returned if the copy would result in loss of information. // An error is returned if the copy would result in loss of information.
// dest should be a pointer type. // dest should be a pointer type.
func convertAssign(dest, src any) error { func convertAssign(dest, src interface{}) error {
// Common cases, without reflect. // Common cases, without reflect.
switch s := src.(type) { switch s := src.(type) {
case string: case string:
@@ -55,7 +55,7 @@ func convertAssign(dest, src any) error {
} }
*d = string(s) *d = string(s)
return nil return nil
case *any: case *interface{}:
if d == nil { if d == nil {
return errNilPtr return errNilPtr
} }
@@ -97,7 +97,7 @@ func convertAssign(dest, src any) error {
} }
case nil: case nil:
switch d := dest.(type) { switch d := dest.(type) {
case *any: case *interface{}:
if d == nil { if d == nil {
return errNilPtr return errNilPtr
} }
@@ -149,7 +149,7 @@ func convertAssign(dest, src any) error {
*d = bv.(bool) *d = bv.(bool)
} }
return err return err
case *any: case *interface{}:
*d = src *d = src
return nil return nil
} }
@@ -256,7 +256,7 @@ func cloneBytes(b []byte) []byte {
return c return c
} }
func asString(src any) string { func asString(src interface{}) string {
switch v := src.(type) { switch v := src.(type) {
case string: case string:
return v return v

View File

@@ -7,7 +7,7 @@ Installation
go get github.com/mattn/go-sqlite3 go get github.com/mattn/go-sqlite3
# Supported Types Supported Types
Currently, go-sqlite3 supports the following data types. Currently, go-sqlite3 supports the following data types.
@@ -24,7 +24,7 @@ Currently, go-sqlite3 supports the following data types.
|time.Time | timestamp/datetime| |time.Time | timestamp/datetime|
+------------------------------+ +------------------------------+
# SQLite3 Extension SQLite3 Extension
You can write your own extension module for sqlite3. For example, below is an You can write your own extension module for sqlite3. For example, below is an
extension for a Regexp matcher operation. extension for a Regexp matcher operation.
@@ -77,7 +77,7 @@ Then, you can use this extension.
rows, err := db.Query("select text from mytable where name regexp '^golang'") rows, err := db.Query("select text from mytable where name regexp '^golang'")
# Connection Hook Connection Hook
You can hook and inject your code when the connection is established by setting You can hook and inject your code when the connection is established by setting
ConnectHook to get the SQLiteConn. ConnectHook to get the SQLiteConn.
@@ -95,13 +95,13 @@ You can also use database/sql.Conn.Raw (Go >= 1.13):
conn, err := db.Conn(context.Background()) conn, err := db.Conn(context.Background())
// if err != nil { ... } // if err != nil { ... }
defer conn.Close() defer conn.Close()
err = conn.Raw(func (driverConn any) error { err = conn.Raw(func (driverConn interface{}) error {
sqliteConn := driverConn.(*sqlite3.SQLiteConn) sqliteConn := driverConn.(*sqlite3.SQLiteConn)
// ... use sqliteConn // ... use sqliteConn
}) })
// if err != nil { ... } // if err != nil { ... }
# Go SQlite3 Extensions Go SQlite3 Extensions
If you want to register Go functions as SQLite extension functions If you want to register Go functions as SQLite extension functions
you can make a custom driver by calling RegisterFunction from you can make a custom driver by calling RegisterFunction from
@@ -130,5 +130,6 @@ You can then use the custom driver by passing its name to sql.Open.
} }
See the documentation of RegisterFunc for more details. See the documentation of RegisterFunc for more details.
*/ */
package sqlite3 package sqlite3

View File

@@ -7,7 +7,7 @@ package sqlite3
/* /*
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h" #include <sqlite3-binding.h>
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build cgo
// +build cgo // +build cgo
package sqlite3 package sqlite3
@@ -21,10 +20,9 @@ package sqlite3
#cgo CFLAGS: -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 #cgo CFLAGS: -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1
#cgo CFLAGS: -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT #cgo CFLAGS: -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
#cgo CFLAGS: -Wno-deprecated-declarations #cgo CFLAGS: -Wno-deprecated-declarations
#cgo openbsd CFLAGS: -I/usr/local/include #cgo linux,!android CFLAGS: -DHAVE_PREAD64=1 -DHAVE_PWRITE64=1
#cgo openbsd LDFLAGS: -L/usr/local/lib
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h" #include <sqlite3-binding.h>
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif
@@ -47,18 +45,6 @@ package sqlite3
# define SQLITE_DETERMINISTIC 0 # define SQLITE_DETERMINISTIC 0
#endif #endif
#if defined(HAVE_PREAD64) && defined(HAVE_PWRITE64)
# undef USE_PREAD
# undef USE_PWRITE
# define USE_PREAD64 1
# define USE_PWRITE64 1
#elif defined(HAVE_PREAD) && defined(HAVE_PWRITE)
# undef USE_PREAD
# undef USE_PWRITE
# define USE_PREAD64 1
# define USE_PWRITE64 1
#endif
static int static int
_sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs) { _sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs) {
#ifdef SQLITE_OPEN_URI #ifdef SQLITE_OPEN_URI
@@ -245,14 +231,8 @@ const (
columnTimestamp string = "timestamp" columnTimestamp string = "timestamp"
) )
// This variable can be replaced with -ldflags like below:
// go build -ldflags="-X 'github.com/mattn/go-sqlite3.driverName=my-sqlite3'"
var driverName = "sqlite3"
func init() { func init() {
if driverName != "" { sql.Register("sqlite3", &SQLiteDriver{})
sql.Register(driverName, &SQLiteDriver{})
}
} }
// Version returns SQLite library version information. // Version returns SQLite library version information.
@@ -308,51 +288,6 @@ const (
/*SQLITE_RECURSIVE = C.SQLITE_RECURSIVE*/ /*SQLITE_RECURSIVE = C.SQLITE_RECURSIVE*/
) )
// Standard File Control Opcodes
// See: https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html
const (
SQLITE_FCNTL_LOCKSTATE = int(1)
SQLITE_FCNTL_GET_LOCKPROXYFILE = int(2)
SQLITE_FCNTL_SET_LOCKPROXYFILE = int(3)
SQLITE_FCNTL_LAST_ERRNO = int(4)
SQLITE_FCNTL_SIZE_HINT = int(5)
SQLITE_FCNTL_CHUNK_SIZE = int(6)
SQLITE_FCNTL_FILE_POINTER = int(7)
SQLITE_FCNTL_SYNC_OMITTED = int(8)
SQLITE_FCNTL_WIN32_AV_RETRY = int(9)
SQLITE_FCNTL_PERSIST_WAL = int(10)
SQLITE_FCNTL_OVERWRITE = int(11)
SQLITE_FCNTL_VFSNAME = int(12)
SQLITE_FCNTL_POWERSAFE_OVERWRITE = int(13)
SQLITE_FCNTL_PRAGMA = int(14)
SQLITE_FCNTL_BUSYHANDLER = int(15)
SQLITE_FCNTL_TEMPFILENAME = int(16)
SQLITE_FCNTL_MMAP_SIZE = int(18)
SQLITE_FCNTL_TRACE = int(19)
SQLITE_FCNTL_HAS_MOVED = int(20)
SQLITE_FCNTL_SYNC = int(21)
SQLITE_FCNTL_COMMIT_PHASETWO = int(22)
SQLITE_FCNTL_WIN32_SET_HANDLE = int(23)
SQLITE_FCNTL_WAL_BLOCK = int(24)
SQLITE_FCNTL_ZIPVFS = int(25)
SQLITE_FCNTL_RBU = int(26)
SQLITE_FCNTL_VFS_POINTER = int(27)
SQLITE_FCNTL_JOURNAL_POINTER = int(28)
SQLITE_FCNTL_WIN32_GET_HANDLE = int(29)
SQLITE_FCNTL_PDB = int(30)
SQLITE_FCNTL_BEGIN_ATOMIC_WRITE = int(31)
SQLITE_FCNTL_COMMIT_ATOMIC_WRITE = int(32)
SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE = int(33)
SQLITE_FCNTL_LOCK_TIMEOUT = int(34)
SQLITE_FCNTL_DATA_VERSION = int(35)
SQLITE_FCNTL_SIZE_LIMIT = int(36)
SQLITE_FCNTL_CKPT_DONE = int(37)
SQLITE_FCNTL_RESERVE_BYTES = int(38)
SQLITE_FCNTL_CKPT_START = int(39)
SQLITE_FCNTL_EXTERNAL_READER = int(40)
SQLITE_FCNTL_CKSM_FILE = int(41)
)
// SQLiteDriver implements driver.Driver. // SQLiteDriver implements driver.Driver.
type SQLiteDriver struct { type SQLiteDriver struct {
Extensions []string Extensions []string
@@ -505,12 +440,10 @@ func (ai *aggInfo) Done(ctx *C.sqlite3_context) {
// Commit transaction. // Commit transaction.
func (tx *SQLiteTx) Commit() error { func (tx *SQLiteTx) Commit() error {
_, err := tx.c.exec(context.Background(), "COMMIT", nil) _, err := tx.c.exec(context.Background(), "COMMIT", nil)
if err != nil { if err != nil && err.(Error).Code == C.SQLITE_BUSY {
// sqlite3 may leave the transaction open in this scenario. // sqlite3 will leave the transaction open in this scenario.
// However, database/sql considers the transaction complete once we // However, database/sql considers the transaction complete once we
// return from Commit() - we must clean up to honour its semantics. // return from Commit() - we must clean up to honour its semantics.
// We don't know if the ROLLBACK is strictly necessary, but according
// to sqlite's docs, there is no harm in calling ROLLBACK unnecessarily.
tx.c.exec(context.Background(), "ROLLBACK", nil) tx.c.exec(context.Background(), "ROLLBACK", nil)
} }
return err return err
@@ -607,9 +540,10 @@ func (c *SQLiteConn) RegisterAuthorizer(callback func(int, string, string, strin
// RegisterFunc makes a Go function available as a SQLite function. // RegisterFunc makes a Go function available as a SQLite function.
// //
// The Go function can have arguments of the following types: any // The Go function can have arguments of the following types: any
// numeric type except complex, bool, []byte, string and any. // numeric type except complex, bool, []byte, string and
// any arguments are given the direct translation of the SQLite data type: // interface{}. interface{} arguments are given the direct translation
// int64 for INTEGER, float64 for FLOAT, []byte for BLOB, string for TEXT. // of the SQLite data type: int64 for INTEGER, float64 for FLOAT,
// []byte for BLOB, string for TEXT.
// //
// The function can additionally be variadic, as long as the type of // The function can additionally be variadic, as long as the type of
// the variadic argument is one of the above. // the variadic argument is one of the above.
@@ -619,7 +553,7 @@ func (c *SQLiteConn) RegisterAuthorizer(callback func(int, string, string, strin
// optimizations in its queries. // optimizations in its queries.
// //
// See _example/go_custom_funcs for a detailed example. // See _example/go_custom_funcs for a detailed example.
func (c *SQLiteConn) RegisterFunc(name string, impl any, pure bool) error { func (c *SQLiteConn) RegisterFunc(name string, impl interface{}, pure bool) error {
var fi functionInfo var fi functionInfo
fi.f = reflect.ValueOf(impl) fi.f = reflect.ValueOf(impl)
t := fi.f.Type() t := fi.f.Type()
@@ -701,7 +635,7 @@ func sqlite3CreateFunction(db *C.sqlite3, zFunctionName *C.char, nArg C.int, eTe
// return an error in addition to their other return values. // return an error in addition to their other return values.
// //
// See _example/go_custom_funcs for a detailed example. // See _example/go_custom_funcs for a detailed example.
func (c *SQLiteConn) RegisterAggregator(name string, impl any, pure bool) error { func (c *SQLiteConn) RegisterAggregator(name string, impl interface{}, pure bool) error {
var ai aggInfo var ai aggInfo
ai.constructor = reflect.ValueOf(impl) ai.constructor = reflect.ValueOf(impl)
t := ai.constructor.Type() t := ai.constructor.Type()
@@ -847,9 +781,9 @@ func lastError(db *C.sqlite3) error {
// Exec implements Execer. // Exec implements Execer.
func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) { func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
list := make([]driver.NamedValue, len(args)) list := make([]namedValue, len(args))
for i, v := range args { for i, v := range args {
list[i] = driver.NamedValue{ list[i] = namedValue{
Ordinal: i + 1, Ordinal: i + 1,
Value: v, Value: v,
} }
@@ -857,7 +791,7 @@ func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, err
return c.exec(context.Background(), query, list) return c.exec(context.Background(), query, list)
} }
func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { func (c *SQLiteConn) exec(ctx context.Context, query string, args []namedValue) (driver.Result, error) {
start := 0 start := 0
for { for {
s, err := c.prepare(ctx, query) s, err := c.prepare(ctx, query)
@@ -866,7 +800,7 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.Named
} }
var res driver.Result var res driver.Result
if s.(*SQLiteStmt).s != nil { if s.(*SQLiteStmt).s != nil {
stmtArgs := make([]driver.NamedValue, 0, len(args)) stmtArgs := make([]namedValue, 0, len(args))
na := s.NumInput() na := s.NumInput()
if len(args)-start < na { if len(args)-start < na {
s.Close() s.Close()
@@ -875,7 +809,6 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.Named
// consume the number of arguments used in the current // consume the number of arguments used in the current
// statement and append all named arguments not // statement and append all named arguments not
// contained therein // contained therein
if len(args[start:start+na]) > 0 {
stmtArgs = append(stmtArgs, args[start:start+na]...) stmtArgs = append(stmtArgs, args[start:start+na]...)
for i := range args { for i := range args {
if (i < start || i >= na) && args[i].Name != "" { if (i < start || i >= na) && args[i].Name != "" {
@@ -885,7 +818,6 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.Named
for i := range stmtArgs { for i := range stmtArgs {
stmtArgs[i].Ordinal = i + 1 stmtArgs[i].Ordinal = i + 1
} }
}
res, err = s.(*SQLiteStmt).exec(ctx, stmtArgs) res, err = s.(*SQLiteStmt).exec(ctx, stmtArgs)
if err != nil && err != driver.ErrSkip { if err != nil && err != driver.ErrSkip {
s.Close() s.Close()
@@ -896,21 +828,23 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.Named
tail := s.(*SQLiteStmt).t tail := s.(*SQLiteStmt).t
s.Close() s.Close()
if tail == "" { if tail == "" {
if res == nil {
// https://github.com/mattn/go-sqlite3/issues/963
res = &SQLiteResult{0, 0}
}
return res, nil return res, nil
} }
query = tail query = tail
} }
} }
type namedValue struct {
Name string
Ordinal int
Value driver.Value
}
// Query implements Queryer. // Query implements Queryer.
func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, error) { func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, error) {
list := make([]driver.NamedValue, len(args)) list := make([]namedValue, len(args))
for i, v := range args { for i, v := range args {
list[i] = driver.NamedValue{ list[i] = namedValue{
Ordinal: i + 1, Ordinal: i + 1,
Value: v, Value: v,
} }
@@ -918,10 +852,10 @@ func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, erro
return c.query(context.Background(), query, list) return c.query(context.Background(), query, list)
} }
func (c *SQLiteConn) query(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { func (c *SQLiteConn) query(ctx context.Context, query string, args []namedValue) (driver.Rows, error) {
start := 0 start := 0
for { for {
stmtArgs := make([]driver.NamedValue, 0, len(args)) stmtArgs := make([]namedValue, 0, len(args))
s, err := c.prepare(ctx, query) s, err := c.prepare(ctx, query)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -977,12 +911,10 @@ func (c *SQLiteConn) begin(ctx context.Context) (driver.Tx, error) {
// The argument is may be either in parentheses or it may be separated from // The argument is may be either in parentheses or it may be separated from
// the pragma name by an equal sign. The two syntaxes yield identical results. // the pragma name by an equal sign. The two syntaxes yield identical results.
// In many pragmas, the argument is a boolean. The boolean can be one of: // In many pragmas, the argument is a boolean. The boolean can be one of:
//
// 1 yes true on // 1 yes true on
// 0 no false off // 0 no false off
// //
// You can specify a DSN string using a URI as the filename. // You can specify a DSN string using a URI as the filename.
//
// test.db // test.db
// file:test.db?cache=shared&mode=memory // file:test.db?cache=shared&mode=memory
// :memory: // :memory:
@@ -1014,7 +946,6 @@ func (c *SQLiteConn) begin(ctx context.Context) (driver.Tx, error) {
// does in fact change can result in incorrect query results and/or SQLITE_CORRUPT errors. // does in fact change can result in incorrect query results and/or SQLITE_CORRUPT errors.
// //
// go-sqlite3 adds the following query parameters to those used by SQLite: // go-sqlite3 adds the following query parameters to those used by SQLite:
//
// _loc=XXX // _loc=XXX
// Specify location of time format. It's possible to specify "auto". // Specify location of time format. It's possible to specify "auto".
// //
@@ -1075,6 +1006,8 @@ func (c *SQLiteConn) begin(ctx context.Context) (driver.Tx, error) {
// When this pragma is on, the SQLITE_MASTER tables in which database // When this pragma is on, the SQLITE_MASTER tables in which database
// can be changed using ordinary UPDATE, INSERT, and DELETE statements. // can be changed using ordinary UPDATE, INSERT, and DELETE statements.
// Warning: misuse of this pragma can easily result in a corrupt database file. // Warning: misuse of this pragma can easily result in a corrupt database file.
//
//
func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
if C.sqlite3_threadsafe() == 0 { if C.sqlite3_threadsafe() == 0 {
return nil, errors.New("sqlite library was not compiled for thread-safe operation") return nil, errors.New("sqlite library was not compiled for thread-safe operation")
@@ -1476,6 +1409,12 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
return nil, errors.New("sqlite succeeded without returning a database") return nil, errors.New("sqlite succeeded without returning a database")
} }
rv = C.sqlite3_busy_timeout(db, C.int(busyTimeout))
if rv != C.SQLITE_OK {
C.sqlite3_close_v2(db)
return nil, Error{Code: ErrNo(rv)}
}
exec := func(s string) error { exec := func(s string) error {
cs := C.CString(s) cs := C.CString(s)
rv := C.sqlite3_exec(db, cs, nil, nil, nil) rv := C.sqlite3_exec(db, cs, nil, nil, nil)
@@ -1486,12 +1425,6 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
return nil return nil
} }
// Busy timeout
if err := exec(fmt.Sprintf("PRAGMA busy_timeout = %d;", busyTimeout)); err != nil {
C.sqlite3_close_v2(db)
return nil, err
}
// USER AUTHENTICATION // USER AUTHENTICATION
// //
// User Authentication is always performed even when // User Authentication is always performed even when
@@ -1679,7 +1612,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
} }
} }
// Foreign Keys // Forgein Keys
if foreignKeys > -1 { if foreignKeys > -1 {
if err := exec(fmt.Sprintf("PRAGMA foreign_keys = %d;", foreignKeys)); err != nil { if err := exec(fmt.Sprintf("PRAGMA foreign_keys = %d;", foreignKeys)); err != nil {
C.sqlite3_close_v2(db) C.sqlite3_close_v2(db)
@@ -1867,31 +1800,6 @@ func (c *SQLiteConn) SetLimit(id int, newVal int) int {
return int(C._sqlite3_limit(c.db, C.int(id), C.int(newVal))) return int(C._sqlite3_limit(c.db, C.int(id), C.int(newVal)))
} }
// SetFileControlInt invokes the xFileControl method on a given database. The
// dbName is the name of the database. It will default to "main" if left blank.
// The op is one of the opcodes prefixed by "SQLITE_FCNTL_". The arg argument
// and return code are both opcode-specific. Please see the SQLite documentation.
//
// This method is not thread-safe as the returned error code can be changed by
// another call if invoked concurrently.
//
// See: sqlite3_file_control, https://www.sqlite.org/c3ref/file_control.html
func (c *SQLiteConn) SetFileControlInt(dbName string, op int, arg int) error {
if dbName == "" {
dbName = "main"
}
cDBName := C.CString(dbName)
defer C.free(unsafe.Pointer(cDBName))
cArg := C.int(arg)
rv := C.sqlite3_file_control(c.db, cDBName, C.int(op), unsafe.Pointer(&cArg))
if rv != C.SQLITE_OK {
return c.lastError()
}
return nil
}
// Close the statement. // Close the statement.
func (s *SQLiteStmt) Close() error { func (s *SQLiteStmt) Close() error {
s.mu.Lock() s.mu.Lock()
@@ -1908,7 +1816,6 @@ func (s *SQLiteStmt) Close() error {
if rv != C.SQLITE_OK { if rv != C.SQLITE_OK {
return s.c.lastError() return s.c.lastError()
} }
s.c = nil
runtime.SetFinalizer(s, nil) runtime.SetFinalizer(s, nil)
return nil return nil
} }
@@ -1920,7 +1827,7 @@ func (s *SQLiteStmt) NumInput() int {
var placeHolder = []byte{0} var placeHolder = []byte{0}
func (s *SQLiteStmt) bind(args []driver.NamedValue) error { func (s *SQLiteStmt) bind(args []namedValue) error {
rv := C.sqlite3_reset(s.s) rv := C.sqlite3_reset(s.s)
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE { if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
return s.c.lastError() return s.c.lastError()
@@ -1990,9 +1897,9 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
// Query the statement with arguments. Return records. // Query the statement with arguments. Return records.
func (s *SQLiteStmt) Query(args []driver.Value) (driver.Rows, error) { func (s *SQLiteStmt) Query(args []driver.Value) (driver.Rows, error) {
list := make([]driver.NamedValue, len(args)) list := make([]namedValue, len(args))
for i, v := range args { for i, v := range args {
list[i] = driver.NamedValue{ list[i] = namedValue{
Ordinal: i + 1, Ordinal: i + 1,
Value: v, Value: v,
} }
@@ -2000,7 +1907,7 @@ func (s *SQLiteStmt) Query(args []driver.Value) (driver.Rows, error) {
return s.query(context.Background(), list) return s.query(context.Background(), list)
} }
func (s *SQLiteStmt) query(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { func (s *SQLiteStmt) query(ctx context.Context, args []namedValue) (driver.Rows, error) {
if err := s.bind(args); err != nil { if err := s.bind(args); err != nil {
return nil, err return nil, err
} }
@@ -2014,7 +1921,6 @@ func (s *SQLiteStmt) query(ctx context.Context, args []driver.NamedValue) (drive
closed: false, closed: false,
ctx: ctx, ctx: ctx,
} }
runtime.SetFinalizer(rows, (*SQLiteRows).Close)
return rows, nil return rows, nil
} }
@@ -2031,9 +1937,9 @@ func (r *SQLiteResult) RowsAffected() (int64, error) {
// Exec execute the statement with arguments. Return result object. // Exec execute the statement with arguments. Return result object.
func (s *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) { func (s *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) {
list := make([]driver.NamedValue, len(args)) list := make([]namedValue, len(args))
for i, v := range args { for i, v := range args {
list[i] = driver.NamedValue{ list[i] = namedValue{
Ordinal: i + 1, Ordinal: i + 1,
Value: v, Value: v,
} }
@@ -2050,7 +1956,7 @@ func isInterruptErr(err error) bool {
} }
// exec executes a query that doesn't return rows. Attempts to honor context timeout. // exec executes a query that doesn't return rows. Attempts to honor context timeout.
func (s *SQLiteStmt) exec(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result, error) {
if ctx.Done() == nil { if ctx.Done() == nil {
return s.execSync(args) return s.execSync(args)
} }
@@ -2060,7 +1966,6 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []driver.NamedValue) (driver
err error err error
} }
resultCh := make(chan result) resultCh := make(chan result)
defer close(resultCh)
go func() { go func() {
r, err := s.execSync(args) r, err := s.execSync(args)
resultCh <- result{r, err} resultCh <- result{r, err}
@@ -2083,7 +1988,7 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []driver.NamedValue) (driver
return rv.r, rv.err return rv.r, rv.err
} }
func (s *SQLiteStmt) execSync(args []driver.NamedValue) (driver.Result, error) { func (s *SQLiteStmt) execSync(args []namedValue) (driver.Result, error) {
if err := s.bind(args); err != nil { if err := s.bind(args); err != nil {
C.sqlite3_reset(s.s) C.sqlite3_reset(s.s)
C.sqlite3_clear_bindings(s.s) C.sqlite3_clear_bindings(s.s)
@@ -2127,8 +2032,6 @@ func (rc *SQLiteRows) Close() error {
return rc.s.c.lastError() return rc.s.c.lastError()
} }
rc.s.mu.Unlock() rc.s.mu.Unlock()
rc.s = nil
runtime.SetFinalizer(rc, nil)
return nil return nil
} }
@@ -2175,7 +2078,6 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
return rc.nextSyncLocked(dest) return rc.nextSyncLocked(dest)
} }
resultCh := make(chan error) resultCh := make(chan error)
defer close(resultCh)
go func() { go func() {
resultCh <- rc.nextSyncLocked(dest) resultCh <- rc.nextSyncLocked(dest)
}() }()

View File

@@ -8,7 +8,7 @@ package sqlite3
/* /*
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h" #include <sqlite3-binding.h>
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif

View File

@@ -50,15 +50,15 @@ import (
// perhaps using a cryptographic hash function like SHA1. // perhaps using a cryptographic hash function like SHA1.
// CryptEncoderSHA1 encodes a password with SHA1 // CryptEncoderSHA1 encodes a password with SHA1
func CryptEncoderSHA1(pass []byte, hash any) []byte { func CryptEncoderSHA1(pass []byte, hash interface{}) []byte {
h := sha1.Sum(pass) h := sha1.Sum(pass)
return h[:] return h[:]
} }
// CryptEncoderSSHA1 encodes a password with SHA1 with the // CryptEncoderSSHA1 encodes a password with SHA1 with the
// configured salt. // configured salt.
func CryptEncoderSSHA1(salt string) func(pass []byte, hash any) []byte { func CryptEncoderSSHA1(salt string) func(pass []byte, hash interface{}) []byte {
return func(pass []byte, hash any) []byte { return func(pass []byte, hash interface{}) []byte {
s := []byte(salt) s := []byte(salt)
p := append(pass, s...) p := append(pass, s...)
h := sha1.Sum(p) h := sha1.Sum(p)
@@ -67,15 +67,15 @@ func CryptEncoderSSHA1(salt string) func(pass []byte, hash any) []byte {
} }
// CryptEncoderSHA256 encodes a password with SHA256 // CryptEncoderSHA256 encodes a password with SHA256
func CryptEncoderSHA256(pass []byte, hash any) []byte { func CryptEncoderSHA256(pass []byte, hash interface{}) []byte {
h := sha256.Sum256(pass) h := sha256.Sum256(pass)
return h[:] return h[:]
} }
// CryptEncoderSSHA256 encodes a password with SHA256 // CryptEncoderSSHA256 encodes a password with SHA256
// with the configured salt // with the configured salt
func CryptEncoderSSHA256(salt string) func(pass []byte, hash any) []byte { func CryptEncoderSSHA256(salt string) func(pass []byte, hash interface{}) []byte {
return func(pass []byte, hash any) []byte { return func(pass []byte, hash interface{}) []byte {
s := []byte(salt) s := []byte(salt)
p := append(pass, s...) p := append(pass, s...)
h := sha256.Sum256(p) h := sha256.Sum256(p)
@@ -84,15 +84,15 @@ func CryptEncoderSSHA256(salt string) func(pass []byte, hash any) []byte {
} }
// CryptEncoderSHA384 encodes a password with SHA384 // CryptEncoderSHA384 encodes a password with SHA384
func CryptEncoderSHA384(pass []byte, hash any) []byte { func CryptEncoderSHA384(pass []byte, hash interface{}) []byte {
h := sha512.Sum384(pass) h := sha512.Sum384(pass)
return h[:] return h[:]
} }
// CryptEncoderSSHA384 encodes a password with SHA384 // CryptEncoderSSHA384 encodes a password with SHA384
// with the configured salt // with the configured salt
func CryptEncoderSSHA384(salt string) func(pass []byte, hash any) []byte { func CryptEncoderSSHA384(salt string) func(pass []byte, hash interface{}) []byte {
return func(pass []byte, hash any) []byte { return func(pass []byte, hash interface{}) []byte {
s := []byte(salt) s := []byte(salt)
p := append(pass, s...) p := append(pass, s...)
h := sha512.Sum384(p) h := sha512.Sum384(p)
@@ -101,15 +101,15 @@ func CryptEncoderSSHA384(salt string) func(pass []byte, hash any) []byte {
} }
// CryptEncoderSHA512 encodes a password with SHA512 // CryptEncoderSHA512 encodes a password with SHA512
func CryptEncoderSHA512(pass []byte, hash any) []byte { func CryptEncoderSHA512(pass []byte, hash interface{}) []byte {
h := sha512.Sum512(pass) h := sha512.Sum512(pass)
return h[:] return h[:]
} }
// CryptEncoderSSHA512 encodes a password with SHA512 // CryptEncoderSSHA512 encodes a password with SHA512
// with the configured salt // with the configured salt
func CryptEncoderSSHA512(salt string) func(pass []byte, hash any) []byte { func CryptEncoderSSHA512(salt string) func(pass []byte, hash interface{}) []byte {
return func(pass []byte, hash any) []byte { return func(pass []byte, hash interface{}) []byte {
s := []byte(salt) s := []byte(salt)
p := append(pass, s...) p := append(pass, s...)
h := sha512.Sum512(p) h := sha512.Sum512(p)

View File

@@ -3,8 +3,8 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build cgo && go1.8 // +build cgo
// +build cgo,go1.8 // +build go1.8
package sqlite3 package sqlite3
@@ -25,12 +25,20 @@ func (c *SQLiteConn) Ping(ctx context.Context) error {
// QueryContext implement QueryerContext. // QueryContext implement QueryerContext.
func (c *SQLiteConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { func (c *SQLiteConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
return c.query(ctx, query, args) list := make([]namedValue, len(args))
for i, nv := range args {
list[i] = namedValue(nv)
}
return c.query(ctx, query, list)
} }
// ExecContext implement ExecerContext. // ExecContext implement ExecerContext.
func (c *SQLiteConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { func (c *SQLiteConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
return c.exec(ctx, query, args) list := make([]namedValue, len(args))
for i, nv := range args {
list[i] = namedValue(nv)
}
return c.exec(ctx, query, list)
} }
// PrepareContext implement ConnPrepareContext. // PrepareContext implement ConnPrepareContext.
@@ -45,10 +53,18 @@ func (c *SQLiteConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver
// QueryContext implement QueryerContext. // QueryContext implement QueryerContext.
func (s *SQLiteStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { func (s *SQLiteStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
return s.query(ctx, args) list := make([]namedValue, len(args))
for i, nv := range args {
list[i] = namedValue(nv)
}
return s.query(ctx, list)
} }
// ExecContext implement ExecerContext. // ExecContext implement ExecerContext.
func (s *SQLiteStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { func (s *SQLiteStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
return s.exec(ctx, args) list := make([]namedValue, len(args))
for i, nv := range args {
list[i] = namedValue(nv)
}
return s.exec(ctx, list)
} }

View File

@@ -3,7 +3,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build libsqlite3
// +build libsqlite3 // +build libsqlite3
package sqlite3 package sqlite3
@@ -11,13 +10,10 @@ package sqlite3
/* /*
#cgo CFLAGS: -DUSE_LIBSQLITE3 #cgo CFLAGS: -DUSE_LIBSQLITE3
#cgo linux LDFLAGS: -lsqlite3 #cgo linux LDFLAGS: -lsqlite3
#cgo darwin,amd64 LDFLAGS: -L/usr/local/opt/sqlite/lib -lsqlite3 #cgo darwin LDFLAGS: -L/usr/local/opt/sqlite/lib -lsqlite3
#cgo darwin,amd64 CFLAGS: -I/usr/local/opt/sqlite/include #cgo darwin CFLAGS: -I/usr/local/opt/sqlite/include
#cgo darwin,arm64 LDFLAGS: -L/opt/homebrew/opt/sqlite/lib -lsqlite3
#cgo darwin,arm64 CFLAGS: -I/opt/homebrew/opt/sqlite/include
#cgo openbsd LDFLAGS: -lsqlite3 #cgo openbsd LDFLAGS: -lsqlite3
#cgo solaris LDFLAGS: -lsqlite3 #cgo solaris LDFLAGS: -lsqlite3
#cgo windows LDFLAGS: -lsqlite3 #cgo windows LDFLAGS: -lsqlite3
#cgo zos LDFLAGS: -lsqlite3
*/ */
import "C" import "C"

View File

@@ -3,14 +3,13 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !sqlite_omit_load_extension
// +build !sqlite_omit_load_extension // +build !sqlite_omit_load_extension
package sqlite3 package sqlite3
/* /*
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h" #include <sqlite3-binding.h>
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif

View File

@@ -3,7 +3,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build sqlite_omit_load_extension
// +build sqlite_omit_load_extension // +build sqlite_omit_load_extension
package sqlite3 package sqlite3

View File

@@ -4,7 +4,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build sqlite_allow_uri_authority
// +build sqlite_allow_uri_authority // +build sqlite_allow_uri_authority
package sqlite3 package sqlite3

View File

@@ -4,8 +4,8 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !windows && sqlite_app_armor // +build !windows
// +build !windows,sqlite_app_armor // +build sqlite_app_armor
package sqlite3 package sqlite3

View File

@@ -1,4 +1,3 @@
//go:build sqlite_column_metadata
// +build sqlite_column_metadata // +build sqlite_column_metadata
package sqlite3 package sqlite3

View File

@@ -4,7 +4,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build sqlite_foreign_keys
// +build sqlite_foreign_keys // +build sqlite_foreign_keys
package sqlite3 package sqlite3

View File

@@ -3,7 +3,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build sqlite_fts5 || fts5
// +build sqlite_fts5 fts5 // +build sqlite_fts5 fts5
package sqlite3 package sqlite3

View File

@@ -3,7 +3,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build sqlite_icu || icu
// +build sqlite_icu icu // +build sqlite_icu icu
package sqlite3 package sqlite3
@@ -11,10 +10,8 @@ package sqlite3
/* /*
#cgo LDFLAGS: -licuuc -licui18n #cgo LDFLAGS: -licuuc -licui18n
#cgo CFLAGS: -DSQLITE_ENABLE_ICU #cgo CFLAGS: -DSQLITE_ENABLE_ICU
#cgo darwin,amd64 CFLAGS: -I/usr/local/opt/icu4c/include #cgo darwin CFLAGS: -I/usr/local/opt/icu4c/include
#cgo darwin,amd64 LDFLAGS: -L/usr/local/opt/icu4c/lib #cgo darwin LDFLAGS: -L/usr/local/opt/icu4c/lib
#cgo darwin,arm64 CFLAGS: -I/opt/homebrew/opt/icu4c/include
#cgo darwin,arm64 LDFLAGS: -L/opt/homebrew/opt/icu4c/lib
#cgo openbsd LDFLAGS: -lsqlite3 #cgo openbsd LDFLAGS: -lsqlite3
*/ */
import "C" import "C"

View File

@@ -4,7 +4,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build sqlite_introspect
// +build sqlite_introspect // +build sqlite_introspect
package sqlite3 package sqlite3

View File

@@ -0,0 +1,13 @@
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// +build sqlite_json sqlite_json1 json1
package sqlite3
/*
#cgo CFLAGS: -DSQLITE_ENABLE_JSON1
*/
import "C"

View File

@@ -1,15 +0,0 @@
// Copyright (C) 2022 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
//go:build sqlite_math_functions
// +build sqlite_math_functions
package sqlite3
/*
#cgo CFLAGS: -DSQLITE_ENABLE_MATH_FUNCTIONS
#cgo LDFLAGS: -lm
*/
import "C"

View File

@@ -1,15 +0,0 @@
// Copyright (C) 2022 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
//go:build sqlite_os_trace
// +build sqlite_os_trace
package sqlite3
/*
#cgo CFLAGS: -DSQLITE_FORCE_OS_TRACE=1
#cgo CFLAGS: -DSQLITE_DEBUG_OS_TRACE=1
*/
import "C"

View File

@@ -4,7 +4,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build cgo
// +build cgo // +build cgo
package sqlite3 package sqlite3

View File

@@ -4,7 +4,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build sqlite_preupdate_hook
// +build sqlite_preupdate_hook // +build sqlite_preupdate_hook
package sqlite3 package sqlite3
@@ -14,7 +13,7 @@ package sqlite3
#cgo LDFLAGS: -lm #cgo LDFLAGS: -lm
#ifndef USE_LIBSQLITE3 #ifndef USE_LIBSQLITE3
#include "sqlite3-binding.h" #include <sqlite3-binding.h>
#else #else
#include <sqlite3.h> #include <sqlite3.h>
#endif #endif
@@ -34,7 +33,7 @@ import (
// The callback is passed a SQLitePreUpdateData struct with the data for // The callback is passed a SQLitePreUpdateData struct with the data for
// the update, as well as methods for fetching copies of impacted data. // the update, as well as methods for fetching copies of impacted data.
// //
// If there is an existing preupdate hook for this connection, it will be // If there is an existing update hook for this connection, it will be
// removed. If callback is nil the existing hook (if any) will be removed // removed. If callback is nil the existing hook (if any) will be removed
// without creating a new one. // without creating a new one.
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) { func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) {
@@ -55,10 +54,10 @@ func (d *SQLitePreUpdateData) Count() int {
return int(C.sqlite3_preupdate_count(d.Conn.db)) return int(C.sqlite3_preupdate_count(d.Conn.db))
} }
func (d *SQLitePreUpdateData) row(dest []any, new bool) error { func (d *SQLitePreUpdateData) row(dest []interface{}, new bool) error {
for i := 0; i < d.Count() && i < len(dest); i++ { for i := 0; i < d.Count() && i < len(dest); i++ {
var val *C.sqlite3_value var val *C.sqlite3_value
var src any var src interface{}
// Initially I tried making this just a function pointer argument, but // Initially I tried making this just a function pointer argument, but
// it's absurdly complicated to pass C function pointers. // it's absurdly complicated to pass C function pointers.
@@ -96,7 +95,7 @@ func (d *SQLitePreUpdateData) row(dest []any, new bool) error {
// Old populates dest with the row data to be replaced. This works similar to // Old populates dest with the row data to be replaced. This works similar to
// database/sql's Rows.Scan() // database/sql's Rows.Scan()
func (d *SQLitePreUpdateData) Old(dest ...any) error { func (d *SQLitePreUpdateData) Old(dest ...interface{}) error {
if d.Op == SQLITE_INSERT { if d.Op == SQLITE_INSERT {
return errors.New("There is no old row for INSERT operations") return errors.New("There is no old row for INSERT operations")
} }
@@ -105,7 +104,7 @@ func (d *SQLitePreUpdateData) Old(dest ...any) error {
// New populates dest with the replacement row data. This works similar to // New populates dest with the replacement row data. This works similar to
// database/sql's Rows.Scan() // database/sql's Rows.Scan()
func (d *SQLitePreUpdateData) New(dest ...any) error { func (d *SQLitePreUpdateData) New(dest ...interface{}) error {
if d.Op == SQLITE_DELETE { if d.Op == SQLITE_DELETE {
return errors.New("There is no new row for DELETE operations") return errors.New("There is no new row for DELETE operations")
} }

View File

@@ -4,7 +4,6 @@
// Use of this source code is governed by an MIT-style // Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !sqlite_preupdate_hook && cgo
// +build !sqlite_preupdate_hook,cgo // +build !sqlite_preupdate_hook,cgo
package sqlite3 package sqlite3
@@ -14,7 +13,7 @@ package sqlite3
// The callback is passed a SQLitePreUpdateData struct with the data for // The callback is passed a SQLitePreUpdateData struct with the data for
// the update, as well as methods for fetching copies of impacted data. // the update, as well as methods for fetching copies of impacted data.
// //
// If there is an existing preupdate hook for this connection, it will be // If there is an existing update hook for this connection, it will be
// removed. If callback is nil the existing hook (if any) will be removed // removed. If callback is nil the existing hook (if any) will be removed
// without creating a new one. // without creating a new one.
func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) { func (c *SQLiteConn) RegisterPreUpdateHook(callback func(SQLitePreUpdateData)) {

Some files were not shown because too many files have changed in this diff Show More