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