mirror of
https://codeberg.org/vlw/blobber.git
synced 2025-09-13 16:23:41 +02:00
Initial code commit
This commit is contained in:
parent
8b2f369215
commit
42716ed7da
11 changed files with 350 additions and 0 deletions
30
.env.example.ini
Normal file
30
.env.example.ini
Normal file
|
@ -0,0 +1,30 @@
|
|||
; Absolute pathname to a folder where blobs will be stored
|
||||
blob_storage_path = "";
|
||||
|
||||
; Credentials for MaraiaDB
|
||||
mariadb_host = ""
|
||||
mariadb_user = ""
|
||||
mariadb_pass = ""
|
||||
mariadb_db = ""
|
||||
|
||||
; UUID API endpoint
|
||||
uuid_api = "https://uuidgenerator.dev/api/uuid/v7"
|
||||
|
||||
[access]
|
||||
; Allow anyone to upload blobs from /new
|
||||
enable_public_upload = false
|
||||
|
||||
; Allow anyone to create a collection of blobs from /new
|
||||
enable_public_collection = true
|
||||
|
||||
; Allow content to display with inline disposition from /<uuid7> (setting this to false will force-download)
|
||||
enable_public_inline = true
|
||||
|
||||
; Allow anyone to view blob metadata from /<uuid7>/meta
|
||||
enable_public_metadata = true
|
||||
|
||||
; Allow force download of any blob from /<uuid7>/download
|
||||
enable_public_download = true
|
||||
|
||||
[features]
|
||||
enable_blob_versioning = true
|
16
.gitignore
vendored
Executable file
16
.gitignore
vendored
Executable file
|
@ -0,0 +1,16 @@
|
|||
# Bootstrapping #
|
||||
#################
|
||||
vendor
|
||||
.env.ini
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
Icon?
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
.directory
|
5
composer.json
Normal file
5
composer.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"require": {
|
||||
"victorwesterlund/libmysqldriver": "^3.6"
|
||||
}
|
||||
}
|
56
composer.lock
generated
Normal file
56
composer.lock
generated
Normal file
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "c8788fa2c53c43271a259b60098e3faf",
|
||||
"packages": [
|
||||
{
|
||||
"name": "victorwesterlund/libmysqldriver",
|
||||
"version": "3.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/VictorWesterlund/php-libmysqldriver.git",
|
||||
"reference": "adc2fda90a3b8308e8a9df202d5ec418a9220ff8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/VictorWesterlund/php-libmysqldriver/zipball/adc2fda90a3b8308e8a9df202d5ec418a9220ff8",
|
||||
"reference": "adc2fda90a3b8308e8a9df202d5ec418a9220ff8",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"libmysqldriver\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Victor Westerlund",
|
||||
"email": "victor.vesterlund@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Abstraction library for common mysqli features",
|
||||
"support": {
|
||||
"issues": "https://github.com/VictorWesterlund/php-libmysqldriver/issues",
|
||||
"source": "https://github.com/VictorWesterlund/php-libmysqldriver/tree/3.6.1"
|
||||
},
|
||||
"time": "2024-04-29T08:17:12+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
7
public/index.php
Normal file
7
public/index.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Blobber\Router;
|
||||
|
||||
require_once "../src/Init.php";
|
||||
|
||||
(new Router());
|
15
src/Error.php
Normal file
15
src/Error.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Blobber;
|
||||
|
||||
enum Error: int {
|
||||
case SERVICE_UNAVAILABLE = 503;
|
||||
case BLOB_NOT_FOUND = 404;
|
||||
|
||||
case DATABASE_NOT_CONNECTED = 1000;
|
||||
case BLOB_DIR_NOT_READABLE = 1001;
|
||||
|
||||
public static function default(): self {
|
||||
return self::SERVICE_UNAVAILABLE;
|
||||
}
|
||||
}
|
24
src/Init.php
Normal file
24
src/Init.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Blobber;
|
||||
|
||||
require_once "../vendor/autoload.php";
|
||||
|
||||
require_once "Router.php";
|
||||
|
||||
enum ENV: string {
|
||||
// Core configration
|
||||
case BLOB_STORAGE_PATH = "blob_storage_path";
|
||||
|
||||
// Database credentials
|
||||
case MARIADB_HOST = "mariadb_host";
|
||||
case MARIADB_USER = "mariadb_user";
|
||||
case MARIADB_PASS = "mariadb_pass";
|
||||
case MARIADB_DB = "mariadb_db";
|
||||
|
||||
public function get(): string {
|
||||
return $_ENV[$this->value];
|
||||
}
|
||||
}
|
||||
|
||||
$_ENV = parse_ini_file("../.env.ini");
|
106
src/Router.php
Normal file
106
src/Router.php
Normal file
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace Blobber;
|
||||
|
||||
use \mysqli_result;
|
||||
use libmysqldriver\MySQL;
|
||||
|
||||
use Blobber\ENV;
|
||||
use Blobber\Error;
|
||||
use Blobber\Database\Models\{
|
||||
BlobsModel
|
||||
};
|
||||
|
||||
require_once "Error.php";
|
||||
require_once "database/models/Blobs.php";
|
||||
|
||||
const UUID_SUBSTR_LENGTH = 36;
|
||||
|
||||
class Router {
|
||||
private readonly MySQL $db;
|
||||
private readonly mysqli_result|false $blob_meta;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = new MySQL(
|
||||
$_ENV["mariadb_host"],
|
||||
$_ENV["mariadb_user"],
|
||||
$_ENV["mariadb_pass"],
|
||||
$_ENV["mariadb_db"]
|
||||
);
|
||||
|
||||
$this->route();
|
||||
}
|
||||
|
||||
// Get blob UUID from request pathname
|
||||
private static function get_blob_id(): string|false {
|
||||
if (strlen($_SERVER["REQUEST_URI"]) !== UUID_SUBSTR_LENGTH + 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return substr($_SERVER["REQUEST_URI"], 1, UUID_SUBSTR_LENGTH);
|
||||
}
|
||||
|
||||
private static function resp_error(?Error $error): never {
|
||||
// Treat custom error codes as HTTP 500
|
||||
http_response_code($error->value < 600 ? $error->value : 500);
|
||||
|
||||
// Return HTML page if supported by client
|
||||
if ($_SERVER["HTTP_ACCEPT"] === "*/*" || strpos($_SERVER["HTTP_ACCEPT"] ?? "", "text/html") !== false) {
|
||||
die(sprintf(file_get_contents(__DIR__ . "/error.html"), $error->value, $error->name));
|
||||
}
|
||||
|
||||
die($error->name);
|
||||
}
|
||||
|
||||
private function get_blob_meta(): bool {
|
||||
$this->blob_meta = $this->db
|
||||
->for(BlobsModel::TABLE)
|
||||
->where([
|
||||
BlobsModel::ID->value => self::get_blob_id(),
|
||||
BlobsModel::IS_REMOVED->value => false
|
||||
])
|
||||
->limit(1)
|
||||
->select([
|
||||
BlobsModel::MIME_TYPE->value
|
||||
]);
|
||||
|
||||
// Return true if blob exists in database
|
||||
return $this->blob_meta and $this->blob_meta->num_rows === 1;
|
||||
}
|
||||
|
||||
private function resp_blob_inline(): never {
|
||||
$meta = $this->blob_meta->fetch_assoc();
|
||||
|
||||
header("Content-Type: " . $meta[BlobsModel::MIME_TYPE->value]);
|
||||
header("Content-Disposition: inline");
|
||||
|
||||
exit(file_get_contents($this->get_blob_path()));
|
||||
}
|
||||
|
||||
private function get_blob_path(): string {
|
||||
$base_path = ENV::BLOB_STORAGE_PATH->get();
|
||||
|
||||
// Append tailing slash to pathname if absent
|
||||
$base_path = substr($base_path, -1, 1) === "/" ? $base_path : $base_path . "/";
|
||||
|
||||
if (!is_dir($base_path)) {
|
||||
return self::resp_error(Error::BLOB_DIR_NOT_READABLE);
|
||||
}
|
||||
|
||||
return $base_path . self::get_blob_id();
|
||||
}
|
||||
|
||||
private function route() {
|
||||
// Verify database is connected
|
||||
if (!$this->db->ping()) {
|
||||
return self::resp_error(Error::DATABASE_NOT_CONNECTED);
|
||||
}
|
||||
|
||||
// Check that we have a 36-char string and then test it against the database
|
||||
if (!self::get_blob_id() || !$this->get_blob_meta()) {
|
||||
return self::resp_error(Error::BLOB_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->resp_blob_inline();
|
||||
}
|
||||
}
|
67
src/database/blobber.sql
Normal file
67
src/database/blobber.sql
Normal file
|
@ -0,0 +1,67 @@
|
|||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||
START TRANSACTION;
|
||||
SET time_zone = "+00:00";
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8mb4 */;
|
||||
|
||||
|
||||
CREATE TABLE `blobs` (
|
||||
`id` char(36) NOT NULL,
|
||||
`mime_type` varchar(255) NOT NULL DEFAULT 'application/octet-stream',
|
||||
`modify_token` char(255) NOT NULL,
|
||||
`ref_branch_blob` char(36) DEFAULT NULL,
|
||||
`is_removed` tinyint(1) NOT NULL DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE `collections` (
|
||||
`id` char(36) NOT NULL,
|
||||
`modify_token` char(255) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE `rel_collections_blobs` (
|
||||
`ref_collection_id` char(36) NOT NULL,
|
||||
`ref_blob_id` char(36) NOT NULL,
|
||||
`label` varchar(255) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE `rel_collections_collections` (
|
||||
`ref_collection_id` char(36) NOT NULL,
|
||||
`ref_child_collection_id` char(36) NOT NULL,
|
||||
`label` varchar(255) DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
|
||||
ALTER TABLE `blobs`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD KEY `ref_replaces_blob` (`ref_branch_blob`);
|
||||
|
||||
ALTER TABLE `collections`
|
||||
ADD PRIMARY KEY (`id`);
|
||||
|
||||
ALTER TABLE `rel_collections_blobs`
|
||||
ADD KEY `ref_collection_id` (`ref_collection_id`),
|
||||
ADD KEY `ref_blob_id` (`ref_blob_id`);
|
||||
|
||||
ALTER TABLE `rel_collections_collections`
|
||||
ADD KEY `ref_collection_id` (`ref_collection_id`,`ref_child_collection_id`),
|
||||
ADD KEY `ref_child_collection_id` (`ref_child_collection_id`);
|
||||
|
||||
|
||||
ALTER TABLE `blobs`
|
||||
ADD CONSTRAINT `blobs_ibfk_1` FOREIGN KEY (`ref_branch_blob`) REFERENCES `blobs` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `rel_collections_blobs`
|
||||
ADD CONSTRAINT `rel_collections_blobs_ibfk_1` FOREIGN KEY (`ref_blob_id`) REFERENCES `blobs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `rel_collections_blobs_ibfk_2` FOREIGN KEY (`ref_collection_id`) REFERENCES `collections` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `rel_collections_collections`
|
||||
ADD CONSTRAINT `rel_collections_collections_ibfk_1` FOREIGN KEY (`ref_collection_id`) REFERENCES `collections` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `rel_collections_collections_ibfk_2` FOREIGN KEY (`ref_child_collection_id`) REFERENCES `collections` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
COMMIT;
|
||||
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
13
src/database/models/Blobs.php
Normal file
13
src/database/models/Blobs.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace Blobber\Database\Models;
|
||||
|
||||
enum BlobsModel: string {
|
||||
const TABLE = "blobs";
|
||||
|
||||
case ID = "id";
|
||||
case MIME_TYPE = "mime_type";
|
||||
case MODIFY_TOKEN = "modify_token";
|
||||
case REF_BRANCH_BLOB = "ref_branch_blob";
|
||||
case IS_REMOVED = "is_removed";
|
||||
}
|
11
src/error.html
Normal file
11
src/error.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%1$s %2$s</title>
|
||||
</head>
|
||||
<body>
|
||||
<code>ERROR::<strong>%1$s</strong>::<strong>%2$s</strong></code>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue