diff --git a/.gitignore b/.gitignore index 228e67d..6f45e83 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build config.ini +writefreely.db \ No newline at end of file diff --git a/app.go b/app.go index acf0f4e..497d4ca 100644 --- a/app.go +++ b/app.go @@ -256,9 +256,15 @@ func Serve() { connectToDatabase(app) defer shutdown(app) - schema, err := ioutil.ReadFile("schema.sql") + schemaFileName := "schema.sql" + + if 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) } @@ -438,15 +444,15 @@ func connectToDatabase(app *app) { log.Error("%s", err) os.Exit(1) } - app.db = &datastore{db} + app.db = &datastore{db, "mysql"} app.db.SetMaxOpenConns(50) } else if app.cfg.Database.Type == "sqlite3" { - db, err := sql.Open("sqlite3", "./writefreely.db") + db, err := sql.Open("sqlite3", "./writefreely.db?parseTime=true") if err != nil { log.Error("%s", err) os.Exit(1) } - app.db = &datastore{db} + app.db = &datastore{db, "sqlite3"} app.db.SetMaxOpenConns(50) } else { log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type) diff --git a/database.go b/database.go index 468b04d..61a4ad4 100644 --- a/database.go +++ b/database.go @@ -100,6 +100,7 @@ type writestore interface { type datastore struct { *sql.DB + driverName string } func (db *datastore) CreateUser(u *User, collectionTitle string) error { @@ -115,7 +116,7 @@ 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 { @@ -478,7 +479,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 @@ -571,7 +572,12 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos } } - 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(), ?)") + timeFunction := "NOW()" + if db.driverName == "sqlite3" { + timeFunction = "strftime('%Y-%m-%d %H-%M-%S','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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + timeFunction + ", ?)") if err != nil { return nil, err } @@ -668,7 +674,13 @@ func (db *datastore) UpdateOwnedPost(post *AuthenticatedPost, userID int64) erro return ErrPostNoUpdatableVals } - queryUpdates += sep + "updated = NOW()" + timeFunction := "NOW()" + + if db.driverName == "sqlite3" { + timeFunction = "strftime('%Y-%m-%d %H-%M-%S','now')" + } + + queryUpdates += sep + "updated = " + timeFunction res, err := db.Exec("UPDATE posts SET "+queryUpdates+" WHERE id = ? AND "+authCondition, params...) if err != nil { @@ -984,6 +996,10 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) { timeCondition := "" if !includeFuture { timeCondition = "AND created <= NOW()" + + if db.driverName == "sqlite3" { + timeCondition = "AND created <= strftime('%Y-%m-%d %H-%M-%S','now')" + } } err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition, c.ID).Scan(&count) switch { @@ -1023,6 +1039,10 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen timeCondition := "" if !includeFuture { timeCondition = "AND created <= NOW()" + + if db.driverName == "sqlite3" { + timeCondition = "AND created <= strftime('%Y-%m-%d %H-%M-%S','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 { @@ -1080,6 +1100,10 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include timeCondition := "" if !includeFuture { timeCondition = "AND created <= NOW()" + + if db.driverName == "sqlite3" { + timeCondition = "AND created <= strftime('%Y-%m-%d %H-%M-%S','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 { @@ -1455,7 +1479,11 @@ 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) + clipFunction := "LEFT" + if db.driverName == "sqlite3" { + clipFunction = "SUBSTR" + } + rows, err := db.Query("SELECT id, slug, title, "+clipFunction+"(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."} @@ -2141,7 +2169,13 @@ 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) + timeFunction := "NOW()" + + if db.driverName == "sqlite3" { + timeFunction = "strftime('%Y-%m-%d %H-%M-%S','now')" + } + + _, err := db.Exec("INSERT INTO appcontent (id, content, updated) VALUES (?, ?, "+timeFunction+") ON DUPLICATE KEY UPDATE content = ?, updated = "+timeFunction, id, content, content) if err != nil { log.Error("Unable to INSERT appcontent for '%s': %v", id, err) } diff --git a/schema.sql b/schema.sql index 5cbe12d..7058f40 100644 --- a/schema.sql +++ b/schema.sql @@ -13,7 +13,7 @@ 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, PRIMARY KEY (`token`) @@ -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; diff --git a/sqlite.sql b/sqlite.sql new file mode 100644 index 0000000..a4d19de --- /dev/null +++ b/sqlite.sql @@ -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 NOT 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 +);