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

View file

@ -10,82 +10,66 @@
![Docker Pulls](https://img.shields.io/docker/pulls/aceberg/watchyourlan) ![Docker Pulls](https://img.shields.io/docker/pulls/aceberg/watchyourlan)
Lightweight network IP scanner with web GUI Lightweight network IP scanner with web GUI
- [Quick start](https://github.com/aceberg/WatchYourLAN#quick-start) > [!WARNING]
- [Install .deb](https://github.com/aceberg/ppa) > 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)
- [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)
![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 ## 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: 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 ```sh
docker run --name wyl \ docker run --name wyl \
-e "IFACE=$YOURIFACE" \ -e "IFACES=$YOURIFACE" \
-e "TZ=$YOURTIMEZONE" \ -e "TZ=$YOURTIMEZONE" \
--network="host" \ --network="host" \
-v $DOCKERDATAPATH/wyl:/data \ -v $DOCKERDATAPATH/wyl:/data/WatchYourLAN \
aceberg/watchyourlan aceberg/watchyourlan
``` ```
Web GUI should be at http://localhost:8840 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 Configuration can be done through config file or environment variables
| Variable | Description | Default | | 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 | | 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 ## 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`. <details>
All variables could be set there. Example: <summary>Expand</summary>
```yaml
color: light
dbpath: /data/db.sqlite </details>
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
```
## Options ## Options
<details>
<summary>Expand</summary>
| Key | Description | Default | | Key | Description | Default |
| -------- | ----------- | ------- | | -------- | ----------- | ------- |
| -c | Path to config file | /data/config.yaml | | -c | Path to config file | /data/config.yaml |
| -n | Path to node modules (see below) | "" | | -n | Path to node modules (see below) | "" |
</details>
## Local network only ## 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. 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: Run with Docker:
```sh ```sh
@ -103,8 +87,15 @@ docker run --name wyl \
``` ```
Or use [docker-compose](docker-compose-local.yml) Or use [docker-compose](docker-compose-local.yml)
</details>
## Thanks ## Thanks
<details>
<summary>Expand</summary>
- All go packages listed in [dependencies](https://github.com/aceberg/WatchYourLAN/network/dependencies) - 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) - Favicon and logo: [Access point icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/access-point)
- [Bootstrap](https://getbootstrap.com/) - [Bootstrap](https://getbootstrap.com/)
- Themes: [Free themes for Bootstrap](https://bootswatch.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" network_mode: "host"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ~/.dockerdata/wyl:/data - ~/.dockerdata/wyl:/data/WatchYourLAN
environment: environment:
TZ: Asia/Novosibirsk # required: needs your TZ for correct time TZ: Asia/Novosibirsk # required: needs your TZ for correct time
IFACE: "virbr-bw wlxf4ec3892dd51" # required: 1 or more interface 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("HOST", "0.0.0.0")
viper.SetDefault("PORT", "8840") viper.SetDefault("PORT", "8840")
viper.SetDefault("THEME", "solar") viper.SetDefault("THEME", "sand")
viper.SetDefault("COLOR", "dark") viper.SetDefault("COLOR", "dark")
viper.SetDefault("NODEPATH", "") viper.SetDefault("NODEPATH", "")
viper.SetDefault("LOG_LEVEL", "info") viper.SetDefault("LOG_LEVEL", "info")
viper.SetDefault("ARP_ARGS", "") viper.SetDefault("ARP_ARGS", "")
viper.SetDefault("IFACES", "") viper.SetDefault("IFACES", "")
viper.SetDefault("TIMEOUT", 60) viper.SetDefault("TIMEOUT", 120)
viper.SetDefault("TRIM_HIST", 48) viper.SetDefault("TRIM_HIST", 48)
viper.SetDefault("SHOUTRRR_URL", "") viper.SetDefault("SHOUTRRR_URL", "")

View file

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

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