diff --git a/README.md b/README.md index 62d9d66..0cbff1a 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,95 @@  

Write Freely


Latest release Go Report Card Build status #writefreely on freenode

  WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath. It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the _fediverse_ via ActivityPub. It's easy to install and lightweight. **[Start a blog on our instance](https://write.as/new/blog/federated)** [Try the editor](https://write.as/new) [Find another instance](https://writefreely.org/instances) ## Features * Start a blog for yourself, or host a community of writers * Form larger federated networks, and interact over modern protocols like ActivityPub * Write on a dead-simple, distraction-free and super fast editor * Publish drafts and let others proofread them by sharing a private link * Build more advanced apps and extensions with the [well-documented API](https://developers.write.as/docs/api/) ## Quick start > **Note** this is currently alpha software. We're quickly moving out of this v0.x stage, but while we're in it, there are no guarantees that this is ready for production use. First, download the [latest release](https://github.com/writeas/writefreely/releases/latest) for your OS. It includes everything you need to start your blog. Now extract the files from the archive, change into the directory, and do the following steps: ```bash # 1) Log into MySQL and run: # CREATE DATABASE writefreely; # -# 2) Import the schema with: -mysql -u YOURUSERNAME -p writefreely < schema.sql - -# 3) Configure your blog +# 2) Configure your blog ./writefreely --config +# 3) Import the schema with: +./writefreely --init-db + # 4) Generate data encryption keys ./writefreely --gen-keys # 5) Run ./writefreely # 6) Check out your site at the URL you specified in the setup process # 7) There is no Step 7, you're done! ``` For running in production, [see our guide](https://writefreely.org/start#production). ## Development Ready to hack on your site? Here's a quick overview. ### Prerequisites * [Go 1.10+](https://golang.org/dl/) * [Node.js](https://nodejs.org/en/download/) ### Setting up ```bash go get github.com/writeas/writefreely/cmd/writefreely ``` -Create your database, import the schema, and configure your site [as shown above](#quick-start). Then generate the remaining files you'll need: +Configure your site, create your database, and import the schema [as shown above](#quick-start). Then generate the remaining files you'll need: ```bash make install # Generates encryption keys; installs LESS compiler make ui # Generates CSS (run this whenever you update your styles) make run # Runs the application ``` ## License Licensed under the AGPL. diff --git a/app.go b/app.go index 97207e7..7adc513 100644 --- a/app.go +++ b/app.go @@ -1,292 +1,333 @@ package writefreely import ( "database/sql" "flag" "fmt" _ "github.com/go-sql-driver/mysql" "html/template" + "io/ioutil" "net/http" "os" "os/signal" "regexp" + "strings" "syscall" "time" "github.com/gorilla/mux" "github.com/gorilla/schema" "github.com/gorilla/sessions" "github.com/writeas/web-core/converter" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" "github.com/writeas/writefreely/page" ) const ( staticDir = "static/" assumedTitleLen = 80 postsPerPage = 10 serverSoftware = "WriteFreely" softwareURL = "https://writefreely.org" softwareVer = "0.2.1" ) var ( debugging bool // DEPRECATED VARS // TODO: pass app.cfg into GetCollection* calls so we can get these values // from Collection methods and we no longer need these. hostName string isSingleUser bool ) type app struct { router *mux.Router db *datastore cfg *config.Config keys *keychain sessionStore *sessions.CookieStore formDecoder *schema.Decoder } // handleViewHome shows page at root path. Will be the Pad if logged in and the // catch-all landing page otherwise. func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { if app.cfg.App.SingleUser { // Render blog index return handleViewCollection(app, w, r) } // Multi-user instance u := getUserSession(app, r) if u != nil { // User is logged in, so show the Pad return handleViewPad(app, w, r) } p := struct { page.StaticPage Flashes []template.HTML }{ StaticPage: pageForReq(app, r), } // Get error messages session, err := app.sessionStore.Get(r, cookieName) if err != nil { // Ignore this log.Error("Unable to get session in handleViewHome; ignoring: %v", err) } flashes, _ := getSessionFlashes(app, w, r, session) for _, flash := range flashes { p.Flashes = append(p.Flashes, template.HTML(flash)) } // Show landing page return renderPage(w, "landing.tmpl", p) } func pageForReq(app *app, r *http.Request) page.StaticPage { p := page.StaticPage{ AppCfg: app.cfg.App, Path: r.URL.Path, Version: "v" + softwareVer, } // Add user information, if given var u *User accessToken := r.FormValue("t") if accessToken != "" { userID := app.db.GetUserID(accessToken) if userID != -1 { var err error u, err = app.db.GetUserByID(userID) if err == nil { p.Username = u.Username } } } else { u = getUserSession(app, r) if u != nil { p.Username = u.Username } } return p } var shttp = http.NewServeMux() var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") func Serve() { debugPtr := flag.Bool("debug", false, "Enables debug logging.") createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits") doConfig := flag.Bool("config", false, "Run the configuration process") genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys") + createSchema := flag.Bool("init-db", false, "Initialize app database") flag.Parse() debugging = *debugPtr app := &app{} if *createConfig { log.Info("Creating configuration...") c := config.New() log.Info("Saving configuration...") err := config.Save(c) if err != nil { log.Error("Unable to save configuration: %v", err) os.Exit(1) } os.Exit(0) } else if *doConfig { d, err := config.Configure() if err != nil { log.Error("Unable to configure: %v", err) os.Exit(1) } if d.User != nil { app.cfg = d.Config connectToDatabase(app) defer shutdown(app) u := &User{ Username: d.User.Username, HashedPass: d.User.HashedPass, Created: time.Now().Truncate(time.Second).UTC(), } // Create blog log.Info("Creating user %s...\n", u.Username) err = app.db.CreateUser(u, app.cfg.App.SiteName) if err != nil { log.Error("Unable to create user: %s", err) os.Exit(1) } log.Info("Done!") } os.Exit(0) } else if *genKeys { errStatus := 0 err := generateKey(emailKeyPath) if err != nil { errStatus = 1 } err = generateKey(cookieAuthKeyPath) if err != nil { errStatus = 1 } err = generateKey(cookieKeyPath) if err != nil { errStatus = 1 } os.Exit(errStatus) + } else if *createSchema { + log.Info("Loading configuration...") + cfg, err := config.Load() + if err != nil { + log.Error("Unable to load configuration: %v", err) + os.Exit(1) + } + app.cfg = cfg + connectToDatabase(app) + defer shutdown(app) + + schema, err := ioutil.ReadFile("schema.sql") + if err != nil { + log.Error("Unable to load schema.sql: %v", err) + os.Exit(1) + } + + tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`") + + queries := strings.Split(string(schema), ";\n") + for _, q := range queries { + if strings.TrimSpace(q) == "" { + continue + } + parts := tblReg.FindStringSubmatch(q) + if len(parts) >= 3 { + log.Info("Creating table %s...", parts[2]) + } else { + log.Info("Creating table ??? (Weird query) No match in: %v", parts) + } + _, err = app.db.Exec(q) + if err != nil { + log.Error("%s", err) + } else { + log.Info("Created.") + } + } + os.Exit(0) } log.Info("Initializing...") log.Info("Loading configuration...") cfg, err := config.Load() if err != nil { log.Error("Unable to load configuration: %v", err) os.Exit(1) } app.cfg = cfg hostName = cfg.App.Host isSingleUser = cfg.App.SingleUser app.cfg.Server.Dev = *debugPtr initTemplates() // Load keys log.Info("Loading encryption keys...") err = initKeys(app) if err != nil { log.Error("\n%s\n", err) } // Initialize modules app.sessionStore = initSession(app) app.formDecoder = schema.NewDecoder() app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString) app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool) app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString) app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool) app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64) app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64) // Check database configuration if app.cfg.Database.User == "" || app.cfg.Database.Password == "" { log.Error("Database user or password not set.") os.Exit(1) } if app.cfg.Database.Host == "" { app.cfg.Database.Host = "localhost" } if app.cfg.Database.Database == "" { app.cfg.Database.Database = "writeas" } connectToDatabase(app) defer shutdown(app) r := mux.NewRouter() handler := NewHandler(app) handler.SetErrorPages(&ErrorPages{ NotFound: pages["404-general.tmpl"], Gone: pages["410.tmpl"], InternalServerError: pages["500.tmpl"], Blank: pages["blank.tmpl"], }) // Handle app routes initRoutes(handler, r, app.cfg, app.db) // Handle static files fs := http.FileServer(http.Dir(staticDir)) shttp.Handle("/", fs) r.PathPrefix("/").Handler(fs) // Handle shutdown c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c log.Info("Shutting down...") shutdown(app) log.Info("Done.") os.Exit(0) }() // Start web application server http.Handle("/", r) log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port) log.Info("---") err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil) if err != nil { log.Error("Unable to start: %v", err) os.Exit(1) } } func connectToDatabase(app *app) { log.Info("Connecting to database...") db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database)) if err != nil { log.Error("%s", err) os.Exit(1) } app.db = &datastore{db} app.db.SetMaxOpenConns(50) } func shutdown(app *app) { log.Info("Closing database connection...") app.db.Close() }