Go backend (#1555)
* Replaces old Deno server with a Go implementation * Restructured the code base accordingly
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/silverbulletmd/silverbullet/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func buildConfig(bundledFiles fs.FS, args []string) *server.ServerConfig {
|
||||
serverConfig := &server.ServerConfig{
|
||||
BindHost: "127.0.0.1",
|
||||
Port: 3000,
|
||||
}
|
||||
|
||||
rootSpaceConfig := &server.SpaceConfig{
|
||||
IndexPage: "index",
|
||||
SpaceName: "SilverBullet",
|
||||
SpaceDescription: "Powerful and programmable note taking app",
|
||||
}
|
||||
|
||||
// For now just point every request to the rootSpaceConfig
|
||||
serverConfig.SpaceConfigResolver = func(r *http.Request) *server.SpaceConfig {
|
||||
return rootSpaceConfig
|
||||
}
|
||||
|
||||
if os.Getenv("SB_HOSTNAME") != "" {
|
||||
serverConfig.BindHost = os.Getenv("SB_HOSTNAME")
|
||||
}
|
||||
|
||||
if os.Getenv("SB_PORT") != "" {
|
||||
// Parse integer from string
|
||||
port, err := strconv.ParseInt(os.Getenv("SB_PORT"), 10, 32)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse numeric port from SB_PORT: %v", err)
|
||||
}
|
||||
serverConfig.Port = int(port)
|
||||
}
|
||||
|
||||
if os.Getenv("SB_INDEX_PAGE") != "" {
|
||||
rootSpaceConfig.IndexPage = os.Getenv("SB_INDEX_PAGE")
|
||||
}
|
||||
if len(args) > 0 {
|
||||
rootSpaceConfig.SpaceFolderPath = args[0]
|
||||
}
|
||||
if os.Getenv("SB_FOLDER") != "" {
|
||||
rootSpaceConfig.SpaceFolderPath = os.Getenv("SB_FOLDER")
|
||||
}
|
||||
|
||||
if rootSpaceConfig.SpaceFolderPath == "" {
|
||||
log.Fatal("No folder specified. Please pass a folder as an argument or set SB_FOLDER environment variable.")
|
||||
}
|
||||
|
||||
var spacePrimitives server.SpacePrimitives
|
||||
spacePrimitives, err := server.NewDiskSpacePrimitives(rootSpaceConfig.SpaceFolderPath, os.Getenv("SB_GIT_IGNORE"))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
serverConfig.EnableHTTPLogging = os.Getenv("SB_HTTP_LOGGING") != ""
|
||||
|
||||
if os.Getenv("SB_USER") != "" {
|
||||
pieces := strings.Split(os.Getenv("SB_USER"), ":")
|
||||
|
||||
rootSpaceConfig.Auth = &server.AuthOptions{
|
||||
User: pieces[0],
|
||||
Pass: pieces[1],
|
||||
AuthToken: os.Getenv("SB_AUTH_TOKEN"),
|
||||
LockoutLimit: 10,
|
||||
LockoutTime: 60,
|
||||
}
|
||||
|
||||
if os.Getenv("SB_LOCKOUT_LIMIT") != "" {
|
||||
rootSpaceConfig.Auth.LockoutLimit, err = strconv.Atoi(os.Getenv("SB_LOCKOUT_LIMIT"))
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse SB_LOCKOUT_LIMIT as number: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if os.Getenv("SB_LOCKOUT_TIME") != "" {
|
||||
rootSpaceConfig.Auth.LockoutTime, err = strconv.Atoi(os.Getenv("SB_LOCKOUT_TIME"))
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse SB_LOCKOUT_TIME as number: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("User authentication enabled for user \"%s\" with lockout limit %d and lockout time %ds",
|
||||
pieces[0], rootSpaceConfig.Auth.LockoutLimit, rootSpaceConfig.Auth.LockoutTime)
|
||||
}
|
||||
|
||||
rootSpaceConfig.ReadOnlyMode = os.Getenv("SB_READ_ONLY") != ""
|
||||
|
||||
if os.Getenv("SB_NAME") != "" {
|
||||
rootSpaceConfig.SpaceName = os.Getenv("SB_NAME")
|
||||
}
|
||||
|
||||
if os.Getenv("SB_DESCRIPTION") != "" {
|
||||
rootSpaceConfig.SpaceDescription = os.Getenv("SB_DESCRIPTION")
|
||||
}
|
||||
|
||||
if rootSpaceConfig.ReadOnlyMode {
|
||||
log.Println("Starting in read-only mode.")
|
||||
}
|
||||
|
||||
rootSpaceConfig.GitIgnore = os.Getenv("SB_GIT_IGNORE")
|
||||
|
||||
if os.Getenv("SB_SPACE_IGNORE") != "" {
|
||||
log.Printf("Ignoring files matching: %s", os.Getenv("SB_SPACE_IGNORE"))
|
||||
}
|
||||
|
||||
if os.Getenv("SB_URL_PREFIX") != "" {
|
||||
hostUrlPrefix := os.Getenv("SB_URL_PREFIX")
|
||||
// Normalize the host URL prefix (if set)
|
||||
if !strings.HasPrefix(hostUrlPrefix, "/") {
|
||||
hostUrlPrefix = "/" + hostUrlPrefix
|
||||
}
|
||||
hostUrlPrefix = strings.TrimRight(hostUrlPrefix, "/")
|
||||
|
||||
if hostUrlPrefix != "" {
|
||||
log.Printf("Host URL Prefix: %s", hostUrlPrefix)
|
||||
rootSpaceConfig.HostURLPrefix = hostUrlPrefix
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the last modified time from the main binary, best effort
|
||||
bundlePathDate := time.Now()
|
||||
if executablePath, err := os.Executable(); err == nil {
|
||||
if stat, err := os.Stat(executablePath); err == nil {
|
||||
bundlePathDate = stat.ModTime()
|
||||
}
|
||||
}
|
||||
|
||||
serverConfig.ClientBundle = server.NewReadOnlyFallthroughSpacePrimitives(bundledFiles, "dist_client_bundle", bundlePathDate, nil)
|
||||
rootSpaceConfig.SpacePrimitives = server.NewReadOnlyFallthroughSpacePrimitives(bundledFiles, "dist_base_fs_bundle", bundlePathDate, spacePrimitives)
|
||||
|
||||
log.Printf("Starting SilverBullet binding to %s:%d", serverConfig.BindHost, serverConfig.Port)
|
||||
if serverConfig.BindHost == "127.0.0.1" {
|
||||
log.Println("SilverBullet will only be available locally, to allow outside connections, pass -L0.0.0.0 as a flag, and put a TLS terminator on top.")
|
||||
}
|
||||
|
||||
// Initialize shell backend
|
||||
backendConfig := os.Getenv("SB_SHELL_BACKEND")
|
||||
if backendConfig == "" && !rootSpaceConfig.ReadOnlyMode {
|
||||
localShell := server.NewLocalShell(rootSpaceConfig.SpaceFolderPath, os.Getenv("SB_SHELL_WHITELIST"))
|
||||
rootSpaceConfig.ShellBackend = localShell
|
||||
if localShell.AllowAllCmds {
|
||||
log.Println("Local shell command execution enabled for ALL commands.")
|
||||
} else {
|
||||
log.Printf("Local shell command execution enabled for: %v", localShell.CmdWhiteList)
|
||||
}
|
||||
} else {
|
||||
log.Println("Shell running disabled.")
|
||||
rootSpaceConfig.ShellBackend = server.NewNotSupportedShell()
|
||||
}
|
||||
|
||||
return serverConfig
|
||||
}
|
||||
|
||||
func ServerCommand(bundledFiles fs.FS) *cobra.Command {
|
||||
var hostname string
|
||||
var port int
|
||||
var c = &cobra.Command{
|
||||
Use: "silverbullet",
|
||||
Short: "Run the Silverbullet server",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
serverConfig := buildConfig(bundledFiles, args)
|
||||
if port != 0 {
|
||||
serverConfig.Port = port
|
||||
}
|
||||
if hostname != "" {
|
||||
serverConfig.BindHost = hostname
|
||||
}
|
||||
if err := server.RunServer(serverConfig); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
c.Flags().StringVarP(&hostname, "hostname", "L", "127.0.0.1", "Host or address to listen to")
|
||||
c.Flags().IntVarP(&port, "port", "p", 3000, "Port to listen to")
|
||||
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// We can add more now that this will be Go compiled (we can do ARM32 too, for instance)
|
||||
var archMappings map[string]string = map[string]string{
|
||||
"amd64": "x86_64",
|
||||
"arm64": "aarch64",
|
||||
}
|
||||
|
||||
func UpgradeCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Now going to attempt an upgrade...")
|
||||
if err := upgrade("https://github.com/silverbulletmd/silverbullet/releases/latest/download"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func UpgradeEdgeCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "upgrade-edge",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Now going to attempt an upgrade...")
|
||||
if err := upgrade("https://github.com/silverbulletmd/silverbullet/releases/download/edge"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func upgrade(urlPrefix string) error {
|
||||
// Get the directory of the current executable
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
installDir := filepath.Dir(execPath)
|
||||
|
||||
fmt.Println("Install dir:", installDir)
|
||||
|
||||
// Create temporary directory
|
||||
tmpDir, err := os.MkdirTemp("", "silverbullet-upgrade")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Construct download URL
|
||||
zipURL := fmt.Sprintf("%s/silverbullet-server-%s-%s.zip", urlPrefix, runtime.GOOS, archMappings[runtime.GOARCH])
|
||||
zipPath := filepath.Join(tmpDir, "silverbullet.zip")
|
||||
|
||||
fmt.Println("Downloading from", zipURL)
|
||||
|
||||
// Download the zip file
|
||||
resp, err := http.Get(zipURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download failed with status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Create the zip file
|
||||
zipFile, err := os.Create(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create zip file: %w", err)
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
// Copy response body to file
|
||||
_, err = io.Copy(zipFile, resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save zip file: %w", err)
|
||||
}
|
||||
zipFile.Close()
|
||||
|
||||
fmt.Printf("Now going to replace the existing silverbullet binary in %s\n", installDir)
|
||||
|
||||
// Extract the zip file
|
||||
err = extractZip(zipPath, installDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract zip: %w", err)
|
||||
}
|
||||
|
||||
// Set executable permissions on the binary
|
||||
binaryPath := filepath.Join(installDir, "silverbullet")
|
||||
if runtime.GOOS == "windows" {
|
||||
binaryPath += ".exe"
|
||||
}
|
||||
err = os.Chmod(binaryPath, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set executable permissions: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("And done! Restart your server to get the latest and greatest!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractZip(src, dest string) error {
|
||||
reader, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
for _, file := range reader.File {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
path := filepath.Join(dest, file.Name)
|
||||
|
||||
// Check for directory
|
||||
if file.FileInfo().IsDir() {
|
||||
os.MkdirAll(path, file.FileInfo().Mode())
|
||||
continue
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the file
|
||||
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/silverbulletmd/silverbullet/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func VersionCommand(versionFileText string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version, err := server.ParseVersionFromTypeScript(versionFileText)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(version)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user