commit
2422601e89
|
@ -4,3 +4,4 @@
|
|||
|
||||
build
|
||||
config.ini
|
||||
*.db
|
||||
|
|
22
Makefile
22
Makefile
|
@ -16,13 +16,22 @@ build: deps
|
|||
cd cmd/writefreely; $(GOBUILD) -v
|
||||
|
||||
build-linux: deps
|
||||
cd cmd/writefreely; GOOS=linux GOARCH=amd64 $(GOBUILD) -v
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u github.com/karalabe/xgo; \
|
||||
fi
|
||||
xgo --targets=linux/amd64, -dest build/ $(LDFLAGS) -out writefreely ./cmd/writefreely
|
||||
|
||||
build-windows: deps
|
||||
cd cmd/writefreely; GOOS=windows GOARCH=amd64 $(GOBUILD) -v
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u github.com/karalabe/xgo; \
|
||||
fi
|
||||
xgo --targets=windows/amd64, -dest build/ $(LDFLAGS) -out writefreely ./cmd/writefreely
|
||||
|
||||
build-darwin: deps
|
||||
cd cmd/writefreely; GOOS=darwin GOARCH=amd64 $(GOBUILD) -v
|
||||
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
$(GOGET) -u github.com/karalabe/xgo; \
|
||||
fi
|
||||
xgo --targets=darwin/amd64, -dest build/ $(LDFLAGS) -out writefreely ./cmd/writefreely
|
||||
|
||||
build-docker :
|
||||
$(DOCKERCMD) build -t $(IMAGE_NAME):latest -t $(IMAGE_NAME):$(GITREV) .
|
||||
|
@ -48,16 +57,17 @@ release : clean ui
|
|||
cp -r static build
|
||||
mkdir build/keys
|
||||
cp schema.sql build
|
||||
cp sqlite.sql build
|
||||
$(MAKE) build-linux
|
||||
cp cmd/writefreely/$(BINARY_NAME) build
|
||||
mv build/$(BINARY_NAME)-linux-amd64 build/$(BINARY_NAME)
|
||||
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz *
|
||||
rm build/$(BINARY_NAME)
|
||||
$(MAKE) build-darwin
|
||||
cp cmd/writefreely/$(BINARY_NAME) build
|
||||
mv build/$(BINARY_NAME)-darwin-10.6-amd64 build/$(BINARY_NAME)
|
||||
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_darwin_amd64.tar.gz *
|
||||
rm build/$(BINARY_NAME)
|
||||
$(MAKE) build-windows
|
||||
cp cmd/writefreely/$(BINARY_NAME).exe build
|
||||
mv build/$(BINARY_NAME)-windows-4.0-amd64.exe build/$(BINARY_NAME).exe
|
||||
cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./*
|
||||
$(MAKE) build-docker
|
||||
$(MAKE) release-docker
|
||||
|
|
|
@ -46,12 +46,12 @@ First, download the [latest release](https://github.com/writeas/writefreely/rele
|
|||
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) Configure your blog
|
||||
# 1) Configure your blog
|
||||
./writefreely --config
|
||||
|
||||
# 2) (if you chose MySQL in the previous step) Log into MySQL and run:
|
||||
# CREATE DATABASE writefreely;
|
||||
|
||||
# 3) Import the schema with:
|
||||
./writefreely --init-db
|
||||
|
||||
|
|
84
app.go
84
app.go
|
@ -4,7 +4,6 @@ import (
|
|||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
@ -16,6 +15,9 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/gorilla/sessions"
|
||||
|
@ -246,19 +248,19 @@ func Serve() {
|
|||
|
||||
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
|
||||
loadConfig(app)
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
schema, err := ioutil.ReadFile("schema.sql")
|
||||
schemaFileName := "schema.sql"
|
||||
|
||||
if app.cfg.Database.Type == "sqlite3" {
|
||||
schemaFileName = "sqlite.sql"
|
||||
}
|
||||
|
||||
schema, err := ioutil.ReadFile(schemaFileName)
|
||||
if err != nil {
|
||||
log.Error("Unable to load schema.sql: %v", err)
|
||||
log.Error("Unable to load schema file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
@ -291,13 +293,7 @@ func Serve() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
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
|
||||
loadConfig(app)
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
|
@ -333,13 +329,7 @@ func Serve() {
|
|||
os.Exit(0)
|
||||
} else if *resetPassUser != "" {
|
||||
// Connect to the database
|
||||
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
|
||||
loadConfig(app)
|
||||
connectToDatabase(app)
|
||||
defer shutdown(app)
|
||||
|
||||
|
@ -377,23 +367,17 @@ func Serve() {
|
|||
|
||||
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
|
||||
loadConfig(app)
|
||||
|
||||
hostName = cfg.App.Host
|
||||
isSingleUser = cfg.App.SingleUser
|
||||
hostName = app.cfg.App.Host
|
||||
isSingleUser = app.cfg.App.SingleUser
|
||||
app.cfg.Server.Dev = *debugPtr
|
||||
|
||||
initTemplates()
|
||||
|
||||
// Load keys
|
||||
log.Info("Loading encryption keys...")
|
||||
err = initKeys(app)
|
||||
err := initKeys(app)
|
||||
if err != nil {
|
||||
log.Error("\n%s\n", err)
|
||||
}
|
||||
|
@ -483,20 +467,40 @@ func Serve() {
|
|||
}
|
||||
}
|
||||
|
||||
func connectToDatabase(app *app) {
|
||||
if app.cfg.Database.Type != "mysql" {
|
||||
log.Error("Invalid database type '%s'. Only 'mysql' is supported right now.", app.cfg.Database.Type)
|
||||
func loadConfig(app *app) {
|
||||
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
|
||||
}
|
||||
|
||||
func connectToDatabase(app *app) {
|
||||
log.Info("Connecting to %s database...", app.cfg.Database.Type)
|
||||
db, err := sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String())))
|
||||
|
||||
var db *sql.DB
|
||||
var err error
|
||||
if app.cfg.Database.Type == "mysql" {
|
||||
db, err = sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String())))
|
||||
db.SetMaxOpenConns(50)
|
||||
} else if app.cfg.Database.Type == "sqlite3" {
|
||||
if app.cfg.Database.FileName == "" {
|
||||
log.Error("SQLite database filename value in config.ini is empty.")
|
||||
os.Exit(1)
|
||||
}
|
||||
db, err = sql.Open("sqlite3", app.cfg.Database.FileName+"?parseTime=true&cached=shared")
|
||||
db.SetMaxOpenConns(1)
|
||||
} else {
|
||||
log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
app.db = &datastore{db}
|
||||
app.db.SetMaxOpenConns(50)
|
||||
app.db = &datastore{db, app.cfg.Database.Type}
|
||||
}
|
||||
|
||||
func shutdown(app *app) {
|
||||
|
|
|
@ -22,6 +22,7 @@ type (
|
|||
|
||||
DatabaseCfg struct {
|
||||
Type string `ini:"type"`
|
||||
FileName string `ini:"filename"`
|
||||
User string `ini:"username"`
|
||||
Password string `ini:"password"`
|
||||
Database string `ini:"database"`
|
||||
|
@ -59,16 +60,11 @@ type (
|
|||
)
|
||||
|
||||
func New() *Config {
|
||||
return &Config{
|
||||
c := &Config{
|
||||
Server: ServerCfg{
|
||||
Port: 8080,
|
||||
Bind: "localhost", /* IPV6 support when not using localhost? */
|
||||
},
|
||||
Database: DatabaseCfg{
|
||||
Type: "mysql",
|
||||
Host: "localhost",
|
||||
Port: 3306,
|
||||
},
|
||||
App: AppCfg{
|
||||
Host: "http://localhost:8080",
|
||||
Theme: "write",
|
||||
|
@ -80,6 +76,25 @@ func New() *Config {
|
|||
PublicStats: true,
|
||||
},
|
||||
}
|
||||
c.UseMySQL(true)
|
||||
return c
|
||||
}
|
||||
|
||||
// UseMySQL resets the Config's Database to use default values for a MySQL setup.
|
||||
func (cfg *Config) UseMySQL(fresh bool) {
|
||||
cfg.Database.Type = "mysql"
|
||||
if fresh {
|
||||
cfg.Database.Host = "localhost"
|
||||
cfg.Database.Port = 3306
|
||||
}
|
||||
}
|
||||
|
||||
// UseSQLite resets the Config's Database to use default values for a SQLite setup.
|
||||
func (cfg *Config) UseSQLite(fresh bool) {
|
||||
cfg.Database.Type = "sqlite3"
|
||||
if fresh {
|
||||
cfg.Database.FileName = "writefreely.db"
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) IsSecureStandalone() bool {
|
||||
|
|
125
config/setup.go
125
config/setup.go
|
@ -20,10 +20,12 @@ func Configure() (*SetupData, error) {
|
|||
|
||||
data.Config, err = Load()
|
||||
var action string
|
||||
isNewCfg := false
|
||||
if err != nil {
|
||||
fmt.Println("No configuration yet. Creating new.")
|
||||
data.Config = New()
|
||||
action = "generate"
|
||||
isNewCfg = true
|
||||
} else {
|
||||
fmt.Println("Configuration loaded.")
|
||||
action = "update"
|
||||
|
@ -126,62 +128,91 @@ func Configure() (*SetupData, error) {
|
|||
title(" Database setup ")
|
||||
fmt.Println()
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Username",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.User,
|
||||
selPrompt = promptui.Select{
|
||||
Templates: selTmpls,
|
||||
Label: "Database driver",
|
||||
Items: []string{"MySQL", "SQLite"},
|
||||
}
|
||||
data.Config.Database.User, err = prompt.Run()
|
||||
sel, _, err := selPrompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Password",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.Password,
|
||||
Mask: '*',
|
||||
}
|
||||
data.Config.Database.Password, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if sel == 0 {
|
||||
// Configure for MySQL
|
||||
data.Config.UseMySQL(isNewCfg)
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Database name",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.Database,
|
||||
}
|
||||
data.Config.Database.Database, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Username",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.User,
|
||||
}
|
||||
data.Config.Database.User, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Host",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.Host,
|
||||
}
|
||||
data.Config.Database.Host, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Password",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.Password,
|
||||
Mask: '*',
|
||||
}
|
||||
data.Config.Database.Password, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Port",
|
||||
Validate: validatePort,
|
||||
Default: fmt.Sprintf("%d", data.Config.Database.Port),
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Database name",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.Database,
|
||||
}
|
||||
data.Config.Database.Database, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Host",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.Host,
|
||||
}
|
||||
data.Config.Database.Host, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Port",
|
||||
Validate: validatePort,
|
||||
Default: fmt.Sprintf("%d", data.Config.Database.Port),
|
||||
}
|
||||
dbPort, err := prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number
|
||||
} else if sel == 1 {
|
||||
// Configure for SQLite
|
||||
data.Config.UseSQLite(isNewCfg)
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Templates: tmpls,
|
||||
Label: "Filename",
|
||||
Validate: validateNonEmpty,
|
||||
Default: data.Config.Database.FileName,
|
||||
}
|
||||
data.Config.Database.FileName, err = prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
}
|
||||
dbPort, err := prompt.Run()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number
|
||||
|
||||
fmt.Println()
|
||||
title(" App setup ")
|
||||
|
|
156
database.go
156
database.go
|
@ -8,6 +8,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/guregu/null"
|
||||
"github.com/guregu/null/zero"
|
||||
uuid "github.com/nu7hatch/gouuid"
|
||||
|
@ -24,6 +26,9 @@ import (
|
|||
|
||||
const (
|
||||
mySQLErrDuplicateKey = 1062
|
||||
|
||||
driverMySQL = "mysql"
|
||||
driverSQLite = "sqlite3"
|
||||
)
|
||||
|
||||
type writestore interface {
|
||||
|
@ -100,6 +105,47 @@ type writestore interface {
|
|||
|
||||
type datastore struct {
|
||||
*sql.DB
|
||||
driverName string
|
||||
}
|
||||
|
||||
func (db *datastore) now() string {
|
||||
if db.driverName == driverSQLite {
|
||||
return "strftime('%Y-%m-%d %H:%M:%S','now')"
|
||||
}
|
||||
return "NOW()"
|
||||
}
|
||||
|
||||
func (db *datastore) clip(field string, l int) string {
|
||||
if db.driverName == driverSQLite {
|
||||
return fmt.Sprintf("SUBSTR(%s, 0, %d)", field, l)
|
||||
}
|
||||
return fmt.Sprintf("LEFT(%s, %d)", field, l)
|
||||
}
|
||||
|
||||
func (db *datastore) upsert(indexedCols ...string) string {
|
||||
if db.driverName == driverSQLite {
|
||||
// NOTE: SQLite UPSERT syntax only works in v3.24.0 (2018-06-04) or later
|
||||
// Leaving this for whenever we can upgrade and include it in our binary
|
||||
cc := strings.Join(indexedCols, ", ")
|
||||
return "ON CONFLICT(" + cc + ") DO UPDATE SET"
|
||||
}
|
||||
return "ON DUPLICATE KEY UPDATE"
|
||||
}
|
||||
|
||||
func (db *datastore) isDuplicateKeyErr(err error) bool {
|
||||
if db.driverName == driverSQLite {
|
||||
if err, ok := err.(sqlite3.Error); ok {
|
||||
return err.Code == sqlite3.ErrConstraint
|
||||
}
|
||||
} else if db.driverName == driverMySQL {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
return mysqlErr.Number == mySQLErrDuplicateKey
|
||||
}
|
||||
} else {
|
||||
log.Error("isDuplicateKeyErr: failed check for unrecognized driver '%s'", db.driverName)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (db *datastore) CreateUser(u *User, collectionTitle string) error {
|
||||
|
@ -115,13 +161,11 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
|
|||
|
||||
// 1. Add to `users` table
|
||||
// NOTE: Assumes User's Password is already hashed!
|
||||
res, err := t.Exec("INSERT INTO users (username, password, email, created) VALUES (?, ?, ?, NOW())", u.Username, u.HashedPass, u.Email)
|
||||
res, err := t.Exec("INSERT INTO users (username, password, email) VALUES (?, ?, ?)", u.Username, u.HashedPass, u.Email)
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number == mySQLErrDuplicateKey {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
if db.isDuplicateKeyErr(err) {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
|
||||
log.Error("Rolling back users INSERT: %v\n", err)
|
||||
|
@ -141,10 +185,8 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
|
|||
res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, "", CollUnlisted, u.ID, 0)
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number == mySQLErrDuplicateKey {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
if db.isDuplicateKeyErr(err) {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
log.Error("Rolling back collections INSERT: %v\n", err)
|
||||
return err
|
||||
|
@ -213,10 +255,8 @@ func (db *datastore) CreateCollection(alias, title string, userID int64) (*Colle
|
|||
// All good, so create new collection
|
||||
res, err := db.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", alias, title, "", CollUnlisted, userID, 0)
|
||||
if err != nil {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number == mySQLErrDuplicateKey {
|
||||
return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."}
|
||||
}
|
||||
if db.isDuplicateKeyErr(err) {
|
||||
return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."}
|
||||
}
|
||||
log.Error("Couldn't add to collections: %v\n", err)
|
||||
return nil, err
|
||||
|
@ -478,7 +518,7 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int,
|
|||
expirationVal = fmt.Sprintf("DATE_ADD(NOW(), INTERVAL %d SECOND)", validSecs)
|
||||
}
|
||||
|
||||
_, err = db.Exec("INSERT INTO accesstokens (token, user_id, created, one_time, expires) VALUES (?, ?, NOW(), ?, "+expirationVal+")", string(binTok), userID, oneTime)
|
||||
_, err = db.Exec("INSERT INTO accesstokens (token, user_id, one_time, expires) VALUES (?, ?, ?, "+expirationVal+")", string(binTok), userID, oneTime)
|
||||
if err != nil {
|
||||
log.Error("Couldn't INSERT accesstoken: %v", err)
|
||||
return "", err
|
||||
|
@ -563,32 +603,36 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
|
|||
}
|
||||
|
||||
created := time.Now()
|
||||
if db.driverName == driverSQLite {
|
||||
// SQLite stores datetimes in UTC, so convert time.Now() to it here
|
||||
created = created.UTC()
|
||||
}
|
||||
if post.Created != nil {
|
||||
created, err = time.Parse("2006-01-02T15:04:05Z", *post.Created)
|
||||
if err != nil {
|
||||
log.Error("Unable to parse Created time '%s': %v", *post.Created, err)
|
||||
created = time.Now()
|
||||
if db.driverName == driverSQLite {
|
||||
// SQLite stores datetimes in UTC, so convert time.Now() to it here
|
||||
created = created.UTC()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stmt, err := db.Prepare("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, privacy, owner_id, collection_id, created, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)")
|
||||
stmt, err := db.Prepare("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, privacy, owner_id, collection_id, created, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + db.now() + ", ?)")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec(friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, 0, ownerID, ownerCollID, created, 0)
|
||||
if err != nil {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number == mySQLErrDuplicateKey {
|
||||
// Duplicate entry error; try a new slug
|
||||
// TODO: make this a little more robust
|
||||
slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true}
|
||||
_, err = stmt.Exec(friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, 0, ownerID, ownerCollID, created, 0)
|
||||
if err != nil {
|
||||
return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err))
|
||||
}
|
||||
} else {
|
||||
return nil, handleFailedPostInsert(err)
|
||||
if db.isDuplicateKeyErr(err) {
|
||||
// Duplicate entry error; try a new slug
|
||||
// TODO: make this a little more robust
|
||||
slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true}
|
||||
_, err = stmt.Exec(friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, 0, ownerID, ownerCollID, created, 0)
|
||||
if err != nil {
|
||||
return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err))
|
||||
}
|
||||
} else {
|
||||
return nil, handleFailedPostInsert(err)
|
||||
|
@ -668,7 +712,7 @@ func (db *datastore) UpdateOwnedPost(post *AuthenticatedPost, userID int64) erro
|
|||
return ErrPostNoUpdatableVals
|
||||
}
|
||||
|
||||
queryUpdates += sep + "updated = NOW()"
|
||||
queryUpdates += sep + "updated = " + db.now()
|
||||
|
||||
res, err := db.Exec("UPDATE posts SET "+queryUpdates+" WHERE id = ? AND "+authCondition, params...)
|
||||
if err != nil {
|
||||
|
@ -789,7 +833,11 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
|
||||
// Update MathJax value
|
||||
if c.MathJax {
|
||||
_, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", collID, "render_mathjax", "1", "1")
|
||||
if db.driverName == driverSQLite {
|
||||
_, err = db.Exec("INSERT OR REPLACE INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?)", collID, "render_mathjax", "1")
|
||||
} else {
|
||||
_, err = db.Exec("INSERT INTO collectionattributes (collection_id, attribute, value) VALUES (?, ?, ?) "+db.upsert("collection_id", "attribute")+" value = ?", collID, "render_mathjax", "1", "1")
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Unable to insert render_mathjax value: %v", err)
|
||||
return err
|
||||
|
@ -831,7 +879,11 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
|
|||
log.Error("Unable to create hash: %s", err)
|
||||
return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
|
||||
}
|
||||
_, err = db.Exec("INSERT INTO collectionpasswords (collection_id, password) VALUES ((SELECT id FROM collections WHERE alias = ?), ?) ON DUPLICATE KEY UPDATE password = ?", alias, hashedPass, hashedPass)
|
||||
if db.driverName == driverSQLite {
|
||||
_, err = db.Exec("INSERT OR REPLACE INTO collectionpasswords (collection_id, password) VALUES ((SELECT id FROM collections WHERE alias = ?), ?)", alias, hashedPass)
|
||||
} else {
|
||||
_, err = db.Exec("INSERT INTO collectionpasswords (collection_id, password) VALUES ((SELECT id FROM collections WHERE alias = ?), ?) "+db.upsert("collection_id")+" password = ?", alias, hashedPass, hashedPass)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -983,7 +1035,7 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) {
|
|||
var count int64
|
||||
timeCondition := ""
|
||||
if !includeFuture {
|
||||
timeCondition = "AND created <= NOW()"
|
||||
timeCondition = "AND created <= " + db.now()
|
||||
}
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition, c.ID).Scan(&count)
|
||||
switch {
|
||||
|
@ -1022,7 +1074,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen
|
|||
}
|
||||
timeCondition := ""
|
||||
if !includeFuture {
|
||||
timeCondition = "AND created <= NOW()"
|
||||
timeCondition = "AND created <= " + db.now()
|
||||
}
|
||||
rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition+" ORDER BY created "+order+limitStr, collID)
|
||||
if err != nil {
|
||||
|
@ -1079,7 +1131,7 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include
|
|||
}
|
||||
timeCondition := ""
|
||||
if !includeFuture {
|
||||
timeCondition = "AND created <= NOW()"
|
||||
timeCondition = "AND created <= " + db.now()
|
||||
}
|
||||
rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND LOWER(content) RLIKE ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, "#"+strings.ToLower(tag)+"[[:>:]]")
|
||||
if err != nil {
|
||||
|
@ -1154,17 +1206,15 @@ func (db *datastore) CanCollect(cpr *ClaimPostRequest, userID int64) bool {
|
|||
func (db *datastore) AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error) {
|
||||
qRes, err := db.Exec(query, params...)
|
||||
if err != nil {
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number == mySQLErrDuplicateKey && slugIdx > -1 {
|
||||
s := id.GenSafeUniqueSlug(p.Slug)
|
||||
if s == p.Slug {
|
||||
// Sanity check to prevent infinite recursion
|
||||
return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s)
|
||||
}
|
||||
p.Slug = s
|
||||
params[slugIdx] = p.Slug
|
||||
return db.AttemptClaim(p, query, params, slugIdx)
|
||||
if db.isDuplicateKeyErr(err) && slugIdx > -1 {
|
||||
s := id.GenSafeUniqueSlug(p.Slug)
|
||||
if s == p.Slug {
|
||||
// Sanity check to prevent infinite recursion
|
||||
return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s)
|
||||
}
|
||||
p.Slug = s
|
||||
params[slugIdx] = p.Slug
|
||||
return db.AttemptClaim(p, query, params, slugIdx)
|
||||
}
|
||||
return qRes, fmt.Errorf("attemptClaim: %s", err)
|
||||
}
|
||||
|
@ -1455,7 +1505,8 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 {
|
|||
}
|
||||
|
||||
func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) {
|
||||
rows, err := db.Query("SELECT id, slug, title, LEFT(content, 80), pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID)
|
||||
// FIXME: sqlite-backed instances don't include ellipsis on truncated titles
|
||||
rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting pinned posts: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."}
|
||||
|
@ -1734,10 +1785,8 @@ func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
|
|||
_, err = t.Exec("UPDATE users SET username = ? WHERE id = ?", newUsername, u.ID)
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number == mySQLErrDuplicateKey {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
if db.isDuplicateKeyErr(err) {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
log.Error("Unable to update users table: %v", err)
|
||||
return ErrInternalGeneral
|
||||
|
@ -1746,10 +1795,8 @@ func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
|
|||
_, err = t.Exec("UPDATE collections SET alias = ? WHERE alias = ? AND owner_id = ?", newUsername, u.Username, u.ID)
|
||||
if err != nil {
|
||||
t.Rollback()
|
||||
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||
if mysqlErr.Number == mySQLErrDuplicateKey {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
if db.isDuplicateKeyErr(err) {
|
||||
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
|
||||
}
|
||||
log.Error("Unable to update collection: %v", err)
|
||||
return ErrInternalGeneral
|
||||
|
@ -2141,7 +2188,12 @@ func (db *datastore) GetDynamicContent(id string) (string, *time.Time, error) {
|
|||
}
|
||||
|
||||
func (db *datastore) UpdateDynamicContent(id, content string) error {
|
||||
_, err := db.Exec("INSERT INTO appcontent (id, content, updated) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE content = ?, updated = NOW()", id, content, content)
|
||||
var err error
|
||||
if db.driverName == driverSQLite {
|
||||
_, err = db.Exec("INSERT OR REPLACE INTO appcontent (id, content, updated) VALUES (?, ?, "+db.now()+")", id, content)
|
||||
} else {
|
||||
_, err = db.Exec("INSERT INTO appcontent (id, content, updated) VALUES (?, ?, "+db.now()+") "+db.upsert("id")+" content = ?, updated = "+db.now(), id, content, content)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Unable to INSERT appcontent for '%s': %v", id, err)
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ CREATE TABLE IF NOT EXISTS `accesstokens` (
|
|||
`user_id` int(6) NOT NULL,
|
||||
`sudo` tinyint(1) NOT NULL DEFAULT '0',
|
||||
`one_time` tinyint(1) NOT NULL DEFAULT '0',
|
||||
`created` datetime NOT NULL,
|
||||
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`expires` datetime DEFAULT NULL,
|
||||
`user_agent` varchar(255) NOT NULL,
|
||||
`user_agent` varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (`token`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
|
@ -197,7 +197,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|||
`username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
|
||||
`password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
|
||||
`email` varbinary(255) DEFAULT NULL,
|
||||
`created` datetime NOT NULL,
|
||||
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
--
|
||||
-- Database: writefreely
|
||||
--
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table accesstokens
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `accesstokens` (
|
||||
token TEXT NOT NULL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
sudo INTEGER NOT NULL DEFAULT '0',
|
||||
one_time INTEGER NOT NULL DEFAULT '0',
|
||||
created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires DATETIME DEFAULT NULL,
|
||||
user_agent TEXT DEFAULT NULL
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table appcontent
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `appcontent` (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table collectionattributes
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `collectionattributes` (
|
||||
collection_id INTEGER NOT NULL,
|
||||
attribute TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (collection_id, attribute)
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table collectionkeys
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `collectionkeys` (
|
||||
collection_id INTEGER PRIMARY KEY,
|
||||
public_key blob NOT NULL,
|
||||
private_key blob NOT NULL
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table collectionpasswords
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `collectionpasswords` (
|
||||
collection_id INTEGER PRIMARY KEY,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table collectionredirects
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `collectionredirects` (
|
||||
prev_alias TEXT NOT NULL PRIMARY KEY,
|
||||
new_alias TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table collections
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `collections` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
alias TEXT DEFAULT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
style_sheet TEXT,
|
||||
script TEXT,
|
||||
format TEXT DEFAULT NULL,
|
||||
privacy INTEGER NOT NULL,
|
||||
owner_id INTEGER NOT NULL,
|
||||
view_count INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table posts
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `posts` (
|
||||
id TEXT NOT NULL,
|
||||
slug TEXT DEFAULT NULL,
|
||||
modify_token TEXT DEFAULT NULL,
|
||||
text_appearance TEXT NOT NULL DEFAULT 'norm',
|
||||
language TEXT DEFAULT NULL,
|
||||
rtl INTEGER DEFAULT NULL,
|
||||
privacy INTEGER NOT NULL,
|
||||
owner_id INTEGER DEFAULT NULL,
|
||||
collection_id INTEGER DEFAULT NULL,
|
||||
pinned_position INTEGER UNSIGNED DEFAULT NULL,
|
||||
created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
view_count INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
CONSTRAINT id_slug UNIQUE (collection_id, slug),
|
||||
CONSTRAINT owner_id UNIQUE (owner_id, id),
|
||||
CONSTRAINT privacy_id UNIQUE (privacy, id)
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table remotefollows
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `remotefollows` (
|
||||
collection_id INTEGER NOT NULL,
|
||||
remote_user_id INTEGER NOT NULL,
|
||||
created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (collection_id,remote_user_id)
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table remoteuserkeys
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `remoteuserkeys` (
|
||||
id TEXT NOT NULL,
|
||||
remote_user_id INTEGER NOT NULL,
|
||||
public_key blob NOT NULL,
|
||||
CONSTRAINT follower_id UNIQUE (remote_user_id)
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table remoteusers
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `remoteusers` (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
actor_id TEXT NOT NULL,
|
||||
inbox TEXT NOT NULL,
|
||||
shared_inbox TEXT NOT NULL,
|
||||
CONSTRAINT collection_id UNIQUE (actor_id)
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table userattributes
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `userattributes` (
|
||||
user_id INTEGER NOT NULL,
|
||||
attribute TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, attribute)
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table users
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT DEFAULT NULL,
|
||||
created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
Loading…
Reference in New Issue