mirror of
https://github.com/nkanaev/yarr.git
synced 2025-11-07 18:09:36 +00:00
Compare commits
116 Commits
v2.4
...
7fe688e97c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fe688e97c | ||
|
|
6b02a09f75 | ||
|
|
f0d2ab6493 | ||
|
|
42ee0372fe | ||
|
|
9762e09cb3 | ||
|
|
dd8b7ab27d | ||
|
|
c348593ef4 | ||
|
|
a51da7b8ec | ||
|
|
33503f7896 | ||
|
|
da569b3321 | ||
|
|
11285e4af0 | ||
|
|
9fe02931d8 | ||
|
|
e4f9dc8c72 | ||
|
|
88ed1de58b | ||
|
|
9bc89123f8 | ||
|
|
9fb3da2b4a | ||
|
|
58bb2c22c3 | ||
|
|
29d9ec6ef1 | ||
|
|
d2224399e2 | ||
|
|
67fbed7f6b | ||
|
|
c1df3f8068 | ||
|
|
0aed9b51a9 | ||
|
|
0bd7a66086 | ||
|
|
2b6823a277 | ||
|
|
dd7ed84a6c | ||
|
|
2c6a5ca971 | ||
|
|
5bf7647cba | ||
|
|
f721034ae5 | ||
|
|
a32361fab2 | ||
|
|
572e489db6 | ||
|
|
efcb6f8bf0 | ||
|
|
7e367ef537 | ||
|
|
b9a3326a98 | ||
|
|
484b155a3c | ||
|
|
9cba4e8deb | ||
|
|
749d7b682e | ||
|
|
35850d6310 | ||
|
|
15db17d834 | ||
|
|
a0d86e884a | ||
|
|
acf97c8a3b | ||
|
|
34bf9e5160 | ||
|
|
4420f3a8ae | ||
|
|
8d2ea6cf8a | ||
|
|
e244237474 | ||
|
|
ff81c9d689 | ||
|
|
11d99f106e | ||
|
|
b8afa82a81 | ||
|
|
097a2da5cb | ||
|
|
e6d32946c1 | ||
|
|
fe4eaa4b8d | ||
|
|
48a671b285 | ||
|
|
011c9c7668 | ||
|
|
f06fc1f750 | ||
|
|
0e88d4284d | ||
|
|
1615c6869f | ||
|
|
800f43b299 | ||
|
|
15bff0a0c4 | ||
|
|
e1481f4aac | ||
|
|
7ef97ee6db | ||
|
|
d785fe4c5a | ||
|
|
5254df53dc | ||
|
|
7301eab99c | ||
|
|
ad138c3017 | ||
|
|
b09c95d7ea | ||
|
|
64611a0dd3 | ||
|
|
321ad7608f | ||
|
|
2a8b6ea935 | ||
|
|
e9cbea500b | ||
|
|
223039b2c6 | ||
|
|
7402dfc4e6 | ||
|
|
6b12715506 | ||
|
|
2dc58c5c8e | ||
|
|
0cef51c6ac | ||
|
|
2a4d974965 | ||
|
|
f71792d6a5 | ||
|
|
b571042c5d | ||
|
|
349c966c63 | ||
|
|
4a42b239cc | ||
|
|
b9b3d2350c | ||
|
|
b13cd85f0b | ||
|
|
daffd721eb | ||
|
|
24232d72e9 | ||
|
|
4983e18e23 | ||
|
|
e1954e4cba | ||
|
|
58420ae52b | ||
|
|
b01f71de1a | ||
|
|
379aaed39e | ||
|
|
dc20932060 | ||
|
|
96835ebd33 | ||
|
|
c896f779b5 | ||
|
|
5f606b1c40 | ||
|
|
9d5b8d99f7 | ||
|
|
13c047fc21 | ||
|
|
55751b3eb6 | ||
|
|
b961502a17 | ||
|
|
a895145586 | ||
|
|
5aec3b4dab | ||
|
|
d787060a24 | ||
|
|
c1a29418eb | ||
|
|
17847f999c | ||
|
|
3adcddc70c | ||
|
|
c76ff26bd6 | ||
|
|
50f8648f64 | ||
|
|
5f82a9e339 | ||
|
|
3278ba4eac | ||
|
|
9fc72f8b68 | ||
|
|
b7b707bd43 | ||
|
|
7cf27e0fde | ||
|
|
66f2a973a3 | ||
|
|
7ecbbff18a | ||
|
|
850ce195a0 | ||
|
|
479aebd023 | ||
|
|
9b178d1fb3 | ||
|
|
3ab098db5c | ||
|
|
6d16e93008 | ||
|
|
98934daee4 |
25
.github/actions/prepare/action.yml
vendored
Normal file
25
.github/actions/prepare/action.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
41
.github/workflows/build-docker.yml
vendored
Normal file
41
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
203
.github/workflows/build.yml
vendored
203
.github/workflows/build.yml
vendored
@@ -1,144 +1,143 @@
|
|||||||
name: build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ['v*', 'test*']
|
tags:
|
||||||
|
- 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@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
go-version: '^1.23'
|
||||||
- name: "Setup Go"
|
- name: Build arm64 gui
|
||||||
uses: actions/setup-go@v2
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
go-version: '^1.17'
|
id: darwin_arm64_gui
|
||||||
- name: Cache Go Modules
|
cmd: make darwin_arm64_gui
|
||||||
uses: actions/cache@v2
|
out: out/darwin_arm64_gui/yarr.app
|
||||||
|
- name: Build amd64 gui
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
id: darwin_amd64_gui
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
cmd: make darwin_amd64_gui
|
||||||
restore-keys: |
|
out: out/darwin_amd64_gui/yarr.app
|
||||||
${{ runner.os }}-go-
|
- name: Build arm64 cli
|
||||||
- name: "Build"
|
uses: ./.github/actions/prepare
|
||||||
run: make build_macos
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
with:
|
||||||
name: macos
|
id: darwin_arm64
|
||||||
path: _output/macos/yarr.app
|
cmd: make darwin_arm64
|
||||||
|
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@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
go-version: '^1.23'
|
||||||
- name: "Setup Go"
|
- name: Build amd64 gui
|
||||||
uses: actions/setup-go@v2
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
go-version: '^1.17'
|
id: windows_amd64_gui
|
||||||
- name: Cache Go Modules
|
cmd: make windows_amd64_gui
|
||||||
uses: actions/cache@v2
|
out: out/windows_amd64_gui/yarr.exe
|
||||||
|
- name: Build arm64 gui
|
||||||
|
if: false
|
||||||
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
id: windows_arm64_gui
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
cmd: make windows_arm64_gui
|
||||||
restore-keys: |
|
out: out/windows_arm64_gui/yarr.exe
|
||||||
${{ 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_linux:
|
build_multi_cli:
|
||||||
name: Build for Linux
|
name: Build for Windows/MacOS/Linux CLI
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout"
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
submodules: 'recursive'
|
go-version: '^1.23'
|
||||||
- name: "Setup Go"
|
- name: Setup Zig
|
||||||
uses: actions/setup-go@v2
|
uses: mlugg/setup-zig@v1
|
||||||
with:
|
with:
|
||||||
go-version: '^1.17'
|
version: 0.14.0
|
||||||
- name: Cache Go Modules
|
- name: Build linux/amd64
|
||||||
uses: actions/cache@v2
|
uses: ./.github/actions/prepare
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
id: linux_amd64
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
cmd: make linux_amd64
|
||||||
restore-keys: |
|
out: out/linux_amd64/yarr
|
||||||
${{ runner.os }}-go-
|
- name: Build linux/arm64
|
||||||
- name: "Build"
|
uses: ./.github/actions/prepare
|
||||||
run: make build_linux
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
with:
|
||||||
name: linux
|
id: linux_arm64
|
||||||
path: _output/linux/yarr
|
cmd: make linux_arm64
|
||||||
|
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
|
||||||
if: ${{ !contains(github.ref, 'test') }}
|
needs: [build_macos, build_windows, build_multi_cli]
|
||||||
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@v2
|
uses: actions/download-artifact@v4.1.7
|
||||||
with:
|
with:
|
||||||
path: .
|
path: .
|
||||||
- name: Preparation
|
- name: Preparation
|
||||||
run: |
|
run: |
|
||||||
|
set -ex
|
||||||
ls -R
|
ls -R
|
||||||
chmod u+x macos/Contents/MacOS/yarr
|
for tarfile in `ls **/*.tar`; do
|
||||||
chmod u+x linux/yarr
|
tar -xvf $tarfile
|
||||||
|
done
|
||||||
mv macos yarr.app && zip -r yarr-macos.zip yarr.app
|
for dir in out/*; do
|
||||||
mv windows/yarr.exe . && zip yarr-windows.zip yarr.exe
|
echo "Compressing: $dir"
|
||||||
mv linux/yarr . && zip yarr-linux.zip yarr
|
(test -d "$dir" && cd $dir && zip -r ../yarr_`basename $dir`.zip *)
|
||||||
- name: Upload MacOS
|
done
|
||||||
uses: actions/upload-release-asset@v1
|
ls out
|
||||||
|
- name: Upload Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
draft: true
|
||||||
asset_path: ./yarr-macos.zip
|
prerelease: true
|
||||||
asset_name: yarr-${{ github.ref }}-macos64.zip
|
files: |
|
||||||
asset_content_type: application/zip
|
out/*.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
|
|
||||||
|
|||||||
19
.github/workflows/test.yml
vendored
Normal file
19
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,8 @@
|
|||||||
/_output
|
/_output
|
||||||
|
/out
|
||||||
/yarr
|
/yarr
|
||||||
*.db
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
*.syso
|
*.syso
|
||||||
versioninfo.rc
|
versioninfo.rc
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
44
build.md
44
build.md
@@ -1,44 +0,0 @@
|
|||||||
## Compilation
|
|
||||||
|
|
||||||
Install `Go >= 1.17` and `GCC`. Get the source code:
|
|
||||||
|
|
||||||
git clone https://github.com/nkanaev/yarr.git
|
|
||||||
|
|
||||||
Then run one of the corresponding commands:
|
|
||||||
|
|
||||||
# create an executable for the host os
|
|
||||||
make build_macos # -> _output/macos/yarr.app
|
|
||||||
make build_linux # -> _output/linux/yarr
|
|
||||||
make build_windows # -> _output/windows/yarr.exe
|
|
||||||
|
|
||||||
# host-specific cli version (no gui)
|
|
||||||
make build_default # -> _output/yarr
|
|
||||||
|
|
||||||
# ... or start a dev server locally
|
|
||||||
make serve # starts a server at http://localhost:7070
|
|
||||||
|
|
||||||
# ... or build a docker image
|
|
||||||
docker build -t yarr .
|
|
||||||
|
|
||||||
## ARM compilation
|
|
||||||
|
|
||||||
The instructions below are to cross-compile *yarr* to `Linux/ARM*`.
|
|
||||||
|
|
||||||
Build:
|
|
||||||
|
|
||||||
docker build -t yarr.arm -f dockerfile.arm .
|
|
||||||
|
|
||||||
Test:
|
|
||||||
|
|
||||||
# inside host
|
|
||||||
docker run -it --rm yarr.arm
|
|
||||||
|
|
||||||
# then, inside container
|
|
||||||
cd /root/out
|
|
||||||
qemu-aarch64 -L /usr/aarch64-linux-gnu/ yarr.arm64
|
|
||||||
|
|
||||||
Extract files from images:
|
|
||||||
|
|
||||||
CID=$(docker create yarr.arm)
|
|
||||||
docker cp -a "$CID:/root/out" .
|
|
||||||
docker rm "$CID"
|
|
||||||
@@ -13,6 +13,7 @@ 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"
|
||||||
@@ -89,12 +90,16 @@ 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)
|
||||||
@@ -105,6 +110,7 @@ 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 {
|
||||||
@@ -131,6 +137,7 @@ 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 != "" {
|
||||||
56
doc/build.md
Normal file
56
doc/build.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
## Compilation
|
||||||
|
|
||||||
|
Prerequisies:
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
Compile:
|
||||||
|
|
||||||
|
# create cli for the host OS/architecture
|
||||||
|
make host # out/yarr
|
||||||
|
|
||||||
|
# create GUI, works only in the target OS
|
||||||
|
make windows_amd64_gui # out/windows_amd64_gui/yarr.exe
|
||||||
|
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
|
||||||
|
make linux_amd64
|
||||||
|
make linux_arm64
|
||||||
|
make linux_armv7
|
||||||
|
make windows_amd64
|
||||||
|
make windows_arm64
|
||||||
|
|
||||||
|
# ... or build a docker image
|
||||||
|
docker build -t yarr -f etc/dockerfile .
|
||||||
|
|
||||||
|
## ARM compilation
|
||||||
|
|
||||||
|
The instructions below are to cross-compile *yarr* to `Linux/ARM*`.
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
docker build -t yarr.arm -f etc/dockerfile.arm .
|
||||||
|
|
||||||
|
Test:
|
||||||
|
|
||||||
|
# inside host
|
||||||
|
docker run -it --rm yarr.arm
|
||||||
|
|
||||||
|
# then, inside container
|
||||||
|
cd /root/out
|
||||||
|
qemu-aarch64 -L /usr/aarch64-linux-gnu/ yarr.arm64
|
||||||
|
|
||||||
|
Extract files from images:
|
||||||
|
|
||||||
|
CID=$(docker create yarr.arm)
|
||||||
|
docker cp -a "$CID:/root/out" .
|
||||||
|
docker rm "$CID"
|
||||||
@@ -1,5 +1,32 @@
|
|||||||
# upcoming
|
# upcoming
|
||||||
|
|
||||||
|
- (new) serve on unix socket (thanks to @rvighne)
|
||||||
|
- (fix) smooth scrolling on iOS (thanks to gatheraled)
|
||||||
|
|
||||||
|
# v2.5 (2025-03-26)
|
||||||
|
|
||||||
|
- (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) relative article links (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) 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)
|
||||||
|
|
||||||
- (new) ARM build support (thanks to @tillcash & @fenuks)
|
- (new) ARM build support (thanks to @tillcash & @fenuks)
|
||||||
- (new) auth configuration via param or env variable (thanks to @pierreprinetti)
|
- (new) auth configuration via param or env variable (thanks to @pierreprinetti)
|
||||||
- (new) web app manifest for an app-like experience on mobile (thanks to @qbit)
|
- (new) web app manifest for an app-like experience on mobile (thanks to @qbit)
|
||||||
19
doc/fever.md
Normal file
19
doc/fever.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Fever API support
|
||||||
|
|
||||||
|
Fever API is a kind of RSS HTTP API interface, because the Fever API definition is not very clear, so the implementation of Fever server and Client may have some compatibility problems.
|
||||||
|
|
||||||
|
The Fever API implemented by Yarr is based on the Fever API spec: https://github.com/DigitalDJ/tinytinyrss-fever-plugin/blob/master/fever-api.md.
|
||||||
|
|
||||||
|
Here are some Apps that have been tested to work with yarr. Feel free to test other Clients/Apps and update the list here.
|
||||||
|
|
||||||
|
> Different apps support different URL/Address formats. Please note whether the URL entered has `http://` scheme and `/` suffix.
|
||||||
|
|
||||||
|
| App | Platforms | Config Server URL |
|
||||||
|
|:------------------------------------------------------------------------- | ---------------- |:--------------------------------------------------- |
|
||||||
|
| [Reeder](https://reederapp.com/) | MacOS<br>iOS | 127.0.0.1:7070/fever<br>http://127.0.0.1:7070/fever |
|
||||||
|
| [ReadKit](https://readkit.app/) | MacOS<br>iOS | http://127.0.0.1:7070/fever |
|
||||||
|
| [Fluent Reader](https://github.com/yang991178/fluent-reader) | MacOS<br>Windows | http://127.0.0.1:7070/fever/ |
|
||||||
|
| [Unread](https://apps.apple.com/us/app/unread-an-rss-reader/id1363637349) | iOS | http://127.0.0.1:7070/fever |
|
||||||
|
| [Fiery Feeds](https://voidstern.net/fiery-feeds) | MacOS<br>iOS | http://127.0.0.1:7070/fever |
|
||||||
|
|
||||||
|
If you are having trouble using Fever, please open an issue and @icefed, thanks.
|
||||||
68
doc/samples.yml
Normal file
68
doc/samples.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
- 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]
|
||||||
12
dockerfile
12
dockerfile
@@ -1,12 +0,0 @@
|
|||||||
FROM golang:alpine AS build
|
|
||||||
RUN apk add build-base git
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . .
|
|
||||||
RUN make build_linux
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
RUN apk add --no-cache ca-certificates && \
|
|
||||||
update-ca-certificates
|
|
||||||
COPY --from=build /src/_output/linux/yarr /usr/local/bin/yarr
|
|
||||||
EXPOSE 7070
|
|
||||||
CMD ["/usr/local/bin/yarr", "-addr", "0.0.0.0:7070", "-db", "/data/yarr.db"]
|
|
||||||
14
etc/dockerfile
Normal file
14
etc/dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM golang:alpine3.21 AS build
|
||||||
|
RUN apk add build-base git
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/root/go/pkg \
|
||||||
|
make host
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk add --no-cache ca-certificates && update-ca-certificates
|
||||||
|
COPY --from=build /src/out/yarr /usr/local/bin/yarr
|
||||||
|
EXPOSE 7070
|
||||||
|
ENTRYPOINT ["/usr/local/bin/yarr"]
|
||||||
|
CMD ["-addr", "0.0.0.0:7070", "-db", "/data/yarr.db"]
|
||||||
@@ -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.18.2.linux-amd64.tar.gz && \
|
RUN wget --quiet https://go.dev/dl/go1.24.1.linux-amd64.tar.gz && \
|
||||||
rm -rf /usr/local/go && \
|
rm -rf /usr/local/go && \
|
||||||
tar -C /usr/local -xzf go1.18.2.linux-amd64.tar.gz
|
tar -C /usr/local -xzf go1.24.1.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,18 +27,12 @@ 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 \
|
||||||
go build \
|
make host && mv out/yarr /root/out/yarr.arm64
|
||||||
-tags "sqlite_foreign_keys release linux" \
|
|
||||||
-ldflags="-s -w" \
|
|
||||||
-o /root/out/yarr.arm64 src/main.go
|
|
||||||
|
|
||||||
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 \
|
||||||
go build \
|
make host && mv out/yarr /root/out/yarr.armv7
|
||||||
-tags "sqlite_foreign_keys release linux" \
|
|
||||||
-ldflags="-s -w" \
|
|
||||||
-o /root/out/yarr.arm7 src/main.go
|
|
||||||
|
|
||||||
CMD ["/bin/bash"]
|
CMD ["/bin/bash"]
|
||||||
BIN
etc/icon.icns
Normal file
BIN
etc/icon.icns
Normal file
Binary file not shown.
BIN
etc/icon_macos.png
Normal file
BIN
etc/icon_macos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -1,5 +1,9 @@
|
|||||||
#!/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
|
||||||
@@ -9,6 +13,10 @@ Type=Application
|
|||||||
Categories=Internet;
|
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">
|
||||||
|
|||||||
62
etc/macos_package.sh
Executable file
62
etc/macos_package.sh
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#/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
|
||||||
BIN
etc/promo.png
BIN
etc/promo.png
Binary file not shown.
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 173 KiB |
89
etc/windows_versioninfo.sh
Executable file
89
etc/windows_versioninfo.sh
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/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
12
go.mod
@@ -1,11 +1,13 @@
|
|||||||
module github.com/nkanaev/yarr
|
module github.com/nkanaev/yarr
|
||||||
|
|
||||||
go 1.17
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mattn/go-sqlite3 v1.14.7
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
golang.org/x/net v0.8.0
|
golang.org/x/net v0.38.0
|
||||||
golang.org/x/sys v0.6.0
|
golang.org/x/sys v0.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.8.0 // indirect
|
require golang.org/x/text v0.23.0 // indirect
|
||||||
|
|||||||
47
go.sum
47
go.sum
@@ -1,39 +1,8 @@
|
|||||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
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/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/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/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=
|
|
||||||
|
|||||||
98
makefile
98
makefile
@@ -1,33 +1,89 @@
|
|||||||
VERSION=2.4
|
VERSION=2.5
|
||||||
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
GITHASH=$(shell git rev-parse --short=8 HEAD)
|
||||||
|
|
||||||
CGO_ENABLED=1
|
GO_TAGS = sqlite_foreign_keys sqlite_json
|
||||||
|
GO_LDFLAGS = -s -w -X 'main.Version=$(VERSION)' -X 'main.GitHash=$(GITHASH)'
|
||||||
|
|
||||||
GO_LDFLAGS = -s -w
|
GO_FLAGS = -tags "$(GO_TAGS)" -ldflags="$(GO_LDFLAGS)"
|
||||||
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"
|
||||||
|
|
||||||
build_default:
|
export CGO_ENABLED=1
|
||||||
mkdir -p _output
|
|
||||||
go build -tags "sqlite_foreign_keys release" -ldflags="$(GO_LDFLAGS)" -o _output/yarr src/main.go
|
|
||||||
|
|
||||||
build_macos:
|
default: test host
|
||||||
mkdir -p _output/macos
|
|
||||||
GOOS=darwin GOARCH=amd64 go build -tags "sqlite_foreign_keys release macos" -ldflags="$(GO_LDFLAGS)" -o _output/macos/yarr src/main.go
|
|
||||||
cp src/platform/icon.png _output/macos/icon.png
|
|
||||||
go run bin/package_macos.go -outdir _output/macos -version "$(VERSION)"
|
|
||||||
|
|
||||||
build_linux:
|
# platform-specific files
|
||||||
mkdir -p _output/linux
|
|
||||||
GOOS=linux GOARCH=amd64 go build -tags "sqlite_foreign_keys release linux" -ldflags="$(GO_LDFLAGS)" -o _output/linux/yarr src/main.go
|
|
||||||
|
|
||||||
build_windows:
|
etc/icon.icns: etc/icon_macos.png
|
||||||
mkdir -p _output/windows
|
mkdir -p etc/icon.iconset
|
||||||
go run bin/generate_versioninfo.go -version "$(VERSION)" -outfile src/platform/versioninfo.rc
|
sips -s format png --resampleWidth 1024 etc/icon_macos.png --out etc/icon.iconset/icon_512x512@2x.png
|
||||||
|
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 release windows" -ldflags="$(GO_LDFLAGS) -H windowsgui" -o _output/windows/yarr.exe src/main.go
|
|
||||||
|
# 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 -tags "sqlite_foreign_keys" src/main.go -db local.db
|
go run $(GO_FLAGS) ./cmd/yarr -db local.db
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cd src && go test -tags "sqlite_foreign_keys release" ./...
|
go test $(GO_FLAGS) ./...
|
||||||
|
|
||||||
|
.PHONY: \
|
||||||
|
host \
|
||||||
|
darwin_amd64 darwin_amd64_gui \
|
||||||
|
darwin_arm64 darwin_arm64_gui \
|
||||||
|
windows_amd64 windows_amd64_gui \
|
||||||
|
windows_arm64 windows_arm64_gui \
|
||||||
|
serve test
|
||||||
|
|||||||
30
readme.md
30
readme.md
@@ -3,7 +3,7 @@
|
|||||||
**yarr** (yet another rss reader) is a web-based feed aggregator which can be used both
|
**yarr** (yet another rss reader) is a web-based feed aggregator which can be used both
|
||||||
as a desktop application and a personal self-hosted server.
|
as a desktop application and a personal self-hosted server.
|
||||||
|
|
||||||
It is written in Go with the frontend in Vue.js. The storage is backed by SQLite.
|
The app is a single binary with an embedded database (SQLite).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -11,24 +11,28 @@ It is written in Go with the frontend in Vue.js. The storage is backed by SQLite
|
|||||||
|
|
||||||
The latest prebuilt binaries for Linux/MacOS/Windows are available
|
The latest prebuilt binaries for Linux/MacOS/Windows are available
|
||||||
[here](https://github.com/nkanaev/yarr/releases/latest).
|
[here](https://github.com/nkanaev/yarr/releases/latest).
|
||||||
|
The archives follow the naming convention `yarr_{OS}_{ARCH}[_gui].zip`, where:
|
||||||
|
|
||||||
### macos
|
* `OS` is the target operating system
|
||||||
|
* `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
|
||||||
|
|
||||||
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".
|
Usage instructions:
|
||||||
|
|
||||||
|
* MacOS: place `yarr.app` in `/Applications` folder, [open the app][macos-open], click the anchor menu bar icon, select "Open".
|
||||||
|
|
||||||
|
* Windows: open `yarr.exe`, click the anchor system tray icon, select "Open".
|
||||||
|
|
||||||
|
* Linux: 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
|
||||||
|
|
||||||
### 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).
|
|
||||||
|
|
||||||
For self-hosting, see `yarr -h` for auth, tls & server configuration flags.
|
For self-hosting, see `yarr -h` for auth, tls & server configuration flags.
|
||||||
For building from source code, see [build.md](build.md)
|
|
||||||
|
See more:
|
||||||
|
|
||||||
|
* [Building from source code](doc/build.md)
|
||||||
|
* [Fever API support](doc/fever.md)
|
||||||
|
|
||||||
## credits
|
## credits
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
//go:build release
|
|
||||||
// +build release
|
|
||||||
|
|
||||||
package assets
|
package assets
|
||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|||||||
@@ -25,18 +25,21 @@
|
|||||||
<div class="flex-grow-1"></div>
|
<div class="flex-grow-1"></div>
|
||||||
<button class="toolbar-item"
|
<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"
|
<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"
|
<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>
|
||||||
@@ -59,10 +62,12 @@
|
|||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
<header class="dropdown-header">Theme</header>
|
<header class="dropdown-header" role="heading" aria-level="2">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>
|
||||||
@@ -71,25 +76,25 @@
|
|||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
<header class="dropdown-header">Auto Refresh</header>
|
<header class="dropdown-header" role="heading" aria-level="2">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" :class="{active: !refreshRate}" @click.stop="refreshRate = 0">0</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="!refreshRate" :class="{active: !refreshRate}" @click.stop="refreshRate = 0">0</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 10}" @click.stop="refreshRate = 10">10m</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 10" :class="{active: refreshRate == 10}" @click.stop="refreshRate = 10">10m</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 30}" @click.stop="refreshRate = 30">30m</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 30" :class="{active: refreshRate == 30}" @click.stop="refreshRate = 30">30m</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 60}" @click.stop="refreshRate = 60">1h</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 60" :class="{active: refreshRate == 60}" @click.stop="refreshRate = 60">1h</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 120}" @click.stop="refreshRate = 120">2h</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 120" :class="{active: refreshRate == 120}" @click.stop="refreshRate = 120">2h</button>
|
||||||
<button class="dropdown-item col-4 px-0" :class="{active: refreshRate == 240}" @click.stop="refreshRate = 240">4h</button>
|
<button class="dropdown-item col-4 px-0" :aria-pressed="refreshRate == 240" :class="{active: refreshRate == 240}" @click.stop="refreshRate = 240">4h</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
|
|
||||||
<header class="dropdown-header">Show first</header>
|
<header class="dropdown-header" role="heading" aria-level="2">Show first</header>
|
||||||
<div class="d-flex text-center">
|
<div class="d-flex text-center">
|
||||||
<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=true">New</button>
|
||||||
<button class="dropdown-item px-0" :class="{active: !itemSortNewestFirst}" @click.stop="itemSortNewestFirst=false">Old</button>
|
<button class="dropdown-item px-0" :aria-pressed="!itemSortNewestFirst" :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">Subscriptions</header>
|
<header class="dropdown-header" role="heading" aria-level="2">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"
|
||||||
@@ -117,7 +122,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</dropdown>
|
</dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div id="feed-list-scroll" class="p-2 overflow-auto border-top flex-grow-1">
|
<div id="feed-list-scroll" class="p-2 overflow-auto scroll-touch 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">
|
||||||
@@ -206,12 +211,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">{{ current.feed.title }}</header>
|
<header class="dropdown-header" role="heading" aria-level="2">{{ current.feed.title }}</header>
|
||||||
<a class="dropdown-item" :href="current.feed.link" target="_blank" v-if="current.feed.link">
|
<a class="dropdown-item" :href="current.feed.link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" 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" target="_blank" v-if="current.feed.feed_link">
|
<a class="dropdown-item" :href="current.feed.feed_link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" 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>
|
||||||
@@ -220,8 +225,12 @@
|
|||||||
<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">Move to...</header>
|
<header class="dropdown-header" role="heading" aria-level="2">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"
|
||||||
@@ -251,7 +260,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">{{ current.folder.title }}</header>
|
<header class="dropdown-header" role="heading" aria-level="2">{{ 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
|
||||||
@@ -263,7 +272,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</dropdown>
|
</dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div id="item-list-scroll" class="p-2 overflow-auto border-top flex-grow-1" v-scroll="loadMoreItems" ref="itemlist">
|
<div id="item-list-scroll" class="p-2 overflow-auto scroll-touch 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">
|
||||||
@@ -322,29 +331,45 @@
|
|||||||
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" target="_blank" title="Open Link">
|
<a class="toolbar-item" :href="itemSelectedDetails.link" rel="noopener noreferrer" target="_blank" referrerpolicy="no-referrer" 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"
|
class="content px-4 pt-3 pb-5 border-top overflow-auto scroll-touch"
|
||||||
: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>{{ (feedsById[itemSelectedDetails.feed_id] || {}).title }}</div>
|
<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">
|
||||||
<img :src="itemSelectedDetails.image" v-if="itemSelectedDetails.image" class="mb-3">
|
<div v-if="contentImages.length">
|
||||||
<audio class="w-100" controls v-if="itemSelectedDetails.podcast_url" :src="itemSelectedDetails.podcast_url"></audio>
|
<figure v-for="media in contentImages">
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -2,6 +2,26 @@
|
|||||||
|
|
||||||
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() {
|
||||||
@@ -278,6 +298,18 @@ 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')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'theme': {
|
'theme': {
|
||||||
@@ -407,7 +439,7 @@ var vm = new Vue({
|
|||||||
vm.feeds = values[1]
|
vm.feeds = values[1]
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
refreshItems: function(loadMore) {
|
refreshItems: function(loadMore = false) {
|
||||||
if (this.feedSelected === null) {
|
if (this.feedSelected === null) {
|
||||||
vm.items = []
|
vm.items = []
|
||||||
vm.itemsHasMore = false
|
vm.itemsHasMore = false
|
||||||
@@ -420,7 +452,7 @@ var vm = new Vue({
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.loading.items = true
|
this.loading.items = true
|
||||||
api.items.list(query).then(function(data) {
|
return 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 {
|
||||||
@@ -443,13 +475,17 @@ 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()) this.refreshItems(true)
|
if (this.itemListCloseToBottom()) return 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()
|
||||||
@@ -523,6 +559,14 @@ 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) {
|
||||||
@@ -675,6 +719,65 @@ 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
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +1,4 @@
|
|||||||
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')
|
||||||
@@ -92,7 +17,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')
|
window.open(vm.itemSelectedDetails.link, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleReadability: function() {
|
toggleReadability: function() {
|
||||||
@@ -118,16 +43,16 @@ var shortcutFunctions = {
|
|||||||
document.getElementById("searchbar").focus()
|
document.getElementById("searchbar").focus()
|
||||||
},
|
},
|
||||||
nextItem(){
|
nextItem(){
|
||||||
helperFunctions.navigateToItem(+1)
|
vm.navigateToItem(+1)
|
||||||
},
|
},
|
||||||
previousItem() {
|
previousItem() {
|
||||||
helperFunctions.navigateToItem(-1)
|
vm.navigateToItem(-1)
|
||||||
},
|
},
|
||||||
nextFeed(){
|
nextFeed(){
|
||||||
helperFunctions.navigateToFeed(+1)
|
vm.navigateToFeed(+1)
|
||||||
},
|
},
|
||||||
previousFeed() {
|
previousFeed() {
|
||||||
helperFunctions.navigateToFeed(-1)
|
vm.navigateToFeed(-1)
|
||||||
},
|
},
|
||||||
scrollForward: function() {
|
scrollForward: function() {
|
||||||
helperFunctions.scrollContent(+1)
|
helperFunctions.scrollContent(+1)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="theme-{% .settings.theme_name %}">
|
||||||
<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 %}
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ 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 {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package htmlutil
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Any(els []string, el string, match func(string, string) bool) bool {
|
func Any(els []string, el string, match func(string, string) bool) bool {
|
||||||
@@ -31,3 +32,7 @@ func URLDomain(val string) string {
|
|||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsAPossibleLink(val string) bool {
|
||||||
|
return strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
@@ -61,3 +62,16 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,3 +24,21 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`}
|
return []string{"loading"}, []string{`loading="lazy"`, `referrerpolicy="no-referrer"`}
|
||||||
default:
|
default:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ 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>`
|
||||||
output := Sanitize("http://example.org/", input)
|
want := `<p>This is a <strong>text</strong> with an image: <img src="http://example.org/" alt="Test" loading="lazy" referrerpolicy="no-referrer">.</p>`
|
||||||
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if input != output {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, input, output)
|
t.Errorf("Wrong output: \nwant: %#v\nhave: %#v", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,31 +28,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">`
|
||||||
expected := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy">`
|
want := `<img src="data:image/gif;base64,test" alt="Example" loading="lazy" referrerpolicy="no-referrer">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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">`
|
||||||
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">`
|
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">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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">`
|
||||||
expected := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example" loading="lazy">`
|
want := `<img srcset="data:image/gif;base64,test" src="http://example.org/example-320w.jpg" alt="Example" loading="lazy" referrerpolicy="no-referrer">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,16 +68,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">`
|
||||||
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">`
|
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">`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if output != expected {
|
if have != want {
|
||||||
t.Errorf(`Wrong output: %s`, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelfClosingTags(t *testing.T) {
|
func TestSelfClosingTags(t *testing.T) {
|
||||||
input := `<p>This <br> is a <strong>text</strong> <br/>with an image: <img src="http://example.org/" alt="Test" loading="lazy"/>.</p>`
|
input := `<p>This <br> is a <strong>text</strong><br/>.</p>`
|
||||||
output := Sanitize("http://example.org/", input)
|
output := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if input != output {
|
if input != output {
|
||||||
@@ -95,11 +96,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"/>`
|
||||||
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"/>`
|
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"/>`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if want != have {
|
||||||
t.Errorf(`Wrong output: "%s" != "%s"`, expected, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +166,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>`
|
||||||
expected := `<iframe src="http://example.org/" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
|
want := `<iframe src="http://example.org/" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe>`
|
||||||
output := Sanitize("http://example.org/", input)
|
have := Sanitize("http://example.org/", input)
|
||||||
|
|
||||||
if expected != output {
|
if want != have {
|
||||||
t.Errorf("Wrong output:\nwant: %s\nhave: %s", expected, output)
|
t.Errorf("Wrong output:\nwant: %s\nhave: %s", want, have)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package scraper
|
package scraper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nkanaev/yarr/src/content/htmlutil"
|
"github.com/nkanaev/yarr/src/content/htmlutil"
|
||||||
@@ -35,6 +36,18 @@ 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"html"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -47,6 +46,8 @@ type atomLinks []atomLink
|
|||||||
func (a *atomText) Text() string {
|
func (a *atomText) Text() string {
|
||||||
if a.Type == "html" {
|
if a.Type == "html" {
|
||||||
return htmlutil.ExtractText(a.Data)
|
return htmlutil.ExtractText(a.Data)
|
||||||
|
} else if a.Type == "xhtml" {
|
||||||
|
return htmlutil.ExtractText(a.XML)
|
||||||
}
|
}
|
||||||
return a.Data
|
return a.Data
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,7 @@ func (a *atomText) String() string {
|
|||||||
if a.Type == "xhtml" {
|
if a.Type == "xhtml" {
|
||||||
data = a.XML
|
data = a.XML
|
||||||
}
|
}
|
||||||
return html.UnescapeString(strings.TrimSpace(data))
|
return strings.TrimSpace(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (links atomLinks) First(rel string) string {
|
func (links atomLinks) First(rel string) string {
|
||||||
@@ -81,15 +82,23 @@ func ParseAtom(r io.Reader) (*Feed, error) {
|
|||||||
SiteURL: firstNonEmpty(srcfeed.Links.First("alternate"), srcfeed.Links.First("")),
|
SiteURL: firstNonEmpty(srcfeed.Links.First("alternate"), srcfeed.Links.First("")),
|
||||||
}
|
}
|
||||||
for _, srcitem := range srcfeed.Entries {
|
for _, srcitem := range srcfeed.Entries {
|
||||||
link := firstNonEmpty(srcitem.OrigLink, srcitem.Links.First("alternate"), srcitem.Links.First(""))
|
linkFromID := ""
|
||||||
|
guidFromID := ""
|
||||||
|
if htmlutil.IsAPossibleLink(srcitem.ID) {
|
||||||
|
linkFromID = srcitem.ID
|
||||||
|
guidFromID = srcitem.ID + "::" + srcitem.Updated
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaLinks := srcitem.mediaLinks()
|
||||||
|
|
||||||
|
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(srcitem.ID, link),
|
GUID: firstNonEmpty(guidFromID, srcitem.ID, link),
|
||||||
Date: dateParse(firstNonEmpty(srcitem.Published, srcitem.Updated)),
|
Date: dateParse(firstNonEmpty(srcitem.Published, srcitem.Updated)),
|
||||||
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()),
|
||||||
ImageURL: srcitem.firstMediaThumbnail(),
|
MediaLinks: mediaLinks,
|
||||||
AudioURL: "",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return dstfeed, nil
|
return dstfeed, nil
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ 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: "",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -94,6 +92,44 @@ func TestAtomHTMLTitle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAtomXHTMLTitle(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<entry><title type="xhtml">say <code>what</code>?</entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items[0].Title
|
||||||
|
want := "say what?"
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomXHTMLNestedTitle(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<entry>
|
||||||
|
<title type="xhtml">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<a href="https://example.com">Link to Example</a>
|
||||||
|
</div>
|
||||||
|
</title>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items[0].Title
|
||||||
|
want := "Link to Example"
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAtomImageLink(t *testing.T) {
|
func TestAtomImageLink(t *testing.T) {
|
||||||
feed, _ := Parse(strings.NewReader(`
|
feed, _ := Parse(strings.NewReader(`
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
@@ -103,9 +139,15 @@ func TestAtomImageLink(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>
|
</feed>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].ImageURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := `https://example.com/image.png?width=100&height=100`
|
t.Fatalf("Expected 1 media link, got: %#v", feed.Items[0].MediaLinks)
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +169,68 @@ 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 feed.Items[0].ImageURL != "" {
|
if len(feed.Items[0].MediaLinks) != 0 {
|
||||||
t.Fatal("item.image_url must be unset if present in the content")
|
t.Fatal("item media link must be excluded if present in the content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomLinkInID(t *testing.T) {
|
||||||
|
feed, _ := Parse(strings.NewReader(`
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
|
||||||
|
<entry>
|
||||||
|
<title>one updated</title>
|
||||||
|
<id>https://example.com/posts/1</id>
|
||||||
|
<updated>2003-12-13T09:17:51</updated>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>two</title>
|
||||||
|
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<title>one</title>
|
||||||
|
<id>https://example.com/posts/1</id>
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items
|
||||||
|
want := []Item{
|
||||||
|
Item{
|
||||||
|
GUID: "https://example.com/posts/1::2003-12-13T09:17:51",
|
||||||
|
Date: time.Date(2003, time.December, 13, 9, 17, 51, 0, time.UTC),
|
||||||
|
URL: "https://example.com/posts/1",
|
||||||
|
Title: "one updated",
|
||||||
|
},
|
||||||
|
Item{
|
||||||
|
GUID: "urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6",
|
||||||
|
Date: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), URL: "",
|
||||||
|
Title: "two",
|
||||||
|
},
|
||||||
|
Item{
|
||||||
|
GUID: "https://example.com/posts/1::",
|
||||||
|
Date: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
URL: "https://example.com/posts/1",
|
||||||
|
Title: "one",
|
||||||
|
Content: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(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">&lt;script&gt;alert(1);&lt;/script&gt;</summary></entry>
|
||||||
|
</feed>
|
||||||
|
`))
|
||||||
|
have := feed.Items[0].Content
|
||||||
|
want := "<script>alert(1);</script>"
|
||||||
|
if !reflect.DeepEqual(want, have) {
|
||||||
|
t.Logf("want: %#v", want)
|
||||||
|
t.Logf("have: %#v", have)
|
||||||
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -119,6 +120,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,11 +134,14 @@ 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 item.ImageURL != "" && strings.Contains(item.Content, item.ImageURL) {
|
if len(feed.Items[i].MediaLinks) > 0 {
|
||||||
feed.Items[i].ImageURL = ""
|
mediaLinks := make([]MediaLink, 0)
|
||||||
|
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].AudioURL = ""
|
feed.Items[i].MediaLinks = mediaLinks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,3 +173,12 @@ 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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,3 +150,32 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
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"`
|
||||||
@@ -8,12 +12,17 @@ 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 {
|
||||||
@@ -22,7 +31,7 @@ type mediaThumbnail struct {
|
|||||||
|
|
||||||
type mediaDescription struct {
|
type mediaDescription struct {
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
Description string `xml:",chardata"`
|
Text string `xml:",chardata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *media) firstMediaThumbnail() string {
|
func (m *media) firstMediaThumbnail() string {
|
||||||
@@ -44,12 +53,59 @@ 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.Description)
|
return plain2html(d.Text)
|
||||||
}
|
}
|
||||||
for _, g := range m.MediaGroups {
|
for _, g := range m.MediaGroups {
|
||||||
for _, d := range g.MediaDescriptions {
|
for _, d := range g.MediaDescriptions {
|
||||||
return plain2html(d.Description)
|
return plain2html(d.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ type Item struct {
|
|||||||
Title string
|
Title string
|
||||||
|
|
||||||
Content string
|
Content string
|
||||||
ImageURL string
|
MediaLinks []MediaLink
|
||||||
AudioURL string
|
}
|
||||||
|
|
||||||
|
type MediaLink struct {
|
||||||
|
URL string
|
||||||
|
Type string
|
||||||
|
Description string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
podcastURL := ""
|
mediaLinks := srcitem.mediaLinks()
|
||||||
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,9 +96,8 @@ 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),
|
Content: firstNonEmpty(srcitem.ContentEncoded, srcitem.Description, srcitem.firstMediaDescription()),
|
||||||
AudioURL: podcastURL,
|
MediaLinks: mediaLinks,
|
||||||
ImageURL: srcitem.firstMediaThumbnail(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return dstfeed, nil
|
return dstfeed, nil
|
||||||
|
|||||||
@@ -75,9 +75,15 @@ func TestRSSMediaContentThumbnail(t *testing.T) {
|
|||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].ImageURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := "https://i.vimeocdn.com/video/1092705247_960.jpg"
|
t.Fatalf("Expected 1 media link, got %#v", feed.Items[0].MediaLinks)
|
||||||
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()
|
||||||
@@ -127,9 +133,15 @@ func TestRSSPodcast(t *testing.T) {
|
|||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].AudioURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := "http://example.com/audio.ext"
|
t.Fatal("Invalid media links")
|
||||||
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()
|
||||||
@@ -147,9 +159,15 @@ func TestRSSOpusPodcast(t *testing.T) {
|
|||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
`))
|
`))
|
||||||
have := feed.Items[0].AudioURL
|
if len(feed.Items[0].MediaLinks) != 1 {
|
||||||
want := "http://example.com/audio.ext"
|
t.Fatal("Invalid media links")
|
||||||
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()
|
||||||
@@ -176,8 +194,9 @@ 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 != "" {
|
|
||||||
t.Fatal("item.audio_url must be unset if present in the content")
|
if len(feed.Items[0].MediaLinks) != 0 {
|
||||||
|
t.Fatal("item media must be excluded if present in the content")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +242,47 @@ func TestRSSIsPermalink(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for i := 0; i < len(want); i++ {
|
for i := 0; i < len(want); i++ {
|
||||||
if want[i] != have[i] {
|
if !reflect.DeepEqual(want, have) {
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build macos || windows
|
//go:build (darwin || windows) && gui
|
||||||
// +build macos windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
@@ -11,6 +10,7 @@ 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()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !windows && !macos
|
//go:build !gui
|
||||||
// +build !windows,!macos
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build macos
|
//go:build darwin && gui
|
||||||
// +build macos
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build windows
|
//go:build windows && gui
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !windows && !darwin
|
//go:build linux || freebsd
|
||||||
// +build !windows,!darwin
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build darwin
|
//go:build darwin
|
||||||
// +build darwin
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ 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 {
|
||||||
Username string
|
Username string
|
||||||
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 {
|
||||||
@@ -20,10 +22,12 @@ func unsafeMethod(method string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Middleware) Handler(c *router.Context) {
|
func (m *Middleware) Handler(c *router.Context) {
|
||||||
if strings.HasPrefix(c.Req.URL.Path, m.BasePath+m.Public) {
|
for _, path := range m.Public {
|
||||||
|
if strings.HasPrefix(c.Req.URL.Path, m.BasePath+path) {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if IsAuthenticated(c.Req, m.Username, m.Password) {
|
if IsAuthenticated(c.Req, m.Username, m.Password) {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
@@ -44,12 +48,15 @@ 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]string{
|
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]interface{}{
|
||||||
"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"), nil)
|
c.HTML(http.StatusOK, assets.Template("login.html"), map[string]interface{}{
|
||||||
|
"settings": m.DB.GetSettings(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
393
src/server/fever.go
Normal file
393
src/server/fever.go
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nkanaev/yarr/src/server/auth"
|
||||||
|
"github.com/nkanaev/yarr/src/server/router"
|
||||||
|
"github.com/nkanaev/yarr/src/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeverGroup struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverFeedsGroup struct {
|
||||||
|
GroupID int64 `json:"group_id"`
|
||||||
|
FeedIDs string `json:"feed_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverFeed struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
FaviconID int64 `json:"favicon_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
SiteUrl string `json:"site_url"`
|
||||||
|
IsSpark int `json:"is_spark"`
|
||||||
|
LastUpdated int64 `json:"last_updated_on_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverItem struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
FeedID int64 `json:"feed_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
HTML string `json:"html"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
IsSaved int `json:"is_saved"`
|
||||||
|
IsRead int `json:"is_read"`
|
||||||
|
CreatedAt int64 `json:"created_on_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeverFavicon struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFeverJSON(c *router.Context, data map[string]interface{}, lastRefreshed int64) {
|
||||||
|
data["api_version"] = 3
|
||||||
|
data["auth"] = 1
|
||||||
|
data["last_refreshed_on_time"] = lastRefreshed
|
||||||
|
c.JSON(http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLastRefreshedOnTime(httpStates map[int64]storage.HTTPState) int64 {
|
||||||
|
if len(httpStates) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastRefreshed int64
|
||||||
|
for _, state := range httpStates {
|
||||||
|
if state.LastRefreshed.Unix() > lastRefreshed {
|
||||||
|
lastRefreshed = state.LastRefreshed.Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastRefreshed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverAuth(c *router.Context) bool {
|
||||||
|
if s.Username != "" && s.Password != "" {
|
||||||
|
apiKey := c.Req.FormValue("api_key")
|
||||||
|
apiKey = strings.ToLower(apiKey)
|
||||||
|
md5HashValue := md5.Sum([]byte(fmt.Sprintf("%s:%s", s.Username, s.Password)))
|
||||||
|
hexMD5HashValue := fmt.Sprintf("%x", md5HashValue[:])
|
||||||
|
if !auth.StringsEqual(apiKey, hexMD5HashValue) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func formHasValue(values url.Values, value string) bool {
|
||||||
|
if _, ok := values[value]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleFever(c *router.Context) {
|
||||||
|
c.Req.ParseForm()
|
||||||
|
if !s.feverAuth(c) {
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"api_version": 3,
|
||||||
|
"auth": 0,
|
||||||
|
"last_refreshed_on_time": 0,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case formHasValue(c.Req.Form, "groups"):
|
||||||
|
s.feverGroupsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "feeds"):
|
||||||
|
s.feverFeedsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "unread_item_ids"):
|
||||||
|
s.feverUnreadItemIDsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "saved_item_ids"):
|
||||||
|
s.feverSavedItemIDsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "favicons"):
|
||||||
|
s.feverFaviconsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "items"):
|
||||||
|
s.feverItemsHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "links"):
|
||||||
|
s.feverLinksHandler(c)
|
||||||
|
case formHasValue(c.Req.Form, "mark"):
|
||||||
|
s.feverMarkHandler(c)
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"api_version": 3,
|
||||||
|
"auth": 1,
|
||||||
|
"last_refreshed_on_time": getLastRefreshedOnTime(s.db.ListHTTPStates()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinInts(values []int64) string {
|
||||||
|
var result strings.Builder
|
||||||
|
for i, val := range values {
|
||||||
|
fmt.Fprintf(&result, "%d", val)
|
||||||
|
if i != len(values)-1 {
|
||||||
|
result.WriteString(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func feedGroups(db *storage.Storage) []*FeverFeedsGroup {
|
||||||
|
feeds := db.ListFeeds()
|
||||||
|
|
||||||
|
groupFeeds := make(map[int64][]int64)
|
||||||
|
for _, feed := range feeds {
|
||||||
|
if feed.FolderId == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groupFeeds[*feed.FolderId] = append(groupFeeds[*feed.FolderId], feed.Id)
|
||||||
|
}
|
||||||
|
result := make([]*FeverFeedsGroup, 0)
|
||||||
|
for groupId, feedIds := range groupFeeds {
|
||||||
|
result = append(result, &FeverFeedsGroup{
|
||||||
|
GroupID: groupId,
|
||||||
|
FeedIDs: joinInts(feedIds),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverGroupsHandler(c *router.Context) {
|
||||||
|
folders := s.db.ListFolders()
|
||||||
|
groups := make([]*FeverGroup, len(folders))
|
||||||
|
for i, folder := range folders {
|
||||||
|
groups[i] = &FeverGroup{ID: folder.Id, Title: folder.Title}
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"groups": groups,
|
||||||
|
"feeds_groups": feedGroups(s.db),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverFeedsHandler(c *router.Context) {
|
||||||
|
feeds := s.db.ListFeeds()
|
||||||
|
httpStates := s.db.ListHTTPStates()
|
||||||
|
|
||||||
|
feverFeeds := make([]*FeverFeed, len(feeds))
|
||||||
|
for i, feed := range feeds {
|
||||||
|
var lastUpdated int64
|
||||||
|
if state, ok := httpStates[feed.Id]; ok {
|
||||||
|
lastUpdated = state.LastRefreshed.Unix()
|
||||||
|
}
|
||||||
|
feverFeeds[i] = &FeverFeed{
|
||||||
|
ID: feed.Id,
|
||||||
|
FaviconID: feed.Id,
|
||||||
|
Title: feed.Title,
|
||||||
|
Url: feed.FeedLink,
|
||||||
|
SiteUrl: feed.Link,
|
||||||
|
IsSpark: 0,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"feeds": feverFeeds,
|
||||||
|
"feeds_groups": feedGroups(s.db),
|
||||||
|
}, getLastRefreshedOnTime(httpStates))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverFaviconsHandler(c *router.Context) {
|
||||||
|
feeds := s.db.ListFeeds()
|
||||||
|
favicons := make([]*FeverFavicon, len(feeds))
|
||||||
|
for i, feed := range feeds {
|
||||||
|
data := "data:image/gif;base64,R0lGODlhAQABAAAAACw="
|
||||||
|
if feed.HasIcon {
|
||||||
|
icon := s.db.GetFeed(feed.Id).Icon
|
||||||
|
data = fmt.Sprintf(
|
||||||
|
"data:%s;base64,%s",
|
||||||
|
http.DetectContentType(*icon),
|
||||||
|
base64.StdEncoding.EncodeToString(*icon),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
favicons[i] = &FeverFavicon{ID: feed.Id, Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"favicons": favicons,
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// for memory pressure reasons, we only return a limited number of items
|
||||||
|
// documented at https://github.com/DigitalDJ/tinytinyrss-fever-plugin/blob/master/fever-api.md#items
|
||||||
|
const listLimit = 50
|
||||||
|
|
||||||
|
func (s *Server) feverItemsHandler(c *router.Context) {
|
||||||
|
filter := storage.ItemFilter{}
|
||||||
|
query := c.Req.URL.Query()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case query.Get("with_ids") != "":
|
||||||
|
ids := make([]int64, 0)
|
||||||
|
for _, idstr := range strings.Split(query.Get("with_ids"), ",") {
|
||||||
|
if idnum, err := strconv.ParseInt(idstr, 10, 64); err == nil {
|
||||||
|
ids = append(ids, idnum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter.IDs = &ids
|
||||||
|
case query.Get("since_id") != "":
|
||||||
|
idstr := query.Get("since_id")
|
||||||
|
if idnum, err := strconv.ParseInt(idstr, 10, 64); err == nil {
|
||||||
|
filter.SinceID = &idnum
|
||||||
|
}
|
||||||
|
case query.Get("max_id") != "":
|
||||||
|
idstr := query.Get("max_id")
|
||||||
|
if idnum, err := strconv.ParseInt(idstr, 10, 64); err == nil {
|
||||||
|
filter.MaxID = &idnum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := s.db.ListItems(filter, listLimit, true, true)
|
||||||
|
|
||||||
|
feverItems := make([]FeverItem, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
date := item.Date
|
||||||
|
time := date.Unix()
|
||||||
|
|
||||||
|
isSaved := 0
|
||||||
|
if item.Status == storage.STARRED {
|
||||||
|
isSaved = 1
|
||||||
|
}
|
||||||
|
isRead := 0
|
||||||
|
if item.Status == storage.READ {
|
||||||
|
isRead = 1
|
||||||
|
}
|
||||||
|
feverItems[i] = FeverItem{
|
||||||
|
ID: item.Id,
|
||||||
|
FeedID: item.FeedId,
|
||||||
|
Title: item.Title,
|
||||||
|
Author: "",
|
||||||
|
HTML: item.Content,
|
||||||
|
Url: item.Link,
|
||||||
|
IsSaved: isSaved,
|
||||||
|
IsRead: isRead,
|
||||||
|
CreatedAt: time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalItems := s.db.CountItems(storage.ItemFilter{})
|
||||||
|
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"items": feverItems,
|
||||||
|
"total_items": totalItems,
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverLinksHandler(c *router.Context) {
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"links": make([]interface{}, 0),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverUnreadItemIDsHandler(c *router.Context) {
|
||||||
|
status := storage.UNREAD
|
||||||
|
itemIds := make([]int64, 0)
|
||||||
|
|
||||||
|
itemFilter := storage.ItemFilter{
|
||||||
|
Status: &status,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
items := s.db.ListItems(itemFilter, listLimit, true, false)
|
||||||
|
if len(items) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
itemIds = append(itemIds, item.Id)
|
||||||
|
}
|
||||||
|
itemFilter.After = &items[len(items)-1].Id
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"unread_item_ids": joinInts(itemIds),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverSavedItemIDsHandler(c *router.Context) {
|
||||||
|
status := storage.STARRED
|
||||||
|
itemIds := make([]int64, 0)
|
||||||
|
|
||||||
|
itemFilter := storage.ItemFilter{
|
||||||
|
Status: &status,
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
items := s.db.ListItems(itemFilter, listLimit, true, false)
|
||||||
|
if len(items) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
itemIds = append(itemIds, item.Id)
|
||||||
|
}
|
||||||
|
itemFilter.After = &items[len(items)-1].Id
|
||||||
|
}
|
||||||
|
writeFeverJSON(c, map[string]interface{}{
|
||||||
|
"saved_item_ids": joinInts(itemIds),
|
||||||
|
}, getLastRefreshedOnTime(s.db.ListHTTPStates()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) feverMarkHandler(c *router.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Req.Form.Get("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("invalid id:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Req.Form.Get("mark") {
|
||||||
|
case "item":
|
||||||
|
var status storage.ItemStatus
|
||||||
|
switch c.Req.Form.Get("as") {
|
||||||
|
case "read":
|
||||||
|
status = storage.READ
|
||||||
|
case "unread":
|
||||||
|
status = storage.UNREAD
|
||||||
|
case "saved":
|
||||||
|
status = storage.STARRED
|
||||||
|
case "unsaved":
|
||||||
|
status = storage.READ
|
||||||
|
default:
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.db.UpdateItemStatus(id, status)
|
||||||
|
case "feed":
|
||||||
|
if c.Req.Form.Get("as") != "read" {
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
markFilter := storage.MarkFilter{FeedID: &id}
|
||||||
|
x, _ := strconv.ParseInt(c.Req.Form.Get("before"), 10, 64)
|
||||||
|
if x > 0 {
|
||||||
|
before := time.Unix(x, 0)
|
||||||
|
markFilter.Before = &before
|
||||||
|
}
|
||||||
|
s.db.MarkItemsRead(markFilter)
|
||||||
|
case "group":
|
||||||
|
if c.Req.Form.Get("as") != "read" {
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
markFilter := storage.MarkFilter{FolderID: &id}
|
||||||
|
x, _ := strconv.ParseInt(c.Req.Form.Get("before"), 10, 64)
|
||||||
|
if x > 0 {
|
||||||
|
before := time.Unix(x, 0)
|
||||||
|
markFilter.Before = &before
|
||||||
|
}
|
||||||
|
s.db.MarkItemsRead(markFilter)
|
||||||
|
default:
|
||||||
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"api_version": 3,
|
||||||
|
"auth": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/nkanaev/yarr/src/assets"
|
"github.com/nkanaev/yarr/src/assets"
|
||||||
|
"github.com/nkanaev/yarr/src/content/htmlutil"
|
||||||
"github.com/nkanaev/yarr/src/content/readability"
|
"github.com/nkanaev/yarr/src/content/readability"
|
||||||
"github.com/nkanaev/yarr/src/content/sanitizer"
|
"github.com/nkanaev/yarr/src/content/sanitizer"
|
||||||
"github.com/nkanaev/yarr/src/content/silo"
|
"github.com/nkanaev/yarr/src/content/silo"
|
||||||
@@ -33,7 +34,8 @@ 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: "/static",
|
Public: []string{"/static", "/fever"},
|
||||||
|
DB: s.db,
|
||||||
}
|
}
|
||||||
r.Use(a.Handler)
|
r.Use(a.Handler)
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ func (s *Server) handler() http.Handler {
|
|||||||
r.For("/opml/export", s.handleOPMLExport)
|
r.For("/opml/export", s.handleOPMLExport)
|
||||||
r.For("/page", s.handlePageCrawl)
|
r.For("/page", s.handlePageCrawl)
|
||||||
r.For("/logout", s.handleLogout)
|
r.For("/logout", s.handleLogout)
|
||||||
|
r.For("/fever/", s.handleFever)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -84,7 +87,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": s.BasePath,
|
"start_url": "/" + strings.TrimPrefix(s.BasePath, "/"),
|
||||||
"icons": []map[string]interface{}{
|
"icons": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"src": s.BasePath + "/static/graphicarts/favicon.png",
|
"src": s.BasePath + "/static/graphicarts/favicon.png",
|
||||||
@@ -291,6 +294,11 @@ 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)
|
||||||
@@ -312,7 +320,18 @@ func (s *Server) handleItem(c *router.Context) {
|
|||||||
c.Out.WriteHeader(http.StatusBadRequest)
|
c.Out.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runtime fix for relative links
|
||||||
|
if !htmlutil.IsAPossibleLink(item.Link) {
|
||||||
|
if feed := s.db.GetFeed(item.FeedId); feed != nil {
|
||||||
|
item.Link = htmlutil.AbsoluteUrl(item.Link, feed.Link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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" {
|
||||||
@@ -355,12 +374,19 @@ 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)
|
items := s.db.ListItems(filter, perPage+1, newestFirst, true)
|
||||||
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,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ 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"
|
||||||
@@ -53,14 +56,31 @@ func (s *Server) Start() {
|
|||||||
s.worker.RefreshFeeds()
|
s.worker.RefreshFeeds()
|
||||||
}
|
}
|
||||||
|
|
||||||
httpserver := &http.Server{Addr: s.Addr, Handler: s.handler()}
|
var ln net.Listener
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if s.CertFile != "" && s.KeyFile != "" {
|
|
||||||
err = httpserver.ListenAndServeTLS(s.CertFile, s.KeyFile)
|
if path, isUnix := strings.CutPrefix(s.Addr, "unix:"); isUnix {
|
||||||
} else {
|
err = os.Remove(path)
|
||||||
err = httpserver.ListenAndServe()
|
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 != "" {
|
||||||
|
err = httpserver.ServeTLS(ln, s.CertFile, s.KeyFile)
|
||||||
|
ln.Close()
|
||||||
|
} else {
|
||||||
|
err = httpserver.Serve(ln)
|
||||||
|
}
|
||||||
|
|
||||||
if err != http.ErrServerClosed {
|
if err != http.ErrServerClosed {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,18 +20,19 @@ func (s *Storage) CreateFeed(title, description, link, feedLink string, folderId
|
|||||||
if title == "" {
|
if title == "" {
|
||||||
title = feedLink
|
title = feedLink
|
||||||
}
|
}
|
||||||
result, err := s.db.Exec(`
|
row := s.db.QueryRow(`
|
||||||
insert into feeds (title, description, link, feed_link, folder_id)
|
insert into feeds (title, description, link, feed_link, folder_id)
|
||||||
values (?, ?, ?, ?, ?)
|
values (?, ?, ?, ?, ?)
|
||||||
on conflict (feed_link) do update set folder_id=?`,
|
on conflict (feed_link) do update set folder_id = ?
|
||||||
|
returning id`,
|
||||||
title, description, link, feedLink, folderId,
|
title, description, link, feedLink, folderId,
|
||||||
folderId,
|
folderId,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var id int64
|
||||||
|
err := row.Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
log.Print(err)
|
||||||
}
|
|
||||||
id, idErr := result.LastInsertId()
|
|
||||||
if idErr != nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &Feed{
|
return &Feed{
|
||||||
@@ -70,6 +71,11 @@ 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
|
||||||
|
|||||||
@@ -17,6 +17,23 @@ func TestCreateFeed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateFeedSameLink(t *testing.T) {
|
||||||
|
db := testDB()
|
||||||
|
feed1 := db.CreateFeed("title", "", "", "http://example1.com/feed.xml", nil)
|
||||||
|
if feed1 == nil || feed1.Id == 0 {
|
||||||
|
t.Fatal("expected feed")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
db.CreateFeed("title", "", "", "http://example2.com/feed.xml", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
feed2 := db.CreateFeed("title", "", "http://example.com", "http://example1.com/feed.xml", nil)
|
||||||
|
if feed1.Id != feed2.Id {
|
||||||
|
t.Fatalf("expected the same feed.\nwant: %#v\nhave: %#v", feed1, feed2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReadFeed(t *testing.T) {
|
func TestReadFeed(t *testing.T) {
|
||||||
db := testDB()
|
db := testDB()
|
||||||
if db.GetFeed(100500) != nil {
|
if db.GetFeed(100500) != nil {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,35 +12,21 @@ type Folder struct {
|
|||||||
|
|
||||||
func (s *Storage) CreateFolder(title string) *Folder {
|
func (s *Storage) CreateFolder(title string) *Folder {
|
||||||
expanded := true
|
expanded := true
|
||||||
result, err := s.db.Exec(`
|
row := s.db.QueryRow(`
|
||||||
insert into folders (title, is_expanded) values (?, ?)
|
insert into folders (title, is_expanded) values (?, ?)
|
||||||
on conflict (title) do nothing`,
|
on conflict (title) do update set title = ?
|
||||||
|
returning id`,
|
||||||
title, expanded,
|
title, expanded,
|
||||||
|
// provide title again so that we can extract row id
|
||||||
|
title,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var id int64
|
var id int64
|
||||||
numrows, err := result.RowsAffected()
|
err := row.Scan(&id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if numrows == 1 {
|
|
||||||
id, err = result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = s.db.QueryRow(`select id, is_expanded from folders where title=?`, title).Scan(&id, &expanded)
|
|
||||||
if err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &Folder{Id: id, Title: title, IsExpanded: expanded}
|
return &Folder{Id: id, Title: title, IsExpanded: expanded}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -43,6 +45,25 @@ 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 {
|
||||||
|
if data, ok := src.([]byte); ok {
|
||||||
|
return json.Unmarshal(data, m)
|
||||||
|
}
|
||||||
|
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"`
|
||||||
@@ -52,8 +73,7 @@ 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"`
|
||||||
ImageURL *string `json:"image"`
|
MediaLinks MediaLinks `json:"media_links"`
|
||||||
AudioURL *string `json:"podcast_url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ItemFilter struct {
|
type ItemFilter struct {
|
||||||
@@ -62,11 +82,35 @@ type ItemFilter struct {
|
|||||||
Status *ItemStatus
|
Status *ItemStatus
|
||||||
Search *string
|
Search *string
|
||||||
After *int64
|
After *int64
|
||||||
|
IDs *[]int64
|
||||||
|
SinceID *int64
|
||||||
|
MaxID *int64
|
||||||
|
Before *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarkFilter struct {
|
type MarkFilter struct {
|
||||||
FolderID *int64
|
FolderID *int64
|
||||||
FeedID *int64
|
FeedID *int64
|
||||||
|
|
||||||
|
Before *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemList []Item
|
||||||
|
|
||||||
|
func (list ItemList) Len() int {
|
||||||
|
return len(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list ItemList) SortKey(i int) string {
|
||||||
|
return list[i].Date.Format(time.RFC3339) + "::" + list[i].GUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list ItemList) Less(i, j int) bool {
|
||||||
|
return list.SortKey(i) < list.SortKey(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list ItemList) Swap(i, j int) {
|
||||||
|
list[i], list[j] = list[j], list[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) CreateItems(items []Item) bool {
|
func (s *Storage) CreateItems(items []Item) bool {
|
||||||
@@ -78,17 +122,24 @@ func (s *Storage) CreateItems(items []Item) bool {
|
|||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
for _, item := range items {
|
itemsSorted := ItemList(items)
|
||||||
|
sort.Sort(itemsSorted)
|
||||||
|
|
||||||
|
for _, item := range itemsSorted {
|
||||||
_, 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, image, podcast_url,
|
content, media_links,
|
||||||
date_arrived, status
|
date_arrived, status
|
||||||
)
|
)
|
||||||
values (?, ?, ?, ?, strftime('%Y-%m-%d %H:%M:%f', ?), ?, ?, ?, ?, ?)
|
values (
|
||||||
|
?, ?, ?, ?, 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.ImageURL, item.AudioURL,
|
item.Content, item.MediaLinks,
|
||||||
now, UNREAD,
|
now, UNREAD,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -140,6 +191,28 @@ func listQueryPredicate(filter ItemFilter, newestFirst bool) (string, []interfac
|
|||||||
cond = append(cond, fmt.Sprintf("(i.date, i.id) %s (select date, id from items where id = ?)", compare))
|
cond = append(cond, fmt.Sprintf("(i.date, i.id) %s (select date, id from items where id = ?)", compare))
|
||||||
args = append(args, *filter.After)
|
args = append(args, *filter.After)
|
||||||
}
|
}
|
||||||
|
if filter.IDs != nil && len(*filter.IDs) > 0 {
|
||||||
|
qmarks := make([]string, len(*filter.IDs))
|
||||||
|
idargs := make([]interface{}, len(*filter.IDs))
|
||||||
|
for i, id := range *filter.IDs {
|
||||||
|
qmarks[i] = "?"
|
||||||
|
idargs[i] = id
|
||||||
|
}
|
||||||
|
cond = append(cond, "i.id in ("+strings.Join(qmarks, ",")+")")
|
||||||
|
args = append(args, idargs...)
|
||||||
|
}
|
||||||
|
if filter.SinceID != nil {
|
||||||
|
cond = append(cond, "i.id > ?")
|
||||||
|
args = append(args, filter.SinceID)
|
||||||
|
}
|
||||||
|
if filter.MaxID != nil {
|
||||||
|
cond = append(cond, "i.id < ?")
|
||||||
|
args = append(args, filter.MaxID)
|
||||||
|
}
|
||||||
|
if filter.Before != nil {
|
||||||
|
cond = append(cond, "i.date < ?")
|
||||||
|
args = append(args, filter.Before)
|
||||||
|
}
|
||||||
|
|
||||||
predicate := "1"
|
predicate := "1"
|
||||||
if len(cond) > 0 {
|
if len(cond) > 0 {
|
||||||
@@ -149,7 +222,24 @@ func listQueryPredicate(filter ItemFilter, newestFirst bool) (string, []interfac
|
|||||||
return predicate, args
|
return predicate, args
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool) []Item {
|
func (s *Storage) CountItems(filter ItemFilter) int {
|
||||||
|
predicate, args := listQueryPredicate(filter, false)
|
||||||
|
|
||||||
|
var count int
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
select count(*)
|
||||||
|
from items
|
||||||
|
where %s
|
||||||
|
`, predicate)
|
||||||
|
err := s.db.QueryRow(query, args...).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool, withContent bool) []Item {
|
||||||
predicate, args := listQueryPredicate(filter, newestFirst)
|
predicate, args := listQueryPredicate(filter, newestFirst)
|
||||||
result := make([]Item, 0, 0)
|
result := make([]Item, 0, 0)
|
||||||
|
|
||||||
@@ -157,17 +247,26 @@ func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool) []It
|
|||||||
if !newestFirst {
|
if !newestFirst {
|
||||||
order = "date asc, id asc"
|
order = "date asc, id asc"
|
||||||
}
|
}
|
||||||
|
if filter.IDs != nil || filter.SinceID != nil {
|
||||||
|
order = "i.id asc"
|
||||||
|
}
|
||||||
|
if filter.MaxID != nil {
|
||||||
|
order = "i.id desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCols := "i.id, i.guid, i.feed_id, i.title, i.link, i.date, i.status, i.media_links"
|
||||||
|
if withContent {
|
||||||
|
selectCols += ", i.content"
|
||||||
|
} else {
|
||||||
|
selectCols += ", '' as content"
|
||||||
|
}
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
select
|
select %s
|
||||||
i.id, i.guid, i.feed_id,
|
|
||||||
i.title, i.link, i.date,
|
|
||||||
i.status, i.image, i.podcast_url
|
|
||||||
from items i
|
from items i
|
||||||
where %s
|
where %s
|
||||||
order by %s
|
order by %s
|
||||||
limit %d
|
limit %d
|
||||||
`, predicate, order, limit)
|
`, selectCols, predicate, order, limit)
|
||||||
rows, err := s.db.Query(query, args...)
|
rows, err := s.db.Query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -178,7 +277,7 @@ func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool) []It
|
|||||||
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.ImageURL, &x.AudioURL,
|
&x.Status, &x.MediaLinks, &x.Content,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -194,12 +293,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.image, i.podcast_url
|
i.date, i.status, i.media_links
|
||||||
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.ImageURL, &i.AudioURL,
|
&i.Date, &i.Status, &i.MediaLinks,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Print(err)
|
log.Print(err)
|
||||||
@@ -214,7 +313,11 @@ func (s *Storage) UpdateItemStatus(item_id int64, status ItemStatus) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) MarkItemsRead(filter MarkFilter) bool {
|
func (s *Storage) MarkItemsRead(filter MarkFilter) bool {
|
||||||
predicate, args := listQueryPredicate(ItemFilter{FolderID: filter.FolderID, FeedID: filter.FeedID}, false)
|
predicate, args := listQueryPredicate(ItemFilter{
|
||||||
|
FolderID: filter.FolderID,
|
||||||
|
FeedID: filter.FeedID,
|
||||||
|
Before: filter.Before,
|
||||||
|
}, false)
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
update items as i set status = %d
|
update items as i set status = %d
|
||||||
where %s and i.status != %d
|
where %s and i.status != %d
|
||||||
|
|||||||
@@ -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.image, i.podcast_url
|
i.date, i.status, i.media_links
|
||||||
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.ImageURL, &i.AudioURL,
|
&i.Date, &i.Status, &i.MediaLinks,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -104,7 +104,7 @@ func TestListItems(t *testing.T) {
|
|||||||
|
|
||||||
// filter by folder_id
|
// filter by folder_id
|
||||||
|
|
||||||
have := getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder1.Id}, 10, false))
|
have := getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder1.Id}, 10, false, false))
|
||||||
want := []string{"item111", "item112", "item113", "item121", "item122"}
|
want := []string{"item111", "item112", "item113", "item121", "item122"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -112,7 +112,7 @@ func TestListItems(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder2.Id}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{FolderID: &scope.folder2.Id}, 10, false, false))
|
||||||
want = []string{"item211", "item212"}
|
want = []string{"item211", "item212"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -122,7 +122,7 @@ func TestListItems(t *testing.T) {
|
|||||||
|
|
||||||
// filter by feed_id
|
// filter by feed_id
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed11.Id}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed11.Id}, 10, false, false))
|
||||||
want = []string{"item111", "item112", "item113"}
|
want = []string{"item111", "item112", "item113"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -130,7 +130,7 @@ func TestListItems(t *testing.T) {
|
|||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed01.Id}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{FeedID: &scope.feed01.Id}, 10, false, false))
|
||||||
want = []string{"item011", "item012", "item013"}
|
want = []string{"item011", "item012", "item013"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -141,7 +141,7 @@ func TestListItems(t *testing.T) {
|
|||||||
// filter by status
|
// filter by status
|
||||||
|
|
||||||
var starred ItemStatus = STARRED
|
var starred ItemStatus = STARRED
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{Status: &starred}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{Status: &starred}, 10, false, false))
|
||||||
want = []string{"item113", "item212", "item013"}
|
want = []string{"item113", "item212", "item013"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -150,7 +150,7 @@ func TestListItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var unread ItemStatus = UNREAD
|
var unread ItemStatus = UNREAD
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{Status: &unread}, 10, false))
|
have = getItemGuids(db.ListItems(ItemFilter{Status: &unread}, 10, false, false))
|
||||||
want = []string{"item111", "item121", "item011"}
|
want = []string{"item111", "item121", "item011"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -160,7 +160,7 @@ func TestListItems(t *testing.T) {
|
|||||||
|
|
||||||
// limit
|
// limit
|
||||||
|
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{}, 2, false))
|
have = getItemGuids(db.ListItems(ItemFilter{}, 2, false, false))
|
||||||
want = []string{"item111", "item112"}
|
want = []string{"item111", "item112"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -171,7 +171,7 @@ func TestListItems(t *testing.T) {
|
|||||||
// filter by search
|
// filter by search
|
||||||
db.SyncSearch()
|
db.SyncSearch()
|
||||||
search1 := "title111"
|
search1 := "title111"
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{Search: &search1}, 4, true))
|
have = getItemGuids(db.ListItems(ItemFilter{Search: &search1}, 4, true, false))
|
||||||
want = []string{"item111"}
|
want = []string{"item111"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -180,7 +180,7 @@ func TestListItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sort by date
|
// sort by date
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{}, 4, true))
|
have = getItemGuids(db.ListItems(ItemFilter{}, 4, true, false))
|
||||||
want = []string{"item013", "item012", "item011", "item212"}
|
want = []string{"item013", "item012", "item011", "item212"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -197,7 +197,7 @@ func TestListItemsPaginated(t *testing.T) {
|
|||||||
item121 := getItem(db, "item121")
|
item121 := getItem(db, "item121")
|
||||||
|
|
||||||
// all, newest first
|
// all, newest first
|
||||||
have := getItemGuids(db.ListItems(ItemFilter{After: &item012.Id}, 3, true))
|
have := getItemGuids(db.ListItems(ItemFilter{After: &item012.Id}, 3, true, false))
|
||||||
want := []string{"item011", "item212", "item211"}
|
want := []string{"item011", "item212", "item211"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -207,7 +207,7 @@ func TestListItemsPaginated(t *testing.T) {
|
|||||||
|
|
||||||
// unread, newest first
|
// unread, newest first
|
||||||
unread := UNREAD
|
unread := UNREAD
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{After: &item012.Id, Status: &unread}, 3, true))
|
have = getItemGuids(db.ListItems(ItemFilter{After: &item012.Id, Status: &unread}, 3, true, false))
|
||||||
want = []string{"item011", "item121", "item111"}
|
want = []string{"item011", "item121", "item111"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -217,7 +217,7 @@ func TestListItemsPaginated(t *testing.T) {
|
|||||||
|
|
||||||
// starred, oldest first
|
// starred, oldest first
|
||||||
starred := STARRED
|
starred := STARRED
|
||||||
have = getItemGuids(db.ListItems(ItemFilter{After: &item121.Id, Status: &starred}, 3, false))
|
have = getItemGuids(db.ListItems(ItemFilter{After: &item121.Id, Status: &starred}, 3, false, false))
|
||||||
want = []string{"item212", "item013"}
|
want = []string{"item212", "item013"}
|
||||||
if !reflect.DeepEqual(have, want) {
|
if !reflect.DeepEqual(have, want) {
|
||||||
t.Logf("want: %#v", want)
|
t.Logf("want: %#v", want)
|
||||||
@@ -233,7 +233,7 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
db1 := testDB()
|
db1 := testDB()
|
||||||
testItemsSetup(db1)
|
testItemsSetup(db1)
|
||||||
db1.MarkItemsRead(MarkFilter{})
|
db1.MarkItemsRead(MarkFilter{})
|
||||||
have := getItemGuids(db1.ListItems(ItemFilter{Status: &read}, 10, false))
|
have := getItemGuids(db1.ListItems(ItemFilter{Status: &read}, 10, false, false))
|
||||||
want := []string{
|
want := []string{
|
||||||
"item111", "item112", "item121", "item122",
|
"item111", "item112", "item121", "item122",
|
||||||
"item211", "item011", "item012",
|
"item211", "item011", "item012",
|
||||||
@@ -247,7 +247,7 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
db2 := testDB()
|
db2 := testDB()
|
||||||
scope2 := testItemsSetup(db2)
|
scope2 := testItemsSetup(db2)
|
||||||
db2.MarkItemsRead(MarkFilter{FolderID: &scope2.folder1.Id})
|
db2.MarkItemsRead(MarkFilter{FolderID: &scope2.folder1.Id})
|
||||||
have = getItemGuids(db2.ListItems(ItemFilter{Status: &read}, 10, false))
|
have = getItemGuids(db2.ListItems(ItemFilter{Status: &read}, 10, false, false))
|
||||||
want = []string{
|
want = []string{
|
||||||
"item111", "item112", "item121", "item122",
|
"item111", "item112", "item121", "item122",
|
||||||
"item211", "item012",
|
"item211", "item012",
|
||||||
@@ -261,7 +261,7 @@ func TestMarkItemsRead(t *testing.T) {
|
|||||||
db3 := testDB()
|
db3 := testDB()
|
||||||
scope3 := testItemsSetup(db3)
|
scope3 := testItemsSetup(db3)
|
||||||
db3.MarkItemsRead(MarkFilter{FeedID: &scope3.feed11.Id})
|
db3.MarkItemsRead(MarkFilter{FeedID: &scope3.feed11.Id})
|
||||||
have = getItemGuids(db3.ListItems(ItemFilter{Status: &read}, 10, false))
|
have = getItemGuids(db3.ListItems(ItemFilter{Status: &read}, 10, false, false))
|
||||||
want = []string{
|
want = []string{
|
||||||
"item111", "item112", "item122",
|
"item111", "item112", "item122",
|
||||||
"item211", "item012",
|
"item211", "item012",
|
||||||
@@ -319,7 +319,7 @@ func TestDeleteOldItems(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
db.DeleteOldItems()
|
db.DeleteOldItems()
|
||||||
feedItems := db.ListItems(ItemFilter{FeedID: &feed.Id}, 1000, false)
|
feedItems := db.ListItems(ItemFilter{FeedID: &feed.Id}, 1000, false, false)
|
||||||
if len(feedItems) != len(items)-3 {
|
if len(feedItems) != len(items)-3 {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"invalid number of old items kept\nwant: %d\nhave: %d",
|
"invalid number of old items kept\nwant: %d\nhave: %d",
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ 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
|
||||||
db.QueryRow("pragma user_version").Scan(&version)
|
if err := db.QueryRow("pragma user_version").Scan(&version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if version >= maxVersion {
|
if version >= maxVersion {
|
||||||
return nil
|
return nil
|
||||||
@@ -294,3 +298,34 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,14 +13,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build darwin || windows
|
||||||
// +build darwin windows
|
// +build darwin windows
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build never
|
||||||
// +build never
|
// +build never
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build darwin
|
||||||
// +build darwin
|
// +build darwin
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package systray
|
package systray
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ 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,
|
||||||
|
|||||||
@@ -143,13 +143,9 @@ 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
|
||||||
var audioURL *string = nil
|
mediaLinks := make(storage.MediaLinks, 0)
|
||||||
if item.AudioURL != "" {
|
for _, link := range item.MediaLinks {
|
||||||
audioURL = &item.AudioURL
|
mediaLinks = append(mediaLinks, storage.MediaLink(link))
|
||||||
}
|
|
||||||
var imageURL *string = nil
|
|
||||||
if item.ImageURL != "" {
|
|
||||||
imageURL = &item.ImageURL
|
|
||||||
}
|
}
|
||||||
result[i] = storage.Item{
|
result[i] = storage.Item{
|
||||||
GUID: item.GUID,
|
GUID: item.GUID,
|
||||||
@@ -159,8 +155,7 @@ 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,
|
||||||
ImageURL: imageURL,
|
MediaLinks: mediaLinks,
|
||||||
AudioURL: audioURL,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
155
vendor/github.com/mattn/go-sqlite3/README.md
generated
vendored
155
vendor/github.com/mattn/go-sqlite3/README.md
generated
vendored
@@ -1,23 +1,23 @@
|
|||||||
go-sqlite3
|
go-sqlite3
|
||||||
==========
|
==========
|
||||||
|
|
||||||
[](http://godoc.org/github.com/mattn/go-sqlite3)
|
[](https://pkg.go.dev/github.com/mattn/go-sqlite3)
|
||||||
[](https://github.com/mattn/go-sqlite3/actions?query=workflow%3AGo)
|
[](https://github.com/mattn/go-sqlite3/actions?query=workflow%3AGo)
|
||||||
[](https://opencollective.com/mattn-go-sqlite3)
|
[](https://opencollective.com/mattn-go-sqlite3)
|
||||||
[](https://codecov.io/gh/mattn/go-sqlite3)
|
[](https://codecov.io/gh/mattn/go-sqlite3)
|
||||||
[](https://goreportcard.com/report/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
|
||||||
|
|
||||||
sqlite3 driver conforming to the built-in database/sql interface
|
A sqlite3 driver that conforms 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 @@ Supported Golang version: See [.github/workflows/go.yaml](./.github/workflows/go
|
|||||||
- [Alpine](#alpine)
|
- [Alpine](#alpine)
|
||||||
- [Fedora](#fedora)
|
- [Fedora](#fedora)
|
||||||
- [Ubuntu](#ubuntu)
|
- [Ubuntu](#ubuntu)
|
||||||
- [Mac OSX](#mac-osx)
|
- [macOS](#mac-osx)
|
||||||
- [Windows](#windows)
|
- [Windows](#windows)
|
||||||
- [Errors](#errors)
|
- [Errors](#errors)
|
||||||
- [User Authentication](#user-authentication)
|
- [User Authentication](#user-authentication)
|
||||||
@@ -64,7 +64,7 @@ Supported Golang version: See [.github/workflows/go.yaml](./.github/workflows/go
|
|||||||
|
|
||||||
# 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` compile 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` compiler 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 string. (Data Source Name).
|
This is also known as a DSN (Data Source Name) string.
|
||||||
|
|
||||||
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 seperated by an `?` (Question Mark).
|
The database filename and options are separated 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,24 +138,23 @@ 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 for more information about build tags / constraints.](https://golang.org/pkg/go/build/#hdr-Build_Constraints)
|
Click [here](https://golang.org/pkg/go/build/#hdr-Build_Constraints) for more information about build tags / constraints.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
If you wish to build this library with additional extensions / features.
|
If you wish to build this library with additional extensions / features, use the following command:
|
||||||
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 delimted.
|
When using multiple build tags, all the different tags should be space delimited.
|
||||||
|
|
||||||
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
|
||||||
@@ -166,6 +165,7 @@ 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,17 +173,20 @@ 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 `CGO_ENABLED=1` ennvironment variable if not set by default, and the presence of the `gcc` compiler.
|
This package requires the `CGO_ENABLED=1` environment 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
|
||||||
|
|
||||||
@@ -191,14 +194,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++ \
|
||||||
@@ -216,15 +219,14 @@ 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 MAC OSX
|
## Cross Compiling from macOS
|
||||||
The simplest way to cross compile from OSX is to use [xgo](https://github.com/karalabe/xgo).
|
The simplest way to cross compile from macOS is to use [xgo](https://github.com/karalabe/xgo).
|
||||||
|
|
||||||
Steps:
|
Steps:
|
||||||
- Install [xgo](https://github.com/karalabe/xgo) (`go get github.com/karalabe/xgo`).
|
- Install [musl-cross](https://github.com/FiloSottile/homebrew-musl-cross) (`brew install FiloSottile/musl-cross/musl-cross`).
|
||||||
- Ensure that your project is within your `GOPATH`.
|
- 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"`.
|
||||||
- Run `xgo local/path/to/project`.
|
|
||||||
|
|
||||||
Please refer to the project's [README](https://github.com/karalabe/xgo/blob/master/README.md) for further information.
|
Please refer to the project's [README](https://github.com/FiloSottile/homebrew-musl-cross#readme) for further information.
|
||||||
|
|
||||||
# Google Cloud Platform
|
# Google Cloud Platform
|
||||||
|
|
||||||
@@ -234,23 +236,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
|
||||||
@@ -268,34 +270,43 @@ sudo yum groupinstall "Development Tools" "Development Libraries"
|
|||||||
sudo apt-get install build-essential
|
sudo apt-get install build-essential
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mac OSX
|
## macOS
|
||||||
|
|
||||||
OSX should have all the tools present to compile this package, if not install XCode this will add all the developers tools.
|
macOS should have all the tools present to compile this package. If not, install XCode to add all the developers tools.
|
||||||
|
|
||||||
Required dependency
|
Required dependency:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install sqlite3
|
brew install sqlite3
|
||||||
```
|
```
|
||||||
|
|
||||||
For OSX there is an additional package install which is required if you wish to build the `icu` extension.
|
For macOS, there is an additional package to 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 Mac OSX.
|
To compile for macOS on x86:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build --tags "darwin"
|
go build -tags "darwin amd64"
|
||||||
```
|
```
|
||||||
|
|
||||||
If you wish to link directly to libsqlite3 then you can use the `libsqlite3` build tag.
|
To compile for macOS on ARM chips:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags "darwin arm64"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you wish to link directly to libsqlite3, use the `libsqlite3` build tag:
|
||||||
|
|
||||||
```
|
```
|
||||||
go build --tags "libsqlite3 darwin"
|
# x86
|
||||||
|
go build -tags "libsqlite3 darwin amd64"
|
||||||
|
# ARM
|
||||||
|
go build -tags "libsqlite3 darwin arm64"
|
||||||
```
|
```
|
||||||
|
|
||||||
Additional information:
|
Additional information:
|
||||||
@@ -304,14 +315,14 @@ Additional information:
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
To compile this package on Windows OS you must have the `gcc` compiler installed.
|
To compile this package on Windows, you must have the `gcc` compiler installed.
|
||||||
|
|
||||||
1) Install a Windows `gcc` toolchain.
|
1) Install a Windows `gcc` toolchain.
|
||||||
2) Add the `bin` folders to the Windows path if the installer did not do this by default.
|
2) Add the `bin` folder to the Windows path, if the installer did not do this by default.
|
||||||
3) Open a terminal for the TDM-GCC toolchain, can be found in the Windows Start menu.
|
3) Open a terminal for the TDM-GCC toolchain, which 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://sourceforge.net/projects/tdm-gcc/).
|
For example the TDM-GCC Toolchain can be found [here](https://jmeubank.github.io/tdm-gcc/).
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
@@ -349,28 +360,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 on the connection string user authentication will be enabled and the provided user will be created
|
When `_auth` is present in 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 string:
|
Example connection strings:
|
||||||
|
|
||||||
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`
|
||||||
|
|
||||||
@@ -396,11 +407,11 @@ salt this can be configured with `_auth_salt`.
|
|||||||
|
|
||||||
### Restrictions
|
### Restrictions
|
||||||
|
|
||||||
Operations on the database regarding to user management can only be preformed by an administrator user.
|
Operations on the database regarding 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
|
||||||
@@ -411,7 +422,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 |
|
||||||
|----------|-----------|-------------|
|
|----------|-----------|-------------|
|
||||||
@@ -420,7 +431,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
|
||||||
@@ -441,7 +452,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 |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
@@ -452,16 +463,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
|
||||||
|
|
||||||
@@ -471,7 +482,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
|
||||||
|
|
||||||
@@ -491,7 +502,7 @@ For an example see [dinedal/go-sqlite3-extension-functions](https://github.com/d
|
|||||||
|
|
||||||
- Can I use this in multiple routines concurrently?
|
- Can I use this in multiple routines concurrently?
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
- Why I'm getting `no such table` error?
|
- Why I'm getting `no such table` error?
|
||||||
|
|
||||||
@@ -505,7 +516,7 @@ For an example see [dinedal/go-sqlite3-extension-functions](https://github.com/d
|
|||||||
|
|
||||||
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
|
||||||
@@ -515,20 +526,20 @@ For an example see [dinedal/go-sqlite3-extension-functions](https://github.com/d
|
|||||||
|
|
||||||
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`
|
||||||
|
|
||||||
@@ -537,24 +548,24 @@ For an example see [dinedal/go-sqlite3-extension-functions](https://github.com/d
|
|||||||
db, err := sql.Open("sqlite3", "file:locked.sqlite?cache=shared")
|
db, err := sql.Open("sqlite3", "file:locked.sqlite?cache=shared")
|
||||||
```
|
```
|
||||||
|
|
||||||
Second please set the database connections of the SQL package to 1.
|
Next, please set the database connections of the SQL package to 1:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
db.SetMaxOpenConns(1)
|
db.SetMaxOpenConns(1)
|
||||||
```
|
```
|
||||||
|
|
||||||
More information see [#209](https://github.com/mattn/go-sqlite3/issues/209)
|
For 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. [[Contribute](CONTRIBUTING.md)].
|
This project exists thanks to all the people who [[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](https://opencollective.com/mattn-go-sqlite3/contribute)]
|
Become a financial contributor and help us sustain our community. [[Contribute here](https://opencollective.com/mattn-go-sqlite3/contribute)].
|
||||||
|
|
||||||
#### Individuals
|
#### Individuals
|
||||||
|
|
||||||
|
|||||||
2
vendor/github.com/mattn/go-sqlite3/backup.go
generated
vendored
2
vendor/github.com/mattn/go-sqlite3/backup.go
generated
vendored
@@ -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
|
||||||
|
|||||||
29
vendor/github.com/mattn/go-sqlite3/callback.go
generated
vendored
29
vendor/github.com/mattn/go-sqlite3/callback.go
generated
vendored
@@ -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 interface{}
|
val any
|
||||||
}
|
}
|
||||||
|
|
||||||
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 interface{}) unsafe.Pointer {
|
func newHandle(db *SQLiteConn, v any) 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) interface{} {
|
func lookupHandle(handle unsafe.Pointer) any {
|
||||||
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 interface{}")
|
return nil, errors.New("the only supported interface type is any")
|
||||||
}
|
}
|
||||||
return callbackArgGeneric, nil
|
return callbackArgGeneric, nil
|
||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
@@ -353,6 +353,20 @@ 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:
|
||||||
@@ -360,6 +374,11 @@ 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 {
|
||||||
|
|||||||
10
vendor/github.com/mattn/go-sqlite3/convert.go
generated
vendored
10
vendor/github.com/mattn/go-sqlite3/convert.go
generated
vendored
@@ -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 interface{}) error {
|
func convertAssign(dest, src any) 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 interface{}) error {
|
|||||||
}
|
}
|
||||||
*d = string(s)
|
*d = string(s)
|
||||||
return nil
|
return nil
|
||||||
case *interface{}:
|
case *any:
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return errNilPtr
|
return errNilPtr
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ func convertAssign(dest, src interface{}) error {
|
|||||||
}
|
}
|
||||||
case nil:
|
case nil:
|
||||||
switch d := dest.(type) {
|
switch d := dest.(type) {
|
||||||
case *interface{}:
|
case *any:
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return errNilPtr
|
return errNilPtr
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ func convertAssign(dest, src interface{}) error {
|
|||||||
*d = bv.(bool)
|
*d = bv.(bool)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
case *interface{}:
|
case *any:
|
||||||
*d = src
|
*d = src
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,7 @@ func cloneBytes(b []byte) []byte {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func asString(src interface{}) string {
|
func asString(src any) string {
|
||||||
switch v := src.(type) {
|
switch v := src.(type) {
|
||||||
case string:
|
case string:
|
||||||
return v
|
return v
|
||||||
|
|||||||
11
vendor/github.com/mattn/go-sqlite3/doc.go
generated
vendored
11
vendor/github.com/mattn/go-sqlite3/doc.go
generated
vendored
@@ -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 interface{}) error {
|
err = conn.Raw(func (driverConn any) 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,6 +130,5 @@ 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
|
||||||
|
|||||||
2
vendor/github.com/mattn/go-sqlite3/error.go
generated
vendored
2
vendor/github.com/mattn/go-sqlite3/error.go
generated
vendored
@@ -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
|
||||||
|
|||||||
58232
vendor/github.com/mattn/go-sqlite3/sqlite3-binding.c
generated
vendored
58232
vendor/github.com/mattn/go-sqlite3/sqlite3-binding.c
generated
vendored
File diff suppressed because it is too large
Load Diff
1597
vendor/github.com/mattn/go-sqlite3/sqlite3-binding.h
generated
vendored
1597
vendor/github.com/mattn/go-sqlite3/sqlite3-binding.h
generated
vendored
File diff suppressed because it is too large
Load Diff
182
vendor/github.com/mattn/go-sqlite3/sqlite3.go
generated
vendored
182
vendor/github.com/mattn/go-sqlite3/sqlite3.go
generated
vendored
@@ -4,6 +4,7 @@
|
|||||||
// 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
|
||||||
@@ -20,9 +21,10 @@ 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 linux,!android CFLAGS: -DHAVE_PREAD64=1 -DHAVE_PWRITE64=1
|
#cgo openbsd CFLAGS: -I/usr/local/include
|
||||||
|
#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
|
||||||
@@ -45,6 +47,18 @@ 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
|
||||||
@@ -231,8 +245,14 @@ 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() {
|
||||||
sql.Register("sqlite3", &SQLiteDriver{})
|
if driverName != "" {
|
||||||
|
sql.Register(driverName, &SQLiteDriver{})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version returns SQLite library version information.
|
// Version returns SQLite library version information.
|
||||||
@@ -288,6 +308,51 @@ 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
|
||||||
@@ -440,10 +505,12 @@ 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 && err.(Error).Code == C.SQLITE_BUSY {
|
if err != nil {
|
||||||
// sqlite3 will leave the transaction open in this scenario.
|
// sqlite3 may 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
|
||||||
@@ -540,10 +607,9 @@ 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
|
// numeric type except complex, bool, []byte, string and any.
|
||||||
// interface{}. interface{} arguments are given the direct translation
|
// any arguments are given the direct translation of the SQLite data type:
|
||||||
// of the SQLite data type: int64 for INTEGER, float64 for FLOAT,
|
// int64 for INTEGER, float64 for FLOAT, []byte for BLOB, string for TEXT.
|
||||||
// []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.
|
||||||
@@ -553,7 +619,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 interface{}, pure bool) error {
|
func (c *SQLiteConn) RegisterFunc(name string, impl any, 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()
|
||||||
@@ -635,7 +701,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 interface{}, pure bool) error {
|
func (c *SQLiteConn) RegisterAggregator(name string, impl any, 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()
|
||||||
@@ -781,9 +847,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([]namedValue, len(args))
|
list := make([]driver.NamedValue, len(args))
|
||||||
for i, v := range args {
|
for i, v := range args {
|
||||||
list[i] = namedValue{
|
list[i] = driver.NamedValue{
|
||||||
Ordinal: i + 1,
|
Ordinal: i + 1,
|
||||||
Value: v,
|
Value: v,
|
||||||
}
|
}
|
||||||
@@ -791,7 +857,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 []namedValue) (driver.Result, error) {
|
func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||||
start := 0
|
start := 0
|
||||||
for {
|
for {
|
||||||
s, err := c.prepare(ctx, query)
|
s, err := c.prepare(ctx, query)
|
||||||
@@ -800,7 +866,7 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []namedValue)
|
|||||||
}
|
}
|
||||||
var res driver.Result
|
var res driver.Result
|
||||||
if s.(*SQLiteStmt).s != nil {
|
if s.(*SQLiteStmt).s != nil {
|
||||||
stmtArgs := make([]namedValue, 0, len(args))
|
stmtArgs := make([]driver.NamedValue, 0, len(args))
|
||||||
na := s.NumInput()
|
na := s.NumInput()
|
||||||
if len(args)-start < na {
|
if len(args)-start < na {
|
||||||
s.Close()
|
s.Close()
|
||||||
@@ -809,6 +875,7 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []namedValue)
|
|||||||
// 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 != "" {
|
||||||
@@ -818,6 +885,7 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []namedValue)
|
|||||||
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()
|
||||||
@@ -828,23 +896,21 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []namedValue)
|
|||||||
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([]namedValue, len(args))
|
list := make([]driver.NamedValue, len(args))
|
||||||
for i, v := range args {
|
for i, v := range args {
|
||||||
list[i] = namedValue{
|
list[i] = driver.NamedValue{
|
||||||
Ordinal: i + 1,
|
Ordinal: i + 1,
|
||||||
Value: v,
|
Value: v,
|
||||||
}
|
}
|
||||||
@@ -852,10 +918,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 []namedValue) (driver.Rows, error) {
|
func (c *SQLiteConn) query(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||||||
start := 0
|
start := 0
|
||||||
for {
|
for {
|
||||||
stmtArgs := make([]namedValue, 0, len(args))
|
stmtArgs := make([]driver.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
|
||||||
@@ -911,10 +977,12 @@ 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:
|
||||||
@@ -946,6 +1014,7 @@ 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".
|
||||||
//
|
//
|
||||||
@@ -1006,8 +1075,6 @@ 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")
|
||||||
@@ -1409,12 +1476,6 @@ 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)
|
||||||
@@ -1425,6 +1486,12 @@ 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
|
||||||
@@ -1612,7 +1679,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forgein Keys
|
// Foreign 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)
|
||||||
@@ -1800,6 +1867,31 @@ 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()
|
||||||
@@ -1816,6 +1908,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -1827,7 +1920,7 @@ func (s *SQLiteStmt) NumInput() int {
|
|||||||
|
|
||||||
var placeHolder = []byte{0}
|
var placeHolder = []byte{0}
|
||||||
|
|
||||||
func (s *SQLiteStmt) bind(args []namedValue) error {
|
func (s *SQLiteStmt) bind(args []driver.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()
|
||||||
@@ -1897,9 +1990,9 @@ func (s *SQLiteStmt) bind(args []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([]namedValue, len(args))
|
list := make([]driver.NamedValue, len(args))
|
||||||
for i, v := range args {
|
for i, v := range args {
|
||||||
list[i] = namedValue{
|
list[i] = driver.NamedValue{
|
||||||
Ordinal: i + 1,
|
Ordinal: i + 1,
|
||||||
Value: v,
|
Value: v,
|
||||||
}
|
}
|
||||||
@@ -1907,7 +2000,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 []namedValue) (driver.Rows, error) {
|
func (s *SQLiteStmt) query(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||||
if err := s.bind(args); err != nil {
|
if err := s.bind(args); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1921,6 +2014,7 @@ func (s *SQLiteStmt) query(ctx context.Context, args []namedValue) (driver.Rows,
|
|||||||
closed: false,
|
closed: false,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}
|
}
|
||||||
|
runtime.SetFinalizer(rows, (*SQLiteRows).Close)
|
||||||
|
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
@@ -1937,9 +2031,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([]namedValue, len(args))
|
list := make([]driver.NamedValue, len(args))
|
||||||
for i, v := range args {
|
for i, v := range args {
|
||||||
list[i] = namedValue{
|
list[i] = driver.NamedValue{
|
||||||
Ordinal: i + 1,
|
Ordinal: i + 1,
|
||||||
Value: v,
|
Value: v,
|
||||||
}
|
}
|
||||||
@@ -1956,7 +2050,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 []namedValue) (driver.Result, error) {
|
func (s *SQLiteStmt) exec(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||||||
if ctx.Done() == nil {
|
if ctx.Done() == nil {
|
||||||
return s.execSync(args)
|
return s.execSync(args)
|
||||||
}
|
}
|
||||||
@@ -1966,6 +2060,7 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result
|
|||||||
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}
|
||||||
@@ -1988,7 +2083,7 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result
|
|||||||
return rv.r, rv.err
|
return rv.r, rv.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLiteStmt) execSync(args []namedValue) (driver.Result, error) {
|
func (s *SQLiteStmt) execSync(args []driver.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)
|
||||||
@@ -2032,6 +2127,8 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2078,6 +2175,7 @@ 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)
|
||||||
}()
|
}()
|
||||||
|
|||||||
2
vendor/github.com/mattn/go-sqlite3/sqlite3_context.go
generated
vendored
2
vendor/github.com/mattn/go-sqlite3/sqlite3_context.go
generated
vendored
@@ -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
|
||||||
|
|||||||
24
vendor/github.com/mattn/go-sqlite3/sqlite3_func_crypt.go
generated
vendored
24
vendor/github.com/mattn/go-sqlite3/sqlite3_func_crypt.go
generated
vendored
@@ -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 interface{}) []byte {
|
func CryptEncoderSHA1(pass []byte, hash any) []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 interface{}) []byte {
|
func CryptEncoderSSHA1(salt string) func(pass []byte, hash any) []byte {
|
||||||
return func(pass []byte, hash interface{}) []byte {
|
return func(pass []byte, hash any) []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 interface{}) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CryptEncoderSHA256 encodes a password with SHA256
|
// CryptEncoderSHA256 encodes a password with SHA256
|
||||||
func CryptEncoderSHA256(pass []byte, hash interface{}) []byte {
|
func CryptEncoderSHA256(pass []byte, hash any) []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 interface{}) []byte {
|
func CryptEncoderSSHA256(salt string) func(pass []byte, hash any) []byte {
|
||||||
return func(pass []byte, hash interface{}) []byte {
|
return func(pass []byte, hash any) []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 interface{}) []byte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CryptEncoderSHA384 encodes a password with SHA384
|
// CryptEncoderSHA384 encodes a password with SHA384
|
||||||
func CryptEncoderSHA384(pass []byte, hash interface{}) []byte {
|
func CryptEncoderSHA384(pass []byte, hash any) []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 interface{}) []byte {
|
func CryptEncoderSSHA384(salt string) func(pass []byte, hash any) []byte {
|
||||||
return func(pass []byte, hash interface{}) []byte {
|
return func(pass []byte, hash any) []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 interface{}) []byte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CryptEncoderSHA512 encodes a password with SHA512
|
// CryptEncoderSHA512 encodes a password with SHA512
|
||||||
func CryptEncoderSHA512(pass []byte, hash interface{}) []byte {
|
func CryptEncoderSHA512(pass []byte, hash any) []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 interface{}) []byte {
|
func CryptEncoderSSHA512(salt string) func(pass []byte, hash any) []byte {
|
||||||
return func(pass []byte, hash interface{}) []byte {
|
return func(pass []byte, hash any) []byte {
|
||||||
s := []byte(salt)
|
s := []byte(salt)
|
||||||
p := append(pass, s...)
|
p := append(pass, s...)
|
||||||
h := sha512.Sum512(p)
|
h := sha512.Sum512(p)
|
||||||
|
|||||||
28
vendor/github.com/mattn/go-sqlite3/sqlite3_go18.go
generated
vendored
28
vendor/github.com/mattn/go-sqlite3/sqlite3_go18.go
generated
vendored
@@ -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.
|
||||||
|
|
||||||
// +build cgo
|
//go:build cgo && go1.8
|
||||||
// +build go1.8
|
// +build cgo,go1.8
|
||||||
|
|
||||||
package sqlite3
|
package sqlite3
|
||||||
|
|
||||||
@@ -25,20 +25,12 @@ 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) {
|
||||||
list := make([]namedValue, len(args))
|
return c.query(ctx, query, 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) {
|
||||||
list := make([]namedValue, len(args))
|
return c.exec(ctx, query, args)
|
||||||
for i, nv := range args {
|
|
||||||
list[i] = namedValue(nv)
|
|
||||||
}
|
|
||||||
return c.exec(ctx, query, list)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareContext implement ConnPrepareContext.
|
// PrepareContext implement ConnPrepareContext.
|
||||||
@@ -53,18 +45,10 @@ 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) {
|
||||||
list := make([]namedValue, len(args))
|
return s.query(ctx, 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) {
|
||||||
list := make([]namedValue, len(args))
|
return s.exec(ctx, args)
|
||||||
for i, nv := range args {
|
|
||||||
list[i] = namedValue(nv)
|
|
||||||
}
|
|
||||||
return s.exec(ctx, list)
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
vendor/github.com/mattn/go-sqlite3/sqlite3_libsqlite3.go
generated
vendored
8
vendor/github.com/mattn/go-sqlite3/sqlite3_libsqlite3.go
generated
vendored
@@ -3,6 +3,7 @@
|
|||||||
// 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
|
||||||
@@ -10,10 +11,13 @@ package sqlite3
|
|||||||
/*
|
/*
|
||||||
#cgo CFLAGS: -DUSE_LIBSQLITE3
|
#cgo CFLAGS: -DUSE_LIBSQLITE3
|
||||||
#cgo linux LDFLAGS: -lsqlite3
|
#cgo linux LDFLAGS: -lsqlite3
|
||||||
#cgo darwin LDFLAGS: -L/usr/local/opt/sqlite/lib -lsqlite3
|
#cgo darwin,amd64 LDFLAGS: -L/usr/local/opt/sqlite/lib -lsqlite3
|
||||||
#cgo darwin CFLAGS: -I/usr/local/opt/sqlite/include
|
#cgo darwin,amd64 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"
|
||||||
|
|||||||
3
vendor/github.com/mattn/go-sqlite3/sqlite3_load_extension.go
generated
vendored
3
vendor/github.com/mattn/go-sqlite3/sqlite3_load_extension.go
generated
vendored
@@ -3,13 +3,14 @@
|
|||||||
// 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
|
||||||
|
|||||||
1
vendor/github.com/mattn/go-sqlite3/sqlite3_load_extension_omit.go
generated
vendored
1
vendor/github.com/mattn/go-sqlite3/sqlite3_load_extension_omit.go
generated
vendored
@@ -3,6 +3,7 @@
|
|||||||
// 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
|
||||||
|
|||||||
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_allow_uri_authority.go
generated
vendored
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_allow_uri_authority.go
generated
vendored
@@ -4,6 +4,7 @@
|
|||||||
// 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
|
||||||
|
|||||||
4
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_app_armor.go
generated
vendored
4
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_app_armor.go
generated
vendored
@@ -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.
|
||||||
|
|
||||||
// +build !windows
|
//go:build !windows && sqlite_app_armor
|
||||||
// +build sqlite_app_armor
|
// +build !windows,sqlite_app_armor
|
||||||
|
|
||||||
package sqlite3
|
package sqlite3
|
||||||
|
|
||||||
|
|||||||
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_column_metadata.go
generated
vendored
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_column_metadata.go
generated
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build sqlite_column_metadata
|
||||||
// +build sqlite_column_metadata
|
// +build sqlite_column_metadata
|
||||||
|
|
||||||
package sqlite3
|
package sqlite3
|
||||||
|
|||||||
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_foreign_keys.go
generated
vendored
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_foreign_keys.go
generated
vendored
@@ -4,6 +4,7 @@
|
|||||||
// 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
|
||||||
|
|||||||
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_fts5.go
generated
vendored
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_fts5.go
generated
vendored
@@ -3,6 +3,7 @@
|
|||||||
// 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
|
||||||
|
|||||||
7
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_icu.go
generated
vendored
7
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_icu.go
generated
vendored
@@ -3,6 +3,7 @@
|
|||||||
// 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
|
||||||
@@ -10,8 +11,10 @@ package sqlite3
|
|||||||
/*
|
/*
|
||||||
#cgo LDFLAGS: -licuuc -licui18n
|
#cgo LDFLAGS: -licuuc -licui18n
|
||||||
#cgo CFLAGS: -DSQLITE_ENABLE_ICU
|
#cgo CFLAGS: -DSQLITE_ENABLE_ICU
|
||||||
#cgo darwin CFLAGS: -I/usr/local/opt/icu4c/include
|
#cgo darwin,amd64 CFLAGS: -I/usr/local/opt/icu4c/include
|
||||||
#cgo darwin LDFLAGS: -L/usr/local/opt/icu4c/lib
|
#cgo darwin,amd64 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"
|
||||||
|
|||||||
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_introspect.go
generated
vendored
1
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_introspect.go
generated
vendored
@@ -4,6 +4,7 @@
|
|||||||
// 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
|
||||||
|
|||||||
13
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_json1.go
generated
vendored
13
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_json1.go
generated
vendored
@@ -1,13 +0,0 @@
|
|||||||
// 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"
|
|
||||||
15
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_math_functions.go
generated
vendored
Normal file
15
vendor/github.com/mattn/go-sqlite3/sqlite3_opt_math_functions.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// 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"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user