PostgreSQL - switch to SQLite on error

This commit is contained in:
aceberg 2024-08-29 21:37:16 +07:00
parent d6ee3a059f
commit eaa0c27bd7
13 changed files with 145 additions and 197 deletions

View file

@ -6,8 +6,6 @@ builds:
env: [CGO_ENABLED=0]
goos:
- linux
# - windows
# - darwin
goarch:
- 386
- amd64
@ -17,15 +15,6 @@ builds:
- "5"
- "6"
- "7"
# ignore:
# - goos: darwin
# goarch: 386
# - goos: darwin
# goarch: arm
# - goos: windows
# goarch: 386
# - goos: windows
# goarch: arm
nfpms:
- maintainer: aceberg <aceberg_a@proton.me>

View file

@ -10,82 +10,66 @@
![Docker Pulls](https://img.shields.io/docker/pulls/aceberg/watchyourlan)
Lightweight network IP scanner with web GUI
- [Quick start](https://github.com/aceberg/WatchYourLAN#quick-start)
- [Install .deb](https://github.com/aceberg/ppa)
- [Config](https://github.com/aceberg/WatchYourLAN#config)
- [Config file](https://github.com/aceberg/WatchYourLAN#config-file)
- [Options](https://github.com/aceberg/WatchYourLAN#options)
- [Local network only](https://github.com/aceberg/WatchYourLAN#local-network-only)
- [Thanks](https://github.com/aceberg/WatchYourLAN#thanks)
> [!WARNING]
> This README is about version 2.0, which is going to be released soon. There will be BREAKING CHANGES! Version 1.0 can be found in this brunch: [v1](https://github.com/aceberg/WatchYourLAN/tree/v1)
![Screenshot_v0.6](https://raw.githubusercontent.com/aceberg/WatchYourLAN/main/assets/Screenshot_v0.6.png)
![Screenshot_1](https://raw.githubusercontent.com/aceberg/WatchYourLAN/main/assets/Screenshot_1.png)
## Quick start
<details>
<summary>Expand</summary>
Replace `$YOURTIMEZONE` with correct time zone and `$YOURIFACE` with network interface you want to scan. Network mode must be `host`. Set `$DOCKERDATAPATH` for container to save data:
```sh
docker run --name wyl \
-e "IFACE=$YOURIFACE" \
-e "IFACES=$YOURIFACE" \
-e "TZ=$YOURTIMEZONE" \
--network="host" \
-v $DOCKERDATAPATH/wyl:/data \
-v $DOCKERDATAPATH/wyl:/data/WatchYourLAN \
aceberg/watchyourlan
```
Web GUI should be at http://localhost:8840
## Config
</details>
## Config
<details>
<summary>Expand</summary>
Configuration can be done through config file or environment variables
| Variable | Description | Default |
| -------- | ----------- | ------- |
| ARP_TIMEOUT | Per host timeout for <b>arp-scan</b> (in milliseconds) | 500 |
| AUTH | Enable Session-Cookie authentication | false |
| AUTH_EXPIRE | Session expiration time. A number and suffix: **m, h, d** or **M**. | 7d |
| AUTH_USER | Username | "" |
| AUTH_PASSWORD | Encrypted password (bcrypt). [How to encrypt password with bcrypt?](docs/BCRYPT.md) | "" |
| COLOR | Background color: light or dark | dark |
| DBPATH | Path to Database | /data/db.sqlite |
| GUIIP | Address for web GUI | 0.0.0.0 |
| GUIPORT | Port for web GUI | 8840 |
| HISTORY_DAYS | Keep devices online/offline history for (days) | 30 |
| IFACE | Interface to scan. Could be one or more, separated by space. Currently `docker0` is not allowed, as arp-scan wouldn't work with it correctly | enp1s0 |
| IGNOREIP | If you want to detect unknown hosts by MAC only, set this variable to "yes" | no |
| LOGLEVEL | How much log output you want to see ("short" or "verbose") | verbose |
| SHOUTRRR_URL | Url to any notification service supported by [Shoutrrr](https://github.com/containrrr/shoutrrr) (gotify, email, telegram and others) or [Generic Webhook](https://github.com/containrrr/shoutrrr/blob/main/docs/services/generic.md) | "" |
| THEME | Any theme name from https://bootswatch.com in lowcase or [additional](https://github.com/aceberg/aceberg-bootswatch-fork) (emerald, grass, sand) | solar |
| TIMEOUT | Time between scans (seconds) | 60 (1 minute) |
</details>
## Config file
> [!WARNING]
> Config file format has been migrated to YAML in release [v1.0.0](https://github.com/aceberg/WatchYourLAN/releases/tag/1.0.0).
Config file path is `/data/config.yaml`.
All variables could be set there. Example:
```yaml
color: light
dbpath: /data/db.sqlite
guiip: 192.168.2.1
guiport: "8840"
iface: enp1s0
ignoreip: "no"
loglevel: short
shoutrrr_url: gotify://192.168.2.1:8083/AwQqpAae.rrl5Ob/?title=Unknown host detected&DisableTLS=yes
theme: solar
timeout: 120
```
<details>
<summary>Expand</summary>
</details>
## Options
<details>
<summary>Expand</summary>
| Key | Description | Default |
| -------- | ----------- | ------- |
| -c | Path to config file | /data/config.yaml |
| -n | Path to node modules (see below) | "" |
</details>
## Local network only
<details>
<summary>Expand</summary>
By default, this app pulls themes, icons and fonts from the internet. But, in some cases, it may be useful to have an independent from global network setup. I created a separate [image](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) with all necessary modules and fonts.
Run with Docker:
```sh
@ -103,8 +87,15 @@ docker run --name wyl \
```
Or use [docker-compose](docker-compose-local.yml)
</details>
## Thanks
<details>
<summary>Expand</summary>
- All go packages listed in [dependencies](https://github.com/aceberg/WatchYourLAN/network/dependencies)
- Favicon and logo: [Access point icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/access-point)
- [Bootstrap](https://getbootstrap.com/)
- Themes: [Free themes for Bootstrap](https://bootswatch.com)
</details>

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
assets/Screenshot_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View file

@ -5,7 +5,7 @@ services:
network_mode: "host"
restart: unless-stopped
volumes:
- ~/.dockerdata/wyl:/data
- ~/.dockerdata/wyl:/data/WatchYourLAN
environment:
TZ: Asia/Novosibirsk # required: needs your TZ for correct time
IFACE: "virbr-bw wlxf4ec3892dd51" # required: 1 or more interface

View file

@ -12,13 +12,13 @@ func Get(path string) (config models.Conf) {
viper.SetDefault("HOST", "0.0.0.0")
viper.SetDefault("PORT", "8840")
viper.SetDefault("THEME", "solar")
viper.SetDefault("THEME", "sand")
viper.SetDefault("COLOR", "dark")
viper.SetDefault("NODEPATH", "")
viper.SetDefault("LOG_LEVEL", "info")
viper.SetDefault("ARP_ARGS", "")
viper.SetDefault("IFACES", "")
viper.SetDefault("TIMEOUT", 60)
viper.SetDefault("TIMEOUT", 120)
viper.SetDefault("TRIM_HIST", 48)
viper.SetDefault("SHOUTRRR_URL", "")

View file

@ -9,6 +9,7 @@ import (
// Data to connect to DB
type Data struct {
Use string
Path string
SQLitePath string
PGConnect string
PrimaryKey string
@ -16,25 +17,18 @@ type Data struct {
var currentDB Data
func dbExec(sqlStatement string) {
func setCurrentDB() {
if currentDB.Use == "postgres" {
dbExecPG(sqlStatement)
if currentDB.Use == "postgres" && currentDB.PGConnect != "" {
currentDB.Path = currentDB.PGConnect
currentDB.PrimaryKey = "BIGSERIAL PRIMARY KEY"
} else {
dbExecLite(sqlStatement)
}
}
// Select - select all from table
func Select(table string) (dbHosts []models.Host) {
if currentDB.Use == "postgres" {
dbHosts = selectPG(table)
} else {
dbHosts = selectLite(table)
currentDB.Use = "sqlite"
currentDB.Path = currentDB.SQLitePath
currentDB.PrimaryKey = "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE"
}
return dbHosts
slog.Info("Using DB", "type", currentDB.Use)
}
// SetCurrent - set paths and which DB to use
@ -44,12 +38,5 @@ func SetCurrent(config models.Conf) {
currentDB.SQLitePath = config.DBPath
currentDB.PGConnect = config.PGConnect
if currentDB.Use == "postgres" && currentDB.PGConnect != "" {
currentDB.PrimaryKey = "BIGSERIAL PRIMARY KEY"
} else {
currentDB.Use = "sqlite"
currentDB.PrimaryKey = "INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE"
}
slog.Info("Using DB", "type", currentDB.Use)
setCurrentDB()
}

36
internal/db/connect.go Normal file
View file

@ -0,0 +1,36 @@
package db
import (
"log/slog"
"github.com/jmoiron/sqlx"
// import postgres
_ "github.com/lib/pq"
// Import sqlite module
_ "modernc.org/sqlite"
"github.com/aceberg/WatchYourLAN/internal/check"
)
func connectDB() (*sqlx.DB, bool) {
var ok bool
db, err := sqlx.Open(currentDB.Use, currentDB.Path)
check.IfError(err)
err = db.Ping()
if check.IfError(err) && currentDB.Use == "postgres" {
slog.Warn("PostgreSQL connection error. Falling back to SQLite.")
currentDB.Use = "sqlite"
setCurrentDB()
Create()
}
if err == nil {
ok = true
}
return db, ok
}

View file

@ -1,49 +0,0 @@
package db
import (
"log/slog"
"github.com/jmoiron/sqlx"
// import postgres
_ "github.com/lib/pq"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func dbExecPG(sqlStatement string) {
// slog.Debug("PG Exec " + sqlStatement)
db, err := sqlx.Connect("postgres", currentDB.PGConnect)
if check.IfError(err) {
slog.Warn("PostgreSQL connection error. Falling back to SQLite.")
currentDB.Use = "sqlite"
Create()
}
defer db.Close()
_, err = db.Exec(sqlStatement)
check.IfError(err)
}
func selectPG(table string) (dbHosts []models.Host) {
sqlStatement := `SELECT * FROM ` + table + ` ORDER BY "DATE" DESC`
// slog.Debug("PG Select " + sqlStatement)
db, err := sqlx.Connect("postgres", currentDB.PGConnect)
if check.IfError(err) {
slog.Warn("PostgreSQL connection error. Falling back to SQLite.")
currentDB.Use = "sqlite"
Create()
}
defer db.Close()
err = db.Select(&dbHosts, sqlStatement)
check.IfError(err)
return dbHosts
}

View file

@ -0,0 +1,42 @@
package db
import (
// "log/slog"
"sync"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
var mu sync.Mutex
func dbExec(sqlStatement string) {
db, ok := connectDB()
defer db.Close()
if ok {
mu.Lock()
_, err := db.Exec(sqlStatement)
mu.Unlock()
check.IfError(err)
}
}
// Select - get all hosts
func Select(table string) (dbHosts []models.Host) {
sqlStatement := `SELECT * FROM ` + table + ` ORDER BY "DATE" DESC`
db, ok := connectDB()
defer db.Close()
if ok {
mu.Lock()
err := db.Select(&dbHosts, sqlStatement)
mu.Unlock()
check.IfError(err)
}
return dbHosts
}

View file

@ -1,44 +0,0 @@
package db
import (
"sync"
"github.com/jmoiron/sqlx"
// Import sqlite module
_ "modernc.org/sqlite"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
var mu sync.Mutex
func dbExecLite(sqlStatement string) {
mu.Lock()
db, err := sqlx.Connect("sqlite", currentDB.SQLitePath)
check.IfError(err)
defer db.Close()
_, err = db.Exec(sqlStatement)
mu.Unlock()
check.IfError(err)
}
func selectLite(table string) (dbHosts []models.Host) {
sqlStatement := "SELECT * FROM " + table + " ORDER BY DATE DESC"
mu.Lock()
db, _ := sqlx.Connect("sqlite", currentDB.SQLitePath)
defer db.Close()
err := db.Select(&dbHosts, sqlStatement)
mu.Unlock()
check.IfError(err)
return dbHosts
}

View file

@ -12,46 +12,42 @@ function displayArrayData(someArray) {
}
function sortByAny(someArray, field) {
// console.log("Field =", field);
if (field != oldField) {
if (field == 'IP') {
someArray.sort((a, b) => sortIP(a, b, true));
} else {
someArray.sort(byFieldDown(field));
}
oldField = field;
down = true;
} else {
if (field == 'IP') {
someArray.sort((a, b) => sortIP(a, b, false));
} else {
someArray.sort(byFieldUp(field));
}
oldField = '';
down = false;
}
if (field == 'IP') {
someArray.sort((a, b) => sortIP(a, b, down));
} else {
someArray.sort((a, b) => byField(a, b, field, down));
}
displayArrayData(someArray);
}
function byFieldUp(fieldName){
return (a, b) => a[fieldName] < b[fieldName] ? 1 : -1;
}
function byFieldDown(fieldName){
return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
function byField(a, b, fieldName, down){
if (a[fieldName] > b[fieldName]) {
return down;
} else {
return !down;
}
}
function sortIP(a, b, down) {
const num1 = Number(a.IP.split(".").map((num) => (`000${num}`).slice(-3) ).join(""));
const num2 = Number(b.IP.split(".").map((num) => (`000${num}`).slice(-3) ).join(""));
const num1 = numIP(a);
const num2 = numIP(b);
if (down) {
return num1-num2;
} else {
return num2-num1;
}
}
function numIP(a) {
return Number(a.IP.split(".").map((num) => (`000${num}`).slice(-3) ).join(""));
}