commit
5896386f2e
106 changed files with 331064 additions and 0 deletions
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
# Default ignored files |
||||
/shelf/ |
||||
/workspace.xml |
||||
# Editor-based HTTP Client requests |
||||
/httpRequests/ |
||||
# Datasource local storage ignored files |
||||
/dataSources/ |
||||
/dataSources.local.xml |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<module type="WEB_MODULE" version="4"> |
||||
<component name="Go" enabled="true" /> |
||||
<component name="NewModuleRootManager"> |
||||
<content url="file://$MODULE_DIR$" /> |
||||
<orderEntry type="inheritedJdk" /> |
||||
<orderEntry type="sourceFolder" forTests="false" /> |
||||
</component> |
||||
</module> |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="ProjectModuleManager"> |
||||
<modules> |
||||
<module fileurl="file://$PROJECT_DIR$/.idea/etascrobbler-ng.iml" filepath="$PROJECT_DIR$/.idea/etascrobbler-ng.iml" /> |
||||
</modules> |
||||
</component> |
||||
</project> |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
.PHONY: all |
||||
|
||||
all: |
||||
CC=musl-gcc go build -ldflags='-extldflags=-static' |
@ -0,0 +1,274 @@
@@ -0,0 +1,274 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"crawshaw.io/sqlite/sqlitex" |
||||
"encoding/json" |
||||
"errors" |
||||
"flag" |
||||
"golang.org/x/crypto/acme/autocert" |
||||
"html/template" |
||||
"io" |
||||
"log" |
||||
"net/http" |
||||
"strconv" |
||||
"time" |
||||
) |
||||
|
||||
var databasePath = flag.String("database", "./data.sqlite3", "database path") |
||||
var pageTemplate = `{{ define "base" }} |
||||
<!DOCTYPE html> |
||||
<html lang='en'> |
||||
<head> |
||||
<title>etascrobbler</title> |
||||
<link rel='stylesheet' type='text/css' href='//eta.st/assets/webfonts/stylesheet.css'>
|
||||
<link rel='stylesheet' type='text/css' href='//eta.st/css/main.css'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'> |
||||
</head> |
||||
<header class='site-header'> |
||||
<div class='wrapper'> |
||||
<a class='site-title' href='/'> |
||||
<img src='//eta.st/assets/img/logo.svg' alt='ฮท'>(eta)
|
||||
</a> |
||||
<nav class='site-nav'> |
||||
<div class='trigger'>etascrobbler</div> |
||||
</nav> |
||||
</div> |
||||
</header> |
||||
<div class='page-content'> |
||||
<div class='wrapper'> |
||||
<h1>now playing</h1> |
||||
<p> |
||||
This shows what I was last listening to on any of my devices, and the song before that. This tiny webapp has observed <b>{{ .TotalSongs }}</b> |
||||
songs being played so far. |
||||
</p> |
||||
<p>This site is very beta. More information should be added shortly!</p> |
||||
{{if .SomethingPlaying}} |
||||
<div class="book-meta"> |
||||
<p class="book-title">๐ต {{ .CurrentTrack.Artist }} โ {{ .CurrentTrack.Name }} |
||||
</div> |
||||
{{end}} |
||||
<p> |
||||
<i>Previously: </i> |
||||
{{ .PreviousTrack.Artist }} โ {{ .PreviousTrack.Name }} |
||||
</p> |
||||
<p class='detail'> |
||||
Artists (<strong>{{ .TotalArtists }}</strong>): {{range .Artists}}{{.}} · {{end}} |
||||
</p> |
||||
</div> |
||||
</div> |
||||
</html> |
||||
{{ end }} |
||||
` |
||||
var tmpl *template.Template = template.Must(template.New("base").Parse(pageTemplate)) |
||||
var dbpool *sqlitex.Pool |
||||
|
||||
type Scrobble struct { |
||||
Artist string `json:"artist"` |
||||
Name string `json:"track"` |
||||
Ts time.Time |
||||
} |
||||
|
||||
var currentScrobble *Scrobble |
||||
|
||||
func getLastScrobble() (s *Scrobble, err error) { |
||||
conn := dbpool.Get(context.Background()) |
||||
if conn == nil { |
||||
err = errors.New("no connection whoopsie") |
||||
return s, err |
||||
} |
||||
defer dbpool.Put(conn) |
||||
stmt := conn.Prep("SELECT artist, title FROM plays ORDER BY ts DESC LIMIT 1") |
||||
hasRow, err := stmt.Step() |
||||
if hasRow { |
||||
s = &Scrobble{ |
||||
Artist: stmt.GetText("artist"), |
||||
Name: stmt.GetText("title"), |
||||
// FIXME(eta): lol don't actually set `ts` because who cares
|
||||
} |
||||
stmt.Step() |
||||
return s, nil |
||||
} else { |
||||
return s, nil |
||||
} |
||||
} |
||||
|
||||
func getAllArtists() (artists []string, err error) { |
||||
conn := dbpool.Get(context.Background()) |
||||
if conn == nil { |
||||
err = errors.New("no connection whoopsie") |
||||
return nil, err |
||||
} |
||||
defer dbpool.Put(conn) |
||||
artists = make([]string, 0) |
||||
stmt := conn.Prep("SELECT DISTINCT artist FROM plays ORDER BY artist ASC") |
||||
for { |
||||
hasRow, err := stmt.Step() |
||||
if err != nil { |
||||
return nil, err |
||||
} else if hasRow { |
||||
artists = append(artists, stmt.GetText("artist")) |
||||
} else { |
||||
break |
||||
} |
||||
} |
||||
return artists, nil |
||||
} |
||||
|
||||
func countAllScrobbles() (count int64, err error) { |
||||
conn := dbpool.Get(context.Background()) |
||||
if conn == nil { |
||||
err = errors.New("no connection whoopsie") |
||||
return -1, err |
||||
} |
||||
defer dbpool.Put(conn) |
||||
stmt := conn.Prep("SELECT COUNT(*) AS c FROM plays") |
||||
hasRow, err := stmt.Step() |
||||
if hasRow { |
||||
count = stmt.GetInt64("c") |
||||
stmt.Step() |
||||
return count, nil |
||||
} else { |
||||
err = errors.New("wut") |
||||
return -1, nil |
||||
} |
||||
} |
||||
|
||||
type ListenBrainzPayload struct { |
||||
ListenedAt interface{} `json:"listened_at"` |
||||
TrackMetadata struct { |
||||
ArtistName string `json:"artist_name"` |
||||
ReleaseName string `json:"release_name"` |
||||
TrackName string `json:"track_name"` |
||||
} `json:"track_metadata"` |
||||
} |
||||
|
||||
func (payload *ListenBrainzPayload) fudgeListenedAt() int64 { |
||||
data, ok := payload.ListenedAt.(string) |
||||
if ok { |
||||
ret, _ := strconv.Atoi(data) |
||||
return int64(ret) |
||||
} |
||||
data2, ok := payload.ListenedAt.(int64) |
||||
if ok { |
||||
return data2 |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
func insertTrack(track ListenBrainzPayload) (err error) { |
||||
conn := dbpool.Get(context.Background()) |
||||
if conn == nil { |
||||
err = errors.New("no connection whoopsie") |
||||
return |
||||
} |
||||
defer dbpool.Put(conn) |
||||
stmt := conn.Prep("INSERT INTO plays (artist, title, ts) VALUES (?, ?, ?)") |
||||
stmt.BindText(1, track.TrackMetadata.ArtistName) |
||||
stmt.BindText(2, track.TrackMetadata.TrackName) |
||||
stmt.BindInt64(3, track.fudgeListenedAt()) |
||||
_, err = stmt.Step() |
||||
return |
||||
} |
||||
|
||||
type SubmitListens struct { |
||||
ListenType string `json:"listen_type"` |
||||
Payload []ListenBrainzPayload `json:"payload"` |
||||
} |
||||
|
||||
func validateToken(w http.ResponseWriter, r *http.Request) { |
||||
log.Printf("validate token") |
||||
r.Header.Set("Content-Type", "application/json") |
||||
w.Write([]byte(`{"code": 200, "message": "wiggly donkers", "valid": true, "user": "eta"}`)) |
||||
} |
||||
|
||||
func submitListen(w http.ResponseWriter, r *http.Request) { |
||||
var payload SubmitListens |
||||
body, err := io.ReadAll(r.Body) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
log.Printf("submit listen: %v", string(body)) |
||||
err = json.NewDecoder(bytes.NewReader(body)).Decode(&payload) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
log.Printf("parsed: %#v\n", payload) |
||||
if payload.ListenType == "playing_now" { |
||||
currentScrobble = &Scrobble{ |
||||
Artist: payload.Payload[0].TrackMetadata.ArtistName, |
||||
Name: payload.Payload[0].TrackMetadata.TrackName, |
||||
Ts: time.Now(), |
||||
} |
||||
log.Printf("now playing: %v โ %v", currentScrobble.Artist, currentScrobble.Name) |
||||
} else if payload.ListenType == "single" || payload.ListenType == "import" { |
||||
log.Printf("submission of type %v", payload.ListenType) |
||||
for _, item := range payload.Payload { |
||||
log.Printf("inserting: %v โ %v", item.TrackMetadata.ArtistName, item.TrackMetadata.TrackName) |
||||
err := insertTrack(item) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
} |
||||
r.Header.Set("Content-Type", "application/json") |
||||
w.Write([]byte(`{"status": "ok"}`)) |
||||
} |
||||
|
||||
func nowPlayingJson(w http.ResponseWriter, r *http.Request) { |
||||
log.Printf("now playing") |
||||
if currentScrobble == nil { |
||||
http.NotFound(w, r) |
||||
return |
||||
} |
||||
if time.Since(currentScrobble.Ts) > 30*time.Minute { |
||||
http.NotFound(w, r) |
||||
return |
||||
} |
||||
r.Header.Set("Content-Type", "application/json") |
||||
json.NewEncoder(w).Encode(*currentScrobble) |
||||
} |
||||
|
||||
func rootPage(w http.ResponseWriter, r *http.Request) { |
||||
log.Printf("root page %v", r.URL.String()) |
||||
pageData := make(map[string]interface{}) |
||||
var err error |
||||
pageData["TotalSongs"], err = countAllScrobbles() |
||||
if err != nil { |
||||
log.Fatalf("failed to count scrobbles: %v", err) |
||||
} |
||||
artists, err := getAllArtists() |
||||
if err != nil { |
||||
log.Fatalf("failed to get artists: %v", err) |
||||
} |
||||
pageData["Artists"] = artists |
||||
pageData["TotalArtists"] = len(artists) |
||||
pageData["PreviousTrack"], err = getLastScrobble() |
||||
if err != nil { |
||||
log.Fatalf("failed to get last scrobble: %v", err) |
||||
} |
||||
pageData["SomethingPlaying"] = currentScrobble != nil |
||||
if currentScrobble != nil { |
||||
pageData["CurrentTrack"] = *currentScrobble |
||||
} |
||||
tmpl.ExecuteTemplate(w, "base", pageData) |
||||
} |
||||
|
||||
func main() { |
||||
flag.Parse() |
||||
|
||||
var err error |
||||
dbpool, err = sqlitex.Open(*databasePath, 0, 10) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
mux := http.NewServeMux() |
||||
mux.HandleFunc("/", rootPage) |
||||
mux.HandleFunc("/now-playing.json", nowPlayingJson) |
||||
mux.HandleFunc("/1/validate-token", validateToken) |
||||
mux.HandleFunc("/1/submit-listens", submitListen) |
||||
|
||||
log.Fatal(http.Serve(autocert.NewListener("etascrobbler.i.eta.st"), mux)) |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
module etascrobbler-ng |
||||
|
||||
go 1.17 |
||||
|
||||
require ( |
||||
crawshaw.io/sqlite v0.3.2 |
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f |
||||
) |
||||
|
||||
require ( |
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect |
||||
golang.org/x/text v0.3.6 // indirect |
||||
) |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= |
||||
crawshaw.io/sqlite v0.3.2 h1:N6IzTjkiw9FItHAa0jp+ZKC6tuLzXqAYIv+ccIWos1I= |
||||
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= |
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= |
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= |
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= |
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= |
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= |
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
sqlite3.o |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
language: "go" |
||||
|
||||
go: |
||||
- "1.13.x" |
||||
|
||||
os: |
||||
- "linux" |
||||
- "osx" |
||||
|
||||
script: |
||||
- make test |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2018 David Crawshaw <david@zentus.com> |
||||
|
||||
Permission to use, copy, modify, and distribute this software for any |
||||
purpose with or without fee is hereby granted, provided that the above |
||||
copyright notice and this permission notice appear in all copies. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
## Copyright (c) 2018 David Crawshaw <david@zentus.com>
|
||||
##
|
||||
## Permission to use, copy, modify, and distribute this software for any
|
||||
## purpose with or without fee is hereby granted, provided that the above
|
||||
## copyright notice and this permission notice appear in all copies.
|
||||
##
|
||||
## THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
## WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
## MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
## ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
## WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
## ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
## OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
# This Makefile is simply for development purposes. Normally, when this package
|
||||
# is imported, Go will build the ./c/sqlite.c file that is included directly by
|
||||
# static.go. However this is pretty slow ~30s. When developing this is very
|
||||
# annoying. Use this Makefile to pre-build the sqlite3.o object and then build
|
||||
# the package with the build tag linksqlite3, which will ignore static.go and
|
||||
# use link.go instead to link against sqlite.o. This reduces compilation times
|
||||
# down to <3 sec!
|
||||
#
|
||||
# If you are using an editor that builds the project as you work on it, you'll
|
||||
# want to build the sqlite3.o object and tell your editor to use the
|
||||
# linksqlite3 go build tag when working on this project.
|
||||
# For vim-go, use the command `GoBuildTags linksqlite3` or
|
||||
# `let g:go_build_tags = # 'linksqlite3'`
|
||||
|
||||
export GOFLAGS=-tags=linksqlite3 |
||||
|
||||
.PHONY: clean all env test release |
||||
all: sqlite3.o |
||||
go build ./... |
||||
|
||||
test: sqlite3.o |
||||
go test ./... |
||||
|
||||
test-race: sqlite3.o |
||||
go test -race ./... |
||||
env: |
||||
go env |
||||
|
||||
## This builds the package statically.
|
||||
release: |
||||
go build -tags=!linksqlite3 |
||||
|
||||
VPATH = ./c # Look in ./c for source files |
||||
|
||||
# !!! THESE DEFINES SHOULD MATCH sqlite.go for linux !!!
|
||||
CFLAGS += -std=c99 |
||||
CFLAGS += -DSQLITE_THREADSAFE=2 |
||||
CFLAGS += -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 |
||||
CFLAGS += -DSQLITE_ENABLE_UNLOCK_NOTIFY |
||||
CFLAGS += -DSQLITE_ENABLE_FTS5 |
||||
CFLAGS += -DSQLITE_ENABLE_RTREE |
||||
CFLAGS += -DSQLITE_LIKE_DOESNT_MATCH_BLOBS |
||||
CFLAGS += -DSQLITE_OMIT_DEPRECATED |
||||
CFLAGS += -DSQLITE_ENABLE_JSON1 |
||||
CFLAGS += -DSQLITE_ENABLE_SESSION |
||||
CFLAGS += -DSQLITE_ENABLE_SNAPSHOT |
||||
CFLAGS += -DSQLITE_ENABLE_PREUPDATE_HOOK |
||||
CFLAGS += -DSQLITE_USE_ALLOCA |
||||
CFLAGS += -DSQLITE_ENABLE_COLUMN_METADATA |
||||
CFLAGS += -DHAVE_USLEEP=1 |
||||
CFLAGS += -DSQLITE_DQS=0 |
||||
CFLAGS += -DSQLITE_ENABLE_GEOPOLY |
||||
LDFLAGS = -ldl -lm |
||||
# !!! THESE DEFINES SHOULD MATCH sqlite.go !!!
|
||||
|
||||
sqlite3.o: sqlite3.c sqlite3.h sqlite3ext.h |
||||
|
||||
|
||||
clean: |
||||
rm -f sqlite3.o |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
# Go interface to SQLite. |
||||
|
||||
[](https://godoc.org/crawshaw.io/sqlite) [](https://travis-ci.org/crawshaw/sqlite) (linux and macOS) [](https://ci.appveyor.com/project/crawshaw/sqlite) (windows) |
||||
|
||||
This package provides a low-level Go interface to SQLite 3. Connections are [pooled](https://godoc.org/crawshaw.io/sqlite#Pool) and if the SQLite [shared cache](https://www.sqlite.org/sharedcache.html) mode is enabled the package takes advantage of the [unlock-notify API](https://www.sqlite.org/unlock_notify.html) to minimize the amount of handling user code needs for dealing with database lock contention. |
||||
|
||||
It has interfaces for some of SQLite's more interesting extensions, such as [incremental BLOB I/O](https://www.sqlite.org/c3ref/blob_open.html) and the [session extension](https://www.sqlite.org/sessionintro.html). |
||||
|
||||
A utility package, [sqlitex](https://godoc.org/crawshaw.io/sqlite/sqlitex), provides some higher-level tools for making it easier to perform common tasks with SQLite. In particular it provides support to make nested transactions easy to use via [sqlitex.Save](https://godoc.org/crawshaw.io/sqlite/sqlitex#Save). |
||||
|
||||
This is not a database/sql driver. |
||||
|
||||
```go get -u crawshaw.io/sqlite``` |
||||
|
||||
## Example |
||||
|
||||
A HTTP handler that uses a multi-threaded pool of SQLite connections via a shared cache. |
||||
|
||||
```go |
||||
var dbpool *sqlitex.Pool |
||||
|
||||
func main() { |
||||
var err error |
||||
dbpool, err = sqlitex.Open("file:memory:?mode=memory", 0, 10) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
http.HandleFunc("/", handler) |
||||
log.Fatal(http.ListenAndServe(":8080", nil)) |
||||
} |
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) { |
||||
conn := dbpool.Get(r.Context()) |
||||
if conn == nil { |
||||
return |
||||
} |
||||
defer dbpool.Put(conn) |
||||
stmt := conn.Prep("SELECT foo FROM footable WHERE id = $id;") |
||||
stmt.SetText("$id", "_user_id_") |
||||
for { |
||||
if hasRow, err := stmt.Step(); err != nil { |
||||
// ... handle error |
||||
} else if !hasRow { |
||||
break |
||||
} |
||||
foo := stmt.GetText("foo") |
||||
// ... use foo |
||||
} |
||||
} |
||||
``` |
||||
|
||||
https://godoc.org/crawshaw.io/sqlite |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
version: "{build}" |
||||
platform: "mingw" |
||||
|
||||
# Source Config |
||||
|
||||
clone_folder: C:\gopath\src\crawshaw.io\sqlite |
||||
|
||||
# Build host |
||||
|
||||
environment: |
||||
GOPATH: C:\gopath |
||||
|
||||
# Build |
||||
|
||||
install: |
||||
- set PATH=C:\go\bin;C:\gopath\bin;C:\mingw-w64\x86_64-7.3.0-posix-seh-rt_v5-rev0\mingw64\bin;%PATH% |
||||
- go version |
||||
- go env |
||||
|
||||
build: false |
||||
deploy: false |
||||
|
||||
test_script: |
||||
- go get -v -t ./... |
||||
- go test -v ./... |
@ -0,0 +1,359 @@
@@ -0,0 +1,359 @@
|
||||
package sqlite |
||||
|
||||
// #include <stdint.h>
|
||||
// #include <sqlite3.h>
|
||||
// extern int go_sqlite_auth_tramp(uintptr_t, int, char*, char*, char*, char*);
|
||||
// static int c_auth_tramp(void *userData, int action, const char* arg1, const char* arg2, const char* db, const char* trigger) {
|
||||
// return go_sqlite_auth_tramp((uintptr_t)userData, action, (char*)arg1, (char*)arg2, (char*)db, (char*)trigger);
|
||||
// }
|
||||
// static int sqlite3_go_set_authorizer(sqlite3* conn, uintptr_t id) {
|
||||
// return sqlite3_set_authorizer(conn, c_auth_tramp, (void*)id);
|
||||
// }
|
||||
import "C" |
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strings" |
||||
"sync" |
||||
) |
||||
|
||||
// An Authorizer is called during statement preparation to see whether an action
|
||||
// is allowed by the application. See https://sqlite.org/c3ref/set_authorizer.html
|
||||
type Authorizer interface { |
||||
Authorize(info ActionInfo) AuthResult |
||||
} |
||||
|
||||
// SetAuthorizer registers an authorizer for the database connection.
|
||||
// SetAuthorizer(nil) clears any authorizer previously set.
|
||||
func (conn *Conn) SetAuthorizer(auth Authorizer) error { |
||||
if auth == nil { |
||||
if conn.authorizer == -1 { |
||||
return nil |
||||
} |
||||
conn.releaseAuthorizer() |
||||
res := C.sqlite3_set_authorizer(conn.conn, nil, nil) |
||||
return reserr("SetAuthorizer", "", "", res) |
||||
} |
||||
|
||||
authFuncs.mu.Lock() |
||||
id := authFuncs.next |
||||
next := authFuncs.next + 1 |
||||
if next < 0 { |
||||
authFuncs.mu.Unlock() |
||||
return errors.New("sqlite: authorizer function id overflow") |
||||
} |
||||
authFuncs.next = next |
||||
authFuncs.m[id] = auth |
||||
authFuncs.mu.Unlock() |
||||
|
||||
res := C.sqlite3_go_set_authorizer(conn.conn, C.uintptr_t(id)) |
||||
return reserr("SetAuthorizer", "", "", res) |
||||
} |
||||
|
||||
func (conn *Conn) releaseAuthorizer() { |
||||
if conn.authorizer == -1 { |
||||
return |
||||
} |
||||
authFuncs.mu.Lock() |
||||
delete(authFuncs.m, conn.authorizer) |
||||
authFuncs.mu.Unlock() |
||||
conn.authorizer = -1 |
||||
} |
||||
|
||||
var authFuncs = struct { |
||||
mu sync.RWMutex |
||||
m map[int]Authorizer |
||||
next int |
||||
}{ |
||||
m: make(map[int]Authorizer), |
||||
} |
||||
|
||||
//export go_sqlite_auth_tramp
|
||||
func go_sqlite_auth_tramp(id uintptr, action C.int, cArg1, cArg2 *C.char, cDB *C.char, cTrigger *C.char) C.int { |
||||
authFuncs.mu.RLock() |
||||
auth := authFuncs.m[int(id)] |
||||
authFuncs.mu.RUnlock() |
||||
var arg1, arg2, database, trigger string |
||||
if cArg1 != nil { |
||||
arg1 = C.GoString(cArg1) |
||||
} |
||||
if cArg2 != nil { |
||||
arg2 = C.GoString(cArg2) |
||||
} |
||||
if cDB != nil { |
||||
database = C.GoString(cDB) |
||||
} |
||||
if cTrigger != nil { |
||||
trigger = C.GoString(cTrigger) |
||||
} |
||||
return C.int(auth.Authorize(newActionInfo(OpType(action), arg1, arg2, database, trigger))) |
||||
} |
||||
|
||||
// AuthorizeFunc is a function that implements Authorizer.
|
||||
type AuthorizeFunc func(info ActionInfo) AuthResult |
||||
|
||||
// Authorize calls f.
|
||||
func (f AuthorizeFunc) Authorize(info ActionInfo) AuthResult { |
||||
return f(info) |
||||
} |
||||
|
||||
// AuthResult is the result of a call to an Authorizer. The zero value is
|
||||
// SQLITE_OK.
|
||||
type AuthResult int |
||||
|
||||
// Possible return values of an Authorizer.
|
||||
const ( |
||||
// Cause the entire SQL statement to be rejected with an error.
|
||||
SQLITE_DENY = AuthResult(C.SQLITE_DENY) |
||||
// Disallow the specific action but allow the SQL statement to continue to
|
||||
// be compiled.
|
||||
SQLITE_IGNORE = AuthResult(C.SQLITE_IGNORE) |
||||
) |
||||
|
||||
// String returns the C constant name of the result.
|
||||
func (result AuthResult) String() string { |
||||
switch result { |
||||
default: |
||||
var buf [20]byte |
||||
return "SQLITE_UNKNOWN_AUTH_RESULT(" + string(itoa(buf[:], int64(result))) + ")" |
||||
case AuthResult(C.SQLITE_OK): |
||||
return "SQLITE_OK" |
||||
case SQLITE_DENY: |
||||
return "SQLITE_DENY" |
||||
case SQLITE_IGNORE: |
||||
return "SQLITE_IGNORE" |
||||
} |
||||
} |
||||
|
||||
// ActionInfo holds information about an action to be authorized.
|
||||
//
|
||||
// Only the fields relevant to the Action are populated when this is passed to
|
||||
// an Authorizer.
|
||||
//
|
||||
// https://sqlite.org/c3ref/c_alter_table.html
|
||||
type ActionInfo struct { |
||||
Action OpType |
||||
|
||||
Index string |
||||
Table string |
||||
Column string |
||||
Trigger string |
||||
View string |
||||
Function string |
||||
Pragma string |
||||
PragmaArg string |
||||
Operation string |
||||
Filename string |
||||
Module string |
||||
Database string |
||||
Savepoint string |
||||
|
||||
InnerMostTrigger string |
||||
} |
||||
|
||||
// newActionInfo returns an ActionInfo with the correct fields relevant to the
|
||||
// action.
|
||||
func newActionInfo(action OpType, arg1, arg2, database, trigger string) ActionInfo { |
||||
|
||||
// We use the blank identifier with unused args below simply for visual
|
||||
// consistency between the cases.
|
||||
|
||||
a := ActionInfo{Action: action, Database: database, InnerMostTrigger: trigger} |
||||
switch action { |
||||
case SQLITE_DROP_INDEX, |
||||
SQLITE_DROP_TEMP_INDEX, |
||||
SQLITE_CREATE_INDEX, |
||||
SQLITE_CREATE_TEMP_INDEX: |
||||
/* Index Name Table Name */ |
||||
a.Index = arg1 |
||||
a.Table = arg2 |
||||
|
||||
case SQLITE_DELETE, |
||||
SQLITE_DROP_TABLE, |
||||
SQLITE_DROP_TEMP_TABLE, |
||||
SQLITE_INSERT, |
||||
SQLITE_ANALYZE, |
||||
SQLITE_CREATE_TABLE, |
||||
SQLITE_CREATE_TEMP_TABLE: |
||||
/* Table Name NULL */ |
||||
a.Table = arg1 |
||||
_ = arg2 |
||||
|
||||
case SQLITE_CREATE_TEMP_TRIGGER, |
||||
SQLITE_CREATE_TRIGGER, |
||||
SQLITE_DROP_TEMP_TRIGGER, |
||||
SQLITE_DROP_TRIGGER: |
||||
/* Trigger Name Table Name */ |
||||
a.Trigger = arg1 |
||||
a.Table = arg2 |
||||
|
||||
case SQLITE_CREATE_TEMP_VIEW, |
||||
SQLITE_CREATE_VIEW, |
||||
SQLITE_DROP_TEMP_VIEW, |
||||
SQLITE_DROP_VIEW: |
||||
/* View Name NULL */ |
||||
a.View = arg1 |
||||
_ = arg2 |
||||
|
||||
case SQLITE_PRAGMA: |
||||
/* Pragma Name 1st arg or NULL */ |
||||
a.Pragma = arg1 |
||||
a.PragmaArg = arg2 |
||||
|
||||
case SQLITE_READ, |
||||
SQLITE_UPDATE: |
||||
/* Table Name Column Name */ |
||||
a.Table = arg1 |
||||
a.Column = arg2 |
||||
|
||||
case SQLITE_TRANSACTION: |
||||
/* Operation NULL */ |
||||
a.Operation = arg1 |
||||
_ = arg2 |
||||
|
||||
case SQLITE_ATTACH: |
||||
/* Filename NULL */ |
||||
a.Filename = arg1 |
||||
_ = arg2 |
||||
|
||||
case SQLITE_DETACH: |
||||
/* Database Name NULL */ |
||||
a.Database = arg1 |
||||
_ = arg2 |
||||
|
||||
case SQLITE_ALTER_TABLE: |
||||
/* Database Name Table Name */ |
||||
a.Database = arg1 |
||||
a.Table = arg2 |
||||
|
||||
case SQLITE_REINDEX: |
||||
/* Index Name NULL */ |
||||
a.Index = arg1 |
||||
_ = arg2 |
||||
|
||||
case SQLITE_CREATE_VTABLE, |
||||
SQLITE_DROP_VTABLE: |
||||
/* Table Name Module Name */ |
||||
a.Table = arg1 |
||||
a.Module = arg2 |
||||
|
||||
case SQLITE_FUNCTION: |
||||
/* NULL Function Name */ |
||||
_ = arg1 |
||||
a.Function = arg2 |
||||
|
||||
case SQLITE_SAVEPOINT: |
||||
/* Operation Savepoint Name */ |
||||
a.Operation = arg1 |
||||
a.Savepoint = arg2 |
||||
|
||||
case SQLITE_RECURSIVE, |
||||
SQLITE_SELECT: |
||||
/* NULL NULL */ |
||||
_ = arg1 |
||||
_ = arg2 |
||||
|
||||
case SQLITE_COPY: |
||||
/* No longer used */ |
||||
default: |
||||
panic(fmt.Errorf("unknown action: %v", action)) |
||||
} |
||||
return a |
||||
} |
||||
|
||||
// String returns a string describing only the fields relevant to `a.Action`.
|
||||
func (a ActionInfo) String() string { |
||||
|
||||
switch a.Action { |
||||
case SQLITE_DROP_INDEX, |
||||
SQLITE_DROP_TEMP_INDEX, |
||||
SQLITE_CREATE_INDEX, |
||||
SQLITE_CREATE_TEMP_INDEX: |
||||
/* Index Name Table Name */ |
||||
return fmt.Sprintf("%v: Index: %q Table: %q", |
||||
a.Action, a.Index, a.Table) |
||||
|
||||
case SQLITE_DELETE, |
||||
SQLITE_DROP_TABLE, |
||||
SQLITE_DROP_TEMP_TABLE, |
||||
SQLITE_INSERT, |
||||
SQLITE_ANALYZE, |
||||
SQLITE_CREATE_TABLE, |
||||
SQLITE_CREATE_TEMP_TABLE: |
||||
/* Table Name NULL */ |
||||
return fmt.Sprintf("%v: Table: %q", a.Action, a.Table) |
||||
|
||||
case SQLITE_CREATE_TEMP_TRIGGER, |
||||
SQLITE_CREATE_TRIGGER, |
||||
SQLITE_DROP_TEMP_TRIGGER, |
||||
SQLITE_DROP_TRIGGER: |
||||
/* Trigger Name Table Name */ |
||||
return fmt.Sprintf("%v: Trigger: %q Table: %q", |
||||
a.Action, a.Trigger, a.Table) |
||||
|
||||
case SQLITE_CREATE_TEMP_VIEW, |
||||
SQLITE_CREATE_VIEW, |
||||
SQLITE_DROP_TEMP_VIEW, |
||||
SQLITE_DROP_VIEW: |
||||
/* View Name NULL */ |
||||
return fmt.Sprintf("%v: View: %q", a.Action, a.View) |
||||
|
||||
case SQLITE_PRAGMA: |
||||
/* Pragma Name 1st arg or NULL */ |
||||
return fmt.Sprintf("%v: Pragma: %q", |
||||
a.Action, strings.TrimSpace(a.Pragma+" "+a.PragmaArg)) |
||||
|
||||
case SQLITE_READ, |
||||
SQLITE_UPDATE: |
||||
/* Table Name Column Name */ |
||||
return fmt.Sprintf("%v: Table: %q Column: %q", |
||||
a.Action, a.Table, a.Column) |
||||
|
||||
case SQLITE_TRANSACTION: |
||||
/* Operation NULL */ |
||||
return fmt.Sprintf("%v: Operation: %q", a.Action, a.Operation) |
||||
|
||||
case SQLITE_ATTACH: |
||||
/* Filename NULL */ |
||||
return fmt.Sprintf("%v: Filename: %q", a.Action, a.Filename) |
||||
|
||||
case SQLITE_DETACH: |
||||
/* Database Name NULL */ |
||||
return fmt.Sprintf("%v: Database: %q", a.Action, a.Database) |
||||
|
||||
case SQLITE_ALTER_TABLE: |
||||
/* Database Name Table Name */ |
||||
return fmt.Sprintf("%v: Database: %q Table: %q", |
||||
a.Action, a.Database, a.Table) |
||||
|
||||
case SQLITE_REINDEX: |
||||
/* Index Name NULL */ |
||||
return fmt.Sprintf("%v: Index: %q", a.Action, a.Index) |
||||
|
||||
case SQLITE_CREATE_VTABLE, |
||||
SQLITE_DROP_VTABLE: |
||||
/* Table Name Module Name */ |
||||
return fmt.Sprintf("%v: Table: %q Module: %q", |
||||
a.Action, a.Table, a.Module) |
||||
|
||||
case SQLITE_FUNCTION: |
||||
/* NULL Function Name */ |
||||
return fmt.Sprintf("%v: Function: %q", a.Action, a.Function) |
||||
|
||||
case SQLITE_SAVEPOINT: |
||||
/* Operation Savepoint Name */ |
||||
return fmt.Sprintf("%v: Operation: %q Savepoint: %q", |
||||
a.Action, a.Operation, a.Savepoint) |
||||
|
||||
case SQLITE_RECURSIVE, |
||||
SQLITE_SELECT: |
||||
/* NULL NULL */ |
||||
return fmt.Sprintf("%v:", a.Action) |
||||
|
||||
case SQLITE_COPY: |
||||
/* No longer used */ |
||||
return fmt.Sprintf("%v:", a.Action) |
||||
default: |
||||
return fmt.Sprintf("unknown action: %v", a.Action) |
||||
} |
||||
} |
@ -0,0 +1,133 @@
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2018 David Crawshaw <david@zentus.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package sqlite |
||||
|
||||
// #include <sqlite3.h>
|
||||
// #include <stdlib.h>
|
||||
import "C" |
||||
import ( |
||||
"runtime" |
||||
"unsafe" |
||||
) |
||||
|
||||
// A Backup copies data between two databases.
|
||||
//
|
||||
// It is used to backup file based or in-memory databases.
|
||||
//
|
||||
// Equivalent to the sqlite3_backup* C object.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html
|
||||
type Backup struct { |
||||
ptr *C.sqlite3_backup |
||||
} |
||||
|
||||
// BackupToDB creates a complete backup of the srcDB on the src Conn to a new
|
||||
// database Conn at dstPath. The resulting dst connection is returned. This
|
||||
// will block until the entire backup is complete.
|
||||
//
|
||||
// If srcDB is "", then a default of "main" is used.
|
||||
//
|
||||
// This is very similar to the first example function implemented on the
|
||||
// following page.
|
||||
//
|
||||
// https://www.sqlite.org/backup.html
|
||||
func (src *Conn) BackupToDB(srcDB, dstPath string) (dst *Conn, err error) { |
||||
if dst, err = OpenConn(dstPath, 0); err != nil { |
||||
return |
||||
} |
||||
defer func() { |
||||
if err != nil { |
||||
dst.Close() |
||||
} |
||||
}() |
||||
b, err := src.BackupInit(srcDB, "", dst) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer b.Finish() |
||||
err = b.Step(-1) |
||||
return |
||||
} |
||||
|
||||
// BackupInit initializes a new Backup object to copy from src to dst.
|
||||
//
|
||||
// If srcDB or dstDB is "", then a default of "main" is used.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
|
||||
func (src *Conn) BackupInit(srcDB, dstDB string, dst *Conn) (*Backup, error) { |
||||
var srcCDB, dstCDB *C.char |
||||
defer setCDB(dstDB, &dstCDB)() |
||||
defer setCDB(srcDB, &srcCDB)() |
||||
var b Backup |
||||
b.ptr = C.sqlite3_backup_init(dst.conn, dstCDB, src.conn, srcCDB) |
||||
if b.ptr == nil { |
||||
res := C.sqlite3_errcode(dst.conn) |
||||
return nil, dst.extreserr("Conn.BackupInit", "", res) |
||||
} |
||||
runtime.SetFinalizer(&b, func(b *Backup) { |
||||
if b.ptr != nil { |
||||
panic("open *sqlite.Backup garbage collected, call Finish method") |
||||
} |
||||
}) |
||||
|
||||
return &b, nil |
||||
} |
||||
func setCDB(db string, cdb **C.char) func() { |
||||
if db == "" || db == "main" { |
||||
*cdb = cmain |
||||
return func() {} |
||||
} |
||||
*cdb = C.CString(db) |
||||
return func() { C.free(unsafe.Pointer(cdb)) } |
||||
} |
||||
|
||||
// Step is called one or more times to transfer nPage pages at a time between
|
||||
// databases.
|
||||
//
|
||||
// Use -1 to transfer the entire database at once.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
|
||||
func (b *Backup) Step(nPage int) error { |
||||
res := C.sqlite3_backup_step(b.ptr, C.int(nPage)) |
||||
if res != C.SQLITE_DONE { |
||||
return reserr("Backup.Step", "", "", res) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Finish is called to clean up the resources allocated by BackupInit.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
|
||||
func (b *Backup) Finish() error { |
||||
res := C.sqlite3_backup_finish(b.ptr) |
||||
b.ptr = nil |
||||
return reserr("Backup.Finish", "", "", res) |
||||
} |
||||
|
||||
// Remaining returns the number of pages still to be backed up at the
|
||||
// conclusion of the most recent b.Step().
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining
|
||||
func (b *Backup) Remaining() int { |
||||
return int(C.sqlite3_backup_remaining(b.ptr)) |
||||
} |
||||
|
||||
// PageCount returns the total number of pages in the source database at the
|
||||
// conclusion of the most recent b.Step().
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount
|
||||
func (b *Backup) PageCount() int { |
||||
return int(C.sqlite3_backup_pagecount(b.ptr)) |
||||
} |
@ -0,0 +1,189 @@
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2018 David Crawshaw <david@zentus.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package sqlite |
||||
|
||||
// #include <blocking_step.h>
|
||||
// #include <sqlite3.h>
|
||||
// #include <stdlib.h>
|
||||
// #include <stdint.h>
|
||||
import "C" |
||||
import ( |
||||
"io" |
||||
"unsafe" |
||||
) |
||||
|
||||
var cmain = C.CString("main") |
||||
var ctemp = C.CString("temp") |
||||
|
||||
// OpenBlob opens a blob in a particular {database,table,column,row}.
|
||||
//
|
||||
// https://www.sqlite.org/c3ref/blob_open.html
|
||||
func (conn *Conn) OpenBlob(dbn, table, column string, row int64, write bool) (*Blob, error) { |
||||
var cdb *C.char |
||||
switch dbn { |
||||
case "", "main": |
||||
cdb = cmain |
||||
case "temp": |
||||
cdb = ctemp |
||||
default: |
||||
cdb = C.CString(dbn) |
||||
defer C.free(unsafe.Pointer(cdb)) |
||||
} |
||||
var flags C.int |
||||
if write { |
||||
flags = 1 |
||||
} |
||||
|
||||
ctable := C.CString(table) |
||||
ccolumn := C.CString(column) |
||||
defer func() { |
||||
C.free(unsafe.Pointer(ctable)) |
||||
C.free(unsafe.Pointer(ccolumn)) |
||||
}() |
||||
|
||||
blob := &Blob{conn: conn} |
||||
|
||||
for { |
||||
conn.count++ |
||||
if err := conn.interrupted("Conn.OpenBlob", ""); err != nil { |
||||
return nil, err |
||||
} |
||||
switch res := C.sqlite3_blob_open(conn.conn, cdb, ctable, ccolumn, |
||||
C.sqlite3_int64(row), flags, &blob.blob); res { |
||||
case C.SQLITE_LOCKED_SHAREDCACHE: |
||||
if res := C.wait_for_unlock_notify( |
||||
conn.conn, conn.unlockNote); res != C.SQLITE_OK { |
||||
return nil, conn.reserr("Conn.OpenBlob(Wait)", "", res) |
||||
} |
||||
// loop
|
||||
case C.SQLITE_OK: |
||||
blob.size = int64(C.sqlite3_blob_bytes(blob.blob)) |
||||
return blob, nil |
||||
default: |
||||
return nil, conn.extreserr("Conn.OpenBlob", "", res) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Blob provides streaming access to SQLite blobs.
|
||||
type Blob struct { |
||||
io.ReadWriteSeeker |
||||
io.ReaderAt |
||||
io.WriterAt |
||||
io.Closer |
||||
|
||||
conn *Conn |
||||
blob *C.sqlite3_blob |
||||
off int64 |
||||
size int64 |
||||
} |
||||
|
||||
// https://www.sqlite.org/c3ref/blob_read.html
|
||||
func (blob *Blob) ReadAt(p []byte, off int64) (n int, err error) { |
||||
if blob.blob == nil { |
||||
return 0, errInvalidBlob |
||||
} |
||||
if err := blob.conn.interrupted("Blob.ReadAt", ""); err != nil { |
||||
return 0, err |
||||
} |
||||
lenp := C.int(len(p)) |
||||
res := C.sqlite3_blob_read(blob.blob, unsafe.Pointer(&p[0]), lenp, C.int(off)) |
||||
if err := blob.conn.reserr("Blob.ReadAt", "", res); err != nil { |
||||
return 0, err |
||||
} |
||||
return len(p), nil |
||||
} |
||||
|
||||
// https://www.sqlite.org/c3ref/blob_write.html
|
||||
func (blob *Blob) WriteAt(p []byte, off int64) (n int, err error) { |
||||
if blob.blob == nil { |
||||
return 0, errInvalidBlob |
||||
} |
||||
if err := blob.conn.interrupted("Blob.WriteAt", ""); err != nil { |
||||
return 0, err |
||||
} |
||||
lenp := C.int(len(p)) |
||||
res := C.sqlite3_blob_write(blob.blob, unsafe.Pointer(&p[0]), lenp, C.int(off)) |
||||
if err := blob.conn.reserr("Blob.WriteAt", "", res); err != nil { |
||||
return 0, err |
||||
} |
||||
return len(p), nil |
||||
} |
||||
|
||||
func (blob *Blob) Read(p []byte) (n int, err error) { |
||||
if blob.off >= blob.size { |
||||
return 0, io.EOF |
||||
} |
||||
if rem := blob.size - blob.off; int64(len(p)) > rem { |
||||
p = p[:rem] |
||||
} |
||||
n, err = blob.ReadAt(p, blob.off) |
||||
blob.off += int64(n) |
||||
return n, err |
||||
} |
||||
|
||||
func (blob *Blob) Write(p []byte) (n int, err error) { |
||||
if rem := blob.size - blob.off; int64(len(p)) > rem { |
||||
return 0, io.ErrShortWrite |
||||
} |
||||
n, err = blob.WriteAt(p, blob.off) |
||||
blob.off += int64(n) |
||||
return n, err |
||||
} |
||||
|
||||
func (blob *Blob) Seek(offset int64, whence int) (int64, error) { |
||||
const ( |
||||
SeekStart = 0 |
||||
SeekCurrent = 1 |
||||
SeekEnd = 2 |
||||
) |
||||
switch whence { |
||||
case SeekStart: |
||||
// use offset directly
|
||||
case SeekCurrent: |
||||
offset += blob.off |
||||
case SeekEnd: |
||||
offset += blob.size |
||||
} |
||||
if offset < 0 { |
||||
var buf [20]byte |
||||
return -1, Error{ |
||||
Code: SQLITE_ERROR, |
||||
Loc: "Blob.Seek", |
||||
Msg: "attempting to seek before beginning of blob: " + string(itoa(buf[:], offset)), |
||||
} |
||||
} |
||||
blob.off = offset |
||||
return offset, nil |
||||
} |
||||
|
||||
// Size returns the total size of a blob.
|
||||
func (blob *Blob) Size() int64 { |
||||
return blob.size |
||||
} |
||||
|
||||
// https://www.sqlite.org/c3ref/blob_close.html
|
||||
func (blob *Blob) Close() error { |
||||
if blob.blob == nil { |
||||
return errInvalidBlob |
||||
} |
||||
err := blob.conn.reserr("Blob.Close", "", C.sqlite3_blob_close(blob.blob)) |
||||
blob.blob = nil |
||||
return err |
||||
} |
||||
|
||||
var errInvalidBlob = Error{Code: SQLITE_ERROR, Msg: "invalid blob"} |
||||
|
||||
// TODO: Blob Reopen
|
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2018 David Crawshaw <david@zentus.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
// This file defines the wait_for_unlock_notify function.
|
||||
// See the documentation on Stmt.Step.
|
||||
|
||||
#include <blocking_step.h> |
||||
#include <stdlib.h> |
||||
|
||||
unlock_note* unlock_note_alloc() { |
||||
unlock_note* un = (unlock_note*)malloc(sizeof(unlock_note)); |
||||
pthread_mutex_init(&un->mu, 0); |
||||
pthread_cond_init(&un->cond, 0); |
||||
return un; |
||||
} |
||||
|
||||
void unlock_note_free(unlock_note* un) { |
||||
pthread_cond_destroy(&un->cond); |
||||
pthread_mutex_destroy(&un->mu); |
||||
free(un); |
||||
} |
||||
|
||||
void unlock_note_fire(unlock_note* un) { |
||||
pthread_mutex_lock(&un->mu); |
||||
un->fired = 1; |
||||
pthread_cond_signal(&un->cond); |
||||
pthread_mutex_unlock(&un->mu); |
||||
} |
||||
|
||||
static void unlock_notify_cb(void **apArg, int nArg) { |
||||
for(int i=0; i < nArg; i++) { |
||||
unlock_note_fire((unlock_note*)apArg[i]); |
||||
} |
||||
} |
||||
|
||||
int wait_for_unlock_notify(sqlite3 *db, unlock_note* un) { |
||||
un->fired = 0; |
||||
|
||||
int res = sqlite3_unlock_notify(db, unlock_notify_cb, (void *)un); |
||||
|
||||
if (res == SQLITE_OK) { |
||||
pthread_mutex_lock(&un->mu); |
||||
if (!un->fired) { |
||||
pthread_cond_wait(&un->cond, &un->mu); |
||||
} |
||||
pthread_mutex_unlock(&un->mu); |
||||
} |
||||
|
||||
return res; |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
// This file declares the wait_for_unlock_notify function.
|
||||
// See the documentation on Stmt.Step.
|
||||
|
||||
#include <sqlite3.h> |
||||
#include <pthread.h> |
||||
|
||||
typedef struct unlock_note { |
||||
int fired; |
||||
pthread_cond_t cond; |
||||
pthread_mutex_t mu; |
||||
} unlock_note; |
||||
|
||||
unlock_note* unlock_note_alloc(); |
||||
void unlock_note_fire(unlock_note* un); |
||||
void unlock_note_free(unlock_note* un); |
||||
|
||||
int wait_for_unlock_notify(sqlite3 *db, unlock_note* un); |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
// +build dummy
|
||||
|
||||
// Package c contains only a C file.
|
||||
//
|
||||
// This Go file is part of a workaround for `go mod vendor`.
|
||||
// Please see the file dummy.go at the root of the module for more information.
|
||||
package c |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2018 David Crawshaw <david@zentus.com>
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
/* |
||||
Package sqlite provides a Go interface to SQLite 3. |
||||
|
||||
The semantics of this package are deliberately close to the |
||||
SQLite3 C API, so it is helpful to be familiar with |
||||
http://www.sqlite.org/c3ref/intro.html.
|
||||
|
||||
An SQLite connection is represented by a *sqlite.Conn. |
||||
Connections cannot be used concurrently. |
||||
A typical Go program will create a pool of connections |
||||
(using Open to create a *sqlitex.Pool) so goroutines can |
||||
borrow a connection while they need to talk to the database. |
||||
|
||||
This package assumes SQLite will be used concurrently by the |
||||
process through several connections, so the build options for |
||||
SQLite enable multi-threading and the shared cache: |
||||
https://www.sqlite.org/sharedcache.html
|
||||
|
||||
The implementation automatically handles shared cache locking, |
||||
see the documentation on Stmt.Step for details. |
||||
|
||||
The optional SQLite3 compiled in are: FTS5, RTree, JSON1, Session, GeoPoly |
||||
|
||||
This is not a database/sql driver. |
||||
|
||||
|
||||
Statement Caching |
||||
|
||||
Statements are prepared with the Prepare and PrepareTransient methods. |
||||
When using Prepare, statements are keyed inside a connection by the |
||||
original query string used to create them. This means long-running |
||||
high-performance code paths can write: |
||||
|
||||
stmt, err := conn.Prepare("SELECT ...") |
||||
|
||||
After all the connections in a pool have been warmed up by passing |
||||
through one of these Prepare calls, subsequent calls are simply a |
||||
map lookup that returns an existing statement. |
||||
|
||||
|
||||
Streaming Blobs |
||||
|
||||
The sqlite package supports the SQLite incremental I/O interface for |
||||
streaming blob data into and out of the the database without loading |
||||
the entire blob into a single []byte. |
||||
(This is important when working either with very large blobs, or |
||||
more commonly, a large number of moderate-sized blobs concurrently.) |
||||
|
||||
To write a blob, first use an INSERT statement to set the size of the |
||||
blob and assign a rowid: |
||||
|
||||
"INSERT INTO blobs (myblob) VALUES (?);" |
||||
|
||||
Use BindZeroBlob or SetZeroBlob to set the size of myblob. |
||||
Then you can open the blob with: |
||||
|
||||
b, err := conn.OpenBlob("", "blobs", "myblob", conn.LastInsertRowID(), true) |
||||
|
||||
|
||||
Deadlines and Cancellation |
||||
|
||||
Every connection can have a done channel associated with it using |
||||
the SetInterrupt method. This is typically the channel returned by |
||||
a context.Context Done method. |
||||
|
||||
For example, a timeout can be associated with a connection session: |
||||
|
||||
ctx := context.WithTimeout(context.Background(), 100*time.Millisecond) |
||||
conn.SetInterrupt(ctx.Done()) |
||||
|
||||
As database connections are long-lived, the SetInterrupt method can |
||||
be called multiple times to reset the associated lifetime. |
||||
|
||||
When using pools, the shorthand for associating a context with a |
||||
connection is: |
||||
|
||||
conn := dbpool.Get(ctx) |
||||
if conn == nil { |
||||
// ... handle error
|
||||
} |
||||
defer dbpool.Put(c) |
||||
|
||||
|
||||
Transactions |
||||
|
||||
SQLite transactions have to be managed manually with this package |
||||
by directly calling BEGIN / COMMIT / ROLLBACK or |
||||
SAVEPOINT / RELEASE/ ROLLBACK. The sqlitex has a Savepoint |
||||
function that helps automate this. |
||||
|
||||
|
||||
A typical HTTP Handler |
||||
|
||||
Using a Pool to execute SQL in a concurrent HTTP handler. |
||||
|
||||
var dbpool *sqlitex.Pool |
||||
|
||||
func main() { |
||||
var err error |
||||
dbpool, err = sqlitex.Open("file:memory:?mode=memory", 0, 10) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
http.HandleFunc("/", handle) |
||||
log.Fatal(http.ListenAndServe(":8080", nil)) |
||||
} |
||||
|
||||
func handle(w http.ResponseWriter, r *http.Request) { |
||||
conn := dbpool.Get(r.Context()) |
||||
if conn == nil { |
||||
return |
||||
} |
||||
defer dbpool.Put(conn) |
||||
stmt := conn.Prep("SELECT foo FROM footable WHERE id = $id;") |
||||
stmt.SetText("$id", "_user_id_") |
||||
for { |
||||
if hasRow, err := stmt.Step(); err != nil { |
||||
// ... handle error
|
||||
} else if !hasRow { |
||||
break |
||||
} |
||||
foo := stmt.GetText("foo") |
||||
// ... use foo
|
||||
} |
||||
} |
||||
|
||||
For helper functions that make some kinds of statements easier to |
||||
write see the sqlitex package. |
||||
*/ |
||||
package sqlite // import "crawshaw.io/sqlite"
|
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
// +build dummy
|
||||
|
||||
// This file is part of a workaround for `go mod vendor` which won't vendor
|
||||
// C files if there's no Go file in the same directory.
|
||||
// This would prevent the c/sqlite3.c file to be vendored.
|
||||
//
|
||||
// This Go file imports the c directory where there is another dummy.go file which
|
||||
// is the second part of this workaround.
|
||||
//
|
||||
// These two files combined make it so `go mod vendor` behaves correctly.
|
||||
//
|
||||
// See this issue for reference: https://github.com/golang/go/issues/26366
|
||||
|
||||
package sqlite |
||||
|
||||
import ( |
||||
_ "crawshaw.io/sqlite/c" |
||||
) |