diff --git a/README.md b/README.md index 5ec4f51..2e751d7 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,8 @@ gobsidian/ To build a standalone binary: ```bash -go build -o gobsidian cmd/main.go +go mod tidy +go build -o gobsidian ./cmd ``` ## Image storing trying to follow Obsidian settings Image storing modes: diff --git a/internal/config/config.go b/internal/config/config.go index 9eff153..2f24a15 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -41,9 +41,9 @@ type Config struct { DBPath string // Auth settings - RequireAdminActivation bool + RequireAdminActivation bool RequireEmailConfirmation bool - MFAEnabledByDefault bool + MFAEnabledByDefault bool // Email (SMTP) settings SMTPHost string @@ -54,11 +54,11 @@ type Config struct { SMTPUseTLS bool // Security settings (failed-login thresholds and auto-ban config) - PwdFailuresThreshold int - MFAFailuresThreshold int - FailuresWindowMinutes int - AutoBanDurationHours int - AutoBanPermanent bool + PwdFailuresThreshold int + MFAFailuresThreshold int + FailuresWindowMinutes int + AutoBanDurationHours int + AutoBanPermanent bool } var defaultConfig = map[string]map[string]string{ @@ -91,9 +91,9 @@ var defaultConfig = map[string]map[string]string{ "PATH": "data/gobsidian.db", }, "AUTH": { - "REQUIRE_ADMIN_ACTIVATION": "true", + "REQUIRE_ADMIN_ACTIVATION": "true", "REQUIRE_EMAIL_CONFIRMATION": "true", - "MFA_ENABLED_BY_DEFAULT": "false", + "MFA_ENABLED_BY_DEFAULT": "false", }, "EMAIL": { "SMTP_HOST": "", @@ -112,8 +112,20 @@ var defaultConfig = map[string]map[string]string{ }, } +// exeDir returns the directory containing the running executable. +func exeDir() string { + exe, err := os.Executable() + if err != nil { + wd, _ := os.Getwd() + return wd + } + return filepath.Dir(exe) +} + func Load() (*Config, error) { - configPath := "settings.ini" + baseDir := exeDir() + // settings.ini lives next to the executable + configPath := filepath.Join(baseDir, "settings.ini") // Ensure config file exists if err := ensureConfigFile(configPath); err != nil { @@ -167,15 +179,23 @@ func Load() (*Config, error) { config.ImageStoragePath = notesSection.Key("IMAGE_STORAGE_PATH").String() config.ImageSubfolderName = notesSection.Key("IMAGE_SUBFOLDER_NAME").String() - // Convert relative paths to absolute + // Convert relative paths to be next to the executable if !filepath.IsAbs(config.NotesDir) { - wd, _ := os.Getwd() - config.NotesDir = filepath.Join(wd, config.NotesDir) + config.NotesDir = filepath.Join(baseDir, config.NotesDir) } if !filepath.IsAbs(config.ImageStoragePath) && config.ImageStorageMode == 2 { - wd, _ := os.Getwd() - config.ImageStoragePath = filepath.Join(wd, config.ImageStoragePath) + config.ImageStoragePath = filepath.Join(baseDir, config.ImageStoragePath) + } + + // Ensure these directories exist + if err := os.MkdirAll(config.NotesDir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create notes directory: %w", err) + } + if config.ImageStorageMode == 2 { + if err := os.MkdirAll(config.ImageStoragePath, 0o755); err != nil { + return nil, fmt.Errorf("failed to create image storage directory: %w", err) + } } // Load DATABASE section @@ -184,8 +204,7 @@ func Load() (*Config, error) { config.DBPath = dbSection.Key("PATH").String() if config.DBType == "sqlite" { if !filepath.IsAbs(config.DBPath) { - wd, _ := os.Getwd() - config.DBPath = filepath.Join(wd, config.DBPath) + config.DBPath = filepath.Join(baseDir, config.DBPath) } // ensure parent dir exists if err := os.MkdirAll(filepath.Dir(config.DBPath), 0o755); err != nil { @@ -222,6 +241,10 @@ func Load() (*Config, error) { func ensureConfigFile(configPath string) error { // Check if file exists if _, err := os.Stat(configPath); os.IsNotExist(err) { + // ensure parent dir exists + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + return err + } return createDefaultConfigFile(configPath) } @@ -290,7 +313,7 @@ func parseCommaSeparated(value string) []string { } func (c *Config) SaveSetting(section, key, value string) error { - configPath := "settings.ini" + configPath := filepath.Join(exeDir(), "settings.ini") cfg, err := ini.Load(configPath) if err != nil { return err diff --git a/internal/server/server.go b/internal/server/server.go index 0410871..fdab476 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,12 +3,15 @@ package server import ( "fmt" "html/template" + "io/fs" + "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/gorilla/sessions" + webassets "gobsidian/web" "gobsidian/internal/auth" "gobsidian/internal/config" "gobsidian/internal/handlers" @@ -173,8 +176,14 @@ func (s *Server) setupRoutes() { } func (s *Server) setupStaticFiles() { - s.router.Static("/static", "./web/static") - s.router.StaticFile("/favicon.ico", "./web/static/favicon.ico") + // Serve /static from embedded web/static + sub, err := fs.Sub(webassets.StaticFS, "static") + if err != nil { + panic(err) + } + s.router.StaticFS("/static", http.FS(sub)) + // Favicon from same sub FS + s.router.StaticFileFS("/favicon.ico", "favicon.ico", http.FS(sub)) } func (s *Server) setupTemplates() { @@ -244,8 +253,12 @@ func (s *Server) setupTemplates() { }, } - // Load templates - make sure base.html is loaded with all the other templates - templates := template.Must(template.New("").Funcs(funcMap).ParseGlob("web/templates/*.html")) + // Load templates from embedded FS + tplFS, err := fs.Sub(webassets.TemplatesFS, "templates") + if err != nil { + panic(err) + } + templates := template.Must(template.New("").Funcs(funcMap).ParseFS(tplFS, "*.html")) s.router.SetHTMLTemplate(templates) fmt.Printf("DEBUG: Templates loaded successfully\n") @@ -253,10 +266,10 @@ func (s *Server) setupTemplates() { // startAccessLogCleanup deletes access logs older than 7 days once at startup and then daily. func (s *Server) startAccessLogCleanup() { - // initial cleanup - _, _ = s.auth.DB.Exec(`DELETE FROM access_logs WHERE created_at < DATETIME('now', '-7 days')`) - ticker := time.NewTicker(24 * time.Hour) - for range ticker.C { - _, _ = s.auth.DB.Exec(`DELETE FROM access_logs WHERE created_at < DATETIME('now', '-7 days')`) - } + // initial cleanup + _, _ = s.auth.DB.Exec(`DELETE FROM access_logs WHERE created_at < DATETIME('now', '-7 days')`) + ticker := time.NewTicker(24 * time.Hour) + for range ticker.C { + _, _ = s.auth.DB.Exec(`DELETE FROM access_logs WHERE created_at < DATETIME('now', '-7 days')`) + } } diff --git a/web/assets_embed.go b/web/assets_embed.go new file mode 100644 index 0000000..6cfa850 --- /dev/null +++ b/web/assets_embed.go @@ -0,0 +1,11 @@ +package webassets + +import "embed" + +// TemplatesFS embeds all HTML templates under web/templates. +//go:embed templates/*.html +var TemplatesFS embed.FS + +// StaticFS embeds all static assets under web/static. +//go:embed static/* +var StaticFS embed.FS