mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Simplify
This commit is contained in:
parent
6ac194e816
commit
5912029c1b
22 changed files with 184 additions and 417 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@ node_modules
|
|||
/test
|
||||
tmp
|
||||
*.db
|
||||
server
|
||||
|
|
|
|||
|
|
@ -4,45 +4,30 @@ This guide documents the steps for installing the Dnote server on your own machi
|
|||
|
||||
## Overview
|
||||
|
||||
Dnote server comes as a single binary file that you can simply download and run. It uses Postgres as the database.
|
||||
Dnote server comes as a single binary file that you can simply download and run. It uses SQLite as the database.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install Postgres 11+.
|
||||
2. Create a `dnote` database by running `createdb dnote`
|
||||
3. Download the official Dnote server release from the [release page](https://github.com/dnote/dnote/releases).
|
||||
4. Extract the archive and move the `dnote-server` executable to `/usr/local/bin`.
|
||||
1. Download the official Dnote server release from the [release page](https://github.com/dnote/dnote/releases).
|
||||
2. Extract the archive and move the `dnote-server` executable to `/usr/local/bin`.
|
||||
|
||||
```bash
|
||||
tar -xzf dnote-server-$version-$os.tar.gz
|
||||
mv ./dnote-server /usr/local/bin
|
||||
```
|
||||
|
||||
4. Run Dnote
|
||||
3. Run Dnote
|
||||
|
||||
```bash
|
||||
GO_ENV=PRODUCTION \
|
||||
DBHost=localhost \
|
||||
DBPort=5432 \
|
||||
DBName=dnote \
|
||||
DBUser=$user \
|
||||
DBPassword=$password \
|
||||
APP_ENV=PRODUCTION \
|
||||
WebURL=$webURL \
|
||||
SmtpHost=$SmtpHost \
|
||||
SmtpPort=$SmtpPort \
|
||||
SmtpUsername=$SmtpUsername \
|
||||
SmtpPassword=$SmtpPassword \
|
||||
DisableRegistration=false \
|
||||
dnote-server start
|
||||
```
|
||||
|
||||
Replace `$user`, `$password` with the credentials of the Postgres user that owns the `dnote` database.
|
||||
|
||||
Replace `$webURL` with the full URL to your server, without a trailing slash (e.g. `https://your.server`).
|
||||
|
||||
Replace `$SmtpHost`, `SmtpPort`, `$SmtpUsername`, `$SmtpPassword` with actual values, if you would like to receive spaced repetition through email.
|
||||
|
||||
Replace `DisableRegistration` to `true` if you would like to disable user registrations.
|
||||
Set `DisableRegistration=true` if you would like to disable user registrations.
|
||||
|
||||
By default, dnote server will run on the port 3000.
|
||||
|
||||
|
|
@ -127,25 +112,32 @@ Restart=always
|
|||
RestartSec=3
|
||||
WorkingDirectory=/home/$user
|
||||
ExecStart=/usr/local/bin/dnote-server start
|
||||
Environment=GO_ENV=PRODUCTION
|
||||
Environment=APP_ENV=PRODUCTION
|
||||
Environment=WebURL=$WebURL
|
||||
Environment=SmtpHost=
|
||||
Environment=SmtpPort=
|
||||
Environment=SmtpUsername=
|
||||
Environment=SmtpPassword=
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Replace `$user`, `$WebURL`, `$DBUser`, and `$DBPassword` with the actual values.
|
||||
Replace `$user` and `$WebURL` with the actual values.
|
||||
|
||||
Optionally, if you would like to send spaced repetitions throught email, populate `SmtpHost`, `SmtpPort`, `SmtpUsername`, and `SmtpPassword`.
|
||||
By default, the database will be stored at `$XDG_DATA_HOME/dnote/server.db` (typically `~/.local/share/dnote/server.db`). To use a custom location, add `Environment=DBPath=/path/to/database.db` to the service file.
|
||||
|
||||
2. Reload the change by running `sudo systemctl daemon-reload`.
|
||||
3. Enable the Daemon by running `sudo systemctl enable dnote`.`
|
||||
4. Start the Daemon by running `sudo systemctl start dnote`
|
||||
|
||||
### Optional: Email Support
|
||||
|
||||
To enable sending emails, add the following environment variables to your configuration. But they are not required.
|
||||
|
||||
- `SmtpHost` - SMTP server hostname
|
||||
- `SmtpPort` - SMTP server port
|
||||
- `SmtpUsername` - SMTP username
|
||||
- `SmtpPassword` - SMTP password
|
||||
|
||||
For systemd, add these as additional `Environment=` lines in `/etc/systemd/system/dnote.service`.
|
||||
|
||||
### Configure clients
|
||||
|
||||
Let's configure Dnote clients to connect to the self-hosted web API endpoint.
|
||||
|
|
@ -169,7 +161,3 @@ e.g.
|
|||
editor: nvim
|
||||
apiEndpoint: my-dnote-server.com/api
|
||||
```
|
||||
|
||||
#### Browser extension
|
||||
|
||||
Navigate into the 'Settings' tab and set the values for 'API URL', and 'Web URL'.
|
||||
|
|
|
|||
|
|
@ -1,28 +1,14 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
environment:
|
||||
POSTGRES_USER: dnote
|
||||
POSTGRES_PASSWORD: dnote
|
||||
POSTGRES_DB: dnote
|
||||
volumes:
|
||||
- ./dnote_data:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
dnote:
|
||||
image: dnote/dnote:latest
|
||||
environment:
|
||||
GO_ENV: PRODUCTION
|
||||
APP_ENV: PRODUCTION
|
||||
WebURL: localhost:3000
|
||||
SmtpHost:
|
||||
SmtpPort:
|
||||
SmtpUsername:
|
||||
SmtpPassword:
|
||||
DisableRegistration: "false"
|
||||
ports:
|
||||
- 3000:3000
|
||||
depends_on:
|
||||
- postgres
|
||||
volumes:
|
||||
- ./dnote_data:/data
|
||||
restart: always
|
||||
|
|
|
|||
|
|
@ -1,25 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
wait_for_db() {
|
||||
HOST=${DBHost:-postgres}
|
||||
PORT=${DBPort:-5432}
|
||||
echo "Waiting for the database connection..."
|
||||
|
||||
attempts=0
|
||||
max_attempts=10
|
||||
while [ $attempts -lt $max_attempts ]; do
|
||||
nc -z "${HOST}" "${PORT}" 2>/dev/null && break
|
||||
echo "Waiting for db at ${HOST}:${PORT}..."
|
||||
sleep 5
|
||||
attempts=$((attempts+1))
|
||||
done
|
||||
|
||||
if [ $attempts -eq $max_attempts ]; then
|
||||
echo "Timed out while waiting for db at ${HOST}:${PORT}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_db
|
||||
# Set default DBPath to /data if not specified
|
||||
export DBPath=${DBPath:-/data/dnote.db}
|
||||
|
||||
exec "$@"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ cd /vagrant
|
|||
|
||||
tar -xvf dnote_server_integration_test_linux_amd64.tar.gz
|
||||
|
||||
GO_ENV=PRODUCTION \
|
||||
APP_ENV=PRODUCTION \
|
||||
DBHost=localhost \
|
||||
DBPort=5432 \
|
||||
DBName=dnote \
|
||||
|
|
|
|||
114
pkg/e2e/server_test.go
Normal file
114
pkg/e2e/server_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestServerStart(t *testing.T) {
|
||||
tmpDB := t.TempDir() + "/test.db"
|
||||
port := "3456" // Use non-standard port to avoid conflicts
|
||||
|
||||
// Start server in background
|
||||
cmd := exec.Command("go", "run", "-tags", "fts5", "../server", "-port", port, "start")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"DBPath="+tmpDB,
|
||||
"WebURL=http://localhost:"+port,
|
||||
"APP_ENV=PRODUCTION",
|
||||
)
|
||||
// Capture output for debugging
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start server: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for server to start and migrations to run
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Verify server responds to health check
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", port))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to reach server health endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, resp.StatusCode, 200, "health endpoint should return 200")
|
||||
|
||||
// Kill server before checking database to avoid locks
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait() // Clean up zombie process
|
||||
}
|
||||
|
||||
// Verify database file was created
|
||||
if _, err := os.Stat(tmpDB); os.IsNotExist(err) {
|
||||
t.Fatalf("database file was not created at %s", tmpDB)
|
||||
}
|
||||
|
||||
// Verify migrations ran by checking database
|
||||
db, err := gorm.Open(sqlite.Open(tmpDB), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open test database: %v", err)
|
||||
}
|
||||
|
||||
// Verify migrations ran
|
||||
var count int64
|
||||
if err := db.Raw("SELECT COUNT(*) FROM schema_migrations").Scan(&count).Error; err != nil {
|
||||
t.Fatalf("schema_migrations table not found: %v", err)
|
||||
}
|
||||
if count == 0 {
|
||||
t.Fatal("no migrations were run")
|
||||
}
|
||||
|
||||
// Verify FTS table exists and is functional
|
||||
if err := db.Exec("SELECT * FROM notes_fts LIMIT 1").Error; err != nil {
|
||||
t.Fatalf("notes_fts table not found or not functional: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerVersion(t *testing.T) {
|
||||
cmd := exec.Command("go", "run", "-tags", "fts5", "../server", "version")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("version command failed: %v", err)
|
||||
}
|
||||
|
||||
outputStr := string(output)
|
||||
if !strings.Contains(outputStr, "dnote-server-") {
|
||||
t.Errorf("expected version output to contain 'dnote-server-', got: %s", outputStr)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
GO_ENV=DEVELOPMENT
|
||||
APP_ENV=DEVELOPMENT
|
||||
|
||||
SmtpUsername=mock-SmtpUsername
|
||||
SmtpPassword=mock-SmtpPassword
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
GO_ENV=TEST
|
||||
APP_ENV=TEST
|
||||
|
||||
SmtpUsername=mock-SmtpUsername
|
||||
SmtpPassword=mock-SmtpPassword
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func NewTest(appParams *App) App {
|
|||
WebURL: "http://127.0.0.0.1",
|
||||
Port: "3000",
|
||||
DisableRegistration: false,
|
||||
DB: config.LoadDBConfig(),
|
||||
DBPath: config.LoadDBPath(),
|
||||
AssetBaseURL: "",
|
||||
HTTP500Page: assets.MustGetHTTP500ErrorPage(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import (
|
|||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/helpers"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/dnote/dnote/pkg/server/token"
|
||||
pkgErrors "github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -40,17 +39,6 @@ func (a *App) TouchLastLoginAt(user database.User, tx *gorm.DB) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func createEmailPreference(user database.User, tx *gorm.DB) error {
|
||||
p := database.EmailPreference{
|
||||
UserID: user.ID,
|
||||
}
|
||||
if err := tx.Save(&p).Error; err != nil {
|
||||
return pkgErrors.Wrap(err, "inserting email preference")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateUser creates a user
|
||||
func (a *App) CreateUser(email, password string, passwordConfirmation string) (database.User, error) {
|
||||
if email == "" {
|
||||
|
|
@ -104,14 +92,6 @@ func (a *App) CreateUser(email, password string, passwordConfirmation string) (d
|
|||
return database.User{}, pkgErrors.Wrap(err, "saving account")
|
||||
}
|
||||
|
||||
if _, err := token.Create(tx, user.ID, database.TokenTypeEmailPreference); err != nil {
|
||||
tx.Rollback()
|
||||
return database.User{}, pkgErrors.Wrap(err, "creating email verificaiton token")
|
||||
}
|
||||
if err := createEmailPreference(user, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return database.User{}, pkgErrors.Wrap(err, "creating email preference")
|
||||
}
|
||||
if err := a.TouchLastLoginAt(user, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return database.User{}, pkgErrors.Wrap(err, "updating last login")
|
||||
|
|
|
|||
|
|
@ -19,18 +19,24 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/dnote/dnote/pkg/dirs"
|
||||
"github.com/dnote/dnote/pkg/server/assets"
|
||||
"github.com/dnote/dnote/pkg/server/log"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// AppEnvProduction represents an app environment for production.
|
||||
AppEnvProduction string = "PRODUCTION"
|
||||
// DefaultDBDir is the default directory name for Dnote data
|
||||
DefaultDBDir = "dnote"
|
||||
// DefaultDBFilename is the default database filename
|
||||
DefaultDBFilename = "server.db"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -42,11 +48,6 @@ var (
|
|||
ErrPortInvalid = errors.New("Invalid Port")
|
||||
)
|
||||
|
||||
// DBConfig holds the database connection configuration.
|
||||
type DBConfig struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func readBoolEnv(name string) bool {
|
||||
if os.Getenv(name) == "true" {
|
||||
return true
|
||||
|
|
@ -55,15 +56,13 @@ func readBoolEnv(name string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func LoadDBConfig() DBConfig {
|
||||
func LoadDBPath() string {
|
||||
path := os.Getenv("DBPath")
|
||||
if path == "" {
|
||||
path = filepath.Join(dirs.DataHome, "dnote", "server.db")
|
||||
path = filepath.Join(dirs.DataHome, DefaultDBDir, DefaultDBFilename)
|
||||
}
|
||||
|
||||
return DBConfig{
|
||||
Path: path,
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// Config is an application configuration
|
||||
|
|
@ -72,14 +71,25 @@ type Config struct {
|
|||
WebURL string
|
||||
DisableRegistration bool
|
||||
Port string
|
||||
DB DBConfig
|
||||
DBPath string
|
||||
AssetBaseURL string
|
||||
HTTP500Page []byte
|
||||
}
|
||||
|
||||
func getDeprecatedEnvVar(deprecated, current string) string {
|
||||
val := os.Getenv(deprecated)
|
||||
if val != "" {
|
||||
log.WithFields(log.Fields{
|
||||
"deprecated": deprecated,
|
||||
"current": current,
|
||||
}).Warn(fmt.Sprintf("%s is deprecated. Please use %s instead.", deprecated, current))
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getAppEnv() string {
|
||||
// DEPRECATED
|
||||
goEnv := os.Getenv("GO_ENV")
|
||||
goEnv := getDeprecatedEnvVar("GO_ENV", "APP_ENV")
|
||||
if goEnv != "" {
|
||||
return goEnv
|
||||
}
|
||||
|
|
@ -87,9 +97,6 @@ func getAppEnv() string {
|
|||
return os.Getenv("APP_ENV")
|
||||
}
|
||||
|
||||
func checkDeprecatedEnvVars() {
|
||||
}
|
||||
|
||||
// Load constructs and returns a new config based on the environment variables.
|
||||
func Load() Config {
|
||||
port := os.Getenv("PORT")
|
||||
|
|
@ -97,14 +104,12 @@ func Load() Config {
|
|||
port = "3000"
|
||||
}
|
||||
|
||||
checkDeprecatedEnvVars()
|
||||
|
||||
c := Config{
|
||||
AppEnv: getAppEnv(),
|
||||
WebURL: os.Getenv("WebURL"),
|
||||
Port: port,
|
||||
DisableRegistration: readBoolEnv("DisableRegistration"),
|
||||
DB: LoadDBConfig(),
|
||||
DBPath: LoadDBPath(),
|
||||
AssetBaseURL: "",
|
||||
HTTP500Page: assets.MustGetHTTP500ErrorPage(),
|
||||
}
|
||||
|
|
@ -134,7 +139,7 @@ func validate(c Config) error {
|
|||
return ErrPortInvalid
|
||||
}
|
||||
|
||||
if c.DB.Path == "" {
|
||||
if c.DBPath == "" {
|
||||
return ErrDBMissingPath
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,7 @@ func TestValidate(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
config: Config{
|
||||
DB: DBConfig{
|
||||
Path: "test.db",
|
||||
},
|
||||
DBPath: "test.db",
|
||||
WebURL: "http://mock.url",
|
||||
Port: "3000",
|
||||
},
|
||||
|
|
@ -43,9 +41,7 @@ func TestValidate(t *testing.T) {
|
|||
},
|
||||
{
|
||||
config: Config{
|
||||
DB: DBConfig{
|
||||
Path: "",
|
||||
},
|
||||
DBPath: "",
|
||||
WebURL: "http://mock.url",
|
||||
Port: "3000",
|
||||
},
|
||||
|
|
@ -53,17 +49,13 @@ func TestValidate(t *testing.T) {
|
|||
},
|
||||
{
|
||||
config: Config{
|
||||
DB: DBConfig{
|
||||
Path: "test.db",
|
||||
},
|
||||
DBPath: "test.db",
|
||||
},
|
||||
expectedErr: ErrWebURLInvalid,
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
DB: DBConfig{
|
||||
Path: "test.db",
|
||||
},
|
||||
DBPath: "test.db",
|
||||
WebURL: "http://mock.url",
|
||||
},
|
||||
expectedErr: ErrPortInvalid,
|
||||
|
|
|
|||
|
|
@ -23,8 +23,6 @@ const (
|
|||
TokenTypeResetPassword = "reset_password"
|
||||
// TokenTypeEmailVerification is a type of a token for verifying email
|
||||
TokenTypeEmailVerification = "email_verification"
|
||||
// TokenTypeEmailPreference is a type of a token for updating email preference
|
||||
TokenTypeEmailPreference = "email_preference"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -39,9 +39,7 @@ func InitSchema(db *gorm.DB) {
|
|||
&Account{},
|
||||
&Book{},
|
||||
&Note{},
|
||||
&Notification{},
|
||||
&Token{},
|
||||
&EmailPreference{},
|
||||
&Session{},
|
||||
); err != nil {
|
||||
panic(err)
|
||||
|
|
@ -56,9 +54,7 @@ func Open(dbPath string) *gorm.DB {
|
|||
panic(errors.Wrapf(err, "creating database directory at %s", dir))
|
||||
}
|
||||
|
||||
// Enable FTS5 extension for full-text search
|
||||
dsn := dbPath + "?_fts5=1"
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "opening database conection"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,21 +88,6 @@ type Token struct {
|
|||
UsedAt *time.Time
|
||||
}
|
||||
|
||||
// Notification is the learning notification sent to the user
|
||||
type Notification struct {
|
||||
Model
|
||||
Type string
|
||||
UserID int `gorm:"index"`
|
||||
}
|
||||
|
||||
// EmailPreference is a preference per user for receiving email communication
|
||||
type EmailPreference struct {
|
||||
Model
|
||||
UserID int `gorm:"index" json:"-"`
|
||||
InactiveReminder bool `json:"inactive_reminder" gorm:"default:false"`
|
||||
ProductUpdate bool `json:"product_update" gorm:"default:true"`
|
||||
}
|
||||
|
||||
// Session represents a user session
|
||||
type Session struct {
|
||||
Model
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
slog "log"
|
||||
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"gorm.io/gorm"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrEmptyDB is an error for missing database connection in the app configuration
|
||||
ErrEmptyDB = errors.New("No database connection was provided")
|
||||
// ErrEmptyClock is an error for missing clock in the app configuration
|
||||
ErrEmptyClock = errors.New("No clock was provided")
|
||||
// ErrEmptyWebURL is an error for missing WebURL content in the app configuration
|
||||
ErrEmptyWebURL = errors.New("No WebURL was provided")
|
||||
// ErrEmptyEmailTemplates is an error for missing EmailTemplates content in the app configuration
|
||||
ErrEmptyEmailTemplates = errors.New("No EmailTemplate store was provided")
|
||||
// ErrEmptyEmailBackend is an error for missing EmailBackend content in the app configuration
|
||||
ErrEmptyEmailBackend = errors.New("No EmailBackend was provided")
|
||||
)
|
||||
|
||||
// Runner is a configuration for job
|
||||
type Runner struct {
|
||||
DB *gorm.DB
|
||||
Clock clock.Clock
|
||||
EmailTmpl mailer.Templates
|
||||
EmailBackend mailer.Backend
|
||||
Config config.Config
|
||||
}
|
||||
|
||||
// NewRunner returns a new runner
|
||||
func NewRunner(db *gorm.DB, c clock.Clock, t mailer.Templates, b mailer.Backend, config config.Config) (Runner, error) {
|
||||
ret := Runner{
|
||||
DB: db,
|
||||
EmailTmpl: t,
|
||||
EmailBackend: b,
|
||||
Clock: c,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
if err := ret.validate(); err != nil {
|
||||
return Runner{}, errors.Wrap(err, "validating runner configuration")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *Runner) validate() error {
|
||||
if r.DB == nil {
|
||||
return ErrEmptyDB
|
||||
}
|
||||
if r.Clock == nil {
|
||||
return ErrEmptyClock
|
||||
}
|
||||
if r.EmailTmpl == nil {
|
||||
return ErrEmptyEmailTemplates
|
||||
}
|
||||
if r.EmailBackend == nil {
|
||||
return ErrEmptyEmailBackend
|
||||
}
|
||||
if r.Config.WebURL == "" {
|
||||
return ErrEmptyWebURL
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scheduleJob(c *cron.Cron, spec string, cmd func()) {
|
||||
s, err := cron.ParseStandard(spec)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "parsing schedule"))
|
||||
}
|
||||
|
||||
c.Schedule(s, cron.FuncJob(cmd))
|
||||
}
|
||||
|
||||
func (r *Runner) schedule(ch chan error) {
|
||||
// Schedule jobs
|
||||
cr := cron.New()
|
||||
cr.Start()
|
||||
|
||||
ch <- nil
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
}
|
||||
|
||||
// Do starts the background tasks in a separate goroutine that runs forever
|
||||
func (r *Runner) Do() error {
|
||||
// validate
|
||||
if err := r.validate(); err != nil {
|
||||
return errors.Wrap(err, "validating job configurations")
|
||||
}
|
||||
|
||||
ch := make(chan error)
|
||||
go r.schedule(ch)
|
||||
if err := <-ch; err != nil {
|
||||
return errors.Wrap(err, "scheduling jobs")
|
||||
}
|
||||
|
||||
slog.Println("Started background tasks")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/dnote/dnote/pkg/assert"
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/dnote/dnote/pkg/server/testutils"
|
||||
"gorm.io/gorm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestNewRunner(t *testing.T) {
|
||||
testCases := []struct {
|
||||
db *gorm.DB
|
||||
clock clock.Clock
|
||||
emailTmpl mailer.Templates
|
||||
emailBackend mailer.Backend
|
||||
webURL string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
db: &gorm.DB{},
|
||||
clock: clock.NewMock(),
|
||||
emailTmpl: mailer.Templates{},
|
||||
emailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
webURL: "http://mock.url",
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
db: nil,
|
||||
clock: clock.NewMock(),
|
||||
emailTmpl: mailer.Templates{},
|
||||
emailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
webURL: "http://mock.url",
|
||||
expectedErr: ErrEmptyDB,
|
||||
},
|
||||
{
|
||||
db: &gorm.DB{},
|
||||
clock: nil,
|
||||
emailTmpl: mailer.Templates{},
|
||||
emailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
webURL: "http://mock.url",
|
||||
expectedErr: ErrEmptyClock,
|
||||
},
|
||||
{
|
||||
db: &gorm.DB{},
|
||||
clock: clock.NewMock(),
|
||||
emailTmpl: nil,
|
||||
emailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
webURL: "http://mock.url",
|
||||
expectedErr: ErrEmptyEmailTemplates,
|
||||
},
|
||||
{
|
||||
db: &gorm.DB{},
|
||||
clock: clock.NewMock(),
|
||||
emailTmpl: mailer.Templates{},
|
||||
emailBackend: nil,
|
||||
webURL: "http://mock.url",
|
||||
expectedErr: ErrEmptyEmailBackend,
|
||||
},
|
||||
{
|
||||
db: &gorm.DB{},
|
||||
clock: clock.NewMock(),
|
||||
emailTmpl: mailer.Templates{},
|
||||
emailBackend: &testutils.MockEmailbackendImplementation{},
|
||||
webURL: "",
|
||||
expectedErr: ErrEmptyWebURL,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test case %d", idx), func(t *testing.T) {
|
||||
|
||||
c := config.Config{
|
||||
WebURL: tc.webURL,
|
||||
}
|
||||
|
||||
_, err := NewRunner(tc.db, tc.clock, tc.emailTmpl, tc.emailBackend, c)
|
||||
|
||||
assert.Equal(t, errors.Cause(err), tc.expectedErr, "error mismatch")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,6 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/dnote/dnote/pkg/clock"
|
||||
"github.com/dnote/dnote/pkg/server/app"
|
||||
|
|
@ -31,7 +30,6 @@ import (
|
|||
"github.com/dnote/dnote/pkg/server/config"
|
||||
"github.com/dnote/dnote/pkg/server/controllers"
|
||||
"github.com/dnote/dnote/pkg/server/database"
|
||||
"github.com/dnote/dnote/pkg/server/job"
|
||||
"github.com/dnote/dnote/pkg/server/mailer"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -40,7 +38,7 @@ import (
|
|||
var port = flag.String("port", "3000", "port to connect to")
|
||||
|
||||
func initDB(c config.Config) *gorm.DB {
|
||||
db := database.Open(c.DB.Path)
|
||||
db := database.Open(c.DBPath)
|
||||
database.InitSchema(db)
|
||||
database.Migrate(db)
|
||||
|
||||
|
|
@ -50,11 +48,11 @@ func initDB(c config.Config) *gorm.DB {
|
|||
func initApp(cfg config.Config) app.App {
|
||||
db := initDB(cfg)
|
||||
|
||||
isProduction := os.Getenv("GO_ENV") == "PRODUCTION"
|
||||
emailBackend, err := mailer.NewDefaultBackend(isProduction)
|
||||
emailBackend, err := mailer.NewDefaultBackend(cfg.IsProd())
|
||||
if err != nil {
|
||||
log.Printf("Email backend not fully configured: %v. Emails will only be logged.", err)
|
||||
emailBackend = &mailer.DefaultBackend{Enabled: false}
|
||||
} else {
|
||||
log.Printf("Email backend configured")
|
||||
}
|
||||
|
||||
return app.App{
|
||||
|
|
@ -67,18 +65,6 @@ func initApp(cfg config.Config) app.App {
|
|||
}
|
||||
}
|
||||
|
||||
func runJob(a app.App) error {
|
||||
runner, err := job.NewRunner(a.DB, a.Clock, a.EmailTemplates, a.EmailBackend, a.Config)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting a job runner")
|
||||
}
|
||||
if err := runner.Do(); err != nil {
|
||||
return errors.Wrap(err, "running job")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startCmd() {
|
||||
cfg := config.Load()
|
||||
cfg.SetAssetBaseURL("/static")
|
||||
|
|
@ -91,13 +77,6 @@ func startCmd() {
|
|||
}
|
||||
}()
|
||||
|
||||
if err := database.Migrate(app.DB); err != nil {
|
||||
panic(errors.Wrap(err, "running migrations"))
|
||||
}
|
||||
if err := runJob(app); err != nil {
|
||||
panic(errors.Wrap(err, "running job"))
|
||||
}
|
||||
|
||||
ctl := controllers.New(&app)
|
||||
rc := controllers.RouteConfig{
|
||||
WebRoutes: controllers.NewWebRoutes(&app, ctl),
|
||||
|
|
@ -119,7 +98,7 @@ func versionCmd() {
|
|||
}
|
||||
|
||||
func rootCmd() {
|
||||
fmt.Printf(`Dnote server - a simple personal knowledge base
|
||||
fmt.Printf(`Dnote server - a simple command line notebook
|
||||
|
||||
Usage:
|
||||
dnote-server [command]
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ func TestTokenAuth(t *testing.T) {
|
|||
user := testutils.SetupUserData(db)
|
||||
tok := database.Token{
|
||||
UserID: user.ID,
|
||||
Type: database.TokenTypeEmailPreference,
|
||||
Type: database.TokenTypeEmailVerification,
|
||||
Value: "xpwFnc0MdllFUePDq9DLeQ==",
|
||||
}
|
||||
testutils.MustExec(t, db.Save(&tok), "preparing token")
|
||||
|
|
@ -193,7 +193,7 @@ func TestTokenAuth(t *testing.T) {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(TokenAuth(db, handler, database.TokenTypeEmailPreference, nil))
|
||||
server := httptest.NewServer(TokenAuth(db, handler, database.TokenTypeEmailVerification, nil))
|
||||
defer server.Close()
|
||||
|
||||
t.Run("with token", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ func Limit(next http.Handler) http.HandlerFunc {
|
|||
func ApplyLimit(h http.HandlerFunc, rateLimit bool) http.Handler {
|
||||
ret := h
|
||||
|
||||
if rateLimit && os.Getenv("GO_ENV") != "TEST" {
|
||||
if rateLimit && os.Getenv("APP_ENV") != "TEST" {
|
||||
ret = Limit(ret)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import (
|
|||
// the environment variable configuration and initalizes a new schema
|
||||
func InitTestDB() *gorm.DB {
|
||||
c := config.Load()
|
||||
return database.Open(c.DB.Path)
|
||||
return database.Open(c.DBPath)
|
||||
}
|
||||
|
||||
// InitMemoryDB creates an in-memory SQLite database with the schema initialized
|
||||
|
|
@ -76,15 +76,9 @@ func ClearData(db *gorm.DB) {
|
|||
if err := db.Where("1 = 1").Delete(&database.Book{}).Error; err != nil {
|
||||
panic(errors.Wrap(err, "Failed to clear books"))
|
||||
}
|
||||
if err := db.Where("1 = 1").Delete(&database.Notification{}).Error; err != nil {
|
||||
panic(errors.Wrap(err, "Failed to clear notifications"))
|
||||
}
|
||||
if err := db.Where("1 = 1").Delete(&database.Token{}).Error; err != nil {
|
||||
panic(errors.Wrap(err, "Failed to clear tokens"))
|
||||
}
|
||||
if err := db.Where("1 = 1").Delete(&database.EmailPreference{}).Error; err != nil {
|
||||
panic(errors.Wrap(err, "Failed to clear email preferences"))
|
||||
}
|
||||
if err := db.Where("1 = 1").Delete(&database.Session{}).Error; err != nil {
|
||||
panic(errors.Wrap(err, "Failed to clear sessions"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func TestCreate(t *testing.T) {
|
|||
kind string
|
||||
}{
|
||||
{
|
||||
kind: database.TokenTypeEmailPreference,
|
||||
kind: database.TokenTypeEmailVerification,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue