Compare commits

...

9 Commits

Author SHA1 Message Date
lexx 454cece1d8 auth fiux 2026-05-18 20:47:14 +03:00
lexx c49acd74de imporve import export 2026-05-18 19:56:11 +03:00
lexx 965b7e660b command rename 2026-05-18 19:09:55 +03:00
lexx 8c7f65abf5 improve fileservice 2026-05-18 19:07:47 +03:00
lexx 507d643aee sourece phpopyion 2026-05-18 19:04:57 +03:00
lexx f1a0d6a2b1 image filter generator 2026-05-18 18:38:43 +03:00
lexx f74c850e01 import fixed 2026-05-18 18:30:56 +03:00
lexx 9d6d39fe62 fixed import 2026-05-18 18:07:38 +03:00
lexx a9851847fc new dist 2026-05-15 17:07:12 +03:00
38 changed files with 1270 additions and 212 deletions
-1
View File
@@ -9,7 +9,6 @@
"ext-imagick": "*", "ext-imagick": "*",
"ext-pdo": "*", "ext-pdo": "*",
"php": "^8.4", "php": "^8.4",
"phpoption/phpoption": "^1.9",
"spatie/image-optimizer": "^1.8", "spatie/image-optimizer": "^1.8",
"staudenmeir/laravel-cte": "^1.0", "staudenmeir/laravel-cte": "^1.0",
"intervention/image": "^4.0" "intervention/image": "^4.0"
Generated
+7 -7
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b2f189b5c64498c6190267db27e55494", "content-hash": "a1bf12f1e2b86bc0da8547f2f6944a86",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -797,16 +797,16 @@
}, },
{ {
"name": "intervention/image", "name": "intervention/image",
"version": "4.0.4", "version": "4.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Intervention/image.git", "url": "https://github.com/Intervention/image.git",
"reference": "f58d379b1f13c036b2ef5c3c26eb4b0c88b647ed" "reference": "fb795553f76afbe55c80d32b6bfe2090a6b1a0af"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/f58d379b1f13c036b2ef5c3c26eb4b0c88b647ed", "url": "https://api.github.com/repos/Intervention/image/zipball/fb795553f76afbe55c80d32b6bfe2090a6b1a0af",
"reference": "f58d379b1f13c036b2ef5c3c26eb4b0c88b647ed", "reference": "fb795553f76afbe55c80d32b6bfe2090a6b1a0af",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -853,7 +853,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Intervention/image/issues", "issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/4.0.4" "source": "https://github.com/Intervention/image/tree/4.1.0"
}, },
"funding": [ "funding": [
{ {
@@ -869,7 +869,7 @@
"type": "ko_fi" "type": "ko_fi"
} }
], ],
"time": "2026-05-03T04:47:13+00:00" "time": "2026-05-15T06:52:36+00:00"
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"main.js": { "main.js": {
"file": "assets/main-DH0OAeUr.js", "file": "assets/main-DtbuHUXl.js",
"name": "main", "name": "main",
"src": "main.js", "src": "main.js",
"isEntry": true, "isEntry": true,
"css": [ "css": [
"assets/main-BVNnoznq.css" "assets/main-BadhVKbO.css"
] ]
} }
} }
+4
View File
@@ -1038,6 +1038,7 @@
"integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1614,6 +1615,7 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0", "immutable": "^4.0.0",
@@ -1654,6 +1656,7 @@
"integrity": "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==", "integrity": "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/sourcemap-codec": "^1.4.15",
@@ -1716,6 +1719,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
"integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==", "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.20.1",
"postcss": "^8.4.36", "postcss": "^8.4.36",
+6
View File
@@ -0,0 +1,6 @@
{
"name": "lucent-laravel",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+2
View File
@@ -50,4 +50,6 @@ interface AuthService
public function registerAdmin(string $name, string $email): User; public function registerAdmin(string $name, string $email): User;
public function validateRoles(array $roles): array; public function validateRoles(array $roles): array;
public function isExternal(): bool;
public function redirectHome(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse;
} }
+9
View File
@@ -220,4 +220,13 @@ readonly class AuthServiceLucent implements AuthService
->values() ->values()
->toArray(); ->toArray();
} }
public function isExternal(): bool
{
return false;
}
public function redirectHome(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
{
return redirect("/home");
}
} }
+9
View File
@@ -102,4 +102,13 @@ readonly class AuthServiceLunar implements AuthService
->values() ->values()
->toArray(); ->toArray();
} }
public function isExternal(): bool
{
return true;
}
public function redirectHome(): \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
{
return redirect("/lunar");
}
} }
+1 -1
View File
@@ -3,7 +3,7 @@
namespace Lucent\Account; namespace Lucent\Account;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use PhpOption\Option; use Lucent\Option\Option;
interface UserRepo interface UserRepo
{ {
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Lucent\Account;
use Carbon\Carbon; use Carbon\Carbon;
use Lucent\Database\Database; use Lucent\Database\Database;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use PhpOption\Option; use Lucent\Option\Option;
class UserRepoLucent implements UserRepo class UserRepoLucent implements UserRepo
{ {
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Lucent\Account;
use Carbon\Carbon; use Carbon\Carbon;
use Lucent\Database\Database; use Lucent\Database\Database;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use PhpOption\Option; use Lucent\Option\Option;
class UserRepoLunar implements UserRepo class UserRepoLunar implements UserRepo
{ {
+1 -1
View File
@@ -8,7 +8,7 @@ use Lucent\File\FileService;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Data\Schema; use Lucent\Data\Schema;
use Lucent\Schema\SchemaService; use Lucent\Schema\SchemaService;
use PhpOption\Option; use Lucent\Option\Option;
final class ChannelService final class ChannelService
{ {
@@ -8,7 +8,7 @@ use Lucent\Data\Schema;
use Lucent\Schema\SchemaService; use Lucent\Schema\SchemaService;
use Lucent\File\FileService; use Lucent\File\FileService;
class CompileSchemas extends Command class CompileSchemasCommand extends Command
{ {
protected $signature = "lucent:schemas"; protected $signature = "lucent:schemas";
@@ -4,12 +4,14 @@ namespace Lucent\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\ResultType\Error;
use Lucent\ResultType\Result;
use Lucent\ResultType\Success;
use ZipArchive; use ZipArchive;
class Export extends Command class ExportCommand extends Command
{ {
protected $signature = "lucent:export"; protected $signature = "lucent:export";
protected $prefix = "lucent_";
protected $description = "Export data and files"; protected $description = "Export data and files";
@@ -32,8 +34,30 @@ class Export extends Command
$stamp = now()->format("Y_m_d_His"); $stamp = now()->format("Y_m_d_His");
$sqlFile = $exportDir . "/dump_{$stamp}.sql"; $sqlFile = $exportDir . "/dump_{$stamp}.sql";
$zipFile = $exportDir . "/export_{$stamp}.zip"; $zipFile = $exportDir . "/export_{$stamp}.zip";
$filesDir = $fileService->loadPublicDisk()->path("lucent/files");
// Dump database $result = $this->dumpDatabase($db, $tables, $sqlFile)->flatMap(
fn($sql) => $this->buildZip($sql, $filesDir, $zipFile),
);
if (file_exists($sqlFile)) {
unlink($sqlFile);
}
if ($result->error()->isDefined()) {
$this->error($result->error()->get());
return;
}
$this->info("Exported to {$zipFile}");
}
/** @return Result<string, string> */
private function dumpDatabase(
array $db,
array $tables,
string $sqlFile,
): Result {
$tableArgs = collect($tables)->map(fn($t) => "-t {$t}")->join(" "); $tableArgs = collect($tables)->map(fn($t) => "-t {$t}")->join(" ");
$command = sprintf( $command = sprintf(
"PGPASSWORD=%s pg_dump -h %s -p %s -U %s -d %s %s --no-owner --no-acl > %s", "PGPASSWORD=%s pg_dump -h %s -p %s -U %s -d %s %s --no-owner --no-acl > %s",
@@ -49,27 +73,29 @@ class Export extends Command
exec($command, result_code: $code); exec($command, result_code: $code);
if ($code !== 0) { if ($code !== 0) {
$this->error("pg_dump failed"); return Error::create("pg_dump failed.");
return;
} }
$this->info("Database dumped."); $this->info("Database dumped.");
// Zip SQL + files return Success::create($sqlFile);
$publicDisk = $fileService->loadPublicDisk(); }
$filesDir = $publicDisk->path("lucent/files");
/** @return Result<string, string> */
private function buildZip(
string $sqlFile,
string $filesDir,
string $zipFile,
): Result {
$zip = new ZipArchive(); $zip = new ZipArchive();
if ( if (
$zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== $zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !==
true true
) { ) {
$this->error("Could not create zip archive."); return Error::create("Could not create zip archive.");
return;
} }
$zip->addFile($sqlFile, "dump_{$stamp}.sql"); $zip->addFile($sqlFile, basename($sqlFile));
if (is_dir($filesDir)) { if (is_dir($filesDir)) {
$iterator = new \RecursiveIteratorIterator( $iterator = new \RecursiveIteratorIterator(
@@ -91,9 +117,6 @@ class Export extends Command
$zip->close(); $zip->close();
// Clean up originals return Success::create($zipFile);
unlink($sqlFile);
$this->info("Exported to {$zipFile}");
} }
} }
@@ -6,7 +6,7 @@ use Illuminate\Console\Command;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Schema\CollectionSchema; use Lucent\Schema\CollectionSchema;
class GenerateCollectionSchema extends Command class GenerateCollectionSchemaCommand extends Command
{ {
protected $signature = 'lucent:generate:collection {name}'; protected $signature = 'lucent:generate:collection {name}';
@@ -3,8 +3,9 @@
namespace Lucent\Commands; namespace Lucent\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Str;
class GenerateImageFilter extends Command class GenerateImageFilterCommand extends Command
{ {
protected $signature = "lucent:generate:image_filter {name}"; protected $signature = "lucent:generate:image_filter {name}";
@@ -20,7 +21,10 @@ class GenerateImageFilter extends Command
mkdir($dir, 0755, true); mkdir($dir, 0755, true);
} }
$filePath = "{$dir}/{$name}.php"; $className = Str::of($name)->camel()->ucfirst() . "ImageFilter";
$pathName = Str::of($name)->snake()->lower();
$filePath = "{$dir}/{$className}.php";
if (file_exists($filePath)) { if (file_exists($filePath)) {
$this->error("Filter {$name} already exists at {$filePath}"); $this->error("Filter {$name} already exists at {$filePath}");
@@ -35,7 +39,7 @@ class GenerateImageFilter extends Command
use Lucent\ImageFilterInterface; use Lucent\ImageFilterInterface;
use Intervention\Image\Interfaces\ImageInterface; use Intervention\Image\Interfaces\ImageInterface;
class {$name} implements ImageFilterInterface class {$className} implements ImageFilterInterface
{ {
public function apply(ImageInterface \$image): ImageInterface public function apply(ImageInterface \$image): ImageInterface
{ {
@@ -47,7 +51,7 @@ class GenerateImageFilter extends Command
} }
public function getPath(): string { public function getPath(): string {
return "{$name}"; return "{$pathName}";
} }
} }
PHP; PHP;
@@ -4,9 +4,12 @@ namespace Lucent\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\ResultType\Error;
use Lucent\ResultType\Result;
use Lucent\ResultType\Success;
use ZipArchive; use ZipArchive;
class Import extends Command class ImportCommand extends Command
{ {
protected $signature = "lucent:import"; protected $signature = "lucent:import";
@@ -44,26 +47,44 @@ class Import extends Command
return; return;
} }
// Extract to temp directory
$tempDir = storage_path("exports/.import_tmp_" . uniqid()); $tempDir = storage_path("exports/.import_tmp_" . uniqid());
$result = $this->extractZip($zipFile, $tempDir)
->flatMap(fn($dir) => $this->restoreDatabase($dir))
->flatMap(fn($dir) => $this->restoreFiles($dir, $fileService));
$this->cleanup($tempDir);
if ($result->error()->isDefined()) {
$this->error($result->error()->get());
return;
}
$this->info("Import complete.");
}
/** @return Result<string, string> */
private function extractZip(string $zipFile, string $tempDir): Result
{
mkdir($tempDir, 0755, true); mkdir($tempDir, 0755, true);
$zip = new ZipArchive(); $zip = new ZipArchive();
if ($zip->open($zipFile) !== true) { if ($zip->open($zipFile) !== true) {
$this->error("Could not open zip archive."); return Error::create("Could not open zip archive.");
$this->cleanup($tempDir);
return;
} }
$zip->extractTo($tempDir); $zip->extractTo($tempDir);
$zip->close(); $zip->close();
// Restore database return Success::create($tempDir);
}
/** @return Result<string, string> */
private function restoreDatabase(string $tempDir): Result
{
$sqlFiles = glob($tempDir . "/*.sql"); $sqlFiles = glob($tempDir . "/*.sql");
if (empty($sqlFiles)) { if (empty($sqlFiles)) {
$this->error("No SQL dump found inside the archive."); return Error::create("No SQL dump found inside the archive.");
$this->cleanup($tempDir);
return;
} }
$db = config("database.connections.pgsql"); $db = config("database.connections.pgsql");
@@ -74,7 +95,6 @@ class Import extends Command
"lucent_edges", "lucent_edges",
]; ];
// Truncate existing tables before restore
$truncate = collect($tables) $truncate = collect($tables)
->map(fn($t) => "TRUNCATE TABLE {$t} CASCADE;") ->map(fn($t) => "TRUNCATE TABLE {$t} CASCADE;")
->join(" "); ->join(" ");
@@ -91,12 +111,6 @@ class Import extends Command
exec($truncateCmd, result_code: $truncateCode); exec($truncateCmd, result_code: $truncateCode);
if ($truncateCode !== 0) {
$this->error("Failed to truncate existing tables.");
$this->cleanup($tempDir);
return;
}
$restoreCmd = sprintf( $restoreCmd = sprintf(
"PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -f %s", "PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -f %s",
$db["password"], $db["password"],
@@ -108,53 +122,53 @@ class Import extends Command
); );
exec($restoreCmd, result_code: $restoreCode); exec($restoreCmd, result_code: $restoreCode);
if ($restoreCode !== 0) { if ($restoreCode !== 0) {
$this->error("Database restore failed."); return Error::create("Database restore failed.");
$this->cleanup($tempDir);
return;
} }
$this->info("Database restored."); $this->info("Database restored.");
// Replace files return Success::create($tempDir);
}
/** @return Result<null, string> */
private function restoreFiles(
string $tempDir,
FileService $fileService,
): Result {
$srcFilesDir = $tempDir . "/files"; $srcFilesDir = $tempDir . "/files";
if (is_dir($srcFilesDir)) { if (!is_dir($srcFilesDir)) {
$publicDisk = $fileService->loadPublicDisk();
$destFilesDir = $publicDisk->path("lucent/files");
// Remove existing files directory or create it if missing
if (is_dir($destFilesDir)) {
exec("rm -rf " . escapeshellarg($destFilesDir));
} else {
mkdir($destFilesDir, 0755, true);
}
exec(
sprintf(
"cp -R %s/* %s",
escapeshellarg($srcFilesDir),
escapeshellarg($destFilesDir),
),
result_code: $copyCode,
);
if ($copyCode !== 0) {
$this->error("Failed to restore files.");
$this->cleanup($tempDir);
return;
}
$this->info("Files restored.");
} else {
$this->warn( $this->warn(
"No files directory found in archive — skipping file restore.", "No files directory found in archive — skipping file restore.",
); );
return Success::create(null);
} }
$this->cleanup($tempDir); $publicDisk = $fileService->loadPublicDisk();
$this->info("Import complete."); $destFilesDir = $publicDisk->path("lucent/files");
if (is_dir($destFilesDir)) {
exec("rm -rf " . escapeshellarg($destFilesDir));
}
mkdir($destFilesDir, 0755, true);
exec(
sprintf(
"cp -R %s/* %s",
escapeshellarg($srcFilesDir),
escapeshellarg($destFilesDir),
),
result_code: $copyCode,
);
if ($copyCode !== 0) {
return Error::create("Failed to restore files.");
}
$this->info("Files restored.");
return Success::create(null);
} }
private function cleanup(string $dir): void private function cleanup(string $dir): void
@@ -5,7 +5,7 @@ namespace Lucent\Commands;
use DirectoryIterator; use DirectoryIterator;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class LiveLink extends Command class LiveLinkCommand extends Command
{ {
protected $signature = 'lucent:livelink'; protected $signature = 'lucent:livelink';
@@ -8,7 +8,7 @@ use Lucent\File\FileRepo;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\Query\Query; use Lucent\Query\Query;
class RebuildThumbnails extends Command class RebuildThumbnailsCommand extends Command
{ {
protected $signature = "lucent:rebuild:thumbnails"; protected $signature = "lucent:rebuild:thumbnails";
@@ -27,8 +27,8 @@ class RebuildThumbnails extends Command
$files = FileRepo::query()->get(); $files = FileRepo::query()->get();
$disk = $this->fileService->loadPublicDisk(); $disk = $this->fileService->loadPublicDisk();
foreach ($files as $file) { foreach ($files as $file) {
$this->fileService->createTemplates($disk, $file->path);
try { try {
$this->fileService->createTemplates($disk, $file->path);
} catch (Exception $e) { } catch (Exception $e) {
$this->error( $this->error(
"File " . $file->filename . " could not be rebuilt \n", "File " . $file->filename . " could not be rebuilt \n",
@@ -6,7 +6,7 @@ use Illuminate\Console\Command;
use Lucent\Edge\EdgeService; use Lucent\Edge\EdgeService;
use Lucent\Query\Query; use Lucent\Query\Query;
class RemoveOrphanEdges extends Command class RemoveOrphanEdgesCommand extends Command
{ {
protected $signature = 'lucent:removeOrphanEdges'; protected $signature = 'lucent:removeOrphanEdges';
@@ -11,7 +11,7 @@ use Lucent\Setup\Step\LucentConfigStep;
use Lucent\Setup\Step\StorageLinkSetupStep; use Lucent\Setup\Step\StorageLinkSetupStep;
use Lucent\Setup\Step\StorageSetupStep; use Lucent\Setup\Step\StorageSetupStep;
class Setup extends Command class SetupCommand extends Command
{ {
protected $signature = "lucent:setup"; protected $signature = "lucent:setup";
@@ -7,7 +7,7 @@ use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Lucent\Database\Database; use Lucent\Database\Database;
class SetupDatabase extends Command class SetupDatabaseCommand extends Command
{ {
protected $signature = "lucent:setup-db"; protected $signature = "lucent:setup-db";
protected $prefix = "lucent_"; protected $prefix = "lucent_";
+53 -25
View File
@@ -4,6 +4,7 @@ namespace Lucent\File;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\Format; use Intervention\Image\Format;
@@ -12,6 +13,9 @@ use Lucent\Channel\ChannelService;
use Lucent\Id\Id; use Lucent\Id\Id;
use Lucent\LucentException; use Lucent\LucentException;
use Lucent\Data\File as DataFile; use Lucent\Data\File as DataFile;
use Lucent\ResultType\Error;
use Lucent\ResultType\Result;
use Lucent\ResultType\Success;
use Spatie\ImageOptimizer\OptimizerChainFactory; use Spatie\ImageOptimizer\OptimizerChainFactory;
class FileService class FileService
@@ -19,6 +23,7 @@ class FileService
public function __construct( public function __construct(
public ChannelService $channelService, public ChannelService $channelService,
public ImageManager $imageManager, public ImageManager $imageManager,
public Logger $logger,
) {} ) {}
public function createFromUrl( public function createFromUrl(
@@ -68,7 +73,10 @@ class FileService
} }
if ($this->isImage($mimetype)) { if ($this->isImage($mimetype)) {
$this->createTemplates($disk, $path); $result = $this->createTemplates($disk, $path);
if ($result->error()->isDefined()) {
throw new LucentException($result->error()->get());
}
} }
[$width, $height] = $this->isImage($mimetype) [$width, $height] = $this->isImage($mimetype)
@@ -124,45 +132,65 @@ class FileService
return Storage::disk(config("lucent.private_disk")); return Storage::disk(config("lucent.private_disk"));
} }
public function createTemplates(Filesystem $disk, string $path): void /** @return Result<null, string> */
public function createTemplates(Filesystem $disk, string $path): Result
{ {
$originalImage = $this->imageManager->decode( $filePath = "lucent/" . $path;
$this->loadPublicDisk()->get("lucent/" . $path),
);
foreach ($this->channelService->channel->imageFilters as $filterClass) {
$imageClone = clone $originalImage;
$filterClassInstance = new $filterClass();
$image = $imageClone->modify($filterClassInstance); if (!$this->loadPublicDisk()->exists($filePath)) {
return Error::create("File not found: {$filePath}");
}
$originalImage = $this->imageManager->decode(
$this->loadPublicDisk()->get($filePath),
);
foreach ($this->channelService->channel->imageFilters as $filterClass) {
$filterClassInstance = new $filterClass();
$templateUri = $templateUri =
"lucent/templates/" . "lucent/templates/" .
$filterClassInstance->getPath() . $filterClassInstance->getPath() .
"/" . "/" .
substr($path, 0, strrpos($path, ".")) . substr($path, 0, strrpos($path, ".")) .
".webp"; ".webp";
$disk->put(
$templateUri,
$image->encodeUsingFormat( $ok = $disk->put(
$templateUri,
(clone $originalImage)
->modify($filterClassInstance)
->encodeUsingFormat(
Format::WEBP,
progressive: true,
quality: 80,
),
);
if (!$ok) {
return Error::create(
"Failed to write template: {$templateUri}",
);
}
}
$thumbUri =
"lucent/thumbs/" . substr($path, 0, strrpos($path, ".")) . ".webp";
$ok = $disk->put(
$thumbUri,
$originalImage
->cover(300, 300)
->encodeUsingFormat(
Format::WEBP, Format::WEBP,
progressive: true, progressive: true,
quality: 80, quality: 80,
), ),
); );
if (!$ok) {
return Error::create("Failed to write thumbnail: {$thumbUri}");
} }
$thumbDir = return Success::create(null);
"lucent/thumbs/" . substr($path, 0, strrpos($path, ".")) . ".webp";
$image = $originalImage->cover(300, 300);
$disk->put(
$thumbDir,
$image->encodeUsingFormat(
Format::WEBP,
progressive: true,
quality: 80,
),
);
} }
/** /**
+26
View File
@@ -28,6 +28,10 @@ class AuthController
public function register(Request $request): View|RedirectResponse public function register(Request $request): View|RedirectResponse
{ {
if ($this->authService->isExternal()) {
return $this->authService->redirectHome();
}
if ($this->accountService->countUsers() > 0) { if ($this->accountService->countUsers() > 0) {
return redirect( return redirect(
$this->channelService->channel->lucentUrl . "/login", $this->channelService->channel->lucentUrl . "/login",
@@ -43,6 +47,10 @@ class AuthController
public function postRegister(Request $request): Response public function postRegister(Request $request): Response
{ {
if ($this->authService->isExternal()) {
abort(400);
}
if ($this->accountService->countUsers() > 0) { if ($this->accountService->countUsers() > 0) {
abort(400); abort(400);
} }
@@ -61,6 +69,9 @@ class AuthController
public function login() public function login()
{ {
if ($this->authService->isExternal()) {
return $this->authService->redirectHome();
}
if ($this->accountService->countUsers() == 0) { if ($this->accountService->countUsers() == 0) {
return redirect( return redirect(
$this->channelService->channel->lucentUrl . "/register", $this->channelService->channel->lucentUrl . "/register",
@@ -76,6 +87,9 @@ class AuthController
public function postLogin(Request $request) public function postLogin(Request $request)
{ {
if ($this->authService->isExternal()) {
abort(400);
}
$this->authService->sendLoginEmail($request->input("email")); $this->authService->sendLoginEmail($request->input("email"));
return []; return [];
} }
@@ -87,6 +101,10 @@ class AuthController
// "token" => $request->input("token"), // "token" => $request->input("token"),
// ]); // ]);
if ($this->authService->isExternal()) {
abort(400);
}
return $this->svelte->render( return $this->svelte->render(
layout: "account", layout: "account",
view: "verify", view: "verify",
@@ -100,6 +118,10 @@ class AuthController
public function postVerify(Request $request) public function postVerify(Request $request)
{ {
if ($this->authService->isExternal()) {
abort(400);
}
try { try {
$this->authService->login( $this->authService->login(
$request->input("email"), $request->input("email"),
@@ -113,6 +135,10 @@ class AuthController
public function logout(): RedirectResponse public function logout(): RedirectResponse
{ {
if ($this->authService->isExternal()) {
abort(400);
}
$this->session->flush(); $this->session->flush();
return redirect($this->channelService->channel->lucentUrl . "/login"); return redirect($this->channelService->channel->lucentUrl . "/login");
} }
+20 -20
View File
@@ -14,16 +14,16 @@ use Lucent\Account\UserRepo;
use Lucent\Account\UserRepoLucent; use Lucent\Account\UserRepoLucent;
use Lucent\Account\UserRepoLunar; use Lucent\Account\UserRepoLunar;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\Commands\CompileSchemas; use Lucent\Commands\CompileSchemasCommand;
use Lucent\Commands\GenerateCollectionSchema; use Lucent\Commands\ExportCommand;
use Lucent\Commands\GenerateImageFilter; use Lucent\Commands\GenerateCollectionSchemaCommand;
use Lucent\Commands\LiveLink; use Lucent\Commands\GenerateImageFilterCommand;
use Lucent\Commands\RebuildThumbnails; use Lucent\Commands\ImportCommand;
use Lucent\Commands\RemoveOrphanEdges; use Lucent\Commands\LiveLinkCommand;
use Lucent\Commands\SetupDatabase; use Lucent\Commands\RebuildThumbnailsCommand;
use Lucent\Commands\Export; use Lucent\Commands\RemoveOrphanEdgesCommand;
use Lucent\Commands\Import; use Lucent\Commands\SetupCommand;
use Lucent\Commands\Setup; use Lucent\Commands\SetupDatabaseCommand;
use Lucent\Data\ChannelAuth; use Lucent\Data\ChannelAuth;
use Lucent\File\FileService; use Lucent\File\FileService;
use Lucent\Query\DatabaseGraph\DatabaseGraph; use Lucent\Query\DatabaseGraph\DatabaseGraph;
@@ -95,16 +95,16 @@ class LucentServiceProvider extends ServiceProvider
if ($this->app->runningInConsole()) { if ($this->app->runningInConsole()) {
$this->commands([ $this->commands([
CompileSchemas::class, CompileSchemasCommand::class,
RebuildThumbnails::class, RebuildThumbnailsCommand::class,
LiveLink::class, LiveLinkCommand::class,
RemoveOrphanEdges::class, RemoveOrphanEdgesCommand::class,
SetupDatabase::class, SetupDatabaseCommand::class,
GenerateCollectionSchema::class, GenerateCollectionSchemaCommand::class,
GenerateImageFilter::class, GenerateImageFilterCommand::class,
Export::class, ExportCommand::class,
Import::class, ImportCommand::class,
Setup::class, SetupCommand::class,
]); ]);
} }
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace Lucent\Option;
use Traversable;
/**
* @template T
*
* @extends Option<T>
*/
final class LazyOption extends Option
{
/** @var callable(mixed...):(Option<T>) */
private $callback;
/** @var array<int, mixed> */
private $arguments;
/** @var Option<T>|null */
private $option;
/**
* @template S
* @param callable(mixed...):(Option<S>) $callback
* @param array<int, mixed> $arguments
*
* @return LazyOption<S>
*/
public static function create($callback, array $arguments = []): self
{
return new self($callback, $arguments);
}
/**
* @param callable(mixed...):(Option<T>) $callback
* @param array<int, mixed> $arguments
*/
public function __construct($callback, array $arguments = [])
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Invalid callback given');
}
$this->callback = $callback;
$this->arguments = $arguments;
}
public function isDefined(): bool
{
return $this->option()->isDefined();
}
public function isEmpty(): bool
{
return $this->option()->isEmpty();
}
public function get()
{
return $this->option()->get();
}
public function getOrElse($default)
{
return $this->option()->getOrElse($default);
}
public function getOrCall($callable)
{
return $this->option()->getOrCall($callable);
}
public function getOrThrow(\Exception $ex)
{
return $this->option()->getOrThrow($ex);
}
public function orElse(Option $else)
{
return $this->option()->orElse($else);
}
public function ifDefined($callable)
{
$this->option()->forAll($callable);
}
public function forAll($callable)
{
return $this->option()->forAll($callable);
}
public function map($callable)
{
return $this->option()->map($callable);
}
public function flatMap($callable)
{
return $this->option()->flatMap($callable);
}
public function filter($callable)
{
return $this->option()->filter($callable);
}
public function filterNot($callable)
{
return $this->option()->filterNot($callable);
}
public function select($value)
{
return $this->option()->select($value);
}
public function reject($value)
{
return $this->option()->reject($value);
}
/** @return Traversable<T> */
public function getIterator(): Traversable
{
return $this->option()->getIterator();
}
public function foldLeft($initialValue, $callable)
{
return $this->option()->foldLeft($initialValue, $callable);
}
public function foldRight($initialValue, $callable)
{
return $this->option()->foldRight($initialValue, $callable);
}
/** @return Option<T> */
private function option(): Option
{
if (null === $this->option) {
/** @var mixed */
$option = call_user_func_array($this->callback, $this->arguments);
if ($option instanceof Option) {
$this->option = $option;
} else {
throw new \RuntimeException(sprintf('Expected instance of %s', Option::class));
}
}
return $this->option;
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
namespace Lucent\Option;
use EmptyIterator;
/**
* @extends Option<mixed>
*/
final class None extends Option
{
/** @var None|null */
private static $instance;
/** @return None */
public static function create(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function get()
{
throw new \RuntimeException('None has no value.');
}
public function getOrCall($callable)
{
return $callable();
}
public function getOrElse($default)
{
return $default;
}
public function getOrThrow(\Exception $ex)
{
throw $ex;
}
public function isEmpty(): bool
{
return true;
}
public function isDefined(): bool
{
return false;
}
public function orElse(Option $else)
{
return $else;
}
public function ifDefined($callable)
{
// no-op
}
public function forAll($callable)
{
return $this;
}
public function map($callable)
{
return $this;
}
public function flatMap($callable)
{
return $this;
}
public function filter($callable)
{
return $this;
}
public function filterNot($callable)
{
return $this;
}
public function select($value)
{
return $this;
}
public function reject($value)
{
return $this;
}
public function getIterator(): EmptyIterator
{
return new EmptyIterator();
}
public function foldLeft($initialValue, $callable)
{
return $initialValue;
}
public function foldRight($initialValue, $callable)
{
return $initialValue;
}
private function __construct()
{
}
}
+230
View File
@@ -0,0 +1,230 @@
<?php
namespace Lucent\Option;
use ArrayAccess;
use IteratorAggregate;
/**
* @template T
*
* @implements IteratorAggregate<T>
*/
abstract class Option implements IteratorAggregate
{
/**
* @template S
*
* @param S $value
* @param S $noneValue
*
* @return Option<S>
*/
public static function fromValue($value, $noneValue = null)
{
if ($value === $noneValue) {
return None::create();
}
return new Some($value);
}
/**
* @template S
*
* @param array<string|int,S>|ArrayAccess<string|int,S>|null $array
* @param string|int|null $key
*
* @return Option<S>
*/
public static function fromArraysValue($array, $key)
{
if ($key === null || !(is_array($array) || $array instanceof ArrayAccess) || !isset($array[$key])) {
return None::create();
}
return new Some($array[$key]);
}
/**
* @template S
*
* @param callable $callback
* @param array $arguments
* @param S $noneValue
*
* @return LazyOption<S>
*/
public static function fromReturn($callback, array $arguments = [], $noneValue = null)
{
return new LazyOption(static function () use ($callback, $arguments, $noneValue) {
/** @var mixed */
$return = call_user_func_array($callback, $arguments);
if ($return === $noneValue) {
return None::create();
}
return new Some($return);
});
}
/**
* @template S
*
* @param Option<S>|callable|S $value
* @param S $noneValue
*
* @return Option<S>|LazyOption<S>
*/
public static function ensure($value, $noneValue = null)
{
if ($value instanceof self) {
return $value;
} elseif (is_callable($value)) {
return new LazyOption(static function () use ($value, $noneValue) {
/** @var mixed */
$return = $value();
if ($return instanceof self) {
return $return;
} else {
return self::fromValue($return, $noneValue);
}
});
} else {
return self::fromValue($value, $noneValue);
}
}
/**
* @template S
*
* @param callable $callback
* @param mixed $noneValue
*
* @return callable
*/
public static function lift($callback, $noneValue = null)
{
return static function () use ($callback, $noneValue) {
/** @var array<int, mixed> */
$args = func_get_args();
$reduced_args = array_reduce(
$args,
/** @param bool $status */
static function ($status, self $o) {
return $o->isEmpty() ? true : $status;
},
false
);
if ($reduced_args) {
return None::create();
}
$args = array_map(
static function (self $o) {
return $o->get();
},
$args
);
return self::ensure(call_user_func_array($callback, $args), $noneValue);
};
}
/** @return T */
abstract public function get();
/**
* @template S
* @param S $default
* @return T|S
*/
abstract public function getOrElse($default);
/**
* @template S
* @param callable():S $callable
* @return T|S
*/
abstract public function getOrCall($callable);
/** @return T */
abstract public function getOrThrow(\Exception $ex);
abstract public function isEmpty(): bool;
abstract public function isDefined(): bool;
/**
* @param Option<T> $else
* @return Option<T>
*/
abstract public function orElse(self $else);
/** @deprecated Use forAll() instead. */
abstract public function ifDefined($callable);
/**
* @param callable(T):mixed $callable
* @return Option<T>
*/
abstract public function forAll($callable);
/**
* @template S
* @param callable(T):S $callable
* @return Option<S>
*/
abstract public function map($callable);
/**
* @template S
* @param callable(T):Option<S> $callable
* @return Option<S>
*/
abstract public function flatMap($callable);
/**
* @param callable(T):bool $callable
* @return Option<T>
*/
abstract public function filter($callable);
/**
* @param callable(T):bool $callable
* @return Option<T>
*/
abstract public function filterNot($callable);
/**
* @param T $value
* @return Option<T>
*/
abstract public function select($value);
/**
* @param T $value
* @return Option<T>
*/
abstract public function reject($value);
/**
* @template S
* @param S $initialValue
* @param callable(S, T):S $callable
* @return S
*/
abstract public function foldLeft($initialValue, $callable);
/**
* @template S
* @param S $initialValue
* @param callable(T, S):S $callable
* @return S
*/
abstract public function foldRight($initialValue, $callable);
}
+147
View File
@@ -0,0 +1,147 @@
<?php
namespace Lucent\Option;
use ArrayIterator;
/**
* @template T
*
* @extends Option<T>
*/
final class Some extends Option
{
/** @var T */
private $value;
/** @param T $value */
public function __construct($value)
{
$this->value = $value;
}
/**
* @template U
* @param U $value
* @return Some<U>
*/
public static function create($value): self
{
return new self($value);
}
public function isDefined(): bool
{
return true;
}
public function isEmpty(): bool
{
return false;
}
public function get()
{
return $this->value;
}
public function getOrElse($default)
{
return $this->value;
}
public function getOrCall($callable)
{
return $this->value;
}
public function getOrThrow(\Exception $ex)
{
return $this->value;
}
public function orElse(Option $else)
{
return $this;
}
public function ifDefined($callable)
{
$this->forAll($callable);
}
public function forAll($callable)
{
$callable($this->value);
return $this;
}
public function map($callable)
{
return new self($callable($this->value));
}
public function flatMap($callable)
{
/** @var mixed */
$rs = $callable($this->value);
if (!$rs instanceof Option) {
throw new \RuntimeException('Callables passed to flatMap() must return an Option. Maybe you should use map() instead?');
}
return $rs;
}
public function filter($callable)
{
if (true === $callable($this->value)) {
return $this;
}
return None::create();
}
public function filterNot($callable)
{
if (false === $callable($this->value)) {
return $this;
}
return None::create();
}
public function select($value)
{
if ($this->value === $value) {
return $this;
}
return None::create();
}
public function reject($value)
{
if ($this->value === $value) {
return None::create();
}
return $this;
}
/** @return ArrayIterator<int, T> */
public function getIterator(): ArrayIterator
{
return new ArrayIterator([$this->value]);
}
public function foldLeft($initialValue, $callable)
{
return $callable($initialValue, $this->value);
}
public function foldRight($initialValue, $callable)
{
return $callable($this->value, $initialValue);
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Lucent\ResultType;
use Lucent\Option\None;
use Lucent\Option\Some;
/**
* @template T
* @template E
*
* @extends \Lucent\ResultType\Result<T,E>
*/
final class Error extends Result
{
/**
* @var E
*/
private $value;
/**
* Internal constructor for an error value.
*
* @param E $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template F
*
* @param F $value
*
* @return \Lucent\ResultType\Result<T,F>
*/
public static function create($value): Error
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \Lucent\Option\Option<T>
*/
public function success()
{
return None::create();
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \Lucent\ResultType\Result<S,E>
*/
public function map(callable $f): Result
{
return self::create($this->value);
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\Lucent\ResultType\Result<S,F> $f
*
* @return \Lucent\ResultType\Result<S,F>
*/
public function flatMap(callable $f): Result
{
/** @var \Lucent\ResultType\Result<S,F> */
return self::create($this->value);
}
/**
* Get the error option value.
*
* @return \Lucent\Option\Option<E>
*/
public function error(): Some
{
return Some::create($this->value);
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \Lucent\ResultType\Result<T,F>
*/
public function mapError(callable $f): Result
{
return self::create($f($this->value));
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Lucent\ResultType;
/**
* @template T
* @template E
*/
abstract class Result
{
/**
* Get the success option value.
*
* @return \Lucent\Option\Option<T>
*/
abstract public function success();
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \Lucent\ResultType\Result<S,E>
*/
abstract public function map(callable $f);
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\Lucent\ResultType\Result<S,F> $f
*
* @return \Lucent\ResultType\Result<S,F>
*/
abstract public function flatMap(callable $f);
/**
* Get the error option value.
*
* @return \Lucent\Option\Option<E>
*/
abstract public function error();
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \Lucent\ResultType\Result<T,F>
*/
abstract public function mapError(callable $f);
}
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Lucent\ResultType;
use Lucent\Option\None;
use Lucent\Option\Some;
/**
* @template T
* @template E
*
* @extends \Lucent\ResultType\Result<T,E>
*/
final class Success extends Result
{
/**
* @var T
*/
private $value;
/**
* Internal constructor for a success value.
*
* @param T $value
*
* @return void
*/
private function __construct($value)
{
$this->value = $value;
}
/**
* Create a new error value.
*
* @template S
*
* @param S $value
*
* @return \Lucent\ResultType\Result<S,E>
*/
public static function create($value): Success
{
return new self($value);
}
/**
* Get the success option value.
*
* @return \Lucent\Option\Option<T>
*/
public function success(): Some
{
return Some::create($this->value);
}
/**
* Map over the success value.
*
* @template S
*
* @param callable(T):S $f
*
* @return \Lucent\ResultType\Result<S,E>
*/
public function map(callable $f): Result
{
return self::create($f($this->value));
}
/**
* Flat map over the success value.
*
* @template S
* @template F
*
* @param callable(T):\Lucent\ResultType\Result<S,F> $f
*
* @return \Lucent\ResultType\Result<S,F>
*/
public function flatMap(callable $f)
{
return $f($this->value);
}
/**
* Get the error option value.
*
* @return \Lucent\Option\Option<E>
*/
public function error()
{
return None::create();
}
/**
* Map over the error value.
*
* @template F
*
* @param callable(E):F $f
*
* @return \Lucent\ResultType\Result<T,F>
*/
public function mapError(callable $f): Result
{
return self::create($this->value);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ use Lucent\Database\Database;
use Lucent\Edge\Edge; use Lucent\Edge\Edge;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Record\RecordData; use Lucent\Record\RecordData;
use PhpOption\Option; use Lucent\Option\Option;
use stdClass; use stdClass;
class RevisionRepo class RevisionRepo
+1 -1
View File
@@ -6,7 +6,7 @@ use Lucent\Channel\ChannelService;
use Lucent\Edge\Edge; use Lucent\Edge\Edge;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Record\Record; use Lucent\Record\Record;
use PhpOption\Option; use Lucent\Option\Option;
readonly class RevisionService readonly class RevisionService
{ {
+2 -2
View File
@@ -1,7 +1,7 @@
<?php <?php
use PhpOption\None; use Lucent\Option\None;
use PhpOption\Some; use Lucent\Option\Some;
if (!function_exists("some")) { if (!function_exists("some")) {
/** /**