Initial commit
This commit is contained in:
commit
c6a49dc6e8
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.sqlite
|
||||||
|
*.txt
|
70
README.md
Normal file
70
README.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# tkr
|
||||||
|
|
||||||
|
A bare-bones status feed for self-hosted personal websites.
|
||||||
|
|
||||||
|
Currently very much a work in progress, but it's baically functional.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Deploy the `/tkr` directory to a web server that supports php. It will work either as the root of a (sub)domain (e.g. tky.mydomain.com) or if served from a subdirectory (e.g. mydomain.com/tkr).
|
||||||
|
|
||||||
|
If you serve it from a subdirectory, set the value of `$basePath` in `/app/config.php` to the subdirectory name, excluding the trailing slash (e.g. `/tkr`)
|
||||||
|
|
||||||
|
It provides an rss feed at `/rss` relative to where it's being served (e.g. `/tkr/rss` if served from `/tkr/`). Each rss entry links to an individual post (which I call "ticks").
|
||||||
|
|
||||||
|
## Serving
|
||||||
|
|
||||||
|
The document root should be `/PATH/TO/tkr/public`. This will ensure that only the files that need to be accessible from the internet are served by your web server.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Ticks are stored in files on the filesystem under `/tkr/ticks`. This directory must be writable by the web server user and so SHOULD NOT be served by the web server. If you set your document root to `/tkr/public/`, then you'll be fine.
|
||||||
|
|
||||||
|
The file structure is `YYYY/MM/DD.txt`. That is, each day's ticks are located in a file whose full path is `/tkr/ticks/YEAR/MONTH/DAY.txt`. This is to prevent any single file from getting too large.
|
||||||
|
|
||||||
|
Each entry takes the form `TIMESTAMP|TICK`, where `TIMESTAMP` is the time that the entry was made and `TICK` is the text of the entry.
|
||||||
|
|
||||||
|
For illustration, here's a sample from the file `/tkr/ticks/2025/05/25` on my test system.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# cat /tkr/ticks/2025/05/25.txt
|
||||||
|
23:27:37|some stuff
|
||||||
|
23:27:45|some more, stuff
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initial config
|
||||||
|
|
||||||
|
I'll write this up when I improve it.
|
||||||
|
|
||||||
|
## Sample images
|
||||||
|
|
||||||
|
Logged out
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Logged in
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
RSS
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Single tick
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* An actual setup script
|
||||||
|
* Validate CSRF token on tick submission
|
||||||
|
* Let people set the time zone for ticks
|
||||||
|
* Support basic custom styling
|
||||||
|
* Do that config improvement I implied in the previous section
|
||||||
|
* See if I can get individual ticks to resolve as urls (e.g. /2025/05/26/00/12) rather than doing the query string thing
|
||||||
|
* Clean up the nginx configs
|
||||||
|
* Add an .htaccess example
|
||||||
|
* Maybe h-feed or JSON feed?
|
||||||
|
* Microformat support?
|
||||||
|
* A little more profile info?
|
||||||
|
* Probably a bunch of other stuff I'm not thinking of
|
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:latest
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./tkr-nginx-folder.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
- ./tkr:/tkr
|
||||||
|
php:
|
||||||
|
image: php:fpm
|
||||||
|
volumes:
|
||||||
|
- ./tkr:/tkr
|
33
tkr-nginx-folder.conf
Normal file
33
tkr-nginx-folder.conf
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
location ^~ /tkr {
|
||||||
|
index index.php;
|
||||||
|
alias /tkr/public;
|
||||||
|
|
||||||
|
#location ~ "^/tkr/([0-9]{4}/[0-9]{2}/[0-9]{2}/[0-9]{2}/[0-9]{2}/[0-9]{2})$" {
|
||||||
|
# rewrite "^/tkr/([0-9/]{19})$ /tkr/tick.php?path=$1" break;
|
||||||
|
#}
|
||||||
|
|
||||||
|
location ~ ^/tkr(/.+\.php)$ {
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME /tkr/public/$1;
|
||||||
|
fastcgi_param SCRIPT_NAME $uri;
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param CONTENT_TYPE $content_type;
|
||||||
|
fastcgi_param CONTENT_LENGTH $content_length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny anything else
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to hidden or stray files
|
||||||
|
location ~* \.(htaccess|env|ini|log|bak)$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
23
tkr-nginx.conf
Normal file
23
tkr-nginx.conf
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
root /app/public;
|
||||||
|
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param REQUEST_METHOD $request_method;
|
||||||
|
fastcgi_param CONTENT_TYPE $content_type;
|
||||||
|
fastcgi_param CONTENT_LENGTH $content_length;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(htaccess|env|ini|log|bak)$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
12
tkr/config.php
Normal file
12
tkr/config.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
$dbLocation = __DIR__ . '/db/tkr.sqlite';
|
||||||
|
$tickLocation = __DIR__ . '/ticks';
|
||||||
|
$basePath = '/tkr';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("sqlite:$dbLocation");
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Database connection failed: " . $e->getMessage());
|
||||||
|
}
|
56
tkr/create_db.php
Normal file
56
tkr/create_db.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
// TODO: Replace this whole thing with a setup.php
|
||||||
|
|
||||||
|
function prompt($prompt) {
|
||||||
|
echo $prompt;
|
||||||
|
return trim(fgets(STDIN));
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptSilent($prompt = "Enter Password: ") {
|
||||||
|
if (strncasecmp(PHP_OS, 'WIN', 3) === 0) {
|
||||||
|
// Windows doesn't support shell-based hidden input
|
||||||
|
echo "Warning: Password input not hidden on Windows.\n";
|
||||||
|
return prompt($prompt);
|
||||||
|
} else {
|
||||||
|
// Use shell to disable echo for password input
|
||||||
|
echo $prompt;
|
||||||
|
system('stty -echo');
|
||||||
|
$password = rtrim(fgets(STDIN), "\n");
|
||||||
|
system('stty echo');
|
||||||
|
echo "\n";
|
||||||
|
return $password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbFile = __DIR__ . '/tkr.sqlite';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("sqlite:$dbFile");
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Could not connect to DB: " . $e->getMessage() . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS user (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL
|
||||||
|
)");
|
||||||
|
|
||||||
|
$username = prompt("Enter username: ");
|
||||||
|
$password = promptSilent("Enter password: ");
|
||||||
|
$confirm = promptSilent("Confirm password: ");
|
||||||
|
|
||||||
|
if ($password !== $confirm) {
|
||||||
|
die("Error: Passwords do not match.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO user(username, password_hash) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$username, $passwordHash]);
|
||||||
|
echo "User '$username' created successfully.\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Failed to create user: " . $e->getMessage() . "\n";
|
||||||
|
}
|
0
tkr/db/.gitkeep
Normal file
0
tkr/db/.gitkeep
Normal file
61
tkr/public/index.php
Normal file
61
tkr/public/index.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||||
|
define('ITEMS_PER_PAGE', 25);
|
||||||
|
|
||||||
|
require APP_ROOT . '/config.php';
|
||||||
|
require APP_ROOT . '/session.php';
|
||||||
|
require_once APP_ROOT . '/stream_ticks.php';
|
||||||
|
|
||||||
|
$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
|
||||||
|
$limit = ITEMS_PER_PAGE;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$ticks = iterator_to_array(stream_ticks($tickLocation, $limit, $offset));
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>My ticker</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; margin: 2em; }
|
||||||
|
.tick { margin-bottom: 1em; }
|
||||||
|
.ticktime { color: gray; font-size: 0.9em; }
|
||||||
|
.ticktext {color: black; font-size: 1.0em; }
|
||||||
|
.pagination a { margin: 0 5px; text-decoration: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Welcome! Here's what's ticking.</h2>
|
||||||
|
|
||||||
|
<?php foreach ($ticks as $tick): ?>
|
||||||
|
<div class="tick">
|
||||||
|
<spam class="ticktime"><?= $tick['timestamp'] ?></span>
|
||||||
|
<span class="ticktext"><?= $tick['tick'] ?></spam>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
|
||||||
|
<?php if ($page > 1): ?>
|
||||||
|
<a href="?page=<?= $page - 1 ?>">« Newer</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (count($ticks) === $limit): ?>
|
||||||
|
<a href="?page=<?= $page + 1 ?>">Older »</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$isLoggedIn): ?>
|
||||||
|
<p><a href="<?= $basePath ?>/login.php">Login</a></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<form action="save_tick.php" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
|
<label for="tick">What's ticking?</label>
|
||||||
|
<input name="tick" id="tick" type="text">
|
||||||
|
|
||||||
|
<button type="submit">Tick</button>
|
||||||
|
</form>
|
||||||
|
<p><a href="<?= $basePath ?>/logout.php">Logout</a> <?= htmlspecialchars($_SESSION['username']) ?> </p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
50
tkr/public/login.php
Normal file
50
tkr/public/login.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||||
|
|
||||||
|
require APP_ROOT . '/config.php';
|
||||||
|
require APP_ROOT . '/session.php';
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (!validateCsrfToken($_POST['csrf_token'])) {
|
||||||
|
die('Invalid CSRF token');
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $_POST['username'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT id, username, password_hash FROM user WHERE username = ?");
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
header('Location: ' . $basePath . '/');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = 'Invalid username or password';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$csrf_token = generateCsrfToken();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Login</title></head>
|
||||||
|
<body>
|
||||||
|
<h2>Login</h2>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<p style="color:red"><?= htmlspecialchars($error) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="post" action="<?= $basePath ?>/login.php">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
||||||
|
<label>Username: <input type="text" name="username" required></label><br>
|
||||||
|
<label>Password: <input type="password" name="password" required></label><br>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
tkr/public/logout.php
Normal file
11
tkr/public/logout.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||||
|
|
||||||
|
require APP_ROOT . '/config.php';
|
||||||
|
require APP_ROOT . '/session.php';
|
||||||
|
|
||||||
|
$_SESSION = [];
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
header('Location: ' . $basePath . '/');
|
||||||
|
exit;
|
43
tkr/public/rss/index.php
Normal file
43
tkr/public/rss/index.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
define('APP_ROOT', realpath(__DIR__ . '/../../'));
|
||||||
|
define('ITEMS_PER_PAGE', 25);
|
||||||
|
|
||||||
|
require APP_ROOT . '/config.php';
|
||||||
|
require_once APP_ROOT . '/stream_ticks.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/rss+xml; charset=utf-8');
|
||||||
|
|
||||||
|
$ticks = iterator_to_array(stream_ticks($tickLocation, ITEMS_PER_PAGE));
|
||||||
|
|
||||||
|
echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
||||||
|
?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>My tkr</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="Tick RSS" href="/tkr/rss/">
|
||||||
|
<description>My tkr</description>
|
||||||
|
<language>en-us</language>
|
||||||
|
<lastBuildDate><?php echo date(DATE_RSS); ?></lastBuildDate>
|
||||||
|
|
||||||
|
<?php foreach ($ticks as $tick):
|
||||||
|
[$date, $time] = explode(' ', $tick['timestamp']);
|
||||||
|
$dateParts = explode('-', $date);
|
||||||
|
$timeParts = explode(':', $time);
|
||||||
|
|
||||||
|
[$year, $month, $day] = $dateParts;
|
||||||
|
[$hour, $minute, $second] = $timeParts;
|
||||||
|
|
||||||
|
$tickPath = "$year/$month/$day/$hour/$minute/$second";
|
||||||
|
?>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title><?php echo htmlspecialchars(date(DATE_RFC7231, strtotime($tick['timestamp']))); ?></title>
|
||||||
|
<link><?php echo htmlspecialchars("$basePath/tick.php?path=$tickPath"); ?></link>
|
||||||
|
<description><?php echo htmlspecialchars($tick['tick']); ?></description>
|
||||||
|
<pubDate><?php echo date(DATE_RSS, strtotime($tick['timestamp'])); ?></pubDate>
|
||||||
|
<guid><?php echo htmlspecialchars($tickPath); ?></guid>
|
||||||
|
</item>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
</channel>
|
||||||
|
</rss>
|
44
tkr/public/save_tick.php
Normal file
44
tkr/public/save_tick.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
define('APP_ROOT', realpath(__DIR__ . '/../'));
|
||||||
|
|
||||||
|
require APP_ROOT . '/config.php';
|
||||||
|
require APP_ROOT . '/session.php';
|
||||||
|
|
||||||
|
// ticks must be sent via POST
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' and isset($_POST['tick'])) {
|
||||||
|
// csrf check
|
||||||
|
if (!validateCsrfToken($_POST['csrf_token'])) {
|
||||||
|
die('Invalid CSRF token');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tick = htmlspecialchars($_POST['tick'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
} else {
|
||||||
|
// just go back to the index
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
# write the tick to a new entry
|
||||||
|
$date = new DateTime();
|
||||||
|
|
||||||
|
$year = $date->format('Y');
|
||||||
|
$month = $date->format('m');
|
||||||
|
$day = $date->format('d');
|
||||||
|
$time = $date->format('H:i:s');
|
||||||
|
|
||||||
|
// build the full path to the tick file
|
||||||
|
$dir = "$tickLocation/$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 . "|" . $tick . "\n";
|
||||||
|
file_put_contents($filename, $content, FILE_APPEND);
|
||||||
|
|
||||||
|
// go back to the index and show the latest tick
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
33
tkr/public/tick.php
Normal file
33
tkr/public/tick.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
require '/app/config.php';
|
||||||
|
|
||||||
|
$path = $_GET['path'] ?? '';
|
||||||
|
$parts = explode('/', $path);
|
||||||
|
|
||||||
|
if (count($parts) !== 6) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo "Invalid tick path.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$y, $m, $d, $H, $i, $s] = $parts;
|
||||||
|
$timestamp = "$H:$i:$s";
|
||||||
|
$file = "$tickLocation/$y/$m/$d.txt";
|
||||||
|
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo "Tick not found.";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (str_starts_with($line, $timestamp)) {
|
||||||
|
echo "<h1>Tick from $timestamp on $y-$m-$d</h1>";
|
||||||
|
echo "<p>" . htmlspecialchars(explode('|', $line)[1]) . "</p>";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(404);
|
||||||
|
echo "Tick not found.";
|
17
tkr/session.php
Normal file
17
tkr/session.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$isLoggedIn = isset($_SESSION['username']);
|
||||||
|
|
||||||
|
function generateCsrfToken() {
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCsrfToken($token) {
|
||||||
|
return hash_equals($_SESSION['csrf_token'], $token);
|
||||||
|
}
|
47
tkr/stream_ticks.php
Normal file
47
tkr/stream_ticks.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
// display the requested block of ticks
|
||||||
|
// without storing all ticks in an array
|
||||||
|
function stream_ticks(string $tickLocation, int $limit, int $offset = 0): Generator {
|
||||||
|
$tick_files = glob($tickLocation . '/*/*/*.txt');
|
||||||
|
usort($tick_files, fn($a, $b) => strcmp($b, $a)); // sort filenames in reverse chronological order
|
||||||
|
|
||||||
|
$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)
|
||||||
|
);
|
||||||
|
|
||||||
|
$pathParts = explode('/', str_replace('\\', '/', $file));
|
||||||
|
$date = $pathParts[count($pathParts) - 3] . '-' .
|
||||||
|
$pathParts[count($pathParts) - 2] . '-' .
|
||||||
|
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
|
||||||
|
$tickParts = explode('|', $line, 2);
|
||||||
|
$time = $tickParts[0];
|
||||||
|
$tick = $tickParts[1];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
'timestamp' => $date . ' ' . $time,
|
||||||
|
'tick' => $tick,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (++$count >= $limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
tkr/ticks/.gitkeep
Normal file
0
tkr/ticks/.gitkeep
Normal file
Loading…
x
Reference in New Issue
Block a user