PostgreSQL - switch to SQLite on error
This commit is contained in:
parent
d6ee3a059f
commit
eaa0c27bd7
13 changed files with 145 additions and 197 deletions
|
|
@ -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>
|
||||
|
|
|
|||
75
README.md
75
README.md
|
|
@ -10,82 +10,66 @@
|
|||

|
||||
|
||||
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)
|
||||
|
||||

|
||||

|
||||
|
||||
## 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>
|
||||
BIN
assets/Screenshot 2024-08-29 at 01-41-12 WatchYourLAN.png
Normal file
BIN
assets/Screenshot 2024-08-29 at 01-41-12 WatchYourLAN.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
assets/Screenshot 2024-08-29 at 11-17-59 WatchYourLAN.png
Normal file
BIN
assets/Screenshot 2024-08-29 at 11-17-59 WatchYourLAN.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
assets/Screenshot_1.png
Normal file
BIN
assets/Screenshot_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", "")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
36
internal/db/connect.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
42
internal/db/select-exec.go
Normal file
42
internal/db/select-exec.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(""));
|
||||
}
|
||||
Loading…
Reference in a new issue