added ServeEvent.InstallerFunc field
This commit is contained in:
		
							parent
							
								
									0155e9333f
								
							
						
					
					
						commit
						26cb1cef37
					
				| 
						 | 
					@ -21,6 +21,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Added extra validators for the collection field `int64` options (e.g. `FileField.MaxSize`) restricting them to the max safe JSON number (2^53-1).
 | 
					- Added extra validators for the collection field `int64` options (e.g. `FileField.MaxSize`) restricting them to the max safe JSON number (2^53-1).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added option to unset/overwrite the default PocketBase superuser installer using `ServeEvent.InstallerFunc`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Invalidate all record tokens when the auth record email is changed programmatically or by a superuser ([#5964](https://github.com/pocketbase/pocketbase/issues/5964)).
 | 
					- Invalidate all record tokens when the auth record email is changed programmatically or by a superuser ([#5964](https://github.com/pocketbase/pocketbase/issues/5964)).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Eagerly interrupt waiting for the email alert send in case it takes longer than 15s.
 | 
					- Eagerly interrupt waiting for the email alert send in case it takes longer than 15s.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,37 +5,31 @@ import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
					 | 
				
			||||||
	"runtime"
 | 
					 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/fatih/color"
 | 
						"github.com/fatih/color"
 | 
				
			||||||
	"github.com/go-ozzo/ozzo-validation/v4/is"
 | 
					 | 
				
			||||||
	"github.com/pocketbase/dbx"
 | 
						"github.com/pocketbase/dbx"
 | 
				
			||||||
	"github.com/pocketbase/pocketbase/core"
 | 
						"github.com/pocketbase/pocketbase/core"
 | 
				
			||||||
	"github.com/pocketbase/pocketbase/tools/security"
 | 
						"github.com/pocketbase/pocketbase/tools/osutils"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @todo consider combining with the installer specific hooks after refactoring cmd
 | 
					// DefaultInstallerFunc is the default PocketBase installer function.
 | 
				
			||||||
func loadInstaller(app core.App, dashboardURL string) error {
 | 
					//
 | 
				
			||||||
	if !needInstallerSuperuser(app) {
 | 
					// It will attempt to open a link in the browser (with a short-lived auth
 | 
				
			||||||
		return nil
 | 
					// token for the systemSuperuser) to the installer UI so that users can
 | 
				
			||||||
	}
 | 
					// create their own custom superuser record.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
	installerRecord, err := findOrCreateInstallerSuperuser(app)
 | 
					// See https://github.com/pocketbase/pocketbase/discussions/5814.
 | 
				
			||||||
	if err != nil {
 | 
					func DefaultInstallerFunc(app core.App, systemSuperuser *core.Record, baseURL string) error {
 | 
				
			||||||
		return err
 | 
						token, err := systemSuperuser.NewStaticAuthToken(30 * time.Minute)
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	token, err := installerRecord.NewStaticAuthToken(30 * time.Minute)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// launch url (ignore errors and always print a help text as fallback)
 | 
						// launch url (ignore errors and always print a help text as fallback)
 | 
				
			||||||
	url := fmt.Sprintf("%s/#/pbinstal/%s", strings.TrimRight(dashboardURL, "/"), token)
 | 
						url := fmt.Sprintf("%s/_/#/pbinstal/%s", strings.TrimRight(baseURL, "/"), token)
 | 
				
			||||||
	_ = launchURL(url)
 | 
						_ = osutils.LaunchURL(url)
 | 
				
			||||||
	color.Magenta("\n(!) Launch the URL below in the browser if it hasn't been open already to create your first superuser account:")
 | 
						color.Magenta("\n(!) Launch the URL below in the browser if it hasn't been open already to create your first superuser account:")
 | 
				
			||||||
	color.New(color.Bold).Add(color.FgCyan).Println(url)
 | 
						color.New(color.Bold).Add(color.FgCyan).Println(url)
 | 
				
			||||||
	color.New(color.FgHiBlack, color.Italic).Printf("(you can also create your first superuser by running: %s superuser upsert EMAIL PASS)\n\n", os.Args[0])
 | 
						color.New(color.FgHiBlack, color.Italic).Printf("(you can also create your first superuser by running: %s superuser upsert EMAIL PASS)\n\n", os.Args[0])
 | 
				
			||||||
| 
						 | 
					@ -43,6 +37,23 @@ func loadInstaller(app core.App, dashboardURL string) error {
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func loadInstaller(
 | 
				
			||||||
 | 
						app core.App,
 | 
				
			||||||
 | 
						baseURL string,
 | 
				
			||||||
 | 
						installerFunc func(app core.App, systemSuperuser *core.Record, baseURL string) error,
 | 
				
			||||||
 | 
					) error {
 | 
				
			||||||
 | 
						if installerFunc == nil || !needInstallerSuperuser(app) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						superuser, err := findOrCreateInstallerSuperuser(app)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return installerFunc(app, superuser, baseURL)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func needInstallerSuperuser(app core.App) bool {
 | 
					func needInstallerSuperuser(app core.App) bool {
 | 
				
			||||||
	total, err := app.CountRecords(core.CollectionNameSuperusers, dbx.Not(dbx.HashExp{
 | 
						total, err := app.CountRecords(core.CollectionNameSuperusers, dbx.Not(dbx.HashExp{
 | 
				
			||||||
		"email": core.DefaultInstallerEmail,
 | 
							"email": core.DefaultInstallerEmail,
 | 
				
			||||||
| 
						 | 
					@ -65,7 +76,7 @@ func findOrCreateInstallerSuperuser(app core.App) (*core.Record, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		record = core.NewRecord(col)
 | 
							record = core.NewRecord(col)
 | 
				
			||||||
		record.SetEmail(core.DefaultInstallerEmail)
 | 
							record.SetEmail(core.DefaultInstallerEmail)
 | 
				
			||||||
		record.SetPassword(security.RandomString(30))
 | 
							record.SetRandomPassword()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = app.Save(record)
 | 
							err = app.Save(record)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
| 
						 | 
					@ -75,20 +86,3 @@ func findOrCreateInstallerSuperuser(app core.App) (*core.Record, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return record, nil
 | 
						return record, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func launchURL(url string) error {
 | 
					 | 
				
			||||||
	if err := is.URL.Validate(url); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch runtime.GOOS {
 | 
					 | 
				
			||||||
	case "darwin":
 | 
					 | 
				
			||||||
		return exec.Command("open", url).Start()
 | 
					 | 
				
			||||||
	case "windows":
 | 
					 | 
				
			||||||
		// not sure if this is the best command but seems to be the most reliable based on the comments in
 | 
					 | 
				
			||||||
		// https://stackoverflow.com/questions/3739327/launching-a-website-via-the-windows-commandline#answer-49115945
 | 
					 | 
				
			||||||
		return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
 | 
					 | 
				
			||||||
	default: // linux, freebsd, etc.
 | 
					 | 
				
			||||||
		return exec.Command("xdg-open", url).Start()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,6 @@ import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"crypto/tls"
 | 
						"crypto/tls"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
| 
						 | 
					@ -17,6 +16,7 @@ import (
 | 
				
			||||||
	"github.com/pocketbase/pocketbase/core"
 | 
						"github.com/pocketbase/pocketbase/core"
 | 
				
			||||||
	"github.com/pocketbase/pocketbase/tools/hook"
 | 
						"github.com/pocketbase/pocketbase/tools/hook"
 | 
				
			||||||
	"github.com/pocketbase/pocketbase/tools/list"
 | 
						"github.com/pocketbase/pocketbase/tools/list"
 | 
				
			||||||
 | 
						"github.com/pocketbase/pocketbase/tools/routine"
 | 
				
			||||||
	"github.com/pocketbase/pocketbase/ui"
 | 
						"github.com/pocketbase/pocketbase/ui"
 | 
				
			||||||
	"golang.org/x/crypto/acme"
 | 
						"golang.org/x/crypto/acme"
 | 
				
			||||||
	"golang.org/x/crypto/acme/autocert"
 | 
						"golang.org/x/crypto/acme/autocert"
 | 
				
			||||||
| 
						 | 
					@ -158,6 +158,7 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
	serveEvent.Router = pbRouter
 | 
						serveEvent.Router = pbRouter
 | 
				
			||||||
	serveEvent.Server = server
 | 
						serveEvent.Server = server
 | 
				
			||||||
	serveEvent.CertManager = certManager
 | 
						serveEvent.CertManager = certManager
 | 
				
			||||||
 | 
						serveEvent.InstallerFunc = DefaultInstallerFunc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var listener net.Listener
 | 
						var listener net.Listener
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -204,6 +205,8 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
	// ---------------------------------------------------------------
 | 
						// ---------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var baseURL string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// trigger the OnServe hook and start the tcp listener
 | 
						// trigger the OnServe hook and start the tcp listener
 | 
				
			||||||
	serveHookErr := app.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {
 | 
						serveHookErr := app.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {
 | 
				
			||||||
		handler, err := e.Router.BuildMux()
 | 
							handler, err := e.Router.BuildMux()
 | 
				
			||||||
| 
						 | 
					@ -213,10 +216,20 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		e.Server.Handler = handler
 | 
							e.Server.Handler = handler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		addr := e.Server.Addr
 | 
							if config.HttpsAddr == "" {
 | 
				
			||||||
 | 
								baseURL = "http://" + serverAddrToHost(serveEvent.Server.Addr)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								baseURL = "https://"
 | 
				
			||||||
 | 
								if len(config.CertificateDomains) > 0 {
 | 
				
			||||||
 | 
									baseURL += config.CertificateDomains[0]
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									baseURL += serverAddrToHost(serveEvent.Server.Addr)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// fallback similar to the std Server.ListenAndServe/ListenAndServeTLS
 | 
							addr := e.Server.Addr
 | 
				
			||||||
		if addr == "" {
 | 
							if addr == "" {
 | 
				
			||||||
 | 
								// fallback similar to the std Server.ListenAndServe/ListenAndServeTLS
 | 
				
			||||||
			if config.HttpsAddr != "" {
 | 
								if config.HttpsAddr != "" {
 | 
				
			||||||
				addr = ":https"
 | 
									addr = ":https"
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
| 
						 | 
					@ -224,11 +237,22 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		var lnErr error
 | 
							listener, err = net.Listen("tcp", addr)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		listener, lnErr = net.Listen("tcp", addr)
 | 
							if e.InstallerFunc != nil {
 | 
				
			||||||
 | 
								app := e.App
 | 
				
			||||||
 | 
								installerFunc := e.InstallerFunc
 | 
				
			||||||
 | 
								routine.FireAndForget(func() {
 | 
				
			||||||
 | 
									if err := loadInstaller(app, baseURL, installerFunc); err != nil {
 | 
				
			||||||
 | 
										app.Logger().Warn("Failed to initialize installer", "error", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return lnErr
 | 
							return nil
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	if serveHookErr != nil {
 | 
						if serveHookErr != nil {
 | 
				
			||||||
		return serveHookErr
 | 
							return serveHookErr
 | 
				
			||||||
| 
						 | 
					@ -238,17 +262,6 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
		return errors.New("The OnServe finalizer wasn't invoked. Did you forget to call the ServeEvent.Next() method?")
 | 
							return errors.New("The OnServe finalizer wasn't invoked. Did you forget to call the ServeEvent.Next() method?")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	schema := "http"
 | 
					 | 
				
			||||||
	addr := server.Addr
 | 
					 | 
				
			||||||
	if config.HttpsAddr != "" {
 | 
					 | 
				
			||||||
		schema = "https"
 | 
					 | 
				
			||||||
		if len(config.CertificateDomains) > 0 {
 | 
					 | 
				
			||||||
			addr = config.CertificateDomains[0]
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	baseURL := fmt.Sprintf("%s://%s", schema, addr)
 | 
					 | 
				
			||||||
	dashboardURL := fmt.Sprintf("%s/_", baseURL)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if config.ShowStartBanner {
 | 
						if config.ShowStartBanner {
 | 
				
			||||||
		date := new(strings.Builder)
 | 
							date := new(strings.Builder)
 | 
				
			||||||
		log.New(date, "", log.LstdFlags).Print()
 | 
							log.New(date, "", log.LstdFlags).Print()
 | 
				
			||||||
| 
						 | 
					@ -262,16 +275,9 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		regular := color.New()
 | 
							regular := color.New()
 | 
				
			||||||
		regular.Printf("├─ REST API:  %s\n", color.CyanString("%s/api/", baseURL))
 | 
							regular.Printf("├─ REST API:  %s\n", color.CyanString("%s/api/", baseURL))
 | 
				
			||||||
		regular.Printf("└─ Dashboard: %s\n", color.CyanString("%s/", dashboardURL))
 | 
							regular.Printf("└─ Dashboard: %s\n", color.CyanString("%s/_/", baseURL))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func() {
 | 
					 | 
				
			||||||
		installerErr := loadInstaller(app, dashboardURL)
 | 
					 | 
				
			||||||
		if installerErr != nil {
 | 
					 | 
				
			||||||
			app.Logger().Warn("Failed to initialize installer", "error", installerErr)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var serveErr error
 | 
						var serveErr error
 | 
				
			||||||
	if config.HttpsAddr != "" {
 | 
						if config.HttpsAddr != "" {
 | 
				
			||||||
		if config.HttpAddr != "" {
 | 
							if config.HttpAddr != "" {
 | 
				
			||||||
| 
						 | 
					@ -280,10 +286,10 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// start HTTPS server
 | 
							// start HTTPS server
 | 
				
			||||||
		serveErr = server.ServeTLS(listener, "", "")
 | 
							serveErr = serveEvent.Server.ServeTLS(listener, "", "")
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// OR start HTTP server
 | 
							// OR start HTTP server
 | 
				
			||||||
		serveErr = server.Serve(listener)
 | 
							serveErr = serveEvent.Server.Serve(listener)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
 | 
						if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
 | 
				
			||||||
		return serveErr
 | 
							return serveErr
 | 
				
			||||||
| 
						 | 
					@ -292,6 +298,14 @@ func Serve(app core.App, config ServeConfig) error {
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// serverAddrToHost loosely converts http.Server.Addr string into a host to print.
 | 
				
			||||||
 | 
					func serverAddrToHost(addr string) string {
 | 
				
			||||||
 | 
						if addr == "" || strings.HasSuffix(addr, ":http") || strings.HasSuffix(addr, ":https") {
 | 
				
			||||||
 | 
							return "127.0.0.1"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return addr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type serverErrorLogWriter struct {
 | 
					type serverErrorLogWriter struct {
 | 
				
			||||||
	app core.App
 | 
						app core.App
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,6 +103,22 @@ type ServeEvent struct {
 | 
				
			||||||
	Router      *router.Router[*RequestEvent]
 | 
						Router      *router.Router[*RequestEvent]
 | 
				
			||||||
	Server      *http.Server
 | 
						Server      *http.Server
 | 
				
			||||||
	CertManager *autocert.Manager
 | 
						CertManager *autocert.Manager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// InstallerFunc is the "installer" function that is called after
 | 
				
			||||||
 | 
						// successfull server tcp bind but only if there is no explicit
 | 
				
			||||||
 | 
						// superuser record created yet.
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// It runs in a separate goroutine and its default value is [apis.DefaultInstallerFunc].
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// It receives a system superuser record as argument that you can use to generate
 | 
				
			||||||
 | 
						// a short-lived auth token (e.g. systemSuperuser.NewStaticAuthToken(30 * time.Minute))
 | 
				
			||||||
 | 
						// and concatenate it as query param for your installer page
 | 
				
			||||||
 | 
						// (if you are using the client-side SDKs, you can then load the
 | 
				
			||||||
 | 
						// token with pb.authStore.save(token) and perform any Web API request
 | 
				
			||||||
 | 
						// e.g. creating a new superuser).
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Set it to nil if you want to skip the installer.
 | 
				
			||||||
 | 
						InstallerFunc func(app App, systemSuperuser *Record, baseURL string) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// -------------------------------------------------------------------
 | 
					// -------------------------------------------------------------------
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					package osutils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/go-ozzo/ozzo-validation/v4/is"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// LaunchURL attempts to open the provided url in the user's default browser.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// It is platform dependent and it uses:
 | 
				
			||||||
 | 
					//   - "open" on macOS
 | 
				
			||||||
 | 
					//   - "rundll32" on Windows
 | 
				
			||||||
 | 
					//   - "xdg-open" on everything else (Linux, FreeBSD, etc.)
 | 
				
			||||||
 | 
					func LaunchURL(url string) error {
 | 
				
			||||||
 | 
						if err := is.URL.Validate(url); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch runtime.GOOS {
 | 
				
			||||||
 | 
						case "darwin":
 | 
				
			||||||
 | 
							return exec.Command("open", url).Start()
 | 
				
			||||||
 | 
						case "windows":
 | 
				
			||||||
 | 
							// not sure if this is the best command but seems to be the most reliable based on the comments in
 | 
				
			||||||
 | 
							// https://stackoverflow.com/questions/3739327/launching-a-website-via-the-windows-commandline#answer-49115945
 | 
				
			||||||
 | 
							return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return exec.Command("xdg-open", url).Start()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -116,4 +116,3 @@ function refreshProtectedFilesCollectionsCache() {
 | 
				
			||||||
        return cache;
 | 
					        return cache;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue