diff --git a/config/migrations/000_init.sql b/config/migrations/000_init.sql new file mode 100644 index 0000000..0bfe9d7 --- /dev/null +++ b/config/migrations/000_init.sql @@ -0,0 +1,31 @@ +CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY, + username TEXT NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NULL, + about TEXT NULL, + website TEXT NULL, + mood TEXT NULL +); + +CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY, + site_title TEXT NOT NULL, + site_description TEXT NULL, + base_url TEXT NOT NULL, + base_path TEXT NOT NULL, + items_per_page INTEGER NOT NULL, + css_id INTEGER NULL +); + +CREATE TABLE IF NOT EXISTS css ( + id INTEGER PRIMARY KEY, + filename TEXT UNIQUE NOT NULL, + description TEXT NULL +); + +CREATE TABLE IF NOT EXISTS emoji( + id INTEGER PRIMARY KEY, + emoji TEXT UNIQUE NOT NULL, + description TEXT NOT NULL +); \ No newline at end of file diff --git a/config/migrations/003_add_tick_table.sql b/config/migrations/003_add_tick_table.sql new file mode 100644 index 0000000..9e48f97 --- /dev/null +++ b/config/migrations/003_add_tick_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS tick ( + id INTEGER PRIMARY KEY, + timestamp TEXT NOT NULL, + tick TEXT NOT NULL +); \ No newline at end of file diff --git a/config/migrations/004_add_tick_timestamp_index.sql b/config/migrations/004_add_tick_timestamp_index.sql new file mode 100644 index 0000000..1647394 --- /dev/null +++ b/config/migrations/004_add_tick_timestamp_index.sql @@ -0,0 +1 @@ +CREATE INDEX idx_tick_timestamp ON tick (timestamp DESC); \ No newline at end of file diff --git a/src/Controller/FeedController/FeedController.php b/src/Controller/FeedController/FeedController.php index a265ae9..5a9c998 100644 --- a/src/Controller/FeedController/FeedController.php +++ b/src/Controller/FeedController/FeedController.php @@ -1,7 +1,5 @@ config = ConfigModel::load(); - $this->ticks = iterator_to_array(TickModel::streamTicks($this->config->itemsPerPage)); + $config = ConfigModel::load(); + $tickModel = new TickModel(); + $ticks = iterator_to_array($tickModel->stream($config->itemsPerPage)); + $this->vars = [ - 'config' => $this->config, - 'ticks' => $this->ticks, + 'config' => $config, + 'ticks' => $ticks, ]; } diff --git a/src/Controller/HomeController/HomeController.php b/src/Controller/HomeController/HomeController.php index 0af4235..bd1ed88 100644 --- a/src/Controller/HomeController/HomeController.php +++ b/src/Controller/HomeController/HomeController.php @@ -7,9 +7,10 @@ class HomeController extends Controller { global $config; global $user; + $tickModel = new TickModel(); $limit = $config->itemsPerPage; $offset = ($page - 1) * $limit; - $ticks = iterator_to_array(TickModel::streamTicks($limit, $offset)); + $ticks = iterator_to_array($tickModel->stream($limit, $offset)); $view = new HomeView(); $tickList = $view->renderTicksSection($config->siteDescription, $ticks, $page, $limit); @@ -29,7 +30,8 @@ class HomeController extends Controller { if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['new_tick'])) { // save the tick if (trim($_POST['new_tick'])){ - TickModel::save($_POST['new_tick'], $_POST['tick_mood']); + $tickModel = new TickModel(); + $tickModel->insert($_POST['new_tick']); } } diff --git a/src/Controller/TickController/TickController.php b/src/Controller/TickController/TickController.php index c708799..7b1f2f3 100644 --- a/src/Controller/TickController/TickController.php +++ b/src/Controller/TickController/TickController.php @@ -1,10 +1,10 @@ get($year, $month, $day, $hour, $minute, $second); - $this->render('tick.php', $tick); + //public function index(string $year, string $month, string $day, string $hour, string $minute, string $second){ + public function index(int $id){ + $tickModel = new TickModel(); + $vars = $tickModel->get($id); + $this->render('tick.php', $vars); } } \ No newline at end of file diff --git a/src/Framework/Database/Database.php b/src/Framework/Database/Database.php index 544116f..a8dc342 100644 --- a/src/Framework/Database/Database.php +++ b/src/Framework/Database/Database.php @@ -27,10 +27,12 @@ class Database{ // The database version will just be an int // stored as PRAGMA user_version. It will // correspond to the most recent migration file applied to the db. + // + // I'm starting from 0, so if the user_version is NULL, I'll return -1. private function getVersion(): int { $db = self::get(); - return $db->query("PRAGMA user_version")->fetchColumn() ?? 0; + return $db->query("PRAGMA user_version")->fetchColumn() ?? -1; } private function migrationNumberFromFile(string $filename): int { @@ -77,7 +79,7 @@ class Database{ return; } - $db = get_db(); + $db = self::get(); $db->beginTransaction(); try { diff --git a/src/Framework/Filesystem/Filesystem.php b/src/Framework/Filesystem/Filesystem.php index 39a5087..da2dcf0 100644 --- a/src/Framework/Filesystem/Filesystem.php +++ b/src/Framework/Filesystem/Filesystem.php @@ -6,6 +6,7 @@ class Filesystem { $this->validateStorageDir(); $this->validateStorageSubdirs(); $this->migrateTickFiles(); + $this->moveTicksToDatabase(); } // Make sure the storage/ directory exists and is writable @@ -74,6 +75,8 @@ class Filesystem { $lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $modified = false; + // &$line creates a reference to the line in the array ($lines) + // so I can modify it in place foreach ($lines as &$line) { $fields = explode('|', $line); if (count($fields) === 2) { @@ -82,10 +85,69 @@ class Filesystem { $modified = true; } } + unset($line); if ($modified) { file_put_contents($filepath, implode("\n", $lines) . "\n"); // TODO: log properly } } + + // TODO: Delete this sometime before 1.0 + // Move ticks into database + private function moveTicksToDatabase(){ + // It's a temporary migration function, so I'm not going to sweat the + // order of operations to let me reuse the global database. + $db = Database::get(); + $count = $db->query("SELECT COUNT(*) FROM tick")->fetchColumn(); + + // Only migrate from filesystem if there are no ticks already in the database. + if ($count !== 0){ + return; + } + + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(TICKS_DIR, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($files as $file) { + if ($file->isFile() && str_ends_with($file->getFilename(), '.txt')) { + // Construct the date from the path and filename + $dir = pathinfo($file, PATHINFO_DIRNAME); + $dir_parts = explode('/', trim($dir, '/')); + [$year, $month] = array_slice($dir_parts, -2); + $day = pathinfo($file, PATHINFO_FILENAME); + + $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + // Get the time and the text, but discard the mood. + // I've decided against using it + $fields = explode('|', $line); + $time = $fields[0]; + $tick = $fields[2]; + + $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', "$year-$month-$day $time"); + $tickDateTimeUTC = $dateTime->format('Y-m-d H:i:s'); + + $ticks[] = [$tickDateTimeUTC, $tick]; + } + } + } + + // Sort the ticks by dateTime + usort($ticks, function($a, $b) { + return strcmp($a[0], $b[0]); + }); + + // Save ticks to database + foreach ($ticks as $tick){ + // Yes, silly, but I'm testing out the datetime/string SQLite conversion + $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', "$tick[0]"); + $timestamp = $dateTime->format('Y-m-d H:i:s'); + $tickText = $tick[1]; + + $stmt = $db->prepare("INSERT INTO tick(timestamp, tick) values (?, ?)"); + $stmt->execute([$timestamp, $tickText]); + } + } } \ No newline at end of file diff --git a/src/Framework/Router/Router.php b/src/Framework/Router/Router.php index b7a6f1a..5be25f9 100644 --- a/src/Framework/Router/Router.php +++ b/src/Framework/Router/Router.php @@ -21,7 +21,7 @@ class Router { ['mood', 'MoodController@handlePost', ['POST']], ['setup', 'AdminController@showSetup'], ['setup', 'AdminController@handleSetup', ['POST']], - ['tick/{y}/{m}/{d}/{h}/{i}/{s}', 'TickController'], + ['tick/{id}', 'TickController'], ['css/custom/{filename}.css', 'CssController@serveCustomCss'], ]; diff --git a/src/Model/TickModel/TickModel.php b/src/Model/TickModel/TickModel.php index 03a820d..577c16e 100644 --- a/src/Model/TickModel/TickModel.php +++ b/src/Model/TickModel/TickModel.php @@ -1,105 +1,41 @@ strcmp($b, $a)); // sort filenames in reverse chronological order + public function stream(int $limit, int $offset = 0): Generator { + global $db; - $count = 0; - foreach ($tick_files as $file) { - // read all the ticks from the current file and reverse the order - // so the most recent ones are first - // - // each file is a single day, so we never hold more than - // one day's ticks in memory - $lines = array_reverse( - file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) - ); + $stmt = $db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?"); + $stmt->execute([$limit, $offset]); - // split the path to the current file into the date components - $pathParts = explode('/', str_replace('\\', '/', $file)); - - // assign the different components to the appropriate part of the date - $year = $pathParts[count($pathParts) - 3]; - $month = $pathParts[count($pathParts) - 2]; - $day = pathinfo($pathParts[count($pathParts) - 1], PATHINFO_FILENAME); - - foreach ($lines as $line) { - // just keep skipping ticks until we get to the starting point - if ($offset > 0) { - $offset--; - continue; - } - - // Ticks are pipe-delimited: timestamp|text - // But just in case a tick contains a pipe, only split on the first one that occurs - list($time, $mood, $tick) = explode('|', $line, 3); - - // Build the timestamp from the date and time - // Ticks are always stored in UTC - $timestampUTC = "$year-$month-$day $time"; - yield [ - 'timestamp' => $timestampUTC, - 'mood' => $mood, - 'tick' => $tick, - ]; - - if (++$count >= $limit) { - return; - } - } + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + yield [ + 'id' => $row['id'], + 'timestamp' => $row['timestamp'], + 'tick' => $row['tick'], + ]; } } - public static function save(string $tick, string $mood=''): void { - // build the tick path and filename from the current time - $now = new DateTime('now', new DateTimeZone('UTC')); + public function insert(string $tick, ?DateTimeImmutable $datetime = null): void { + global $db; + $datetime ??= new DateTimeImmutable('now', new DateTimeZone('UTC')); + $timestamp = $datetime->format('Y-m-d H:i:s'); - $year = $now->format('Y'); - $month = $now->format('m'); - $day = $now->format('d'); - $time = $now->format('H:i:s'); - - // build the full path to the tick file - $dir = TICKS_DIR . "/$year/$month"; - $filename = "$dir/$day.txt"; - - // create the directory if it doesn't exist - if (!is_dir($dir)) { - mkdir($dir, 0770, true); - } - - // write the tick to the file (the file will be created if it doesn't exist) - $content = $time . '|' . $mood . '|' . $tick . "\n"; - file_put_contents($filename, $content, FILE_APPEND); + $stmt = $db->prepare("INSERT INTO tick(timestamp, tick) values (?, ?)"); + $stmt->execute([$timestamp, $tick]); } - public static function get(string $y, string $m, string $d, string $H, string $i, string $s): array{ - $tickTime = new DateTime("$y-$m-$d $H:$i:$s"); - $timestamp = "$H:$i:$s"; - $file = TICKS_DIR . "/$y/$m/$d.txt"; + public function get(int $id): array { + global $db; - if (!file_exists($file)) { - http_response_code(404); - echo "Tick not found: $file."; - exit; - } + $stmt = $db->prepare("SELECT timestamp, tick FROM tick WHERE id=?"); + $stmt->execute([$id]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); - $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { - if (str_starts_with($line, $timestamp)) { - echo $line; - exit; - list($time, $emoji, $tick) = explode('|', $line, 3); - - return [ - 'tickTime' => $tickTime, - 'emoji' => $emoji, - 'tick' => $tick, - 'config' => ConfigModel::load(), - ]; - } - } + // TODO: Test for existence of row and handle absence. + return [ + 'tickTime' => $row['timestamp'], + 'tick' => $row['tick'], + 'config' => ConfigModel::load(), + ]; } } diff --git a/src/View/HomeView/HomeView.php b/src/View/HomeView/HomeView.php index 4fe87d9..836f340 100644 --- a/src/View/HomeView/HomeView.php +++ b/src/View/HomeView/HomeView.php @@ -13,9 +13,6 @@ class HomeView { ?>
  • - showTickMood): ?> - -
  • diff --git a/storage/db/tkr.sqlite.bak b/storage/db/tkr.sqlite.bak new file mode 100755 index 0000000..656a718 Binary files /dev/null and b/storage/db/tkr.sqlite.bak differ diff --git a/templates/feed/atom.php b/templates/feed/atom.php index 2065016..a00936b 100644 --- a/templates/feed/atom.php +++ b/templates/feed/atom.php @@ -22,17 +22,8 @@ echo '' . "\n"; siteTitle) ?> ' . "\n"; en-us baseUrl . $config->basePath . $tickPath); $tickDate = date(DATE_RSS, strtotime($tick['timestamp'])); $tickTitle = Util::escape_xml($tick['tick']); diff --git a/templates/partials/tick.php b/templates/partials/tick.php index 97fccb0..649151a 100644 --- a/templates/partials/tick.php +++ b/templates/partials/tick.php @@ -1,5 +1,4 @@ - -

    Tick from format('Y-m-d H:i:s'); ?>

    +

    Tick from