Compare commits
No commits in common. "2-client-basics" and "main" have entirely different histories.
2-client-b
...
main
@ -7,33 +7,20 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
Linting:
|
Linting:
|
||||||
runs-on: "ubuntu-22.04"
|
runs-on: "ubuntu-22.04"
|
||||||
container:
|
|
||||||
volumes:
|
|
||||||
- "bunker-go-cache:/home/container"
|
|
||||||
steps:
|
steps:
|
||||||
- name: "checkout source"
|
- name: "checkout source"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: "lint"
|
- name: "lint"
|
||||||
uses: docker://code.pztrn.name/containers/go-toolbox:v8
|
uses: docker://code.pztrn.name/containers/go-toolbox:v5
|
||||||
with:
|
with:
|
||||||
command: "/usr/local/bin/task lint"
|
command: "/usr/local/bin/task lint"
|
||||||
|
|
||||||
# Tests:
|
Tests:
|
||||||
# runs-on: "ubuntu-22.04"
|
runs-on: "ubuntu-22.04"
|
||||||
# env:
|
steps:
|
||||||
# GOCACHE: /home/container/go/buildcache
|
- name: "checkout source"
|
||||||
# GOMODCACHE: /home/container/go/pkg
|
uses: actions/checkout@v4
|
||||||
# steps:
|
- name: "tests"
|
||||||
# - name: "checkout source"
|
uses: docker://code.pztrn.name/containers/go-toolbox:v5
|
||||||
# uses: actions/checkout@v4
|
with:
|
||||||
# - name: "cache go dependencies"
|
command: "/usr/local/bin/task test"
|
||||||
# uses: actions/cache@v4
|
|
||||||
# with:
|
|
||||||
# path: |
|
|
||||||
# /home/container/go
|
|
||||||
# /home/container/golangci-lint
|
|
||||||
# key: bunker-go-cache
|
|
||||||
# - name: "tests"
|
|
||||||
# uses: docker://code.pztrn.name/containers/go-toolbox:v8
|
|
||||||
# with:
|
|
||||||
# command: "/usr/local/bin/task test"
|
|
||||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,7 +0,0 @@
|
|||||||
*DS_Store*
|
|
||||||
fyne-cross
|
|
||||||
_build
|
|
||||||
*.apk
|
|
||||||
*.app
|
|
||||||
dist
|
|
||||||
.task
|
|
@ -1,93 +0,0 @@
|
|||||||
---
|
|
||||||
version: "2"
|
|
||||||
|
|
||||||
linters:
|
|
||||||
default: all
|
|
||||||
disable:
|
|
||||||
- containedctx
|
|
||||||
- depguard
|
|
||||||
- exhaustruct
|
|
||||||
- gochecknoglobals
|
|
||||||
- interfacebloat
|
|
||||||
- ireturn
|
|
||||||
- mnd
|
|
||||||
- testpackage
|
|
||||||
- tparallel
|
|
||||||
- unused
|
|
||||||
- varnamelen
|
|
||||||
- noinlineerr
|
|
||||||
- wsl
|
|
||||||
settings:
|
|
||||||
cyclop:
|
|
||||||
max-complexity: 30
|
|
||||||
package-average: 30
|
|
||||||
forbidigo:
|
|
||||||
forbid:
|
|
||||||
- pattern: ^(fmt\.Print(|f|ln)|print|println)$
|
|
||||||
- pattern: ^time\.Now\(\)($|\.F|\.A|\.B|\.L|\.UTC\(\)\.I|,|\))(# Calls of time\.Now() without \.UTC() is prohibited\.)?
|
|
||||||
funlen:
|
|
||||||
lines: 200
|
|
||||||
statements: 60
|
|
||||||
ignore-comments: true
|
|
||||||
gocyclo:
|
|
||||||
min-complexity: 20
|
|
||||||
govet:
|
|
||||||
enable-all: true
|
|
||||||
funcorder:
|
|
||||||
constructor: true
|
|
||||||
struct-method: false
|
|
||||||
alphabetical: true
|
|
||||||
lll:
|
|
||||||
line-length: 120
|
|
||||||
nestif:
|
|
||||||
min-complexity: 20
|
|
||||||
tagliatelle:
|
|
||||||
case:
|
|
||||||
rules:
|
|
||||||
json: snake
|
|
||||||
yaml: camel
|
|
||||||
use-field-name: true
|
|
||||||
wsl_v5:
|
|
||||||
allow-first-in-block: true
|
|
||||||
allow-whole-block: false
|
|
||||||
branch-max-lines: 2
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
rules:
|
|
||||||
- linters:
|
|
||||||
- gosec
|
|
||||||
path: .+_test\.go
|
|
||||||
- linters:
|
|
||||||
- godox
|
|
||||||
text: TODO
|
|
||||||
- linters:
|
|
||||||
- govet
|
|
||||||
text: declaration of "err" shadows
|
|
||||||
- path: (.+)\.go$
|
|
||||||
text: ST1000
|
|
||||||
- path: (.+)\.go$
|
|
||||||
text: package-comments
|
|
||||||
- linters:
|
|
||||||
- cyclop
|
|
||||||
path: (.+)_test\.go
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
issues:
|
|
||||||
max-issues-per-linter: 0
|
|
||||||
max-same-issues: 0
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofmt
|
|
||||||
- gofumpt
|
|
||||||
settings:
|
|
||||||
gofumpt:
|
|
||||||
module-path: "bunker"
|
|
||||||
extra-rules: true
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
|
|
||||||
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
|
|
||||||
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "bunkerd docker debug",
|
|
||||||
"type": "go",
|
|
||||||
"debugAdapter": "dlv-dap",
|
|
||||||
"request": "attach",
|
|
||||||
"mode": "remote",
|
|
||||||
"port": 54401,
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"stopOnEntry": true,
|
|
||||||
"substitutePath": [
|
|
||||||
{
|
|
||||||
"from": "${workspaceFolder}",
|
|
||||||
"to": "/bunkerd/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"preLaunchTask": "Run bunkerd debug build",
|
|
||||||
"postDebugTask": "Stop bunkerd debug build"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
31
.vscode/tasks.json
vendored
31
.vscode/tasks.json
vendored
@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Run bunkerd debug build",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "task server:localdev:bunkerd:down; task server:localdev:bunkerd:up-debug",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
// Do not open terminal every time debug is launched.
|
|
||||||
"reveal": "silent"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Stop bunkerd debug build",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "task server:localdev:bunkerd:down-debug",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
// Do not open terminal every time debug is launched.
|
|
||||||
"reveal": "silent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
Website = "https://bunker.pztrn.name"
|
|
||||||
|
|
||||||
[Details]
|
|
||||||
Icon = "Icon.png"
|
|
||||||
Name = "pztrn's Bunker"
|
|
||||||
ID = "name.pztrn.bunker.client"
|
|
||||||
Build = 1
|
|
45
Taskfile.yml
45
Taskfile.yml
@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
vars:
|
|
||||||
VERSION:
|
|
||||||
sh: scripts/version_generator.sh
|
|
||||||
CLIENT_VERSION:
|
|
||||||
sh: scripts/version_generator.sh client
|
|
||||||
BRANCH:
|
|
||||||
sh: git rev-parse --abbrev-ref HEAD
|
|
||||||
COMMIT:
|
|
||||||
sh: git rev-parse --short HEAD
|
|
||||||
BUILD:
|
|
||||||
sh: git rev-list --count HEAD
|
|
||||||
BUILD_DATE:
|
|
||||||
sh: TZ=UTC date +'%Y-%m-%d %H:%M:%S %Z'
|
|
||||||
BASIC_LDFLAGS: "-X 'bunker/commons.Version={{.VERSION}}' -X 'bunker/commons.Branch={{.BRANCH}}' -X 'bunker/commons.Commit={{.COMMIT}}' -X 'bunker/commons.Build={{.BUILD}}' -X 'bunker/commons.BuildDate={{.BUILD_DATE}}'"
|
|
||||||
|
|
||||||
env:
|
|
||||||
GOFLAGS: "-trimpath"
|
|
||||||
|
|
||||||
includes:
|
|
||||||
client: ./client
|
|
||||||
server: ./server
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
cleanup:
|
|
||||||
desc: "Cleanup _build directory."
|
|
||||||
cmds:
|
|
||||||
- task: server:cmd:bunkerd:cleanup
|
|
||||||
|
|
||||||
ensure-builddir:
|
|
||||||
internal: true
|
|
||||||
cmds:
|
|
||||||
- mkdir -p _build
|
|
||||||
|
|
||||||
lint:
|
|
||||||
desc: "Lints whole workspace."
|
|
||||||
cmds:
|
|
||||||
- golangci-lint run
|
|
||||||
|
|
||||||
test:
|
|
||||||
desc: "Test whole workspace"
|
|
||||||
cmds:
|
|
||||||
- go test -test.v ./...
|
|
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
includes:
|
|
||||||
client: ./cmd/client
|
|
||||||
translations_checker: ./cmd/translations_checker
|
|
@ -1,114 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
vars:
|
|
||||||
ClientBinary: name.pztrn.bunker.client{{exeExt}}
|
|
||||||
MetadataParams: --metadata "Branch={{ .BRANCH }}" --metadata "Build={{ .BUILD }}" --metadata "BuildDate={{ .BUILD_DATE }}" --metadata "Commit={{ .COMMIT }}" --metadata "Version={{ .VERSION }}"
|
|
||||||
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 1
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
desc: "Build client."
|
|
||||||
dir: ./client/cmd/client
|
|
||||||
internal: true
|
|
||||||
cmds:
|
|
||||||
- task: ::ensure-builddir
|
|
||||||
- task: cleanup
|
|
||||||
- fyne build --release -o ../../../_build/{{ .ClientBinary }} --pprof --pprof-port 6060 {{ .MetadataParams }}
|
|
||||||
|
|
||||||
build-debug:
|
|
||||||
desc: "Build client in debug mode."
|
|
||||||
internal: true
|
|
||||||
dir: ./client/cmd/client
|
|
||||||
cmds:
|
|
||||||
- task: ::ensure-builddir
|
|
||||||
- task: cleanup
|
|
||||||
- fyne build -o ../../../_build/{{ .ClientBinary }} --tags debug {{ .MetadataParams }}
|
|
||||||
|
|
||||||
# build-production:
|
|
||||||
# desc: "Build production package for current OS."
|
|
||||||
# dir: ./client/cmd/client
|
|
||||||
# cmds:
|
|
||||||
# - task: ::ensure-builddir
|
|
||||||
# - task: cleanup
|
|
||||||
# - fyne package --release --app-id name.pztrn.bunker.client -executable name.pztrn.bunker.client -icon ../../Icon.png --release --name "pztrn's Bunker" --app-version "{{ .CLIENT_VERSION }}" --app-build "{{ .BUILD }}" {{ .MetadataParams }}
|
|
||||||
|
|
||||||
# build-web:
|
|
||||||
# desc: "Build web version."
|
|
||||||
# dir: ./client/cmd/client
|
|
||||||
# cmds:
|
|
||||||
# - fyne package -os web --app-id name.pztrn.bunker.client -icon ../../Icon.png --release --name "pztrn's Bunker" --app-version "{{ .CLIENT_VERSION }}" --app-build "{{ .BUILD }}" {{ .MetadataParams }}
|
|
||||||
|
|
||||||
build-dist:
|
|
||||||
desc: "Builds windows/amd64, linux/amd64, darwin/arm64, android/arm64 distribution archives."
|
|
||||||
cmds:
|
|
||||||
- task: build-dist-android-arm64
|
|
||||||
- task: build-dist-darwin
|
|
||||||
- task: build-dist-linux-amd64
|
|
||||||
- task: build-dist-windows-amd64
|
|
||||||
|
|
||||||
build-dist-android-arm64:
|
|
||||||
desc: "Build android/arm64 APK file."
|
|
||||||
dir: ./client/cmd/client
|
|
||||||
cmds:
|
|
||||||
- task: ensure-dist-dir
|
|
||||||
#- fyne-cross android -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -arch=arm64 -pull client/cmd/client
|
|
||||||
- fyne package --release --target=android/arm64 --app-id name.pztrn.bunker.client -icon ../../../Icon.png --name "pztrn's Bunker" --app-version "{{ .CLIENT_VERSION }}" --app-build "{{ .BUILD }}" {{ .MetadataParams }}
|
|
||||||
- mv pztrn_s_Bunker.apk "../../../dist/pztrn's Bunker-{{ .CLIENT_VERSION }}-{{ .BUILD }}-android-arm64.apk"
|
|
||||||
|
|
||||||
build-dist-darwin:
|
|
||||||
desc: "Build darwin (macOS) version."
|
|
||||||
dir: ./client/cmd/client
|
|
||||||
cmds:
|
|
||||||
- task: ensure-dist-dir
|
|
||||||
#- fyne-cross darwin -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -arch=arm64 -category=6007 -pull {{ .MetadataParams }} client/cmd/client
|
|
||||||
- fyne package --release --target=darwin --app-id name.pztrn.bunker.client -executable name.pztrn.bunker.client -icon ../../../Icon.png --name "pztrn's Bunker" --app-version "{{ .CLIENT_VERSION }}" --app-build "{{ .BUILD }}" {{ .MetadataParams }}
|
|
||||||
- tar -czf "pztrn's Bunker-{{ .CLIENT_VERSION }}-{{ .BUILD }}-darwin-arm64.tar.gz" *.app
|
|
||||||
- mv *.tar.gz ../../../dist/
|
|
||||||
|
|
||||||
build-dist-ios:
|
|
||||||
desc: "Build iOS version."
|
|
||||||
dir: ./client/cmd/client
|
|
||||||
cmds:
|
|
||||||
- task: ensure-dist-dir
|
|
||||||
- fyne package --release --target=ios --app-id name.pztrn.bunker.client -icon ../../../Icon.png --name "pztrn's Bunker" --app-version "{{ .CLIENT_VERSION }}" --app-build "{{ .BUILD }}" {{ .MetadataParams }}
|
|
||||||
|
|
||||||
build-dist-linux-amd64:
|
|
||||||
desc: "Build linux/amd64 using fyne-cross"
|
|
||||||
cmds:
|
|
||||||
- task: ensure-dist-dir
|
|
||||||
- fyne-cross linux -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -arch=amd64 -pull {{ .MetadataParams }} client/cmd/client
|
|
||||||
- mv "fyne-cross/dist/linux-amd64/pztrn's Bunker.tar.xz" "dist/pztrn's Bunker-{{ .CLIENT_VERSION }}-{{ .BUILD }}-linux-amd64.tar.xz"
|
|
||||||
|
|
||||||
build-dist-windows-amd64:
|
|
||||||
desc: "Build windows/amd64 using fyne-cross"
|
|
||||||
cmds:
|
|
||||||
- task: ensure-dist-dir
|
|
||||||
- fyne-cross windows -app-version="{{ .CLIENT_VERSION }}" -app-build="{{ .BUILD }}" -arch=amd64 -pull {{ .MetadataParams }} client/cmd/client
|
|
||||||
- mv "fyne-cross/dist/windows-amd64/pztrn's Bunker.exe.zip" "dist/pztrn's Bunker-{{ .CLIENT_VERSION }}-{{ .BUILD }}-windows-amd64.zip"
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
desc: "Cleanup build environment."
|
|
||||||
cmds:
|
|
||||||
- rm _build/{{ .ClientBinary }}
|
|
||||||
ignore_error: true
|
|
||||||
|
|
||||||
ensure-dist-dir:
|
|
||||||
desc: "Ensures dist directory in repo root exists."
|
|
||||||
internal: true
|
|
||||||
cmds:
|
|
||||||
- mkdir dist || exit 0
|
|
||||||
|
|
||||||
run:
|
|
||||||
desc: "Launch client."
|
|
||||||
cmds:
|
|
||||||
- task: build
|
|
||||||
- _build/{{ .ClientBinary }}
|
|
||||||
|
|
||||||
run-debug:
|
|
||||||
desc: "Launch client in debug mode."
|
|
||||||
cmds:
|
|
||||||
- task: build-debug
|
|
||||||
- _build/{{ .ClientBinary }}
|
|
@ -1,78 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"bunker/client/internal/application"
|
|
||||||
"bunker/client/internal/services/core/database"
|
|
||||||
"bunker/client/internal/services/core/mainwindow"
|
|
||||||
"bunker/client/internal/services/core/options"
|
|
||||||
"bunker/client/internal/services/core/translations"
|
|
||||||
"bunker/client/internal/services/features/accounts"
|
|
||||||
"bunker/client/internal/services/features/tasks"
|
|
||||||
"bunker/commons"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/app"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := application.New()
|
|
||||||
|
|
||||||
lgr := app.NewLogger("module", "main")
|
|
||||||
lgr.Info(
|
|
||||||
"Starting Bunker client...",
|
|
||||||
"version", app.Fyne().Metadata().Custom["Version"],
|
|
||||||
"build_no", app.Fyne().Metadata().Custom["Build"],
|
|
||||||
"commit", app.Fyne().Metadata().Custom["Commit"],
|
|
||||||
"branch", app.Fyne().Metadata().Custom["Branch"],
|
|
||||||
)
|
|
||||||
|
|
||||||
checkError(translations.Initialize(app))
|
|
||||||
checkError(database.Initialize(app))
|
|
||||||
checkError(options.Initialize(app))
|
|
||||||
checkError(mainwindow.Initialize(app))
|
|
||||||
|
|
||||||
checkError(accounts.Initialize(app))
|
|
||||||
checkError(tasks.Initialize(app))
|
|
||||||
|
|
||||||
checkError(app.Start())
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkError(err error) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fyneApp := app.NewWithID(commons.ClientAppID)
|
|
||||||
|
|
||||||
wnd := fyneApp.NewWindow("pztrn's Bunker - Error occurred!")
|
|
||||||
//nolint:mnd
|
|
||||||
wnd.Resize(fyne.NewSize(550, 300))
|
|
||||||
|
|
||||||
lbl := widget.NewLabel("Error appeared while starting pztrn's Bunker:")
|
|
||||||
lbl.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
errLabel := widget.NewLabel(err.Error())
|
|
||||||
errLabel.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
lbl2 := widget.NewLabel("Please, report this to developer!")
|
|
||||||
lbl2.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
copyToClipboardButton := widget.NewButton("Copy to clipboard", func() {
|
|
||||||
fyneApp.Clipboard().SetContent(err.Error())
|
|
||||||
})
|
|
||||||
|
|
||||||
wnd.SetContent(container.NewVBox(
|
|
||||||
lbl,
|
|
||||||
errLabel,
|
|
||||||
lbl2,
|
|
||||||
copyToClipboardButton,
|
|
||||||
))
|
|
||||||
|
|
||||||
wnd.ShowAndRun()
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
vars:
|
|
||||||
TCClientBinary: name.pztrn.bunker.app.translations_checker{{exeExt}}
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
desc: "Build translations checker."
|
|
||||||
dir: ./client/cmd/translations_checker
|
|
||||||
cmds:
|
|
||||||
- task: ::ensure-builddir
|
|
||||||
- task: cleanup
|
|
||||||
- go build -o ../../../.build/{{ .TCClientBinary }} main.go
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
desc: "Cleanup build environment for translations checker."
|
|
||||||
cmds:
|
|
||||||
- rm .build/{{ .TCClientBinary }}
|
|
||||||
ignore_error: true
|
|
||||||
|
|
||||||
run:
|
|
||||||
desc: "Launch translations checker."
|
|
||||||
cmds:
|
|
||||||
- task: build
|
|
||||||
- .build/{{ .TCClientBinary }} -translations-path client/internal/langfiles/files
|
|
@ -1,129 +0,0 @@
|
|||||||
//nolint:gosec
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
exitCodeFailedToReadEnglishFile = 3
|
|
||||||
exitCodeFailedToReadTranslationsFile = 6
|
|
||||||
exitCodeFailedToNormalizePath = 2
|
|
||||||
exitCodeFailedToUnmarshalEnglishFile = 4
|
|
||||||
exitCodeFailedToUnmarshalTranslationsFile = 7
|
|
||||||
exitCodeFailedToWalkTranslationsPath = 5
|
|
||||||
exitCodeNoTranslationsPath = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
var translationsPath string
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.StringVar(&translationsPath, "translations-path", "", "Path to translations JSON files.")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if translationsPath == "" {
|
|
||||||
slog.Error("No -translations-path specified.")
|
|
||||||
os.Exit(exitCodeNoTranslationsPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info(
|
|
||||||
"Checking translations file for missing translations, using 'en.json' as original source...",
|
|
||||||
"path", translationsPath,
|
|
||||||
)
|
|
||||||
|
|
||||||
dirPath, err := filepath.Abs(translationsPath)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to normalize translations files path!", "error", err.Error())
|
|
||||||
os.Exit(exitCodeFailedToNormalizePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceDataAsBytes, err := os.ReadFile(filepath.Join(dirPath, "en.json"))
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to read 'en.json' file!", "error", err.Error())
|
|
||||||
os.Exit(exitCodeFailedToReadEnglishFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceData := make(map[string]string)
|
|
||||||
|
|
||||||
if err := json.Unmarshal(sourceDataAsBytes, &sourceData); err != nil {
|
|
||||||
slog.Error("Failed to unmarshal 'en.json' file!", "error", err.Error())
|
|
||||||
os.Exit(exitCodeFailedToUnmarshalEnglishFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
langFilesToCheck := make([]string, 0)
|
|
||||||
|
|
||||||
if err := filepath.WalkDir(dirPath, func(path string, _ fs.DirEntry, _ error) error {
|
|
||||||
if strings.Contains(path, ".json") && !strings.Contains(path, "en.json") {
|
|
||||||
langFilesToCheck = append(langFilesToCheck, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
slog.Error("Failed to walk translations path!", "error", err.Error())
|
|
||||||
os.Exit(exitCodeFailedToWalkTranslationsPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Got langfiles to check", "count", len(langFilesToCheck))
|
|
||||||
|
|
||||||
missingTranslations := make(map[string][]string)
|
|
||||||
|
|
||||||
for _, langFile := range langFilesToCheck {
|
|
||||||
slog.Info("Checking language file...", "file", langFile)
|
|
||||||
|
|
||||||
langFileBytes, err := os.ReadFile(langFile)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to read language file!", "file", langFile, "error", err.Error())
|
|
||||||
os.Exit(exitCodeFailedToReadTranslationsFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
langFileData := make(map[string]string)
|
|
||||||
|
|
||||||
if err := json.Unmarshal(langFileBytes, &langFileData); err != nil {
|
|
||||||
slog.Error("Failed to parse language file!", "file", langFile, "error", err.Error())
|
|
||||||
os.Exit(exitCodeFailedToUnmarshalTranslationsFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
for originalTranslation := range sourceData {
|
|
||||||
if _, found := langFileData[originalTranslation]; !found {
|
|
||||||
if _, langFound := missingTranslations[langFile]; !langFound {
|
|
||||||
missingTranslations[langFile] = make([]string, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
missingTranslations[langFile] = append(missingTranslations[langFile], originalTranslation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(missingTranslations) == 0 {
|
|
||||||
slog.Info("Yay, no missing translations!")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
for langFile, missing := range missingTranslations {
|
|
||||||
_, fileName := filepath.Split(langFile)
|
|
||||||
|
|
||||||
slog.Info("Found missing translations for file.", "file", fileName)
|
|
||||||
|
|
||||||
sort.Strings(missing)
|
|
||||||
|
|
||||||
//nolint:forbidigo
|
|
||||||
fmt.Println("============================== " + fileName + " MISSING START")
|
|
||||||
|
|
||||||
for _, name := range missing {
|
|
||||||
//nolint:forbidigo
|
|
||||||
fmt.Println(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:forbidigo
|
|
||||||
fmt.Println("============================== " + fileName + " MISSING END")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,223 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"bunker/commons"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/app"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errApplication = errors.New("application")
|
|
||||||
errNoMainWindow = errors.New("no main window service registered")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Application is a lifecycle controlling structure for application.
|
|
||||||
type Application struct {
|
|
||||||
ctx context.Context
|
|
||||||
cancelFunc context.CancelFunc
|
|
||||||
fyneApp fyne.App
|
|
||||||
baseLogger *slog.Logger
|
|
||||||
appLogger *slog.Logger
|
|
||||||
services []Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates new instance of lifecycle controlling structure.
|
|
||||||
func New() *Application {
|
|
||||||
appl := &Application{}
|
|
||||||
|
|
||||||
appl.initialize()
|
|
||||||
|
|
||||||
return appl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) configure() error {
|
|
||||||
// First iteration - core services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Configure(); err != nil {
|
|
||||||
return fmt.Errorf("configure service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second iteration - rest of the services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Configure(); err != nil {
|
|
||||||
return fmt.Errorf("configure service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) connectDependencies() error {
|
|
||||||
// First iteration - core services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.ConnectDependencies(); err != nil {
|
|
||||||
return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second iteration - rest of the services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.ConnectDependencies(); err != nil {
|
|
||||||
return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContextWithTimeout returns context.Context with requested timeout.
|
|
||||||
func (a *Application) ContextWithTimeout(timeout time.Duration) context.Context {
|
|
||||||
ctx, cancelFunc := context.WithTimeout(a.ctx, timeout)
|
|
||||||
|
|
||||||
// As we do not need to call cancelFunc - make linter happy.
|
|
||||||
// This probably will lead to context leak, so it should be investigated.
|
|
||||||
go func(_ context.CancelFunc) {}(cancelFunc)
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) initialize() {
|
|
||||||
a.ctx, a.cancelFunc = context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
a.initializeLogger()
|
|
||||||
|
|
||||||
a.fyneApp = app.NewWithID(commons.ClientAppID)
|
|
||||||
|
|
||||||
a.services = make([]Service, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) launchStartupTasks() error {
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "mainwindow") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.LaunchStartupTasks(); err != nil {
|
|
||||||
return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.LaunchStartupTasks(); err != nil {
|
|
||||||
return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mainWindowService Service
|
|
||||||
|
|
||||||
for _, srv := range a.services {
|
|
||||||
if srv.Name() == "core/mainwindow" {
|
|
||||||
mainWindowService = srv
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mainWindowService == nil {
|
|
||||||
return fmt.Errorf("launch startup tasks: %w", errNoMainWindow)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mainWindowService.LaunchStartupTasks(); err != nil {
|
|
||||||
return fmt.Errorf("launch startup tasks for %s: %w", mainWindowService.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown stops application.
|
|
||||||
func (a *Application) Shutdown() error {
|
|
||||||
a.appLogger.Info("Stopping pztrn's Bunker...")
|
|
||||||
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "features/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Shutting down service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Shutdown(); err != nil {
|
|
||||||
return fmt.Errorf("shutting down service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Shutting down service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Shutdown(); err != nil {
|
|
||||||
return fmt.Errorf("shutting down service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts application.
|
|
||||||
func (a *Application) Start() error {
|
|
||||||
if err := a.connectDependencies(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errApplication, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.configure(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errApplication, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.launchStartupTasks(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errApplication, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.fyneApp.Run()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fyne returns Fyne instance.
|
|
||||||
func (a *Application) Fyne() fyne.App {
|
|
||||||
return a.fyneApp
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *Application) initializeLogger() {
|
|
||||||
a.baseLogger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
||||||
AddSource: true,
|
|
||||||
Level: slog.LevelDebug,
|
|
||||||
}))
|
|
||||||
|
|
||||||
a.appLogger = a.baseLogger.With("module", "application")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLogger creates new sub-instance of base logger and adds some additional data to it for persistent output.
|
|
||||||
func (a *Application) NewLogger(withs ...interface{}) *slog.Logger {
|
|
||||||
return a.baseLogger.With(withs...)
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrServiceAlreadyRegistered returns if trying to register a service with name already taken by other service.
|
|
||||||
ErrServiceAlreadyRegistered = errors.New("service with such name already registered")
|
|
||||||
// ErrServiceNotFound returns if trying to gather service with unknown name.
|
|
||||||
ErrServiceNotFound = errors.New("service with such name wasn't found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service is an interface every service should conform to. Specific services will have own interface for
|
|
||||||
// cross-service interation.
|
|
||||||
type Service interface {
|
|
||||||
// Configure configures service. Called after ConnectDependencies and before LaunchStartupTasks.
|
|
||||||
Configure() error
|
|
||||||
// ConnectDependencies gets necessary dependencies.
|
|
||||||
ConnectDependencies() error
|
|
||||||
// Initialize initializes service's internal state. Called while registering service with Application
|
|
||||||
// lifecycle controller.
|
|
||||||
Initialize() error
|
|
||||||
// Name returns service name.
|
|
||||||
Name() string
|
|
||||||
// LaunchStartupTasks launches tasks on application start. Called after ConnectDependencies and Configure.
|
|
||||||
LaunchStartupTasks() error
|
|
||||||
// Shutdown stops service.
|
|
||||||
Shutdown() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterService registering service with lifecycle controller for later use in any other service.
|
|
||||||
func (a *Application) RegisterService(srv Service) error {
|
|
||||||
var found bool
|
|
||||||
|
|
||||||
for _, knownService := range a.services {
|
|
||||||
if srv.Name() == knownService.Name() {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: RegisterService: check for service '%s' registration: %w",
|
|
||||||
errApplication,
|
|
||||||
srv.Name(),
|
|
||||||
ErrServiceAlreadyRegistered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.Initialize(); err != nil {
|
|
||||||
return fmt.Errorf("%w: RegisterService: initialize service '%s': %w", errApplication, srv.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.services = append(a.services, srv)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service returns requested service.
|
|
||||||
func (a *Application) Service(name string) Service {
|
|
||||||
var srv Service
|
|
||||||
|
|
||||||
for _, knownService := range a.services {
|
|
||||||
if knownService.Name() == name {
|
|
||||||
srv = knownService
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return srv
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import "runtime"
|
|
||||||
|
|
||||||
// IsMobile returns true if current platform related to mobile devices (phones, tablets).
|
|
||||||
func IsMobile() bool {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "android", "ios":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package helpers
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// IsConstraintFailed возвращает true если ограничение по индексу было нарушено.
|
|
||||||
func IsConstraintFailed(err error) bool {
|
|
||||||
return strings.Contains(err.Error(), "constraint failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEmptySliceForIn возвращает true если sqlx вернул ошибку подготовки слайла параметров в sqlx.In.
|
|
||||||
func IsEmptySliceForIn(err error) bool {
|
|
||||||
return strings.Contains(err.Error(), "empty slice passed to 'in' query")
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"about_dialog.about_tab": "About",
|
|
||||||
"about_dialog.about_tab.copyright": "Copyright © {{ .Years }}, Stanislav Nikitin (a.k.a. pztrn). All rights reserved.",
|
|
||||||
"about_dialog.about_tab.summary": "pztrn's Bunker is a self-hosted family or company intranet.",
|
|
||||||
"about_dialog.licenses_tab": "Licenses",
|
|
||||||
"about_dialog.licenses_tab.dependency.fyne": "Cross platform GUI toolkit in Go inspired by Material Design.",
|
|
||||||
"about_dialog.licenses_tab.dependency.goose": "A database migration tool.",
|
|
||||||
"about_dialog.licenses_tab.dependency.gopsutil": "psutil for golang. Used for getting information about Bunker's process and machine it launched on.",
|
|
||||||
"about_dialog.licenses_tab.dependency.modernc_sqlite": "CGo-free port of SQLite/SQLite3.\n\nSQLite is an in-process implementation of a self-contained, serverless, zero-configuration, transactional SQL database engine.",
|
|
||||||
"about_dialog.licenses_tab.dependency.sqlx": "General purpose extensions to golang's database/sql.",
|
|
||||||
"about_dialog.licenses_tab.summary": "This software uses these third-party libraries:",
|
|
||||||
"about_dialog.close_button": "Close",
|
|
||||||
"about_dialog.sysinfo_tab": "System info",
|
|
||||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.content": "Copied to clipboard!",
|
|
||||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.title": "System information data",
|
|
||||||
"about_dialog.sysinfo_tab.summary": "This tab contains system information that will be needed for bug reporting and features requests.",
|
|
||||||
"about_dialog.title": "About pztrn's Bunker",
|
|
||||||
"about_dialog.version": "Version",
|
|
||||||
"main_menu.about": "?",
|
|
||||||
"main_menu.about.about_bunker_menu_item": "About pztrn's Bunker...",
|
|
||||||
"main_menu.file": "File",
|
|
||||||
"main_menu.file.exit_menu_item": "Exit",
|
|
||||||
"main_menu.file.options_menu_item": "Options",
|
|
||||||
"options_dialog.no_settings": "No settings found for selected section.",
|
|
||||||
"options_dialog.select_section": "Select section on left to see settings.",
|
|
||||||
"search.placeholder": "Enter text to search and press ENTER.",
|
|
||||||
"window.lorem_ipsum.tab_name": "Welcome!",
|
|
||||||
"window.lorem_ipsum.text": "Welcome to pztrn's Bunker!\n\nUnfortunately, something went wrong and while application was successfully initialized and started you see this message.\n\nPlease, restart application using terminal and send logs to developer!",
|
|
||||||
"window.title": "pztrn's Bunker"
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"about_dialog.about_tab": "О программе",
|
|
||||||
"about_dialog.about_tab.copyright": "Copyright © {{ .Years }}, Stanislav Nikitin (a.k.a. pztrn). Все права защищены.",
|
|
||||||
"about_dialog.about_tab.summary": "pztrn's Bunker это интранет для вашей семьи или компании.",
|
|
||||||
"about_dialog.licenses_tab": "Лицензии",
|
|
||||||
"about_dialog.licenses_tab.dependency.fyne": "Cross platform GUI toolkit in Go inspired by Material Design.",
|
|
||||||
"about_dialog.licenses_tab.dependency.goose": "A database migration tool.",
|
|
||||||
"about_dialog.licenses_tab.dependency.gopsutil": "psutil for golang. Used for getting information about Bunker's process and machine it launched on.",
|
|
||||||
"about_dialog.licenses_tab.dependency.modernc_sqlite": "CGo-free port of SQLite/SQLite3.\n\nSQLite is an in-process implementation of a self-contained, serverless, zero-configuration, transactional SQL database engine.",
|
|
||||||
"about_dialog.licenses_tab.dependency.sqlx": "General purpose extensions to golang's database/sql.",
|
|
||||||
"about_dialog.licenses_tab.summary": "Это приложение использует следующие сторонние библиотеки:",
|
|
||||||
"about_dialog.close_button": "Закрыть",
|
|
||||||
"about_dialog.sysinfo_tab": "Информация о системе",
|
|
||||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.content": "Скопировано в буфер обмена!",
|
|
||||||
"about_dialog.sysinfo_tab.copy_to_clipboard.notification.title": "Информация о системе",
|
|
||||||
"about_dialog.sysinfo_tab.summary": "Эта вкладка содержит информацию, которую необходимо предоставить разработчикам при сообщении об ошибке или при запросе новой функциональности.",
|
|
||||||
"about_dialog.title": "О pztrn's Bunker",
|
|
||||||
"about_dialog.version": "Версия",
|
|
||||||
"main_menu.about": "?",
|
|
||||||
"main_menu.about.about_bunker_menu_item": "О pztrn's Bunker...",
|
|
||||||
"main_menu.file": "Файл",
|
|
||||||
"main_menu.file.exit_menu_item": "Выход",
|
|
||||||
"main_menu.file.options_menu_item": "Настройки",
|
|
||||||
"options_dialog.no_settings": "Для выбранной секции настройки отсутствуют.",
|
|
||||||
"options_dialog.select_section": "Выберите секцию слева для отображения настроек.",
|
|
||||||
"search.placeholder": "Введите текст для поиска и нажмите ENTER.",
|
|
||||||
"window.lorem_ipsum.tab_name": "Добро пожаловать!",
|
|
||||||
"window.lorem_ipsum.text": "Welcome to pztrn's Bunker!\n\nUnfortunately, something went wrong and while application was successfully initialized and started you see this message.\n\nPlease, restart application using terminal and send logs to developer!",
|
|
||||||
"window.title": "pztrn's Bunker"
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package langfiles
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
// LangFiles это встроенная ФС, содержащая переводы приложения.
|
|
||||||
//
|
|
||||||
//go:embed files
|
|
||||||
var LangFiles embed.FS
|
|
@ -1,44 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceNameDatabase is a name for database service.
|
|
||||||
const ServiceNameDatabase = "core/database"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrDatabase indicates that error appeared somewhere in database service.
|
|
||||||
ErrDatabase = errors.New("database service")
|
|
||||||
// ErrDatabaseIsInvalid indicates that database service implementation is invalid.
|
|
||||||
ErrDatabaseIsInvalid = errors.New("database service implementation is invalid")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database is an interface for database service.
|
|
||||||
type Database interface {
|
|
||||||
// Exec is a proxy for ExecContext from sqlx.
|
|
||||||
Exec(ctx context.Context, query string, params ...interface{}) error
|
|
||||||
// Get is a proxy for GetContext from sqlx.
|
|
||||||
Get(ctx context.Context, target interface{}, query string, params ...interface{}) error
|
|
||||||
// NamedExec is a proxy for NamedExecContext from sqlx.
|
|
||||||
NamedExec(ctx context.Context, query string, param interface{}) error
|
|
||||||
// RegisterMigrations registers migrations for applying from other services. Migrations should reside
|
|
||||||
// in "migrations" directory in passed filesystem.
|
|
||||||
RegisterMigrations(moduleName string, fs fs.FS) error
|
|
||||||
// Select is a proxy for SelectContext from sqlx.
|
|
||||||
Select(ctx context.Context, target interface{}, query string, params ...interface{}) error
|
|
||||||
// Transaction is a wrapper for transactions processing which wraps sqlx's transactions.
|
|
||||||
Transaction(ctx context.Context) (DatabaseTransaction, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DatabaseTransaction is an interface for database transactions controllers implementations.
|
|
||||||
type DatabaseTransaction interface {
|
|
||||||
Apply(steps ...TransactionFunc) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionFunc is a function that is used in transactions to mangle with data.
|
|
||||||
type TransactionFunc func(*sqlx.Tx) error
|
|
@ -1,118 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/client/internal/application"
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
|
|
||||||
// sqlite adapter.
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = core.Database(&database{})
|
|
||||||
|
|
||||||
type database struct {
|
|
||||||
mainWindow core.MainWindow
|
|
||||||
options core.Options
|
|
||||||
app *application.Application
|
|
||||||
db *sqlx.DB
|
|
||||||
logger *slog.Logger
|
|
||||||
migrations map[string]fs.FS
|
|
||||||
dbPath string
|
|
||||||
version int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
db := &database{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(db); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Configure() error {
|
|
||||||
d.configureDBPath()
|
|
||||||
|
|
||||||
db, err := sqlx.Open("sqlite", d.dbPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("configure: open database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.db = db
|
|
||||||
|
|
||||||
d.logger.Info("Database opened.", "path", d.dbPath)
|
|
||||||
|
|
||||||
if err := d.initializeSysInfoHandler(); err != nil {
|
|
||||||
return fmt.Errorf("configure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := d.initializeOptions(); err != nil {
|
|
||||||
return fmt.Errorf("configure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) ConnectDependencies() error {
|
|
||||||
mainWindowRaw := d.app.Service(core.ServiceNameMainWindow)
|
|
||||||
if mainWindowRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrDatabaseIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.mainWindow = mainWindow
|
|
||||||
|
|
||||||
optionsRaw := d.app.Service(core.ServiceNameOptions)
|
|
||||||
if optionsRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get options: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
options, valid := optionsRaw.(core.Options)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert options: %w", core.ErrOptionsIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.options = options
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Initialize() error {
|
|
||||||
d.logger = d.app.NewLogger("service", core.ServiceNameDatabase)
|
|
||||||
|
|
||||||
d.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
d.migrations = make(map[string]fs.FS, 0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Name() string {
|
|
||||||
return core.ServiceNameDatabase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) LaunchStartupTasks() error {
|
|
||||||
if err := d.applyMigrations(); err != nil {
|
|
||||||
return fmt.Errorf("launch startup tasks: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *database) configureDBPath() {
|
|
||||||
d.dbPath = filepath.Join(d.app.Fyne().Storage().RootURI().Path(), "database.sqlite3")
|
|
||||||
|
|
||||||
d.logger.Info("Database path configured.", "path", d.dbPath)
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
"bunker/commons"
|
|
||||||
|
|
||||||
"github.com/pressly/goose/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errMigrationsAlreadyRegistered = errors.New("migrations already registered")
|
|
||||||
|
|
||||||
func (d *database) applyMigrations() error {
|
|
||||||
d.logger.Info("Migrating database...")
|
|
||||||
|
|
||||||
modules := make([]string, 0)
|
|
||||||
|
|
||||||
for module := range d.migrations {
|
|
||||||
modules = append(modules, module)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(modules)
|
|
||||||
|
|
||||||
_ = goose.SetDialect(string(goose.DialectSQLite3))
|
|
||||||
|
|
||||||
gooseLogger := commons.NewGooseLogger(d.logger)
|
|
||||||
goose.SetLogger(gooseLogger)
|
|
||||||
|
|
||||||
for _, module := range modules {
|
|
||||||
d.logger.Info("Migrating database for module...", "module", module)
|
|
||||||
|
|
||||||
goose.SetBaseFS(d.migrations[module])
|
|
||||||
goose.SetTableName(strings.ReplaceAll(module, "/", "_") + "_migrations")
|
|
||||||
|
|
||||||
if err := goose.Up(d.db.DB, "migrations"); err != nil {
|
|
||||||
return fmt.Errorf("%w: applying migrations for module '%s': %w", core.ErrDatabase, module, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
moduleDBVersion, err := goose.GetDBVersion(d.db.DB)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: get database version for module '%s': %w", core.ErrDatabase, module, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.version += moduleDBVersion
|
|
||||||
|
|
||||||
d.logger.Info(
|
|
||||||
"Database for module migrated to latest version",
|
|
||||||
"module", module,
|
|
||||||
"module_db_version", moduleDBVersion,
|
|
||||||
"db_version", d.version,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Info("Database migrated.", "version", d.version)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) RegisterMigrations(moduleName string, fSys fs.FS) error {
|
|
||||||
slog.Debug("Registering migrations for service.", "service", moduleName)
|
|
||||||
|
|
||||||
if _, found := d.migrations[moduleName]; found {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: RegisterMigrations: module '%s': %w",
|
|
||||||
core.ErrDatabase,
|
|
||||||
moduleName,
|
|
||||||
errMigrationsAlreadyRegistered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.migrations[moduleName] = fSys
|
|
||||||
|
|
||||||
slog.Debug("Migrations for service successfully registered.", "service", moduleName)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core/options/dto"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *database) initializeOptions() error {
|
|
||||||
databaseMaxIdleConnsEntry := widget.NewEntry()
|
|
||||||
databaseMaxIdleConns := widget.NewFormItem("Maximum idle connections", databaseMaxIdleConnsEntry)
|
|
||||||
|
|
||||||
databaseMaxOpenedConnsEntry := widget.NewEntry()
|
|
||||||
databaseMaxOpenedConns := widget.NewFormItem("Maximum opened connections", databaseMaxOpenedConnsEntry)
|
|
||||||
|
|
||||||
optionsWidgetForm := widget.NewForm(
|
|
||||||
databaseMaxIdleConns,
|
|
||||||
databaseMaxOpenedConns,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := d.options.RegisterOptionsWidget(&dto.OptionPane{
|
|
||||||
Widget: optionsWidgetForm,
|
|
||||||
SaveHandler: d.saveOptions,
|
|
||||||
Name: "Database",
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("register options widget: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) saveOptions() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Exec is a proxy for ExecContext from sqlx.
|
|
||||||
func (d *database) Exec(ctx context.Context, query string, params ...interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Executing query.", "query", query, "params", fmt.Sprintf("%+v", params))
|
|
||||||
|
|
||||||
if _, err := d.db.ExecContext(ctx, query, params...); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to Exec(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get is a proxy for GetContext from sqlx.
|
|
||||||
func (d *database) Get(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Getting single data from database with query.", "query", query, "params", fmt.Sprintf("%+v", params))
|
|
||||||
|
|
||||||
if err := d.db.GetContext(ctx, target, query, params...); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to Get(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NamedExec is a proxy for NamedExecContext from sqlx.
|
|
||||||
func (d *database) NamedExec(ctx context.Context, query string, param interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Executing named query.", "query", query, "params", fmt.Sprintf("%+v", param))
|
|
||||||
|
|
||||||
if _, err := d.db.NamedExecContext(ctx, query, param); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to NamedExec(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select is a proxy for SelectContext from sqlx.
|
|
||||||
func (d *database) Select(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Selecting from database with query.", "query", query, "params", fmt.Sprintf("%+v", params))
|
|
||||||
|
|
||||||
if err := d.db.SelectContext(ctx, target, query, params...); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to Select(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *database) initializeSysInfoHandler() error {
|
|
||||||
if err := d.mainWindow.RegisterAboutWindowSysInfoHandler(
|
|
||||||
"database_data",
|
|
||||||
d.sysInfoHandlerDatabaseVersion,
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("register database_data handler: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) sysInfoHandlerDatabaseVersion() string {
|
|
||||||
var dbFileSize, sizeDivisionsCount int64
|
|
||||||
|
|
||||||
var databaseSizeString string
|
|
||||||
|
|
||||||
fileInfo, err := os.Stat(d.dbPath)
|
|
||||||
if err == nil {
|
|
||||||
dbFileSize = fileInfo.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
//nolint:mnd
|
|
||||||
if dbFileSize > 1024 {
|
|
||||||
dbFileSize /= 1024
|
|
||||||
sizeDivisionsCount++
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
switch sizeDivisionsCount {
|
|
||||||
case 1:
|
|
||||||
databaseSizeString = fmt.Sprintf("- Database file size: %d KB", dbFileSize)
|
|
||||||
case 2: //nolint:mnd
|
|
||||||
databaseSizeString = fmt.Sprintf("- Database file size: %d MB", dbFileSize)
|
|
||||||
default:
|
|
||||||
databaseSizeString = fmt.Sprintf("- Database file size: %d GB", dbFileSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("### Database\n\n- Database version: %d\n%s\n", d.version, databaseSizeString)
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type transaction struct {
|
|
||||||
transaction *sqlx.Tx
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Transaction(ctx context.Context) (core.DatabaseTransaction, error) {
|
|
||||||
txn, err := d.db.BeginTxx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: starting transaction: %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
txHandler := &transaction{
|
|
||||||
transaction: txn,
|
|
||||||
logger: d.logger.With("module", "transactioner"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return txHandler, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *transaction) Apply(steps ...core.TransactionFunc) error {
|
|
||||||
for stepNumber, stepFunc := range steps {
|
|
||||||
if err := stepFunc(t.transaction); err != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Error occurred.",
|
|
||||||
"step", stepNumber,
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Transaction rollback failed.",
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := t.transaction.Commit(); err != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Transaction commit failed.",
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Transaction rollback failed.",
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: transaction commit: %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core/mainwindow/dto"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceNameMainWindow is a name for main window service.
|
|
||||||
const ServiceNameMainWindow = "core/mainwindow"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrMainWindow indicates that error appeared somewhere in main window service.
|
|
||||||
ErrMainWindow = errors.New("main window service")
|
|
||||||
// ErrMainWindowIsInvalid indicates that main window service implementation is invalid.
|
|
||||||
ErrMainWindowIsInvalid = errors.New("main window service implementation is invalid")
|
|
||||||
)
|
|
||||||
|
|
||||||
// MainWindow is an interface for main window service.
|
|
||||||
type MainWindow interface {
|
|
||||||
// AddTab adds tab in main window.
|
|
||||||
AddTab(tab *dto.Tab)
|
|
||||||
// MainWindow returns main window instance (e.g. for using as parent with dialogs).
|
|
||||||
MainWindow() fyne.Window
|
|
||||||
// RegisterAboutWindowSysInfoHandler registers handler for System Info tab in About dialog.
|
|
||||||
RegisterAboutWindowSysInfoHandler(name string, hndl SysInfoHandler) error
|
|
||||||
// SetStatusProgressBarCurrentValue sets current value for progressbar in status bar.
|
|
||||||
SetStatusProgressBarCurrentValue(current float64)
|
|
||||||
// SetStatusProgressBarMaxValue sets maximum value for progressbar in status bar.
|
|
||||||
SetStatusProgressBarMaxValue(current float64)
|
|
||||||
// SetStatus sets text in status bar. If non-empty text is passed - then progress bar is also shown, and hidden
|
|
||||||
// if passed text is empty.
|
|
||||||
SetStatus(status string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SysInfoHandler is a function signature for registering with additional system information handler for About dialog.
|
|
||||||
// It should return valid markdown and should end with "\n".
|
|
||||||
type SysInfoHandler func() string
|
|
@ -1,54 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/canvas"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/dialog"
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
minRowsVisibleInSysInfo = 10
|
|
||||||
startYear = 2025
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) showAboutDialog() {
|
|
||||||
appNameLbl := canvas.NewText(
|
|
||||||
"pztrn's Bunker",
|
|
||||||
m.app.Fyne().Settings().Theme().Color(
|
|
||||||
theme.ColorNameForeground,
|
|
||||||
m.app.Fyne().Settings().ThemeVariant(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
appNameLbl.TextSize = 16
|
|
||||||
appNameLbl.TextStyle = fyne.TextStyle{Bold: true}
|
|
||||||
appNameLbl.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
appVersionLbl := canvas.NewText(
|
|
||||||
lang.L("about_dialog.version")+" "+m.app.Fyne().Metadata().Custom["Version"],
|
|
||||||
m.app.Fyne().Settings().Theme().Color(
|
|
||||||
theme.ColorNameForeground,
|
|
||||||
m.app.Fyne().Settings().ThemeVariant(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
appVersionLbl.TextSize = 16
|
|
||||||
appVersionLbl.TextStyle = fyne.TextStyle{Bold: true}
|
|
||||||
appVersionLbl.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
tabs := container.NewAppTabs(
|
|
||||||
m.generateAboutTab(),
|
|
||||||
m.generateLicensesTab(),
|
|
||||||
m.generateSysInfoTab(),
|
|
||||||
)
|
|
||||||
|
|
||||||
vbox := container.NewVBox(
|
|
||||||
appNameLbl,
|
|
||||||
appVersionLbl,
|
|
||||||
tabs,
|
|
||||||
)
|
|
||||||
|
|
||||||
dlg := dialog.NewCustom(lang.L("about_dialog.title"), lang.L("about_dialog.close_button"), vbox, m.window)
|
|
||||||
dlg.Show()
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) generateAboutTab() *container.TabItem {
|
|
||||||
copyrightYears := strconv.Itoa(startYear)
|
|
||||||
if time.Now().Year() > startYear {
|
|
||||||
copyrightYears = strconv.Itoa(startYear) + " - " + strconv.Itoa(time.Now().Year())
|
|
||||||
}
|
|
||||||
|
|
||||||
aboutLabel := widget.NewLabel(
|
|
||||||
lang.L("about_dialog.about_tab.summary") +
|
|
||||||
"\n\n" +
|
|
||||||
lang.L("about_dialog.about_tab.copyright", map[string]any{"Years": copyrightYears}),
|
|
||||||
)
|
|
||||||
aboutLabel.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
aboutVBox := container.NewVBox(aboutLabel)
|
|
||||||
aboutScroll := container.NewVScroll(aboutVBox)
|
|
||||||
|
|
||||||
aboutScroll.SetMinSize(fyne.NewSize(
|
|
||||||
m.window.Content().Size().Width-m.window.Content().Size().Width/3,
|
|
||||||
m.window.Content().Size().Height-m.window.Content().Size().Height/2.5,
|
|
||||||
))
|
|
||||||
|
|
||||||
return container.NewTabItem(lang.L("about_dialog.about_tab"), aboutScroll)
|
|
||||||
}
|
|
@ -1,293 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) generateLicensesTab() *container.TabItem {
|
|
||||||
licensesLabel := widget.NewLabel(lang.L("about_dialog.licenses_tab.summary"))
|
|
||||||
licensesLabel.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
accordion := widget.NewAccordion(
|
|
||||||
m.generateFyneAccordionItem(),
|
|
||||||
m.generateGooseAccordionItem(),
|
|
||||||
m.generateGopsutilAccordionItem(),
|
|
||||||
m.generateModerncSqliteAccordionItem(),
|
|
||||||
m.generateSqlxAccrodionItem(),
|
|
||||||
)
|
|
||||||
|
|
||||||
licensesVBox := container.NewVBox(licensesLabel, accordion)
|
|
||||||
licensesScroll := container.NewVScroll(licensesVBox)
|
|
||||||
|
|
||||||
return container.NewTabItem(lang.L("about_dialog.licenses_tab"), licensesScroll)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) generateFyneAccordionItem() *widget.AccordionItem {
|
|
||||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.fyne"))
|
|
||||||
summary.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
license := widget.NewLabel(`BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (C) 2018 Fyne.io developers (see AUTHORS)
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer in the
|
|
||||||
documentation and/or other materials provided with the distribution.
|
|
||||||
* Neither the name of Fyne.io nor the names of its contributors may be
|
|
||||||
used to endorse or promote products derived from this software without
|
|
||||||
specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
|
||||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
||||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.`)
|
|
||||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
|
||||||
license.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
url, _ := url.Parse("https://github.com/fyne-io/fyne")
|
|
||||||
authorsURL, _ := url.Parse("https://raw.githubusercontent.com/fyne-io/fyne/refs/heads/master/AUTHORS")
|
|
||||||
|
|
||||||
return widget.NewAccordionItem("Fyne v2.6.1", container.NewVBox(
|
|
||||||
summary,
|
|
||||||
container.NewHBox(
|
|
||||||
widget.NewHyperlink("Source", url),
|
|
||||||
widget.NewLabel("|"),
|
|
||||||
widget.NewHyperlink("Authors", authorsURL),
|
|
||||||
),
|
|
||||||
license,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) generateGooseAccordionItem() *widget.AccordionItem {
|
|
||||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.goose"))
|
|
||||||
summary.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
license := widget.NewLabel(`MIT License
|
|
||||||
|
|
||||||
Original work Copyright (c) 2012 Liam Staskawicz
|
|
||||||
Modified work Copyright (c) 2016 Vojtech Vitek
|
|
||||||
Modified work Copyright (c) 2021 Michael Fridman, Vojtech Vitek
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`)
|
|
||||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
|
||||||
license.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
url, _ := url.Parse("https://github.com/pressly/goose")
|
|
||||||
authorsURL, _ := url.Parse("https://github.com/pressly/goose/graphs/contributors")
|
|
||||||
|
|
||||||
return widget.NewAccordionItem("goose v3.24.3", container.NewVBox(
|
|
||||||
summary,
|
|
||||||
container.NewHBox(
|
|
||||||
widget.NewHyperlink("Source", url),
|
|
||||||
widget.NewLabel("|"),
|
|
||||||
widget.NewHyperlink("Authors", authorsURL),
|
|
||||||
),
|
|
||||||
license,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) generateGopsutilAccordionItem() *widget.AccordionItem {
|
|
||||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.gopsutil"))
|
|
||||||
summary.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
license := widget.NewLabel(`gopsutil is distributed under BSD license reproduced below.
|
|
||||||
|
|
||||||
Copyright (c) 2014, WAKAYAMA Shirou
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
|
||||||
are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
* Neither the name of the gopsutil authors nor the names of its contributors
|
|
||||||
may be used to endorse or promote products derived from this software without
|
|
||||||
specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
||||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
||||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
|
|
||||||
-------
|
|
||||||
internal/common/binary.go in the gopsutil is copied and modified from golang/encoding/binary.go.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.`)
|
|
||||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
|
||||||
license.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
url, _ := url.Parse("https://github.com/shirou/gopsutil")
|
|
||||||
authorsURL, _ := url.Parse("https://github.com/shirou/gopsutil/graphs/contributors")
|
|
||||||
|
|
||||||
return widget.NewAccordionItem("gopsutil v3.24.5", container.NewVBox(
|
|
||||||
summary,
|
|
||||||
container.NewHBox(
|
|
||||||
widget.NewHyperlink("Source", url),
|
|
||||||
widget.NewLabel("|"),
|
|
||||||
widget.NewHyperlink("Authors", authorsURL),
|
|
||||||
),
|
|
||||||
license,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) generateModerncSqliteAccordionItem() *widget.AccordionItem {
|
|
||||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.modernc_sqlite"))
|
|
||||||
summary.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
license := widget.NewLabel(`Copyright (c) 2017 The Sqlite Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its contributors
|
|
||||||
may be used to endorse or promote products derived from this software without
|
|
||||||
specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
`)
|
|
||||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
|
||||||
license.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
url, _ := url.Parse("https://gitlab.com/cznic/sqlite")
|
|
||||||
authorsURL, _ := url.Parse("https://gitlab.com/cznic/sqlite/-/raw/master/AUTHORS")
|
|
||||||
|
|
||||||
return widget.NewAccordionItem("modernc/sqlite v1.37.1", container.NewVBox(
|
|
||||||
summary,
|
|
||||||
container.NewHBox(
|
|
||||||
widget.NewHyperlink("Source", url),
|
|
||||||
widget.NewLabel("|"),
|
|
||||||
widget.NewHyperlink("Authors", authorsURL),
|
|
||||||
),
|
|
||||||
license,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) generateSqlxAccrodionItem() *widget.AccordionItem {
|
|
||||||
summary := widget.NewLabel(lang.L("about_dialog.licenses_tab.dependency.sqlx"))
|
|
||||||
summary.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
license := widget.NewLabel(`Copyright (c) 2013, Jason Moiron
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
|
||||||
obtaining a copy of this software and associated documentation
|
|
||||||
files (the "Software"), to deal in the Software without
|
|
||||||
restriction, including without limitation the rights to use,
|
|
||||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.`)
|
|
||||||
license.TextStyle = fyne.TextStyle{Monospace: true}
|
|
||||||
license.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
url, _ := url.Parse("https://github.com/jmoiron/sqlx")
|
|
||||||
authorsURL, _ := url.Parse("https://github.com/jmoiron/sqlx/graphs/contributors")
|
|
||||||
|
|
||||||
return widget.NewAccordionItem("sqlx v1.4.0", container.NewVBox(
|
|
||||||
summary,
|
|
||||||
container.NewHBox(
|
|
||||||
widget.NewHyperlink("Source", url),
|
|
||||||
widget.NewLabel("|"),
|
|
||||||
widget.NewHyperlink("Authors", authorsURL),
|
|
||||||
),
|
|
||||||
license,
|
|
||||||
))
|
|
||||||
}
|
|
@ -1,221 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"runtime/debug"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
"bunker/client/internal/services/core/mainwindow/models"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
|
||||||
"github.com/shirou/gopsutil/v3/host"
|
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
|
||||||
"github.com/shirou/gopsutil/v3/process"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errSysInfoHandlerAlreadyRegistered = errors.New("handler already registered")
|
|
||||||
|
|
||||||
func (m *mainWindow) generateSysInfoTab() *container.TabItem {
|
|
||||||
lbl := widget.NewLabel(lang.L("about_dialog.sysinfo_tab.summary"))
|
|
||||||
lbl.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
buildInfo, _ := debug.ReadBuildInfo()
|
|
||||||
|
|
||||||
var builtForOS, builtForArch string
|
|
||||||
|
|
||||||
for _, bi := range buildInfo.Settings {
|
|
||||||
switch bi.Key {
|
|
||||||
case "GOARCH":
|
|
||||||
builtForArch = bi.Value
|
|
||||||
case "GOOS":
|
|
||||||
builtForOS = bi.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commons, _ := host.Info()
|
|
||||||
|
|
||||||
cpuInfo, _ := cpu.Info()
|
|
||||||
|
|
||||||
memInfo, _ := mem.VirtualMemory()
|
|
||||||
|
|
||||||
memoryTotal := memInfo.Total
|
|
||||||
memoryTotalDivCount := 0
|
|
||||||
|
|
||||||
var memoryTotalString string
|
|
||||||
|
|
||||||
for {
|
|
||||||
//nolint:mnd
|
|
||||||
if memoryTotal > 1024 {
|
|
||||||
memoryTotal /= 1024
|
|
||||||
memoryTotalDivCount++
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:mnd
|
|
||||||
switch memoryTotalDivCount {
|
|
||||||
case 1:
|
|
||||||
memoryTotalString = fmt.Sprintf("%d KB", memoryTotal)
|
|
||||||
case 2:
|
|
||||||
memoryTotalString = fmt.Sprintf("%d MB", memoryTotal)
|
|
||||||
default:
|
|
||||||
memoryTotalString = fmt.Sprintf("%d GB", memoryTotal)
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryFree := memInfo.Available
|
|
||||||
memoryFreeDivCount := 0
|
|
||||||
|
|
||||||
var memoryFreeString string
|
|
||||||
|
|
||||||
//nolint:mnd
|
|
||||||
for {
|
|
||||||
if memoryFree > 1024 {
|
|
||||||
memoryFree /= 1024
|
|
||||||
memoryFreeDivCount++
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:mnd
|
|
||||||
switch memoryFreeDivCount {
|
|
||||||
case 1:
|
|
||||||
memoryFreeString = fmt.Sprintf("%d KB", memoryFree)
|
|
||||||
case 2:
|
|
||||||
memoryFreeString = fmt.Sprintf("%d MB", memoryFree)
|
|
||||||
default:
|
|
||||||
memoryFreeString = fmt.Sprintf("%d GB", memoryFree)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
conns int
|
|
||||||
fds int32
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:gosec
|
|
||||||
proc, err := process.NewProcess(int32(os.Getpid()))
|
|
||||||
if err == nil {
|
|
||||||
connections, _ := proc.Connections()
|
|
||||||
conns = len(connections)
|
|
||||||
|
|
||||||
fds, _ = proc.NumFDs()
|
|
||||||
}
|
|
||||||
|
|
||||||
sysInfoData := fmt.Sprintf(`## Build information
|
|
||||||
|
|
||||||
- Version: %s (#%s, from %s/%s, on %s)
|
|
||||||
- Built for: %s/%s
|
|
||||||
|
|
||||||
## System information
|
|
||||||
|
|
||||||
- CPU: %s (%d cores)
|
|
||||||
- RAM: %s (%s available)
|
|
||||||
- OS: %s %s
|
|
||||||
- Processes running: %d
|
|
||||||
|
|
||||||
## Launch information
|
|
||||||
|
|
||||||
- Running on: %s/%s
|
|
||||||
- Opened network connections: %d
|
|
||||||
- Opened file descriptors: %d
|
|
||||||
|
|
||||||
## Additional information
|
|
||||||
|
|
||||||
%s
|
|
||||||
`,
|
|
||||||
m.app.Fyne().Metadata().Custom["Version"],
|
|
||||||
m.app.Fyne().Metadata().Custom["Build"],
|
|
||||||
m.app.Fyne().Metadata().Custom["Branch"],
|
|
||||||
m.app.Fyne().Metadata().Custom["Commit"],
|
|
||||||
m.app.Fyne().Metadata().Custom["BuildDate"],
|
|
||||||
builtForOS,
|
|
||||||
builtForArch,
|
|
||||||
cpuInfo[0].ModelName,
|
|
||||||
cpuInfo[0].Cores,
|
|
||||||
memoryTotalString,
|
|
||||||
memoryFreeString,
|
|
||||||
commons.OS,
|
|
||||||
commons.PlatformVersion,
|
|
||||||
commons.Procs,
|
|
||||||
runtime.GOOS,
|
|
||||||
runtime.GOARCH,
|
|
||||||
conns,
|
|
||||||
fds,
|
|
||||||
m.prepareSysInfoAdditionalsForAbout(),
|
|
||||||
)
|
|
||||||
|
|
||||||
sysInfo := widget.NewMultiLineEntry()
|
|
||||||
sysInfo.OnChanged = func(_ string) {
|
|
||||||
sysInfo.SetText(sysInfoData)
|
|
||||||
}
|
|
||||||
sysInfo.SetText(sysInfoData)
|
|
||||||
sysInfo.SetMinRowsVisible(minRowsVisibleInSysInfo)
|
|
||||||
|
|
||||||
copyToClipboard := widget.NewButton("Copy to clipboard", func() {
|
|
||||||
m.app.Fyne().Clipboard().SetContent(sysInfoData)
|
|
||||||
|
|
||||||
m.app.Fyne().SendNotification(&fyne.Notification{
|
|
||||||
Title: lang.L("about_dialog.sysinfo_tab.copy_to_clipboard.notification.title"),
|
|
||||||
Content: lang.L("about_dialog.sysinfo_tab.copy_to_clipboard.notification.content"),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sysInfoVBox := container.NewVBox(
|
|
||||||
lbl,
|
|
||||||
sysInfo,
|
|
||||||
copyToClipboard,
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewTabItem(lang.L("about_dialog.sysinfo_tab"), sysInfoVBox)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) prepareSysInfoAdditionalsForAbout() string {
|
|
||||||
additionals := ""
|
|
||||||
|
|
||||||
handlersNames := make([]string, 0, len(m.sysInfoHandlers))
|
|
||||||
|
|
||||||
for name := range m.sysInfoHandlers {
|
|
||||||
handlersNames = append(handlersNames, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(handlersNames)
|
|
||||||
|
|
||||||
for _, name := range handlersNames {
|
|
||||||
additionals += m.sysInfoHandlers[name].Handler() + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return additionals
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) RegisterAboutWindowSysInfoHandler(name string, hndl core.SysInfoHandler) error {
|
|
||||||
if _, found := m.sysInfoHandlers[name]; found {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: RegisterAboutWindowSysInfoHandler: register '%s': %w",
|
|
||||||
core.ErrMainWindow,
|
|
||||||
name,
|
|
||||||
errSysInfoHandlerAlreadyRegistered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sysInfoHandler := &models.SysInfoHandler{
|
|
||||||
Name: name,
|
|
||||||
Handler: hndl,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.sysInfoHandlers[name] = sysInfoHandler
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/canvas"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tab is a DTO of main window's tab that is responsible for showing content.
|
|
||||||
type Tab struct {
|
|
||||||
// Name is a name for tab. Won't render by default on desktop, only on mouse hover, but will be rendered on mobiles.
|
|
||||||
Name string
|
|
||||||
// Sidebar is a sidebar widget. On desktop in will be shown on left side of window, on mobiles as separate window.
|
|
||||||
Sidebar fyne.CanvasObject
|
|
||||||
// Widget is a widget shown in window.
|
|
||||||
Widget fyne.CanvasObject
|
|
||||||
// Icon is an icon to show on tab.
|
|
||||||
Icon canvas.Image
|
|
||||||
// BadgeCount is a number to show on tab, like unread messages, incompleted tasks, etc.
|
|
||||||
BadgeCount uint16
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"bunker/client/internal/application"
|
|
||||||
"bunker/client/internal/helpers"
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
"bunker/client/internal/services/core/mainwindow/models"
|
|
||||||
"bunker/commons"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = core.MainWindow(&mainWindow{})
|
|
||||||
|
|
||||||
type mainWindow struct {
|
|
||||||
app *application.Application
|
|
||||||
logger *slog.Logger
|
|
||||||
window fyne.Window
|
|
||||||
options core.Options
|
|
||||||
tabsWidget *fyne.Container
|
|
||||||
statusBarProgress *widget.ProgressBar
|
|
||||||
statusBarStatus *widget.Label
|
|
||||||
sysInfoHandlers map[string]*models.SysInfoHandler
|
|
||||||
tabs []*models.Tab
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
mainW := &mainWindow{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(mainW); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", core.ErrMainWindow, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) Configure() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) ConnectDependencies() error {
|
|
||||||
optionsRaw := m.app.Service(core.ServiceNameOptions)
|
|
||||||
if optionsRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get options service: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
options, valid := optionsRaw.(core.Options)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert options service: %w", core.ErrOptionsIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.options = options
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) Initialize() error {
|
|
||||||
m.logger = m.app.NewLogger("service", core.ServiceNameMainWindow)
|
|
||||||
|
|
||||||
m.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
m.sysInfoHandlers = make(map[string]*models.SysInfoHandler)
|
|
||||||
|
|
||||||
m.window = m.app.Fyne().NewWindow(lang.L("window.title"))
|
|
||||||
// ToDo: сохранение и восстановление размеров окна.
|
|
||||||
//nolint:mnd
|
|
||||||
m.window.Resize(fyne.NewSize(1100, 800))
|
|
||||||
|
|
||||||
var mainWindowCanvas fyne.CanvasObject
|
|
||||||
|
|
||||||
if helpers.IsMobile() {
|
|
||||||
mainWindowCanvas = m.initializeMainWindowMobile()
|
|
||||||
} else {
|
|
||||||
mainWindowCanvas = m.initializeMainWindowDesktop()
|
|
||||||
}
|
|
||||||
|
|
||||||
m.window.SetContent(mainWindowCanvas)
|
|
||||||
|
|
||||||
m.window.SetCloseIntercept(m.stopApp)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) MainWindow() fyne.Window {
|
|
||||||
return m.window
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) Name() string {
|
|
||||||
return core.ServiceNameMainWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) LaunchStartupTasks() error {
|
|
||||||
m.window.Show()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) stopApp() {
|
|
||||||
if err := m.app.Shutdown(); err != nil {
|
|
||||||
slog.Error("Failed to stop Bunker!", "error", err.Error())
|
|
||||||
|
|
||||||
os.Exit(commons.ExitCodeAppStopFailed)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) initializeMainWindowDesktop() fyne.CanvasObject {
|
|
||||||
switcherButton := m.initializeSwitcherDesktop()
|
|
||||||
appNameLabel := widget.NewLabel("Bunker " + m.app.Fyne().Metadata().Custom["Version"])
|
|
||||||
sidebarHeader := container.NewVBox(container.NewHBox(switcherButton, appNameLabel), widget.NewSeparator())
|
|
||||||
sideBar := container.NewBorder(sidebarHeader, nil, nil, nil)
|
|
||||||
|
|
||||||
splitter := container.NewHSplit(sideBar, container.NewVBox(widget.NewLabel("widget data"), widget.NewSeparator()))
|
|
||||||
splitter.SetOffset(0.2)
|
|
||||||
|
|
||||||
statusBar := m.initializeDesktopStatusBar()
|
|
||||||
|
|
||||||
mainWidget := container.NewBorder(nil, statusBar, nil, nil, splitter)
|
|
||||||
|
|
||||||
return mainWidget
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bunker/client/internal/widgets"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) initializeDesktopStatusBar() fyne.CanvasObject {
|
|
||||||
m.statusBarStatus = widget.NewLabel("Ready.")
|
|
||||||
m.statusBarProgress = widget.NewProgressBar()
|
|
||||||
m.statusBarProgress.Hide()
|
|
||||||
|
|
||||||
statusBarForToolbar := widgets.NewToolbarProgressBarWithBar(m.statusBarProgress)
|
|
||||||
|
|
||||||
statusBar := widget.NewToolbar()
|
|
||||||
statusBar.Append(widgets.NewToolbarLabelWithLabel(m.statusBarStatus))
|
|
||||||
statusBar.Append(statusBarForToolbar)
|
|
||||||
statusBar.Append(widget.NewToolbarSpacer())
|
|
||||||
|
|
||||||
return statusBar
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) SetStatusProgressBarCurrentValue(current float64) {
|
|
||||||
m.statusBarProgress.SetValue(current)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) SetStatusProgressBarMaxValue(maxValue float64) {
|
|
||||||
m.statusBarProgress.Max = maxValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) SetStatus(status string) {
|
|
||||||
m.statusBarStatus.SetText(status)
|
|
||||||
|
|
||||||
if status == "" {
|
|
||||||
m.statusBarStatus.SetText("Ready.")
|
|
||||||
m.statusBarProgress.Hide()
|
|
||||||
} else {
|
|
||||||
m.statusBarProgress.Show()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) initializeMenu() {
|
|
||||||
optionsMenuItem := fyne.NewMenuItem(lang.L("main_menu.file.options_menu_item"), func() {
|
|
||||||
m.logger.Info("Opening options...")
|
|
||||||
|
|
||||||
m.options.ShowOptionsDialog()
|
|
||||||
})
|
|
||||||
|
|
||||||
exitMenuItem := fyne.NewMenuItem(lang.L("main_menu.file.exit_menu_item"), func() {
|
|
||||||
m.stopApp()
|
|
||||||
})
|
|
||||||
exitMenuItem.IsQuit = true
|
|
||||||
|
|
||||||
fileMenu := fyne.NewMenu(lang.L("main_menu.file"), optionsMenuItem, fyne.NewMenuItemSeparator(), exitMenuItem)
|
|
||||||
|
|
||||||
aboutMenuItem := fyne.NewMenuItem(lang.L("main_menu.about.about_bunker_menu_item"), func() {
|
|
||||||
m.showAboutDialog()
|
|
||||||
})
|
|
||||||
|
|
||||||
aboutMenu := fyne.NewMenu(lang.L("main_menu.about"), aboutMenuItem)
|
|
||||||
|
|
||||||
mainMenu := fyne.NewMainMenu(
|
|
||||||
fileMenu,
|
|
||||||
aboutMenu,
|
|
||||||
)
|
|
||||||
m.window.SetMainMenu(mainMenu)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) initializeMainWindowMobile() fyne.CanvasObject {
|
|
||||||
return widget.NewLabel("Mobile interface not yet implemented.")
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/theme"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) initializeSwitcherDesktop() fyne.CanvasObject {
|
|
||||||
m.logger.Debug("Initializing desktop switcher...")
|
|
||||||
|
|
||||||
btn := widget.NewButtonWithIcon(
|
|
||||||
"",
|
|
||||||
m.app.Fyne().Settings().Theme().Icon(theme.IconNameMenu),
|
|
||||||
m.desktopSwitcherButtonTapped,
|
|
||||||
)
|
|
||||||
|
|
||||||
return btn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainWindow) desktopSwitcherButtonTapped() {
|
|
||||||
m.logger.Debug("Showing desktop switcher...")
|
|
||||||
|
|
||||||
popup := widget.NewPopUp(widget.NewLabel("All hail switcher!"), m.window.Canvas())
|
|
||||||
popup.ShowAtRelativePosition(fyne.NewPos(0, 0), m.window.Content())
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
func (m *mainWindow) initializeSwitcherMobile() {
|
|
||||||
m.logger.Debug("Initializing mobile switcher...")
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
// SysInfoHandler содержит в себе информацию об обработчике для Markdown системной информации.
|
|
||||||
type SysInfoHandler struct {
|
|
||||||
Handler func() string
|
|
||||||
Name string
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/canvas"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tab is an internal representation of main window's tab that is responsible for showing content.
|
|
||||||
type Tab struct {
|
|
||||||
// Name is a name for tab. Won't render by default on desktop, only on mouse hover, but will be rendered on mobiles.
|
|
||||||
Name string
|
|
||||||
// Widget is a widget shown in window.
|
|
||||||
Widget fyne.CanvasObject
|
|
||||||
// Icon is an icon to show on tab.
|
|
||||||
Icon canvas.Image
|
|
||||||
// BadgeCount is a number to show on tab, like unread messages, incompleted tasks, etc.
|
|
||||||
BadgeCount uint16
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package mainwindow
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bunker/client/internal/services/core/mainwindow/dto"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *mainWindow) AddTab(_ *dto.Tab) {
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core/options/dto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceNameOptions is a name for options service which controls options dialog and options storage.
|
|
||||||
const ServiceNameOptions = "core/options"
|
|
||||||
|
|
||||||
// ErrOptionsIsInvalid appears when options service implementation is invalid.
|
|
||||||
var ErrOptionsIsInvalid = errors.New("options service implementation is invalid")
|
|
||||||
|
|
||||||
// Options is an interface for options service.
|
|
||||||
type Options interface {
|
|
||||||
// RegisterOptionsWidget registers options widget for options dialog.
|
|
||||||
RegisterOptionsWidget(widgetData *dto.OptionPane) error
|
|
||||||
// ShowOptionsDialog shows options dialog.
|
|
||||||
ShowOptionsDialog()
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/dialog"
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (o *options) closeOptionsDialog(save bool) {
|
|
||||||
o.logger.Debug("Closing options dialog.", "save", save)
|
|
||||||
|
|
||||||
if !save {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, widget := range o.widgets {
|
|
||||||
if widget.SaveHandler == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := widget.SaveHandler(); err != nil {
|
|
||||||
o.logger.Error("Failed to save options for module!", "module", widget.Name, "error", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) optionsListCreateItem() fyne.CanvasObject {
|
|
||||||
return widget.NewLabel("")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) optionsListLength() int {
|
|
||||||
return len(o.widgetsItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) optionsListUpdateItem(itemID widget.ListItemID, obj fyne.CanvasObject) {
|
|
||||||
label, _ := obj.(*widget.Label)
|
|
||||||
|
|
||||||
label.SetText(o.widgets[o.widgetsItems[itemID]].Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) ShowOptionsDialog() {
|
|
||||||
optionsWidgetScroll := container.NewScroll(widget.NewLabel(lang.L("options_dialog.select_section")))
|
|
||||||
|
|
||||||
optionsList := widget.NewList(o.optionsListLength, o.optionsListCreateItem, o.optionsListUpdateItem)
|
|
||||||
optionsList.OnSelected = func(listID widget.ListItemID) {
|
|
||||||
// listID должно быть всегда меньше длины, так как является индексом.
|
|
||||||
// Эта проверка отрабатывает, если настроек по какой-то причине нет.
|
|
||||||
if listID >= len(o.widgetsItems) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsWidget := o.widgets[o.widgetsItems[listID]].Widget
|
|
||||||
|
|
||||||
if optionsWidget != nil {
|
|
||||||
optionsWidgetScroll.Content = optionsWidget
|
|
||||||
} else {
|
|
||||||
optionsWidgetScroll.Content = widget.NewLabel(lang.L("options_dialog.no_settings"))
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsWidgetScroll.Refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsListSplitter := container.NewHSplit(optionsList, optionsWidgetScroll)
|
|
||||||
//nolint:mnd
|
|
||||||
optionsListSplitter.SetOffset(0.25)
|
|
||||||
|
|
||||||
optionsDlg := dialog.NewCustomConfirm(
|
|
||||||
"Options",
|
|
||||||
"Save",
|
|
||||||
"Cancel",
|
|
||||||
optionsListSplitter,
|
|
||||||
o.closeOptionsDialog,
|
|
||||||
o.mainWindow.MainWindow(),
|
|
||||||
)
|
|
||||||
optionsDlg.Resize(o.mainWindow.MainWindow().Canvas().Size())
|
|
||||||
|
|
||||||
optionsDlg.Show()
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import "fyne.io/fyne/v2"
|
|
||||||
|
|
||||||
// OptionPane is a DTO for registering options pane.
|
|
||||||
type OptionPane struct {
|
|
||||||
Widget fyne.CanvasObject
|
|
||||||
SaveHandler OptionSaveHandler
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionSaveHandler is a signature for functions responsible for saving options, resides in service that registers own
|
|
||||||
// options pane.
|
|
||||||
type OptionSaveHandler func() error
|
|
@ -1,17 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed migrations
|
|
||||||
var migrations embed.FS
|
|
||||||
|
|
||||||
func (o *options) registerMigrations() error {
|
|
||||||
if err := o.db.RegisterMigrations("core/options", migrations); err != nil {
|
|
||||||
return fmt.Errorf("register migrations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
CREATE TABLE IF NOT EXISTS options (
|
|
||||||
key VARCHAR(1024) NOT NULL UNIQUE,
|
|
||||||
value VARCHAR(8192)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
DROP TABLE IF EXISTS options;
|
|
@ -1,23 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bunker/client/internal/services/core/options/dto"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OptionPane это описание одной панели настроек в окне настроек.
|
|
||||||
type OptionPane struct {
|
|
||||||
Widget fyne.CanvasObject
|
|
||||||
SaveHandler func() error
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OptionPaneFromDTO конвертирует параметры панели настроек из DTO во внутреннее представление.
|
|
||||||
func OptionPaneFromDTO(dtoData *dto.OptionPane) *OptionPane {
|
|
||||||
return &OptionPane{
|
|
||||||
Widget: dtoData.Widget,
|
|
||||||
SaveHandler: dtoData.SaveHandler,
|
|
||||||
Name: dtoData.Name,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/client/internal/application"
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
"bunker/client/internal/services/core/options/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ = core.Options(&options{})
|
|
||||||
|
|
||||||
errOptions = errors.New("options core service")
|
|
||||||
)
|
|
||||||
|
|
||||||
type options struct {
|
|
||||||
app *application.Application
|
|
||||||
logger *slog.Logger
|
|
||||||
db core.Database
|
|
||||||
mainWindow core.MainWindow
|
|
||||||
|
|
||||||
widgets map[string]*models.OptionPane
|
|
||||||
widgetsItems []string // for Fyne's list widget.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
opts := &options{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(opts); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errOptions, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Configure() error {
|
|
||||||
if err := o.registerMigrations(); err != nil {
|
|
||||||
return fmt.Errorf("configure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) ConnectDependencies() error {
|
|
||||||
databaseRaw := o.app.Service(core.ServiceNameDatabase)
|
|
||||||
if databaseRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
database, valid := databaseRaw.(core.Database)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
o.db = database
|
|
||||||
|
|
||||||
mainWindowRaw := o.app.Service(core.ServiceNameMainWindow)
|
|
||||||
if mainWindowRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
o.mainWindow = mainWindow
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Initialize() error {
|
|
||||||
o.logger = o.app.NewLogger("service", core.ServiceNameOptions)
|
|
||||||
|
|
||||||
o.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
o.widgets = make(map[string]*models.OptionPane)
|
|
||||||
o.widgetsItems = make([]string, 0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Name() string {
|
|
||||||
return core.ServiceNameOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) LaunchStartupTasks() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"bunker/client/internal/services/core/options/dto"
|
|
||||||
"bunker/client/internal/services/core/options/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errWidgetPaneAlreadyRegistered = errors.New("widget pane already registered")
|
|
||||||
|
|
||||||
func (o *options) RegisterOptionsWidget(widgetData *dto.OptionPane) error {
|
|
||||||
if _, found := o.widgets[widgetData.Name]; found {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: RegisterOptionsWidget: '%s': %w",
|
|
||||||
errOptions,
|
|
||||||
widgetData.Name,
|
|
||||||
errWidgetPaneAlreadyRegistered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
o.widgets[widgetData.Name] = models.OptionPaneFromDTO(widgetData)
|
|
||||||
o.widgetsItems = append(o.widgetsItems, widgetData.Name)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceNameTranslations is a name for translations service.
|
|
||||||
const ServiceNameTranslations = "core/translations"
|
|
||||||
|
|
||||||
// ErrTranslationsIsInvalid appears when translations service implementation is invalid.
|
|
||||||
var ErrTranslationsIsInvalid = errors.New("translations service implementation is invalid")
|
|
||||||
|
|
||||||
// Translations is an interface for translations service.
|
|
||||||
type Translations interface{}
|
|
@ -1,73 +0,0 @@
|
|||||||
package translations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"bunker/client/internal/langfiles"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t *translations) initializeSysInfoHandler() error {
|
|
||||||
if err := t.mainWindow.RegisterAboutWindowSysInfoHandler(
|
|
||||||
"translations_languages_and_strings_count",
|
|
||||||
t.sysInfoHandlerTranslationsLanguagesAndStringsCount,
|
|
||||||
); err != nil {
|
|
||||||
return fmt.Errorf("register translations_languages_and_strings_count handler: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translations) sysInfoHandlerTranslationsLanguagesAndStringsCount() string {
|
|
||||||
langString := "- Error occurred while getting translations languages and strings counts. See console log for details."
|
|
||||||
|
|
||||||
// Эту всю информацию надо получать напрямую из файлов, пакет Fyne не предоставляет нужную информацию.
|
|
||||||
languages := make([]string, 0)
|
|
||||||
translationsStringsCount := 0
|
|
||||||
|
|
||||||
entries, err := langfiles.LangFiles.ReadDir("files")
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Error("Failed to read translations filesystem entries.", "error", err.Error())
|
|
||||||
|
|
||||||
return langString
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(entry.Name(), ".json") {
|
|
||||||
fileData, err := langfiles.LangFiles.ReadFile(filepath.Join("files", entry.Name()))
|
|
||||||
if err != nil {
|
|
||||||
t.logger.Error("Failed to read translation file!", "file", entry.Name(), "error", err.Error())
|
|
||||||
|
|
||||||
return langString
|
|
||||||
}
|
|
||||||
|
|
||||||
data := make(map[string]string)
|
|
||||||
|
|
||||||
if err := json.Unmarshal(fileData, &data); err != nil {
|
|
||||||
t.logger.Error("Failed to unmarshal translation file!", "file", entry.Name(), "error", err.Error())
|
|
||||||
|
|
||||||
return langString
|
|
||||||
}
|
|
||||||
|
|
||||||
_, fileName := filepath.Split(entry.Name())
|
|
||||||
langName := strings.Split(fileName, ".")[0]
|
|
||||||
|
|
||||||
languages = append(languages, langName)
|
|
||||||
translationsStringsCount += len(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
langString = fmt.Sprintf(
|
|
||||||
"### Translations\n\n- Languages: %d\n- Translations strings (in total): %d\n",
|
|
||||||
len(languages),
|
|
||||||
translationsStringsCount,
|
|
||||||
)
|
|
||||||
|
|
||||||
return langString
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
package translations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"bunker/client/internal/application"
|
|
||||||
"bunker/client/internal/langfiles"
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2/lang"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ = core.Translations(&translations{})
|
|
||||||
|
|
||||||
errTranslations = errors.New("translations core service")
|
|
||||||
)
|
|
||||||
|
|
||||||
type translations struct {
|
|
||||||
app *application.Application
|
|
||||||
logger *slog.Logger
|
|
||||||
mainWindow core.MainWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
transl := &translations{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(transl); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errTranslations, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translations) Configure() error {
|
|
||||||
if err := t.initializeSysInfoHandler(); err != nil {
|
|
||||||
return fmt.Errorf("configure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translations) ConnectDependencies() error {
|
|
||||||
mainWindowRaw := t.app.Service(core.ServiceNameMainWindow)
|
|
||||||
if mainWindowRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.mainWindow = mainWindow
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translations) Initialize() error {
|
|
||||||
t.logger = t.app.NewLogger("service", core.ServiceNameTranslations)
|
|
||||||
|
|
||||||
t.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
langFromEnv, _ := os.LookupEnv("LANG")
|
|
||||||
|
|
||||||
t.logger.Info("Current system locale.", "locale", lang.SystemLocale().String(), "LANG", langFromEnv)
|
|
||||||
|
|
||||||
if err := lang.AddTranslationsFS(langfiles.LangFiles, "files"); err != nil {
|
|
||||||
return fmt.Errorf("%w: load translations: %w", errTranslations, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translations) Name() string {
|
|
||||||
return core.ServiceNameTranslations
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translations) LaunchStartupTasks() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translations) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package features
|
|
||||||
|
|
||||||
// ServiceNameAccounts is a name for accounts service.
|
|
||||||
const ServiceNameAccounts = "features/accounts"
|
|
@ -1,93 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/client/internal/application"
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
"bunker/client/internal/services/features"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errAccounts = errors.New("accounts feature service")
|
|
||||||
|
|
||||||
type accounts struct {
|
|
||||||
app *application.Application
|
|
||||||
logger *slog.Logger
|
|
||||||
db core.Database
|
|
||||||
mainWindow core.MainWindow
|
|
||||||
|
|
||||||
loginDialogInstanceAddressEntry *widget.Entry
|
|
||||||
loginDialogUsernameEntry *widget.Entry
|
|
||||||
loginDialogPasswordEntry *widget.Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
accts := &accounts{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(accts); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errAccounts, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accounts) Configure() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accounts) ConnectDependencies() error {
|
|
||||||
databaseRaw := a.app.Service(core.ServiceNameDatabase)
|
|
||||||
if databaseRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
database, valid := databaseRaw.(core.Database)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.db = database
|
|
||||||
|
|
||||||
mainWindowRaw := a.app.Service(core.ServiceNameMainWindow)
|
|
||||||
if mainWindowRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mainWindow = mainWindow
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accounts) Initialize() error {
|
|
||||||
a.logger = a.app.NewLogger("service", features.ServiceNameTasks)
|
|
||||||
|
|
||||||
a.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accounts) Name() string {
|
|
||||||
return features.ServiceNameTasks
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accounts) LaunchStartupTasks() error {
|
|
||||||
a.loginDialogShow()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accounts) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
package accounts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/dialog"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *accounts) loginDialogLogin() {
|
|
||||||
a.logger.Info(
|
|
||||||
"Trying to log in...",
|
|
||||||
"instance", a.loginDialogInstanceAddressEntry.Text,
|
|
||||||
"username", a.loginDialogUsernameEntry.Text,
|
|
||||||
"password", a.loginDialogPasswordEntry.Text,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *accounts) loginDialogShow() {
|
|
||||||
if a.loginDialogInstanceAddressEntry == nil {
|
|
||||||
a.loginDialogInstanceAddressEntry = widget.NewEntry()
|
|
||||||
a.loginDialogInstanceAddressEntry.SetText("http://localhost:53400")
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.loginDialogUsernameEntry == nil {
|
|
||||||
a.loginDialogUsernameEntry = widget.NewEntry()
|
|
||||||
a.loginDialogUsernameEntry.SetPlaceHolder("username")
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.loginDialogPasswordEntry == nil {
|
|
||||||
a.loginDialogPasswordEntry = widget.NewEntry()
|
|
||||||
} else {
|
|
||||||
a.loginDialogPasswordEntry.SetText("")
|
|
||||||
}
|
|
||||||
|
|
||||||
loginForm := widget.NewForm(
|
|
||||||
widget.NewFormItem("Instance address:", a.loginDialogInstanceAddressEntry),
|
|
||||||
widget.NewFormItem("Login:", a.loginDialogUsernameEntry),
|
|
||||||
widget.NewFormItem("Password:", a.loginDialogPasswordEntry),
|
|
||||||
)
|
|
||||||
|
|
||||||
loginButton := widget.NewButton("Log in", a.loginDialogLogin)
|
|
||||||
|
|
||||||
loginDialogContent := container.NewBorder(nil, loginButton, nil, nil, loginForm)
|
|
||||||
|
|
||||||
dialog := dialog.NewCustomWithoutButtons(
|
|
||||||
"Login to Bunker instance",
|
|
||||||
loginDialogContent,
|
|
||||||
a.mainWindow.MainWindow(),
|
|
||||||
)
|
|
||||||
dialog.Resize(dialog.MinSize().AddWidthHeight(200, 0))
|
|
||||||
|
|
||||||
dialog.Show()
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package features
|
|
||||||
|
|
||||||
// ServiceNameTasks is a name for tasks service.
|
|
||||||
const ServiceNameTasks = "features/tasks"
|
|
@ -1,85 +0,0 @@
|
|||||||
package tasks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/client/internal/application"
|
|
||||||
"bunker/client/internal/services/core"
|
|
||||||
"bunker/client/internal/services/features"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errTasks = errors.New("tasks feature service")
|
|
||||||
|
|
||||||
type tasks struct {
|
|
||||||
app *application.Application
|
|
||||||
logger *slog.Logger
|
|
||||||
db core.Database
|
|
||||||
mainWindow core.MainWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
tsks := &tasks{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(tsks); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errTasks, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tasks) Configure() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tasks) ConnectDependencies() error {
|
|
||||||
databaseRaw := t.app.Service(core.ServiceNameDatabase)
|
|
||||||
if databaseRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
database, valid := databaseRaw.(core.Database)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.db = database
|
|
||||||
|
|
||||||
mainWindowRaw := t.app.Service(core.ServiceNameMainWindow)
|
|
||||||
if mainWindowRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.mainWindow = mainWindow
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tasks) Initialize() error {
|
|
||||||
t.logger = t.app.NewLogger("service", features.ServiceNameTasks)
|
|
||||||
|
|
||||||
t.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tasks) Name() string {
|
|
||||||
return features.ServiceNameAccounts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tasks) LaunchStartupTasks() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tasks) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package widgets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToolbarLabel is a label widget for toolbar.
|
|
||||||
type ToolbarLabel struct {
|
|
||||||
*widget.Label
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewToolbarLabelWithLabel creates new toolbar label with passed label as base widget.
|
|
||||||
func NewToolbarLabelWithLabel(label *widget.Label) widget.ToolbarItem {
|
|
||||||
return &ToolbarLabel{label}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolbarObject returns toolbar item.
|
|
||||||
func (tl *ToolbarLabel) ToolbarObject() fyne.CanvasObject {
|
|
||||||
return tl.Label
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package widgets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToolbarProgressBar is a progressbar widget for toolbar.
|
|
||||||
type ToolbarProgressBar struct {
|
|
||||||
*widget.ProgressBar
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewToolbarProgressBarWithBar creates new progressbar for toolbar with provided progressbar.
|
|
||||||
func NewToolbarProgressBarWithBar(bar *widget.ProgressBar) *ToolbarProgressBar {
|
|
||||||
return &ToolbarProgressBar{
|
|
||||||
bar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolbarObject returns toolbar item.
|
|
||||||
func (tl *ToolbarProgressBar) ToolbarObject() fyne.CanvasObject {
|
|
||||||
return tl
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package commons
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ClientAppID это идентификатор для клиента.
|
|
||||||
ClientAppID = "name.pztrn.bunker.client"
|
|
||||||
// ServerAppID это идентификатор для сервера.
|
|
||||||
ServerAppID = "name.pztrn.bunker.server"
|
|
||||||
)
|
|
@ -1,6 +0,0 @@
|
|||||||
package commons
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ExitCodeAppStopFailed указывает на ошибку остановки работы приложения.
|
|
||||||
ExitCodeAppStopFailed = 2
|
|
||||||
)
|
|
@ -1,28 +0,0 @@
|
|||||||
package commons
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GooseLogger is a proxy struct that wraps Bunker logging for goose database migrator.
|
|
||||||
type GooseLogger struct {
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGooseLogger creates proxy structure for goose database migrator logging.
|
|
||||||
func NewGooseLogger(logger *slog.Logger) *GooseLogger {
|
|
||||||
return &GooseLogger{
|
|
||||||
logger: logger.With("module", "goose"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalf is a proxy for goose logging.
|
|
||||||
func (gl *GooseLogger) Fatalf(format string, v ...interface{}) {
|
|
||||||
gl.logger.Error(fmt.Sprintf(format, v...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf is a proxy for goose logging.
|
|
||||||
func (gl *GooseLogger) Printf(format string, v ...interface{}) {
|
|
||||||
gl.logger.Info(fmt.Sprintf(format, v...))
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
// File "ids.go" stores all static identificators that is used across Bunker server.
|
|
||||||
package commons
|
|
||||||
|
|
||||||
const (
|
|
||||||
// SystemUserID is an UUID for "System" user. With this ID server's settings are stored in database.
|
|
||||||
SystemUserID = "00000001-0000-0000-0000-000000000000"
|
|
||||||
)
|
|
@ -1,14 +0,0 @@
|
|||||||
package commons
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Branch это ветка, из которой собрано приложение.
|
|
||||||
Branch string
|
|
||||||
// Build это порядковый номер сборки.
|
|
||||||
Build string
|
|
||||||
// BuildDate это таймштамп сборки приложения.
|
|
||||||
BuildDate string
|
|
||||||
// Commit это коммит, из которого собрано приложение.
|
|
||||||
Commit string
|
|
||||||
// Version это версия, из которой собрано приложение.
|
|
||||||
Version string
|
|
||||||
)
|
|
72
go.mod
72
go.mod
@ -1,72 +0,0 @@
|
|||||||
module bunker
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
fyne.io/fyne/v2 v2.6.3
|
|
||||||
github.com/coder/websocket v1.8.14
|
|
||||||
github.com/jackc/pgx/v5 v5.7.6
|
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
|
||||||
github.com/pressly/goose/v3 v3.25.0
|
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5
|
|
||||||
modernc.org/sqlite v1.38.2
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
fyne.io/systray v1.11.0 // indirect
|
|
||||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/fredbi/uri v1.1.0 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
|
||||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
|
||||||
github.com/fyne-io/glfw-js v0.3.0 // indirect
|
|
||||||
github.com/fyne-io/image v0.1.1 // indirect
|
|
||||||
github.com/fyne-io/oksvg v0.1.0 // indirect
|
|
||||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
|
||||||
github.com/go-text/render v0.2.0 // indirect
|
|
||||||
github.com/go-text/typesetting v0.2.1 // indirect
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
|
||||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/rymdport/portal v0.4.1 // indirect
|
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
|
||||||
github.com/stretchr/testify v1.11.0 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
|
||||||
golang.org/x/crypto v0.42.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
|
||||||
golang.org/x/image v0.24.0 // indirect
|
|
||||||
golang.org/x/net v0.43.0 // indirect
|
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
|
||||||
golang.org/x/text v0.29.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
modernc.org/libc v1.66.3 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
|
||||||
modernc.org/memory v1.11.0 // indirect
|
|
||||||
)
|
|
189
go.sum
189
go.sum
@ -1,189 +0,0 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
|
||||||
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
|
|
||||||
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
|
|
||||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
|
||||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
|
||||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
|
||||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
|
||||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
|
||||||
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
|
|
||||||
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
|
||||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
|
||||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
|
||||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
|
|
||||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
|
||||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
|
||||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
|
||||||
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
|
|
||||||
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
|
||||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
|
||||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
|
||||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
|
||||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
|
||||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
|
||||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
|
||||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
|
||||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
|
||||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
|
||||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
|
||||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
|
||||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
|
||||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
|
||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
|
||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
|
||||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
|
||||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
|
||||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
|
||||||
github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjGng=
|
|
||||||
github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
|
||||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
|
||||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
|
||||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
|
||||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
|
||||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
|
||||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
|
||||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
|
||||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
|
||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
|
||||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
|
||||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
|
||||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
|
||||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
|
||||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
|
||||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
|
||||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
|
||||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
|
||||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
|
||||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
|
||||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Generates version based on git tag.
|
|
||||||
LATEST_TAG=$(git tag | tail -n 1)
|
|
||||||
|
|
||||||
# Check latest tag commit. If it is equal to current - use that tag as version.
|
|
||||||
if [ "${LATEST_TAG}" != "" ]; then
|
|
||||||
LATEST_TAG_COMMIT=$(git rev-list -n 1 $LATEST_TAG)
|
|
||||||
LATEST_COMMIT=$(git rev-list -n 1 HEAD)
|
|
||||||
|
|
||||||
if [ "${LATEST_TAG_COMMIT}" == "${LATEST_COMMIT}" ]; then
|
|
||||||
echo "${LATEST_TAG}"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# No tags in repo. Assuming latest tag is "v0.0.0".
|
|
||||||
LATEST_TAG="0.0.0"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If we're here, then latest tag commit is not latest commit on current branch.
|
|
||||||
# We should increase second number and add "-dev" postfix.
|
|
||||||
IFS=. read MAJOR MINOR PATCH <<<"${LATEST_TAG}"
|
|
||||||
MINOR=$(($MINOR + 1))
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
client)
|
|
||||||
echo "${MAJOR}.${MINOR}.${PATCH}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "v${MAJOR}.${MINOR}.${PATCH}-dev"
|
|
||||||
;;
|
|
||||||
esac
|
|
@ -1,16 +0,0 @@
|
|||||||
FROM code.pztrn.name/containers/go-toolbox:v8 AS build
|
|
||||||
|
|
||||||
WORKDIR /bunkerd
|
|
||||||
COPY . /bunkerd
|
|
||||||
RUN --mount=type=cache,target="/home/container/go" task server:cmd:bunkerd:build --force
|
|
||||||
|
|
||||||
FROM debian:13-slim
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y ca-certificates iputils-ping coreutils && \
|
|
||||||
rm -rf /var/lib/apt/* /var/cache/apt/*
|
|
||||||
|
|
||||||
COPY --from=build /bunkerd/_build/bunkerd /bunkerd
|
|
||||||
COPY --from=build /usr/local/bin/dlv /dlv
|
|
||||||
|
|
||||||
ENTRYPOINT ["/bunkerd"]
|
|
@ -1,18 +0,0 @@
|
|||||||
FROM code.pztrn.name/containers/go-toolbox:v8 AS build
|
|
||||||
|
|
||||||
WORKDIR /bunkerd
|
|
||||||
COPY . /bunkerd
|
|
||||||
RUN --mount=type=cache,target="/home/container/go" GOFLAGS="" task server:cmd:bunkerd:build-debug --force
|
|
||||||
|
|
||||||
FROM debian:13-slim
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y ca-certificates iputils-ping coreutils curl net-tools && \
|
|
||||||
rm -rf /var/lib/apt/* /var/cache/apt/*
|
|
||||||
|
|
||||||
COPY --from=build /bunkerd/_build/bunkerd /bunkerd
|
|
||||||
COPY --from=build /usr/local/bin/dlv /dlv
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=1s --timeout=10s --start-period=1s --retries=3 CMD netstat -an | grep 4000 > /dev/null; if [ 0 != $? ]; then exit 1; fi;
|
|
||||||
|
|
||||||
ENTRYPOINT ["/dlv", "--listen=:4000", "--headless=true", "--log=true", "--accept-multiclient", "--api-version=2", "exec", "/bunkerd"]
|
|
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
includes:
|
|
||||||
localdev: ./localdevzone
|
|
||||||
cmd: ./cmd
|
|
@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
includes:
|
|
||||||
bunkerd: ./bunkerd
|
|
@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
desc: "Builds bunkerd binary."
|
|
||||||
cmds:
|
|
||||||
- task: :::ensure-builddir
|
|
||||||
- task: cleanup
|
|
||||||
- go build -ldflags="{{ .BASIC_LDFLAGS }}" -o _build/bunkerd{{exeExt}} ./server/cmd/bunkerd/main.go
|
|
||||||
sources:
|
|
||||||
- ./Taskfile.yml
|
|
||||||
- ./go.mod
|
|
||||||
- ./commons/*
|
|
||||||
- ./server/**/*.go
|
|
||||||
- ./server/Taskfile.yml
|
|
||||||
- ./server/**/Taskfile.yml
|
|
||||||
- ./server/entrypoint.sh
|
|
||||||
generates:
|
|
||||||
- ./_build/bunkerd{{exeExt}}
|
|
||||||
method: timestamp
|
|
||||||
|
|
||||||
build-debug:
|
|
||||||
desc: "Builds bunkerd binary."
|
|
||||||
cmds:
|
|
||||||
- task: :::ensure-builddir
|
|
||||||
- task: cleanup
|
|
||||||
- go build -ldflags="{{ .BASIC_LDFLAGS }}" --gcflags "all=-N -l" -o _build/bunkerd{{exeExt}} ./server/cmd/bunkerd/main.go
|
|
||||||
sources:
|
|
||||||
- ./Taskfile.yml
|
|
||||||
- ./go.mod
|
|
||||||
- ./commons/*
|
|
||||||
- ./server/**/*.go
|
|
||||||
- ./server/Taskfile.yml
|
|
||||||
- ./server/**/Taskfile.yml
|
|
||||||
- ./server/entrypoint.sh
|
|
||||||
generates:
|
|
||||||
- ./_build/bunkerd{{exeExt}}
|
|
||||||
method: timestamp
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
desc: "Deletes bunkerd binary from local build cache."
|
|
||||||
cmds:
|
|
||||||
- rm -f _build/bunkerd{{exeExt}}
|
|
@ -1,63 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"bunker/commons"
|
|
||||||
"bunker/server/internal/application"
|
|
||||||
"bunker/server/internal/services/core/database"
|
|
||||||
"bunker/server/internal/services/core/httpserver"
|
|
||||||
"bunker/server/internal/services/core/options"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := application.New()
|
|
||||||
|
|
||||||
lgr := app.NewLogger("module", "main")
|
|
||||||
lgr.Info(
|
|
||||||
"Starting bunkerd...",
|
|
||||||
"version", commons.Version,
|
|
||||||
"build_no", commons.Build,
|
|
||||||
"buint_on", commons.BuildDate,
|
|
||||||
"commit", commons.Commit,
|
|
||||||
"branch", commons.Branch,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
lgr.Error("Failed to start bunkerd!", "error", err.Error())
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkError(lgr, database.Initialize(app))
|
|
||||||
checkError(lgr, options.Initialize(app))
|
|
||||||
checkError(lgr, httpserver.Initialize(app))
|
|
||||||
|
|
||||||
if err := app.Start(); err != nil {
|
|
||||||
lgr.Error("Failed to start bunkerd", "error", err.Error())
|
|
||||||
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
lgr.Info("bunkerd started.")
|
|
||||||
|
|
||||||
<-app.ShutdownChan()
|
|
||||||
lgr.Info("Shutting down bunkerd...")
|
|
||||||
|
|
||||||
if err := app.Shutdown(); err != nil {
|
|
||||||
lgr.Error("Failed to shutdown bunkerd!", "error", err.Error())
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkError(logger *slog.Logger, err error) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error("Failed to initialize bunkerd.", "error", err.Error())
|
|
||||||
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
@ -1,196 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errApplication = errors.New("application")
|
|
||||||
|
|
||||||
// Application is a lifecycle controlling structure for application.
|
|
||||||
type Application struct {
|
|
||||||
shutdownChan chan struct{}
|
|
||||||
ctx context.Context
|
|
||||||
cancelFunc context.CancelFunc
|
|
||||||
baseLogger *slog.Logger
|
|
||||||
appLogger *slog.Logger
|
|
||||||
services []Service
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates new instance of lifecycle controlling structure.
|
|
||||||
func New() *Application {
|
|
||||||
appl := &Application{}
|
|
||||||
|
|
||||||
appl.initialize()
|
|
||||||
|
|
||||||
return appl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) configure() error {
|
|
||||||
// First iteration - core services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Configure(); err != nil {
|
|
||||||
return fmt.Errorf("configure service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second iteration - rest of the services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching configuration procedure for service", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Configure(); err != nil {
|
|
||||||
return fmt.Errorf("configure service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) connectDependencies() error {
|
|
||||||
// First iteration - core services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.ConnectDependencies(); err != nil {
|
|
||||||
return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second iteration - rest of the services.
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Connecting dependencies for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.ConnectDependencies(); err != nil {
|
|
||||||
return fmt.Errorf("connect dependencies for service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContextWithTimeout returns context.Context with requested timeout.
|
|
||||||
func (a *Application) ContextWithTimeout(timeout time.Duration) context.Context {
|
|
||||||
ctx, cancelFunc := context.WithTimeout(a.ctx, timeout)
|
|
||||||
|
|
||||||
// As we do not need to call cancelFunc - make linter happy.
|
|
||||||
// This probably will lead to context leak, so it should be investigated.
|
|
||||||
go func(_ context.CancelFunc) {}(cancelFunc)
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) initialize() {
|
|
||||||
a.ctx, a.cancelFunc = context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
a.initializeLogger()
|
|
||||||
|
|
||||||
a.services = make([]Service, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) launchStartupTasks() error {
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "mainwindow") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.LaunchStartupTasks(); err != nil {
|
|
||||||
return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range a.services {
|
|
||||||
if strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Launching startup tasks for service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.LaunchStartupTasks(); err != nil {
|
|
||||||
return fmt.Errorf("launch startup tasks for core/%s: %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown stops application.
|
|
||||||
func (a *Application) Shutdown() error {
|
|
||||||
a.appLogger.Info("Stopping pztrn's Bunker...")
|
|
||||||
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "features/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Shutting down service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Shutdown(); err != nil {
|
|
||||||
return fmt.Errorf("shutting down service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, service := range a.services {
|
|
||||||
if !strings.Contains(service.Name(), "core/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.appLogger.Debug("Shutting down service.", "service", service.Name())
|
|
||||||
|
|
||||||
if err := service.Shutdown(); err != nil {
|
|
||||||
return fmt.Errorf("shutting down service '%s': %w", service.Name(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts application.
|
|
||||||
// Server application will start a goroutine that monitors SIGTERM and sends empty struct to channel.
|
|
||||||
func (a *Application) Start() error {
|
|
||||||
if err := a.connectDependencies(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errApplication, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.configure(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errApplication, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.launchStartupTasks(); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errApplication, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.startServer()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultLogLevel slog.Level = slog.LevelInfo
|
|
||||||
|
|
||||||
logLevelEnvVar = "BUNKERD_LOG_LEVEL"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *Application) initializeLogger() {
|
|
||||||
logLevel := defaultLogLevel
|
|
||||||
|
|
||||||
logLevelAsString, found := os.LookupEnv(logLevelEnvVar)
|
|
||||||
if found {
|
|
||||||
switch strings.ToLower(logLevelAsString) {
|
|
||||||
case "debug":
|
|
||||||
logLevel = slog.LevelDebug
|
|
||||||
case "info":
|
|
||||||
logLevel = slog.LevelInfo
|
|
||||||
case "warn":
|
|
||||||
logLevel = slog.LevelWarn
|
|
||||||
case "error":
|
|
||||||
logLevel = slog.LevelError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Warn("Setting log level.", "level", logLevel.String())
|
|
||||||
|
|
||||||
a.baseLogger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
||||||
AddSource: true,
|
|
||||||
Level: logLevel,
|
|
||||||
}))
|
|
||||||
|
|
||||||
a.appLogger = a.baseLogger.With("module", "application")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLogger creates new sub-instance of base logger and adds some additional data to it for persistent output.
|
|
||||||
func (a *Application) NewLogger(withs ...interface{}) *slog.Logger {
|
|
||||||
return a.baseLogger.With(withs...)
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShutdownChan returns shutdown channel for main function.
|
|
||||||
func (a *Application) ShutdownChan() chan struct{} {
|
|
||||||
return a.shutdownChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Application) startServer() {
|
|
||||||
a.shutdownChan = make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
listener := make(chan os.Signal, 1)
|
|
||||||
|
|
||||||
signal.Notify(listener, syscall.SIGTERM, os.Interrupt)
|
|
||||||
|
|
||||||
<-listener
|
|
||||||
|
|
||||||
a.shutdownChan <- struct{}{}
|
|
||||||
}()
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package application
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrServiceAlreadyRegistered returns if trying to register a service with name already taken by other service.
|
|
||||||
ErrServiceAlreadyRegistered = errors.New("service with such name already registered")
|
|
||||||
// ErrServiceNotFound returns if trying to gather service with unknown name.
|
|
||||||
ErrServiceNotFound = errors.New("service with such name wasn't found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service is an interface every service should conform to. Specific services will have own interface for
|
|
||||||
// cross-service interation.
|
|
||||||
type Service interface {
|
|
||||||
// Configure configures service. Called after ConnectDependencies and before LaunchStartupTasks.
|
|
||||||
Configure() error
|
|
||||||
// ConnectDependencies gets necessary dependencies.
|
|
||||||
ConnectDependencies() error
|
|
||||||
// Initialize initializes service's internal state. Called while registering service with Application
|
|
||||||
// lifecycle controller.
|
|
||||||
Initialize() error
|
|
||||||
// Name returns service name.
|
|
||||||
Name() string
|
|
||||||
// LaunchStartupTasks launches tasks on application start. Called after ConnectDependencies and Configure.
|
|
||||||
LaunchStartupTasks() error
|
|
||||||
// Shutdown stops service.
|
|
||||||
Shutdown() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterService registering service with lifecycle controller for later use in any other service.
|
|
||||||
func (a *Application) RegisterService(srv Service) error {
|
|
||||||
var found bool
|
|
||||||
|
|
||||||
for _, knownService := range a.services {
|
|
||||||
if srv.Name() == knownService.Name() {
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: RegisterService: check for service '%s' registration: %w",
|
|
||||||
errApplication,
|
|
||||||
srv.Name(),
|
|
||||||
ErrServiceAlreadyRegistered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.Initialize(); err != nil {
|
|
||||||
return fmt.Errorf("%w: RegisterService: initialize service '%s': %w", errApplication, srv.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a.services = append(a.services, srv)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service returns requested service.
|
|
||||||
func (a *Application) Service(name string) Service {
|
|
||||||
var srv Service
|
|
||||||
|
|
||||||
for _, knownService := range a.services {
|
|
||||||
if knownService.Name() == name {
|
|
||||||
srv = knownService
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return srv
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io/fs"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceNameDatabase is a name for database service.
|
|
||||||
const ServiceNameDatabase = "core/database"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrDatabase indicates that error appeared somewhere in database service.
|
|
||||||
ErrDatabase = errors.New("database service")
|
|
||||||
// ErrDatabaseIsInvalid indicates that database service implementation is invalid.
|
|
||||||
ErrDatabaseIsInvalid = errors.New("database service implementation is invalid")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database is an interface for database service.
|
|
||||||
type Database interface {
|
|
||||||
// Exec is a proxy for ExecContext from sqlx.
|
|
||||||
Exec(ctx context.Context, query string, params ...interface{}) error
|
|
||||||
// Get is a proxy for GetContext from sqlx.
|
|
||||||
Get(ctx context.Context, target interface{}, query string, params ...interface{}) error
|
|
||||||
// NamedExec is a proxy for NamedExecContext from sqlx.
|
|
||||||
NamedExec(ctx context.Context, query string, param interface{}) error
|
|
||||||
// RegisterMigrations registers migrations for applying from other services. Migrations should reside
|
|
||||||
// in "migrations" directory in passed filesystem.
|
|
||||||
RegisterMigrations(moduleName string, fs fs.FS) error
|
|
||||||
// Select is a proxy for SelectContext from sqlx.
|
|
||||||
Select(ctx context.Context, target interface{}, query string, params ...interface{}) error
|
|
||||||
// Transaction is a wrapper for transactions processing which wraps sqlx's transactions.
|
|
||||||
Transaction(ctx context.Context) (DatabaseTransaction, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DatabaseTransaction is an interface for database transactions controllers implementations.
|
|
||||||
type DatabaseTransaction interface {
|
|
||||||
Apply(steps ...TransactionFunc) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionFunc is a function that is used in transactions to mangle with data.
|
|
||||||
type TransactionFunc func(*sqlx.Tx) error
|
|
@ -1,83 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
// postgresql driver.
|
|
||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
databaseDSNEnvVar = "BUNKERD_DATABASE_DSN"
|
|
||||||
databaseMaxIdleConnsEnvVar = "BUNKERD_DATABASE_MAX_IDLE_CONNS"
|
|
||||||
databaseMaxOpenedConnsEnvVar = "BUNKERD_DATABASE_MAX_OPENED_CONNS"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errDSNInvalid = errors.New("BUNKERD_DATABASE_DSN environment variable is empty or invalid")
|
|
||||||
errNoMaxIdleConns = errors.New("no BUNKERD_DATABASE_MAX_IDLE_CONNS defined")
|
|
||||||
errNoMaxOpenedConns = errors.New("no BUNKERD_DATABASE_MAX_OPENED_CONNS defined")
|
|
||||||
errPostgresOnlySupported = errors.New("only PostgreSQL database is currently supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *database) initializeConnection() error {
|
|
||||||
// Getting database DSN from environment as well as other required settings.
|
|
||||||
dsn, found := os.LookupEnv(databaseDSNEnvVar)
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("initialize connection: getting database DSN: %w", errDSNInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxOpenedConnsRaw, found := os.LookupEnv(databaseMaxOpenedConnsEnvVar)
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("initialize connection: getting maximum number of opened conections: %w", errNoMaxOpenedConns)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxOpenedConns, err := strconv.ParseInt(maxOpenedConnsRaw, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialize connection: parsing maximum number of opened conections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxIdleConnsRaw, found := os.LookupEnv(databaseMaxIdleConnsEnvVar)
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("initialize connection: getting maximum number of idle conections: %w", errNoMaxIdleConns)
|
|
||||||
}
|
|
||||||
|
|
||||||
maxIdleConns, err := strconv.ParseInt(maxIdleConnsRaw, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialize connection: parsing maximum number of opened conections: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// While database/sql (and sqlx) supports all possible DSN formats, we will force user to use DSN in form
|
|
||||||
// "proto://user:passowrd@host:port/dbname" as it is easier to parse.
|
|
||||||
if _, err := url.Parse(dsn); err != nil {
|
|
||||||
return fmt.Errorf("initialize connection: validate DSN: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Currently we're support only postgresql, but this may change in future.
|
|
||||||
if !strings.HasPrefix(dsn, "postgres://") {
|
|
||||||
return fmt.Errorf("initialize connection: validate DSN: %w", errPostgresOnlySupported)
|
|
||||||
}
|
|
||||||
|
|
||||||
proto := strings.Split(dsn, ":")[0]
|
|
||||||
if proto == "postgres" {
|
|
||||||
proto = "pgx"
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := sqlx.Open(proto, dsn)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialize connection: open database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.db = db
|
|
||||||
|
|
||||||
d.db.SetMaxOpenConns(int(maxOpenedConns))
|
|
||||||
d.db.SetMaxIdleConns(int(maxIdleConns))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/server/internal/application"
|
|
||||||
"bunker/server/internal/services/core"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = core.Database(&database{})
|
|
||||||
|
|
||||||
type database struct {
|
|
||||||
app *application.Application
|
|
||||||
db *sqlx.DB
|
|
||||||
logger *slog.Logger
|
|
||||||
migrations map[string]fs.FS
|
|
||||||
version int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
db := &database{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(db); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Configure() error {
|
|
||||||
if err := d.initializeConnection(); err != nil {
|
|
||||||
return fmt.Errorf("configure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) ConnectDependencies() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Initialize() error {
|
|
||||||
d.logger = d.app.NewLogger("service", core.ServiceNameDatabase)
|
|
||||||
|
|
||||||
d.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
d.migrations = make(map[string]fs.FS, 0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Name() string {
|
|
||||||
return core.ServiceNameDatabase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) LaunchStartupTasks() error {
|
|
||||||
if err := d.applyMigrations(); err != nil {
|
|
||||||
return fmt.Errorf("launch startup tasks: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"bunker/commons"
|
|
||||||
"bunker/server/internal/services/core"
|
|
||||||
|
|
||||||
"github.com/pressly/goose/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errMigrationsAlreadyRegistered = errors.New("migrations already registered")
|
|
||||||
|
|
||||||
func (d *database) applyMigrations() error {
|
|
||||||
d.logger.Info("Migrating database...")
|
|
||||||
|
|
||||||
modules := make([]string, 0)
|
|
||||||
|
|
||||||
for module := range d.migrations {
|
|
||||||
modules = append(modules, module)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(modules)
|
|
||||||
|
|
||||||
_ = goose.SetDialect(string(goose.DialectPostgres))
|
|
||||||
|
|
||||||
gooseLogger := commons.NewGooseLogger(d.logger)
|
|
||||||
goose.SetLogger(gooseLogger)
|
|
||||||
|
|
||||||
for _, module := range modules {
|
|
||||||
d.logger.Info("Migrating database for module...", "module", module)
|
|
||||||
|
|
||||||
goose.SetBaseFS(d.migrations[module])
|
|
||||||
goose.SetTableName(strings.ReplaceAll(module, "/", "_") + "_migrations")
|
|
||||||
|
|
||||||
if err := goose.Up(d.db.DB, "migrations"); err != nil {
|
|
||||||
return fmt.Errorf("%w: applying migrations for module '%s': %w", core.ErrDatabase, module, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
moduleDBVersion, err := goose.GetDBVersion(d.db.DB)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: get database version for module '%s': %w", core.ErrDatabase, module, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.version += moduleDBVersion
|
|
||||||
|
|
||||||
d.logger.Info(
|
|
||||||
"Database for module migrated to latest version",
|
|
||||||
"module", module,
|
|
||||||
"module_db_version", moduleDBVersion,
|
|
||||||
"db_version", d.version,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Info("Database migrated.", "version", d.version)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) RegisterMigrations(moduleName string, fSys fs.FS) error {
|
|
||||||
slog.Debug("Registering migrations for service.", "service", moduleName)
|
|
||||||
|
|
||||||
if _, found := d.migrations[moduleName]; found {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: RegisterMigrations: module '%s': %w",
|
|
||||||
core.ErrDatabase,
|
|
||||||
moduleName,
|
|
||||||
errMigrationsAlreadyRegistered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.migrations[moduleName] = fSys
|
|
||||||
|
|
||||||
slog.Debug("Migrations for service successfully registered.", "service", moduleName)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"bunker/server/internal/services/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Exec is a proxy for ExecContext from sqlx.
|
|
||||||
func (d *database) Exec(ctx context.Context, query string, params ...interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Executing query.", "query", query, "params", fmt.Sprintf("%+v", params))
|
|
||||||
|
|
||||||
if _, err := d.db.ExecContext(ctx, query, params...); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to Exec(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get is a proxy for GetContext from sqlx.
|
|
||||||
func (d *database) Get(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Getting single data from database with query.", "query", query, "params", fmt.Sprintf("%+v", params))
|
|
||||||
|
|
||||||
if err := d.db.GetContext(ctx, target, query, params...); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to Get(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NamedExec is a proxy for NamedExecContext from sqlx.
|
|
||||||
func (d *database) NamedExec(ctx context.Context, query string, param interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Executing named query.", "query", query, "params", fmt.Sprintf("%+v", param))
|
|
||||||
|
|
||||||
if _, err := d.db.NamedExecContext(ctx, query, param); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to NamedExec(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select is a proxy for SelectContext from sqlx.
|
|
||||||
func (d *database) Select(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
|
||||||
if strings.Contains(query, "?") {
|
|
||||||
query = d.db.Rebind(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.logger.Debug("Selecting from database with query.", "query", query, "params", fmt.Sprintf("%+v", params))
|
|
||||||
|
|
||||||
if err := d.db.SelectContext(ctx, target, query, params...); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to Select(): %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/server/internal/services/core"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type transaction struct {
|
|
||||||
transaction *sqlx.Tx
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *database) Transaction(ctx context.Context) (core.DatabaseTransaction, error) {
|
|
||||||
txn, err := d.db.BeginTxx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: starting transaction: %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
txHandler := &transaction{
|
|
||||||
transaction: txn,
|
|
||||||
logger: d.logger.With("module", "transactioner"),
|
|
||||||
}
|
|
||||||
|
|
||||||
return txHandler, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *transaction) Apply(steps ...core.TransactionFunc) error {
|
|
||||||
for stepNumber, stepFunc := range steps {
|
|
||||||
if err := stepFunc(t.transaction); err != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Error occurred.",
|
|
||||||
"step", stepNumber,
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Transaction rollback failed.",
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := t.transaction.Commit(); err != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Transaction commit failed.",
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
|
||||||
t.logger.Error(
|
|
||||||
"Transaction rollback failed.",
|
|
||||||
"error", err.Error(),
|
|
||||||
"module", "core/database",
|
|
||||||
"subsystem", "transaction",
|
|
||||||
)
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w: transaction commit: %w", core.ErrDatabase, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceNameHTTPServer is a name for HTTP server service.
|
|
||||||
const ServiceNameHTTPServer = "core/http_server"
|
|
||||||
|
|
||||||
// ErrHTTPServerIsInvalid appears when HTTP server service implementation is invalid.
|
|
||||||
var ErrHTTPServerIsInvalid = errors.New("HTTP server service implementation is invalid")
|
|
||||||
|
|
||||||
// HTTPServer is an interface for HTTP server service.
|
|
||||||
//
|
|
||||||
//nolint:iface
|
|
||||||
type HTTPServer interface{}
|
|
@ -1,91 +0,0 @@
|
|||||||
package httpserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"bunker/server/internal/application"
|
|
||||||
"bunker/server/internal/services/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ = core.HTTPServer(&httpServer{})
|
|
||||||
|
|
||||||
errHTTPServer = errors.New("HTTP server core service")
|
|
||||||
)
|
|
||||||
|
|
||||||
type httpServer struct {
|
|
||||||
app *application.Application
|
|
||||||
logger *slog.Logger
|
|
||||||
db core.Database
|
|
||||||
httpSrv *http.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
httpSrv := &httpServer{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(httpSrv); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errHTTPServer, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) Configure() error {
|
|
||||||
h.logger.Debug("Configuring service...")
|
|
||||||
|
|
||||||
if err := h.configureHTTPServer(); err != nil {
|
|
||||||
return fmt.Errorf("configure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) ConnectDependencies() error {
|
|
||||||
databaseRaw := h.app.Service(core.ServiceNameDatabase)
|
|
||||||
if databaseRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
database, valid := databaseRaw.(core.Database)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.db = database
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) Initialize() error {
|
|
||||||
h.logger = h.app.NewLogger("service", core.ServiceNameHTTPServer)
|
|
||||||
|
|
||||||
h.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) Name() string {
|
|
||||||
return core.ServiceNameHTTPServer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) LaunchStartupTasks() error {
|
|
||||||
h.logger.Debug("Launching startup tasks...")
|
|
||||||
|
|
||||||
go h.startHTTPServer()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) Shutdown() error {
|
|
||||||
if err := h.stopHTTPServer(); err != nil {
|
|
||||||
return fmt.Errorf("%w: Shutdown: %w", errHTTPServer, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
package httpserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
const httpServerAddrEnvVar = "BUNKERD_HTTP_ADDRESS"
|
|
||||||
|
|
||||||
var (
|
|
||||||
errHTTPServerAddrInvalid = errors.New("BUNKERD_HTTP_ADDRESS environment variable contains invalid address to " +
|
|
||||||
"listen, should be 'host:port'")
|
|
||||||
errHTTPServerAddrNotFound = errors.New("BUNKERD_HTTP_ADDRESS environment variable empty")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *httpServer) configureHTTPServer() error {
|
|
||||||
httpSrvAddr, found := os.LookupEnv(httpServerAddrEnvVar)
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("configure HTTP server: get address from environment variable: %w", errHTTPServerAddrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
host, port, err := net.SplitHostPort(httpSrvAddr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("configure HTTP server: validate HTTP server address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpSrvAddr != host+":"+port {
|
|
||||||
return fmt.Errorf("configure HTTP server: validate HTTP server address: %w", errHTTPServerAddrInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := new(http.ServeMux)
|
|
||||||
mux.HandleFunc("GET /api/v1/socket", h.handleWebsocketRequest)
|
|
||||||
|
|
||||||
h.httpSrv = &http.Server{
|
|
||||||
Addr: httpSrvAddr,
|
|
||||||
Handler: mux,
|
|
||||||
ReadHeaderTimeout: time.Second * 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) handleWebsocketRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
wsConn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
|
||||||
OnPingReceived: func(_ context.Context, _ []byte) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error("Failed to accept WS connection!", "error", err.Error())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := wsConn.CloseNow(); err != nil {
|
|
||||||
h.logger.Warn("Failed to close WS connection in defer!", "error", err.Error())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) startHTTPServer() {
|
|
||||||
h.logger.Info("Starting listening for HTTP requests.", "address", h.httpSrv.Addr)
|
|
||||||
|
|
||||||
if err := h.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
h.logger.Warn("Error when listening to ", "error", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpServer) stopHTTPServer() error {
|
|
||||||
h.logger.Info("Stopping HTTP server...")
|
|
||||||
|
|
||||||
if err := h.httpSrv.Shutdown(h.app.ContextWithTimeout(time.Second * 3)); err != nil {
|
|
||||||
return fmt.Errorf("stopping HTTP server: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceNameOptions is a name for options service which controls options storage.
|
|
||||||
const ServiceNameOptions = "core/options"
|
|
||||||
|
|
||||||
// ErrOptionsIsInvalid appears when options service implementation is invalid.
|
|
||||||
var ErrOptionsIsInvalid = errors.New("options service implementation is invalid")
|
|
||||||
|
|
||||||
// Options is an interface for options service.
|
|
||||||
//
|
|
||||||
//nolint:iface
|
|
||||||
type Options interface{}
|
|
@ -1,17 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed migrations
|
|
||||||
var migrations embed.FS
|
|
||||||
|
|
||||||
func (o *options) registerMigrations() error {
|
|
||||||
if err := o.db.RegisterMigrations("core/options", migrations); err != nil {
|
|
||||||
return fmt.Errorf("register migrations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
-- +goose Up
|
|
||||||
CREATE TABLE IF NOT EXISTS options (
|
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
|
||||||
user_id UUID NOT NULL,
|
|
||||||
key VARCHAR(1024) NOT NULL,
|
|
||||||
value VARCHAR(8192)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS options_user_id_key_idx ON options(user_id, key);
|
|
||||||
|
|
||||||
-- +goose Down
|
|
||||||
DROP TABLE IF EXISTS options;
|
|
@ -1,79 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"bunker/server/internal/application"
|
|
||||||
"bunker/server/internal/services/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ = core.Options(&options{})
|
|
||||||
|
|
||||||
errOptions = errors.New("options core service")
|
|
||||||
)
|
|
||||||
|
|
||||||
type options struct {
|
|
||||||
app *application.Application
|
|
||||||
logger *slog.Logger
|
|
||||||
db core.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize initializes service.
|
|
||||||
func Initialize(app *application.Application) error {
|
|
||||||
opts := &options{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.RegisterService(opts); err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errOptions, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Configure() error {
|
|
||||||
if err := o.registerMigrations(); err != nil {
|
|
||||||
return fmt.Errorf("configure: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) ConnectDependencies() error {
|
|
||||||
databaseRaw := o.app.Service(core.ServiceNameDatabase)
|
|
||||||
if databaseRaw == nil {
|
|
||||||
return fmt.Errorf("connect dependencies: get database service: %w", application.ErrServiceNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
database, valid := databaseRaw.(core.Database)
|
|
||||||
if !valid {
|
|
||||||
return fmt.Errorf("connect dependencies: type assert database service: %w", core.ErrDatabaseIsInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
o.db = database
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Initialize() error {
|
|
||||||
o.logger = o.app.NewLogger("service", core.ServiceNameOptions)
|
|
||||||
|
|
||||||
o.logger.Info("Initializing...")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Name() string {
|
|
||||||
return core.ServiceNameOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) LaunchStartupTasks() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *options) Shutdown() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
includes:
|
|
||||||
bunkerd: ./bunkerd
|
|
||||||
common: ./common
|
|
||||||
postgresql: ./postgresql
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
down:
|
|
||||||
desc: "Removes development environment."
|
|
||||||
cmds:
|
|
||||||
- task: bunkerd:down
|
|
||||||
- task: bunkerd:down-debug
|
|
||||||
- task: postgresql:down
|
|
||||||
- task: common:network-down
|
|
||||||
|
|
||||||
up:
|
|
||||||
desc: "Creates development environment."
|
|
||||||
cmds:
|
|
||||||
- task: common:network-up
|
|
||||||
- task: postgresql:up
|
|
||||||
- task: bunkerd:up
|
|
@ -1,91 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
desc: "Builds bunkerd's container."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml build
|
|
||||||
|
|
||||||
build-debug:
|
|
||||||
desc: "Builds bunkerd's debug build container."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd-debug -f docker-compose.debug.yaml build
|
|
||||||
|
|
||||||
down:
|
|
||||||
desc: "Deletes all bunkerd's data (down)."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml down --volumes
|
|
||||||
|
|
||||||
down-debug:
|
|
||||||
desc: "Deletes all bunkerd's debug build data (down)."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd-debug -f docker-compose.debug.yaml down --volumes
|
|
||||||
|
|
||||||
logs:
|
|
||||||
desc: "Show bunkerd logs."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml logs -f
|
|
||||||
|
|
||||||
logs-debug:
|
|
||||||
desc: "Show bunkerd debug build logs."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd-debug -f docker-compose.debug.yaml logs -f
|
|
||||||
|
|
||||||
restart:
|
|
||||||
desc: "Restart bunkerd."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml restart
|
|
||||||
|
|
||||||
restart-debug:
|
|
||||||
desc: "Restart bunkerd debug build."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd-debug -f docker-compose.debug.yaml restart
|
|
||||||
|
|
||||||
start:
|
|
||||||
desc: "Start bunkerd."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml start
|
|
||||||
|
|
||||||
start-debug:
|
|
||||||
desc: "Start bunkerd debug build."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml start
|
|
||||||
|
|
||||||
stop:
|
|
||||||
desc: "Stop bunkerd without deleting it's data."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml stop
|
|
||||||
|
|
||||||
stop-debug:
|
|
||||||
desc: "Stop bunkerd debug build without deleting it's data."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd-debug -f docker-compose.debug.yaml stop
|
|
||||||
|
|
||||||
up:
|
|
||||||
desc: "Start bunkerd (up -d)."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- task: :common:network-up
|
|
||||||
- task: build
|
|
||||||
- docker compose -p bunkerd -f docker-compose.yaml up -d
|
|
||||||
|
|
||||||
up-debug:
|
|
||||||
desc: "Start bunkerd debug build (up -d)."
|
|
||||||
dir: "./server/localdevzone/bunkerd"
|
|
||||||
cmds:
|
|
||||||
- task: :common:network-up
|
|
||||||
- task: build-debug
|
|
||||||
- docker compose -p bunkerd-debug -f docker-compose.debug.yaml up -d --wait
|
|
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
services:
|
|
||||||
bunkerd-debug:
|
|
||||||
container_name: "bunkerd-debug"
|
|
||||||
build:
|
|
||||||
context: ../../../
|
|
||||||
dockerfile: server/Dockerfile.debug
|
|
||||||
ports:
|
|
||||||
- "54400:54400"
|
|
||||||
- "54401:4000"
|
|
||||||
volumes:
|
|
||||||
- "./data:/data"
|
|
||||||
networks:
|
|
||||||
bunkerd:
|
|
||||||
ipv4_address: 247.247.0.3
|
|
||||||
environment:
|
|
||||||
BUNKERD_DATABASE_MAX_IDLE_CONNS: 5
|
|
||||||
BUNKERD_DATABASE_MAX_OPENED_CONNS: 20
|
|
||||||
BUNKERD_DATABASE_DSN: "postgres://postgres:postgres@247.247.0.4:5432/bunkerd?sslmode=disable"
|
|
||||||
BUNKERD_HTTP_ADDRESS: "0.0.0.0:53400"
|
|
||||||
BUNKERD_LOG_LEVEL: "debug"
|
|
||||||
cap_add:
|
|
||||||
- SYS_PTRACE
|
|
||||||
|
|
||||||
networks:
|
|
||||||
bunkerd:
|
|
||||||
external: true
|
|
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
services:
|
|
||||||
bunkerd:
|
|
||||||
container_name: "bunkerd"
|
|
||||||
build:
|
|
||||||
context: ../../../
|
|
||||||
dockerfile: server/Dockerfile
|
|
||||||
ports:
|
|
||||||
- "53400:53400"
|
|
||||||
volumes:
|
|
||||||
- "./data:/data"
|
|
||||||
networks:
|
|
||||||
bunkerd:
|
|
||||||
ipv4_address: 247.247.0.2
|
|
||||||
environment:
|
|
||||||
BUNKERD_DATABASE_MAX_IDLE_CONNS: 5
|
|
||||||
BUNKERD_DATABASE_MAX_OPENED_CONNS: 20
|
|
||||||
BUNKERD_DATABASE_DSN: "postgres://postgres:postgres@247.247.0.4:5432/bunkerd?sslmode=disable"
|
|
||||||
BUNKERD_HTTP_ADDRESS: "0.0.0.0:53400"
|
|
||||||
BUNKERD_LOG_LEVEL: "debug"
|
|
||||||
cap_add:
|
|
||||||
- SYS_PTRACE
|
|
||||||
|
|
||||||
networks:
|
|
||||||
bunkerd:
|
|
||||||
external: true
|
|
@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
version: "3"
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
network-down:
|
|
||||||
desc: "Deletes Docker network for development."
|
|
||||||
dir: "./server/localdevzone/common"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd-network -f network.yaml down
|
|
||||||
|
|
||||||
network-up:
|
|
||||||
desc: "Creates Docker network for development"
|
|
||||||
dir: "./server/localdevzone/common"
|
|
||||||
cmds:
|
|
||||||
- docker compose -p bunkerd-network -f network.yaml up -d
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user