WIP uploading files

This commit is contained in:
2026-04-29 19:40:37 +03:00
parent e058ceadee
commit bd01e5c32c
15 changed files with 506 additions and 362 deletions
+13 -22
View File
@@ -11,7 +11,6 @@ final class Channel
{
public string $lucentUrl;
public string $filesUrl;
public array $disks;
public string $previewTargetUrl;
/**
@@ -19,37 +18,29 @@ final class Channel
* @param Collection<UserCommand> $commands
*/
function __construct(
public string $name,
public string $url,
public string $previewTarget,
public string $name,
public string $url,
public string $previewTarget,
public Collection $commands,
public Collection $schemas,
public array $imageFilters,
public array $roles,
)
{
public array $imageFilters,
public array $roles,
) {
$this->lucentUrl = $url . "/lucent";
$this->filesUrl = $this->makeFilesUrl();
$this->disks = $this->getDisksFromSchemas();
$this->previewTargetUrl = $url . "/" . $previewTarget;
}
private function makeFilesUrl(): string
{
return match (config("filesystems.disks.lucent.driver")) {
"s3" => config("filesystems.disks.lucent.endpoint") . "/" . config("filesystems.disks.lucent.bucket"),
"local" => $this->url . "/storage" . config("filesystems.disks.lucent.endpoint"),
default => ""
"s3" => config("filesystems.disks.lucent.endpoint") .
"/" .
config("filesystems.disks.lucent.bucket"),
"local" => $this->url .
"/storage" .
config("filesystems.disks.lucent.endpoint"),
default => "",
};
}
private function getDisksFromSchemas()
{
return $this->schemas->filter(fn(Schema $schema) => get_class($schema) === FilesSchema::class)->reduce(function (array $carry, Schema $schema) {
$carry[$schema->disk] = config("filesystems.disks." . $schema->disk . ".url");
return $carry;
}, []);
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ namespace Lucent\Channel;
use Lucent\Channel\Data\UserCommand;
use Lucent\Primitive\Collection;
use Lucent\Schema\Schema;
use Lucent\Data\Schema;
use Lucent\Schema\SchemaService;
use PhpOption\Option;
+19 -16
View File
@@ -4,22 +4,18 @@ namespace Lucent\Commands;
use DirectoryIterator;
use Illuminate\Console\Command;
use Lucent\Schema\Schema;
use Lucent\Data\Schema;
use Lucent\Schema\SchemaService;
use Lucent\Schema\Type;
class CompileSchemas extends Command
{
protected $signature = "lucent:schemas";
protected $signature = 'lucent:schemas';
protected $description = 'Compiles schemas';
protected $description = "Compiles schemas";
public function handle(SchemaService $schemaService)
{
$configDir = base_path(config('lucent.schemas_path'));
$configDir = base_path(config("lucent.schemas_path"));
$schemasDirIterator = new DirectoryIterator($configDir);
$schemas = [];
@@ -28,31 +24,39 @@ class CompileSchemas extends Command
continue;
}
$schemaJson = file_get_contents($configDir . "/" . $file->getFilename());
$schemaJson = file_get_contents(
$configDir . "/" . $file->getFilename(),
);
$schema = json_decode($schemaJson, true);
if (empty($schema)) {
$this->error("Invalid JSON " . $file->getFilename());
return 0;
}
$schemas[] = $schema;
}
$schemas = collect($schemas)->sortBy("label")->values();
$roles = $schemas
->map([$schemaService, 'fromArray'])
->whereIn("type", [Type::COLLECTION, Type::FILES])
->reduce(fn($carry, Schema $schema) => array_merge(
->map([$schemaService, "fromArray"])
->reduce(
fn($carry, Schema $schema) => array_merge(
$carry,
$schema->read,
$schema->write,
config("lucent.canInvite") ?? [],
config("lucent.canBuild") ?? [],
), []);
),
[],
);
$json = [
"schemas" => $schemas->toArray(),
"roles" => collect($roles)->push("admin")->push("removed")->unique()->values()->toArray()
"roles" => collect($roles)
->push("admin")
->push("removed")
->unique()
->values()
->toArray(),
];
if (!file_exists(storage_path("lucent"))) {
@@ -63,5 +67,4 @@ class CompileSchemas extends Command
$this->info("Lucent Schemas were updated");
}
}
+20 -17
View File
@@ -15,44 +15,47 @@ use Lucent\Schema\Type;
class RebuildThumbnails extends Command
{
protected $signature = "lucent:rebuild:thumbnails";
protected $signature = 'lucent:rebuild:thumbnails';
protected $description = 'Rebuilds thumbnails for path';
protected $description = "Rebuilds thumbnails for path";
public function __construct(
public Query $query,
public FileService $fileService,
)
{
) {
parent::__construct();
}
public function handle(ChannelService $channelService): int
{
$channelService->channel->schemas
->filter(fn(Schema $schema) => get_class($schema) === FilesSchema::class)
->map([$this, 'rebuildThumbnails']);
->filter(
fn(Schema $schema) => get_class($schema) === FilesSchema::class,
)
->map([$this, "rebuildThumbnails"]);
return 0;
}
public function rebuildThumbnails(FilesSchema $schema): void
{
$this->info("Rebuilding thumbnails for ". $schema->name);
$records = $this->query->limit(0)->filter(["schema" => $schema->name])->run()->records;
$this->info("Rebuilding thumbnails for " . $schema->name);
$records = $this->query
->limit(0)
->filter(["schema" => $schema->name])
->run()->records;
$disk = $this->fileService->loadDisk($schema->disk);
foreach ($records as $record) {
try{
$this->fileService->createTemplates($disk, $record->_file->path);
try {
$this->fileService->createTemplates(
$disk,
$record->_file->path,
);
} catch (Exception $e) {
echo "File ". $record->_file->originalName . " could not be rebuilt \n" ;
echo "File " .
$record->_file->originalName .
" could not be rebuilt \n";
}
}
}
}
+26 -3
View File
@@ -18,6 +18,7 @@ class SetupDatabase extends Command
{
$this->tableUsers();
$this->tableRecords();
$this->tableFiles();
$this->tableRevisions();
$this->tableSessions();
$this->tableCommandLogs();
@@ -73,11 +74,8 @@ class SetupDatabase extends Command
$table->string("status");
$table->jsonb("data");
$table->jsonb("_sys");
$table->jsonb("_file");
$table->text("search")->default("");
// $table->index(["schema", "_sys->updatedAt", "status"]);
$table->index("search");
});
@@ -104,6 +102,31 @@ class SetupDatabase extends Command
}
}
private function tableFiles(): void
{
$schema = Database::make()->getSchemaBuilder();
if (!$schema->hasTable($this->prefix . "files")) {
$schema->create($this->prefix . "files", function (
Blueprint $table,
) {
$table->uuid("id")->primary();
$table->uuid("record");
$table->string("name");
$table->string("ogName");
$table->string("mime");
$table->string("path");
$table->integer("size");
$table->integer("width");
$table->integer("height");
$table->string("checksum");
});
DB::statement(
"CREATE INDEX ON " . $this->prefix . "files (record)",
);
}
}
private function tableRevisions(): void
{
$schema = Database::make()->getSchemaBuilder();
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Lucent\Data;
class File
{
function __construct(
public readonly string $id,
public readonly string $recordId,
public readonly string $originalName,
public readonly string $mime,
public readonly string $path,
public readonly int $size,
public readonly int $width,
public readonly int $height,
public readonly string $checksum,
) {}
public static function fromArray(array $data): File
{
return new File(
id: data_get($data, "id"),
recordId: data_get($data, "recordId"),
originalName: data_get($data, "originalName"),
mime: data_get($data, "mime"),
path: data_get($data, "path"),
size: data_get($data, "size"),
width: data_get($data, "width"),
height: data_get($data, "height"),
checksum: data_get($data, "checksum"),
);
}
public function toArray(): array
{
return \json_decode(\json_encode($this), true);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Lucent\Data;
use Lucent\Primitive\Collection;
class Schema
{
/**
* @param Collection<FieldInterface> $fields
* @param array<string> $visible
*/
function __construct(
public string $name,
public string $label,
public array $visible,
public array $groups,
public Collection $fields,
public bool $isEntry = false,
public string $color = "",
public string $sortBy = "-_sys.updatedAt",
public ?string $cardTitle = null,
public ?string $cardImage = null,
public int $revisions = 0,
public array $read = [],
public array $write = [],
) {}
}
+62 -53
View File
@@ -10,46 +10,53 @@ use Illuminate\Support\Str;
use Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService;
use Lucent\Database\Database;
use Lucent\Id\Id;
use Lucent\LucentException;
use Lucent\Record\FileData as RecordFile;
use Lucent\Data\File as DataFile;
use Lucent\Record\QueryRecord;
use Lucent\Schema\FilesSchema;
use Lucent\Schema\Schema;
use Spatie\ImageOptimizer\OptimizerChainFactory;
class FileService
{
public function __construct(
public ChannelService $channelService,
public ImageManager $imageManager,
public Logger $logger
)
{
}
public ImageManager $imageManager,
public Logger $logger,
) {}
public function getPath(QueryRecord $file): string
{
return $this->channelService->channel->filesUrl . "/" . $file->_file->path;
return $this->channelService->channel->filesUrl .
"/" .
$file->_file->path;
}
public function createFromUrl(FilesSchema $schema, string $url): FileUploadResult
{
public function createFromUrl(
FilesSchema $schema,
string $url,
): FileUploadResult {
$pathinfo = pathinfo($url);
$contents = file_get_contents($url);
if ($contents === false) {
throw new LucentException("Failed to upload file from url");
}
$file = '/tmp/' . $pathinfo['basename'];
$file = "/tmp/" . $pathinfo["basename"];
file_put_contents($file, $contents);
$uploadedFile = new UploadedFile($file, $pathinfo['basename']);
$uploadedFile = new UploadedFile($file, $pathinfo["basename"]);
return $this->upload($schema, $uploadedFile);
}
public function upload(FilesSchema $schema, UploadedFile $file): FileUploadResult
public function upload(string $recordId, UploadedFile $file): DataFile
{
$originalName = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$extension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION);
$originalName = pathinfo(
$file->getClientOriginalName(),
PATHINFO_FILENAME,
);
$extension = pathinfo(
$file->getClientOriginalName(),
PATHINFO_EXTENSION,
);
$originalFilename = $file->getClientOriginalName();
$filename = $this->createFileName($originalName, $extension);
$mimetype = $file->getMimeType();
@@ -58,72 +65,74 @@ class FileService
$optimizerChain->setTimeout(30)->optimize($file->getPathName());
$checksum = sha1_file($file);
$recordId = $this->checkDuplicate($schema->name, $checksum, $file->getSize());
if (!empty($recordId)) {
return new FileUploadResult(
recordFile: null,
duplicateId: $recordId,
isDuplicate: true
);
}
$disk = $this->loadDisk($schema);
$path = $schema->path . "/" . $filename;
$disk = $this->loadDisk();
$path = "files/" . $recordId . "/" . $filename;
$res = $disk->put(
$path,
file_get_contents($file),
// 'public' // now managed by aws policy
// 'public' // now managed by aws policy
);
if ($res === false) {
throw new LucentException("File $filename not uploaded");
}
if($this->isImage($mimetype)){
if ($this->isImage($mimetype)) {
$this->createTemplates($disk, $path);
}
list($width, $height) = $this->isImage($mimetype) ? getimagesize($file) : [0, 0];
$recordFile = new RecordFile(
[$width, $height] = $this->isImage($mimetype)
? getimagesize($file)
: [0, 0];
return new DataFile(
id: Id::new(),
recordId: $recordId,
originalName: $originalFilename,
mime: $mimetype,
path: $path,
disk: $schema->disk,
size: $file->getSize(),
width: $width,
height: $height,
checksum: $checksum
);
return new FileUploadResult(
recordFile: $recordFile,
duplicateId: "",
isDuplicate: false
checksum: $checksum,
);
}
private function createFileName(string $originalName, string $extension): string
{
return Str::slug($originalName, '-') . '-' . uniqid() . '.' . $extension;
private function createFileName(
string $originalName,
string $extension,
): string {
return Str::slug($originalName, "-") .
"-" .
uniqid() .
"." .
$extension;
}
private function isImage(string $mimetype): bool
{
$imageMimes = ['image/webp', 'image/gif', 'image/jpeg', 'image/png', 'image/tiff'];
$imageMimes = [
"image/webp",
"image/gif",
"image/jpeg",
"image/png",
"image/tiff",
];
return in_array($mimetype, $imageMimes);
}
public function loadDisk(Schema|string $schema): Filesystem
public function loadDisk(): Filesystem
{
return Storage::disk($schema->disk ?? $schema);
return Storage::disk(config("lucent.disk"));
}
private function checkDuplicate(string $schemaName, string $checksum, int $filesize): string
{
$record = Database::make()->table("lucent_records")
private function checkDuplicate(
string $schemaName,
string $checksum,
int $filesize,
): string {
$record = Database::make()
->table("lucent_records")
->where("schema", $schemaName)
->where("_file->checksum", $checksum)
->where("_file->size", $filesize)
@@ -137,14 +146,14 @@ class FileService
$originalImage = $this->imageManager->make($disk->get($path));
foreach (config("lucent.imageFilters") as $preset => $filterClass) {
$imageClone = clone $originalImage;
$image = $imageClone->filter(new $filterClass);
$image = $imageClone->filter(new $filterClass());
$templateUri = "/templates/" . $preset . "/" . $path;
$disk->put($templateUri, $image->encode('webp', 75));
$disk->put($templateUri, $image->encode("webp", 75));
}
$thumbDir = "thumbs/" . $path;
$image = $originalImage->fit(300, 300);
$disk->put($thumbDir, $image->encode('webp', 75));
$disk->put($thumbDir, $image->encode("webp", 75));
}
}
+13 -31
View File
@@ -15,19 +15,14 @@ use Lucent\Record\Status;
use function Lucent\Response\fail;
use function Lucent\Response\ok;
class FileController extends Controller
{
public function __construct(
private readonly ChannelService $channelService,
private readonly RecordService $recordService,
private readonly FileService $fileService,
private readonly Query $query
)
{
}
private readonly RecordService $recordService,
private readonly FileService $fileService,
private readonly Query $query,
) {}
public function fromDisk(Request $request, string $disk)
{
@@ -38,7 +33,7 @@ class FileController extends Controller
public function thumb(Request $request, string $disk)
{
$imagePath = "thumbs/".$request->route("any");
$imagePath = "thumbs/" . $request->route("any");
$disk = $this->fileService->loadDisk($disk);
return response()->file($disk->path($imagePath));
}
@@ -49,34 +44,21 @@ class FileController extends Controller
return $disk->download($request->input("path"));
}
public function upload(Request $request)
{
$validator = Validator::make($request->all(), [
'files.*' => 'required|file|max:100000',
"files.*" => "required|file|max:100000",
]);
if ($validator->fails()) {
return fail($validator->errors()->first());
}
$schema = $this->channelService->channel->schemas->firstWhere("name", $request->input("schema"));
$files = $request->file('files');
$recordId = $request->input("recordId");
$files = $request->file("files");
foreach ($files as $file) {
$this->recordService->createFromUploadedFile($file, new RecordInputData(
schemaName: $schema->name,
status: Status::PUBLISHED,
), []);
}
$graph = $this->query
->filter([
"schema" => $schema->name
])
->limit(15)
->skip(0)
->sort("-_sys.updatedAt")
->run();
return ok($graph->records->toArray());
return ok(
collect($files)
->map(fn($file) => $this->fileService->upload($recordId, $file))
->toArray(),
);
}
}
+32 -46
View File
@@ -2,59 +2,39 @@
namespace Lucent\Schema;
use Lucent\Data\Schema;
use Lucent\LucentException;
use Lucent\Primitive\Collection;
class SchemaService
{
public function __construct()
{
}
public function __construct() {}
public function fromArray(array $schemaArr): Schema
{
return match ($schemaArr["type"]) {
"collection" => new CollectionSchema(
name: $schemaArr["name"],
label: $schemaArr["label"],
visible: $schemaArr["visible"] ?? [],
groups: $schemaArr["groups"] ?? [],
fields: (new Collection($schemaArr["fields"]))->map([$this, 'mapFields']),
isEntry: $schemaArr["isEntry"] ?? false,
color: $schemaArr["color"] ?? "",
sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt",
cardTitle: $schemaArr["titleTemplate"] ?? $schemaArr["cardTitle"] ?? null,
cardImage: $schemaArr["cardImage"] ?? null,
revisions: $schemaArr["revisions"] ?? 0,
read: $schemaArr["read"] ?? [],
write: $schemaArr["write"] ?? [],
),
"files" => new FilesSchema(
name: $schemaArr["name"],
label: $schemaArr["label"],
fields: (new Collection($schemaArr["fields"]))->map([$this, 'mapFields']),
disk: $schemaArr["disk"] ?? "lucent",
path: $schemaArr["path"] ?? $schemaArr["name"],
groups: $schemaArr["groups"] ?? [],
isEntry: $schemaArr["isEntry"] ?? false,
sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt",
color: $schemaArr["color"] ?? "",
cardTitle: $schemaArr["titleTemplate"] ?? $schemaArr["cardTitle"] ?? null,
cardImage: $schemaArr["cardImage"] ?? null,
revisions: $schemaArr["revisions"] ?? 0,
read: $schemaArr["read"] ?? [],
write: $schemaArr["write"] ?? [],
)
};
return new Schema(
name: $schemaArr["name"],
label: $schemaArr["label"],
visible: $schemaArr["visible"] ?? [],
groups: $schemaArr["groups"] ?? [],
fields: Collection::make($schemaArr["fields"])->map([
$this,
"mapFields",
]),
isEntry: $schemaArr["isEntry"] ?? false,
color: $schemaArr["color"] ?? "",
sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt",
cardTitle: $schemaArr["titleTemplate"] ??
($schemaArr["cardTitle"] ?? null),
cardImage: $schemaArr["cardImage"] ?? null,
revisions: $schemaArr["revisions"] ?? 0,
read: $schemaArr["read"] ?? [],
write: $schemaArr["write"] ?? [],
);
}
public function mapFields(array $field): FieldInterface
{
$schemaFields = [
\Lucent\Schema\Ui\Checkbox::class,
\Lucent\Schema\Ui\Color::class,
@@ -70,16 +50,22 @@ class SchemaService
\Lucent\Schema\Ui\Text::class,
\Lucent\Schema\Ui\Textarea::class,
];
$ui = collect($schemaFields)->filter(function ($className) use ($field) {
return str_ends_with(strtolower($className), "\\" . strtolower($field["ui"]));
})->first();
$ui = collect($schemaFields)
->filter(function ($className) use ($field) {
return str_ends_with(
strtolower($className),
"\\" . strtolower($field["ui"]),
);
})
->first();
if (empty($ui)) {
throw new LucentException("Field UI " . $field["ui"] . " not found");
throw new LucentException(
"Field UI " . $field["ui"] . " not found",
);
}
unset($field["ui"]);
return new $ui(...$field);
}
}