mirror of
https://github.com/sigmasternchen/drnk.me
synced 2025-03-15 01:38:55 +00:00
feat: Basic project setup
This commit is contained in:
parent
9c6b129914
commit
f5d5166b4b
24 changed files with 514 additions and 0 deletions
49
.github/workflows/upload.yml
vendored
Normal file
49
.github/workflows/upload.yml
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
name: 'Publish to prod'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
|
||||
jobs:
|
||||
upload-prod:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3'
|
||||
- name: Upload
|
||||
env:
|
||||
FTP_SERVER: ${{ secrets.FTP_SERVER }}
|
||||
FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
|
||||
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
|
||||
DB_DSN: ${{ secrets.DB_DSN }}
|
||||
DB_SCHEMA: ${{ secrets.DB_SCHEMA }}
|
||||
DB_USERNAME: ${{ secrets.DB_USERNAME }}
|
||||
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
||||
run: |
|
||||
sudo apt install lftp
|
||||
rm -rf .git .github
|
||||
cp credentials.templ.php credentials.php
|
||||
sed -i -e "s/{DB_DSN}/${DB_DSN}/g" credentials.php
|
||||
sed -i -e "s/{DB_SCHEMA}/${DB_SCHEMA}/g" credentials.php
|
||||
sed -i -e "s/{DB_USERNAME}/${DB_USERNAME}/g" credentials.php
|
||||
sed -i -e "s/{DB_PASSWORD}/${DB_PASSWORD}/g" credentials.php
|
||||
mv maintenance.php maintenance-real.php
|
||||
sed -e 's/false/true/g' maintenance-real.php > maintenance.php
|
||||
lftp -e "
|
||||
set sftp:auto-confirm yes;
|
||||
set ssl:verify-certificate no;
|
||||
open -u ${FTP_USERNAME},${FTP_PASSWORD} sftp://${FTP_SERVER};
|
||||
put maintenance.php -o maintenance.php;
|
||||
mirror --exclude=maintenance-real.php -e -R ./ ./;
|
||||
put maintenance-real.php -o maintenance.php;
|
||||
quit;
|
||||
"
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.idea
|
||||
|
||||
credentials.php
|
||||
|
||||
notes.txt
|
4
context.php
Normal file
4
context.php
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
const DB_CONNECTION = "DB_CONNECTION";
|
||||
const REPOSITORIES = "REPOSITORIES";
|
5
controllers/GET.php
Normal file
5
controllers/GET.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return function (array &$context) {
|
||||
echo "Hello World";
|
||||
};
|
18
controllers/routes.php
Normal file
18
controllers/routes.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
require_once(ROOT . "/router/Router.php");
|
||||
|
||||
function fromController(string $path, string $endpoint = null) {
|
||||
return function(array &$context) use ($path, $endpoint) {
|
||||
if ($endpoint)
|
||||
$context[ENDPOINT] = $endpoint;
|
||||
|
||||
return (require(ROOT . "/controllers/" . $path . ".php"))($context);
|
||||
};
|
||||
}
|
||||
|
||||
return function(Router $router) {
|
||||
$router->addRoute(GET, "/", fromController("/GET"));
|
||||
|
||||
$router->addRoute(GET, "/.*", fromController("/slug/GET"));
|
||||
};
|
5
controllers/slug/GET.php
Normal file
5
controllers/slug/GET.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return function (array &$context) {
|
||||
echo "Hello World 2";
|
||||
};
|
25
core.php
Normal file
25
core.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
const ROOT = __DIR__;
|
||||
|
||||
define("MAINTENANCE_MODE", require(ROOT . "/maintenance.php"));
|
||||
|
||||
if (MAINTENANCE_MODE) {
|
||||
require(ROOT . "/templates/maintenance.php");
|
||||
} else {
|
||||
$connection = require_once(ROOT . "/persistence/connection.php");
|
||||
(require(ROOT . "/persistence/migrate.php"))($connection);
|
||||
|
||||
$repositories = (require_once(ROOT . "/persistence/Repositories.php"))($connection);
|
||||
|
||||
$router = require(ROOT . "/router/Router.php");
|
||||
(require(ROOT . "/controllers/routes.php"))($router);
|
||||
|
||||
require_once(ROOT . "/context.php");
|
||||
$context = [
|
||||
DB_CONNECTION => $connection,
|
||||
REPOSITORIES => $repositories,
|
||||
];
|
||||
|
||||
$router->execute($context);
|
||||
}
|
7
credentials.templ.php
Normal file
7
credentials.templ.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
// not constants so we can minimize their scope
|
||||
$DB_DSN = "{DB_DSN}";
|
||||
$DB_SCHEMA = "{DB_SCHEMA}";
|
||||
$DB_USERNAME = "{DB_USERNAME}";
|
||||
$DB_PASSWORD = "{DB_PASSWORD}";
|
13
html/.htaccess
Normal file
13
html/.htaccess
Normal file
|
@ -0,0 +1,13 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
RewriteRule ^index\.php$ - [L]
|
||||
RewriteCond %{REQUEST_URI} !^/static/
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.php [L]
|
||||
</IfModule>
|
||||
|
||||
php_flag log_errors on
|
||||
php_value error_log /usr/home/usefula/php_dm.log
|
3
html/index.php
Normal file
3
html/index.php
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
require("../core.php");
|
3
maintenance.php
Normal file
3
maintenance.php
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
return false;
|
33
persistence/Repositories.php
Normal file
33
persistence/Repositories.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
require_once(__DIR__ . "/repositories/URLs.php");
|
||||
|
||||
class Repositories {
|
||||
private PDO $connection;
|
||||
|
||||
private array $repositoryClasses = [
|
||||
"urls" => URLs::class,
|
||||
];
|
||||
|
||||
private array $repositoryCache = [];
|
||||
|
||||
public function __construct(PDO $connection) {
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function __get(string $name) {
|
||||
$repository = $this->repositoryCache[$name] ?? null;
|
||||
|
||||
if (!$repository) {
|
||||
$repository = new ($this->repositoryClasses[$name])($this->connection);
|
||||
$this->repositoryCache[$name] = $repository;
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return function(PDO $connection): Repositories {
|
||||
return new Repositories($connection);
|
||||
};
|
7
persistence/connection.php
Normal file
7
persistence/connection.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
return (function() {
|
||||
require(ROOT . "/credentials.php");
|
||||
|
||||
return new PDO($DB_DSN, $DB_USERNAME, $DB_PASSWORD);
|
||||
})();
|
116
persistence/migrate.php
Normal file
116
persistence/migrate.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
// has to be variable instead of const because PHP sucks
|
||||
$MIGRATION_TABLE = "dm_migrations";
|
||||
|
||||
function ensureDbStructureForMigrations(PDO $connection) {
|
||||
global $MIGRATION_TABLE;
|
||||
|
||||
require(ROOT . "/credentials.php");
|
||||
|
||||
if ($connection->query(<<<EOF
|
||||
SELECT
|
||||
TABLE_NAME
|
||||
FROM
|
||||
information_schema.TABLES
|
||||
WHERE
|
||||
TABLE_SCHEMA LIKE '${DB_SCHEMA}' AND
|
||||
TABLE_TYPE LIKE 'BASE TABLE' AND
|
||||
TABLE_NAME = '${MIGRATION_TABLE}'
|
||||
EOF)->rowCount() == 0
|
||||
) {
|
||||
if ($connection->exec(<<<EOF
|
||||
CREATE TABLE `${MIGRATION_TABLE}` (
|
||||
`id` INTEGER PRIMARY KEY,
|
||||
`file` VARCHAR(255),
|
||||
`applied` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
EOF) === false
|
||||
) {
|
||||
die("unable to bootstrap migrations: " . $connection->errorCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAllMigrations() {
|
||||
$files = scandir(ROOT . "/persistence/migrations/");
|
||||
$files = array_values(array_filter($files, fn($f) => $f[0] != "."));
|
||||
sort($files);
|
||||
|
||||
$migrations = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$delimiterPos = strpos($file, "_");
|
||||
$migrations[intval(substr($file, 0, $delimiterPos))] = $file;
|
||||
}
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
function getAppliedMigrations(PDO $connection) {
|
||||
global $MIGRATION_TABLE;
|
||||
|
||||
$result = $connection->query(<<<EOF
|
||||
SELECT * FROM `${MIGRATION_TABLE}`
|
||||
EOF);
|
||||
|
||||
$migrations = [];
|
||||
foreach ($result->fetchAll() as $row) {
|
||||
$migrations[$row["id"]] = $row["file"];
|
||||
}
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
function getMigrationsToApply(PDO $connection) {
|
||||
$all = getAllMigrations();
|
||||
$applied = getAppliedMigrations($connection);
|
||||
|
||||
foreach ($applied as $id => $file) {
|
||||
unset($all[$id]);
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
function executeSqlScript(PDO $connection, string $sql, string $file) {
|
||||
if ($connection->exec($sql) === false) {
|
||||
die("failed to apply migration " . $file . ": " . $connection->errorCode());
|
||||
}
|
||||
}
|
||||
|
||||
function applyMigration(PDO $connection, int $id, string $file) {
|
||||
global $MIGRATION_TABLE;
|
||||
|
||||
$connection->beginTransaction();
|
||||
|
||||
$sql = file_get_contents(ROOT . "/persistence/migrations/" . $file);
|
||||
if (!$sql) {
|
||||
die("Unable to read migration file: " . $file);
|
||||
}
|
||||
|
||||
executeSqlScript($connection, $sql, $file);
|
||||
|
||||
$statement = $connection->prepare(<<<EOF
|
||||
INSERT INTO `${MIGRATION_TABLE}`
|
||||
(`id`, `file`) VALUES
|
||||
(?, ?)
|
||||
EOF);
|
||||
$statement->execute([$id, $file]);
|
||||
|
||||
try {
|
||||
$connection->commit();
|
||||
} catch (PDOException $e) {
|
||||
// this might happen if the migration script contains a DDL statement
|
||||
// -> ignore
|
||||
}
|
||||
}
|
||||
|
||||
return function(PDO $connection) {
|
||||
ensureDbStructureForMigrations($connection);
|
||||
|
||||
$migrations = getMigrationsToApply($connection);
|
||||
foreach ($migrations as $id => $file) {
|
||||
applyMigration($connection, $id, $file);
|
||||
}
|
||||
};
|
0
persistence/migrations/.gitkeep
Normal file
0
persistence/migrations/.gitkeep
Normal file
12
persistence/migrations/0001_addUrlTable.sql
Normal file
12
persistence/migrations/0001_addUrlTable.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
CREATE TABLE `dm_urls` (
|
||||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
`created` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`deleted` DATETIME,
|
||||
`slug` VARCHAR(100) NOT NULL,
|
||||
`url` VARCHAR(255) NOT NULL,
|
||||
`access_key` VARCHAR(255),
|
||||
|
||||
INDEX (`slug`)
|
||||
);
|
21
persistence/models/URL.php
Normal file
21
persistence/models/URL.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
class URL {
|
||||
public int $id;
|
||||
public DateTime $created;
|
||||
public DateTime $updated;
|
||||
public DateTime $deleted;
|
||||
public string $slug;
|
||||
public string $url;
|
||||
public string $accessKey;
|
||||
|
||||
public function __construct(
|
||||
string $slug,
|
||||
string $url,
|
||||
string $accessKey
|
||||
) {
|
||||
$this->slug = $slug;
|
||||
$this->url = $url;
|
||||
$this->accessKey = $accessKey;
|
||||
}
|
||||
}
|
40
persistence/repositories/URLs.php
Normal file
40
persistence/repositories/URLs.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
require_once(__DIR__ . "/../models/URL.php");
|
||||
|
||||
class URLs {
|
||||
private string $table = "dm_urls";
|
||||
|
||||
private PDO $connection;
|
||||
|
||||
public function __construct(PDO $connection) {
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function add(URL $entry) {
|
||||
$statement = $this->connection->prepare(<<<EOF
|
||||
INSERT INTO `$this->table`
|
||||
(`slug`, `url`, `access_key`) VALUES
|
||||
(?, ?, ?)
|
||||
EOF);
|
||||
$statement->execute([
|
||||
$entry->slug,
|
||||
$entry->url,
|
||||
$entry->accessKey,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBySlug(string $slug) {
|
||||
$statement = $this->connection->prepare(<<<EOF
|
||||
SELECT * FROM `$this->table`
|
||||
WHERE `slug` = ?
|
||||
EOF);
|
||||
$statement->execute([$slug]);
|
||||
|
||||
if ($statement->rowCount() == 0) {
|
||||
return null;
|
||||
} else {
|
||||
return $statement->fetch();
|
||||
}
|
||||
}
|
||||
}
|
78
router/Router.php
Normal file
78
router/Router.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
require_once(ROOT . "/utils/arrays.php");
|
||||
require_once(ROOT . "/utils/error.php");
|
||||
|
||||
const GET = "GET";
|
||||
const POST = "POST";
|
||||
const PUT = "PUT";
|
||||
const DELETE = "DELETE";
|
||||
const HEAD = "HEAD";
|
||||
const CONNECT = "CONNECT";
|
||||
const OPTIONS = "OPTIONS";
|
||||
const TRACE = "TRACE";
|
||||
const PATCH = "PATCH";
|
||||
|
||||
class Router {
|
||||
private $routes = [];
|
||||
private $prefix = "";
|
||||
public $notFoundHandler;
|
||||
|
||||
function __construct(string $prefix = "") {
|
||||
$this->notFoundHandler = function(array &$context) {
|
||||
setStatusCode(404);
|
||||
require(ROOT . "/templates/404.php");
|
||||
};
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
private function findRoute(string $method, string $url) {
|
||||
if (!key_exists($method, $this->routes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$paths = $this->routes[$method];
|
||||
|
||||
foreach ($paths as $path => $handler) {
|
||||
if (preg_match("/^" . str_replace("/", "\/", $path, ) . "$/", $url)) {
|
||||
return $handler;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getPath($uri) {
|
||||
if (($i = strpos($uri, "?")) !== false) {
|
||||
return substr($uri, 0, $i);
|
||||
} else {
|
||||
return $uri;
|
||||
}
|
||||
}
|
||||
|
||||
public function addRoute(string $method, string $path, $handler) {
|
||||
array_get_or_add($method, $this->routes, [])[$path] = $handler;
|
||||
}
|
||||
|
||||
public function execute(array &$context = []) {
|
||||
$path = $this->getPath($_SERVER["REQUEST_URI"]);
|
||||
$path = substr($path, strlen($this->prefix));
|
||||
|
||||
$route = $this->findRoute($_SERVER["REQUEST_METHOD"], $path);
|
||||
|
||||
if (!$route) {
|
||||
$route = $this->notFoundHandler;
|
||||
}
|
||||
|
||||
$context["REQUEST_PATH"] = $path;
|
||||
|
||||
return $route($context);
|
||||
}
|
||||
|
||||
// calling magic to make the router a handler and thus cascade-able
|
||||
public function __invoke(array &$context = []) {
|
||||
return $this->execute($context);
|
||||
}
|
||||
}
|
||||
|
||||
return new Router();
|
1
templates/404.php
Normal file
1
templates/404.php
Normal file
|
@ -0,0 +1 @@
|
|||
404 Not found
|
1
templates/maintenance.php
Normal file
1
templates/maintenance.php
Normal file
|
@ -0,0 +1 @@
|
|||
We are currently in maintenance mode.
|
10
utils/arrays.php
Normal file
10
utils/arrays.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
function &array_get_or_add($needle, array &$haystack, $default=null) {
|
||||
if (key_exists($needle, $haystack)) {
|
||||
return $haystack[$needle];
|
||||
} else {
|
||||
$haystack[$needle] = $default;
|
||||
return $haystack[$needle];
|
||||
}
|
||||
}
|
13
utils/error.php
Normal file
13
utils/error.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
function setStatusCode(int $status) {
|
||||
header("HTTP/", true, $status);
|
||||
}
|
||||
|
||||
function errorResponse(string $error, string $description) {
|
||||
return [
|
||||
"error" => $error,
|
||||
"description" => $description,
|
||||
"timestamp" => (new DateTime())->format("c")
|
||||
];
|
||||
}
|
45
utils/http.php
Normal file
45
utils/http.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
function request(string $method, string $url, string $body = null, array $headers = []) {
|
||||
$curlResource = curl_init();
|
||||
|
||||
curl_setopt($curlResource, CURLOPT_URL, $url);
|
||||
curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curlResource, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($curlResource, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($curlResource, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($curlResource, CURLOPT_FAILONERROR, true);
|
||||
|
||||
curl_setopt($curlResource, CURLOPT_VERBOSE, true);
|
||||
|
||||
if ($body)
|
||||
curl_setopt($curlResource, CURLOPT_POSTFIELDS, $body);
|
||||
|
||||
if ($headers) {
|
||||
if (!array_is_list($headers))
|
||||
$headers = array_map(fn($k, $v) => $k . ": " . $v, array_keys($headers), array_values($headers));
|
||||
|
||||
|
||||
curl_setopt($curlResource, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
|
||||
$body = curl_exec($curlResource);
|
||||
$result = [
|
||||
"body" => $body,
|
||||
"isError" => curl_errno($curlResource),
|
||||
"error" => curl_error($curlResource),
|
||||
"status" => curl_getinfo($curlResource, CURLINFO_RESPONSE_CODE),
|
||||
];
|
||||
|
||||
curl_close($curlResource);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function getRequest(string $url, array $headers = []) {
|
||||
return request("GET", $url, null, $headers);
|
||||
}
|
||||
|
||||
function postRequest(string $url, string $body = null, $headers = []) {
|
||||
return request("POST", $url, $body, $headers);
|
||||
}
|
Loading…
Reference in a new issue