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
+39 -41
View File
@@ -1,44 +1,42 @@
{ {
"name": "lexx27/lucent", "name": "lexx27/lucent",
"type": "library", "type": "library",
"description": "Lucent cms", "description": "Lucent cms",
"license": "MIT", "license": "MIT",
"require": { "require": {
"ext-xml": "*", "ext-xml": "*",
"ext-zip": "*", "ext-zip": "*",
"ext-sqlite3": "*", "ext-sqlite3": "*",
"ext-imagick": "*", "ext-imagick": "*",
"ext-pdo": "*", "ext-pdo": "*",
"php": "^8.3", "php": "^8.5",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7", "intervention/image": "^2.7",
"phpoption/phpoption": "^1.9", "phpoption/phpoption": "^1.9",
"spatie/image-optimizer": "^1.6", "spatie/image-optimizer": "^1.6",
"staudenmeir/laravel-cte": "^1.0", "staudenmeir/laravel-cte": "^1.0",
"mustache/mustache": "^2.14" "mustache/mustache": "^2.14"
},
"require-dev": {
"phpstan/phpstan": "^1.8",
"laravel/framework": "^10.10"
},
"autoload": {
"psr-4": {
"Lucent\\": "src/"
}, },
"require-dev": { "files": [
"phpstan/phpstan": "^1.8", "src/Response.php",
"laravel/framework": "^10.10" "src/macros.php"
}, ]
"autoload": { },
"psr-4": { "extra": {
"Lucent\\": "src/" "laravel": {
}, "providers": [
"files": [ "Lucent\\LucentServiceProvider"
"src/Response.php", ]
"src/macros.php" }
] },
}, "minimum-stability": "stable",
"extra": { "prefer-stable": true
"laravel": {
"providers": [
"Lucent\\LucentServiceProvider"
]
}
},
"minimum-stability": "stable",
"prefer-stable": true
} }
+49 -66
View File
@@ -4,7 +4,7 @@
import Icon from "../../common/Icon.svelte"; import Icon from "../../common/Icon.svelte";
import SortFields from "./SortFields.svelte"; import SortFields from "./SortFields.svelte";
import AppliedFilter from "./AppliedFilter.svelte"; import AppliedFilter from "./AppliedFilter.svelte";
import {createEventDispatcher, getContext} from "svelte"; import { createEventDispatcher, getContext } from "svelte";
import Dropdown from "../../common/Dropdown.svelte"; import Dropdown from "../../common/Dropdown.svelte";
import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte"; import AppliedFilterNotLinked from "./AppliedFilterNotLinked.svelte";
@@ -41,7 +41,6 @@
} else { } else {
window.location = url; window.location = url;
} }
} }
function uploadComplete(e) { function uploadComplete(e) {
@@ -51,110 +50,94 @@
<div class="toolbar"> <div class="toolbar">
<div class="toolbar-filters"> <div class="toolbar-filters">
<SortFields <SortFields
{schema} {schema}
{sortParam} {sortParam}
{sortField} {sortField}
{systemFields} {systemFields}
{inModal} {inModal}
{modalUrl} {modalUrl}
on:refresh on:refresh
/> />
<FilterFields <FilterFields
bind:schema bind:schema
{systemFields} {systemFields}
{operators} {operators}
{filter} {filter}
{inModal} {inModal}
{modalUrl} {modalUrl}
on:refresh on:refresh
/> />
<form method="GET" on:submit={search}> <form method="GET" on:submit={search}>
<input type="search" name="filter[search_regex]" placeholder="Search" <input
class="search" required> type="search"
name="filter[search_regex]"
placeholder="Search"
class="search"
required
/>
</form> </form>
</div> </div>
<div style="display:flex;align-items: center;gap:4px"> <div style="display:flex;align-items: center;gap:4px">
{#if schema.type === "collection"} {#if !inModal && isWritable}
{#if !inModal && isWritable} <a
<a href="{channel.lucentUrl}/records/new?schema={schema.name}"
href="{channel.lucentUrl}/records/new?schema={schema.name}" class="button"
class="button" >
> New Record
New Record </a>
</a>
{/if}
{:else }
<div>
<Uploader {schema} on:uploadComplete={uploadComplete}/>
</div>
{/if} {/if}
{#if !inModal} {#if !inModal}
<Dropdown orientation="right"> <Dropdown orientation="right">
<div slot="button"> <div slot="button">
<Icon icon="ellipsis-vertical"/> <Icon icon="ellipsis-vertical" />
</div> </div>
{#if filter["status_in"] === "trashed"} {#if filter["status_in"] === "trashed"}
{#if isWritable} {#if isWritable}
<a <a
class="dropdown-item" class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}/emptyTrash" href="{channel.lucentUrl}/content/{schema.name}/emptyTrash"
> >
Empty trash Empty trash
</a> </a>
{/if} {/if}
{:else} {:else}
<a class="dropdown-item" href={csvUrl}>Export to CSV</a>
<a <a
class="dropdown-item" class="dropdown-item"
href={csvUrl} href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed"
>Export to CSV</a >View trashed records</a
> >
<a <a
class="dropdown-item" class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?filter[status_in]=trashed" href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
>View trashed records</a >View unlinked records</a
>
<a
class="dropdown-item"
href="{channel.lucentUrl}/content/{schema.name}?notlinked=*"
>View unlinked records</a
> >
{/if} {/if}
</Dropdown> </Dropdown>
{/if} {/if}
</div> </div>
</div> </div>
<div class="applied-filters"> <div class="applied-filters">
<AppliedFilterNotLinked <AppliedFilterNotLinked {inModal} {modalUrl} on:refresh
{inModal}
{modalUrl}
on:refresh
></AppliedFilterNotLinked> ></AppliedFilterNotLinked>
{#if Object.entries(filter).length > 0} {#if Object.entries(filter).length > 0}
{#each Object.entries(filter) as [k, v]} {#each Object.entries(filter) as [k, v]}
<AppliedFilter <AppliedFilter
{schema} {schema}
{operators} {operators}
key={k} key={k}
value={v} value={v}
{inModal} {inModal}
{modalUrl} {modalUrl}
{graph} {graph}
on:refresh on:refresh
/> />
{/each} {/each}
{/if} {/if}
</div> </div>
+97
View File
@@ -0,0 +1,97 @@
<script>
import { createEventDispatcher, getContext } from "svelte";
import Icon from "../common/Icon.svelte";
import Index from "../content/Index.svelte";
import axios from "axios";
let dialogEl;
const dispatch = createEventDispatcher();
const channel = getContext("channel");
$: data = {};
let selectedRecords = [];
// onMount(() => {
// load();
// });
export function close(e) {
if (e) {
e.preventDefault();
}
dialogEl.close();
selectedRecords = [];
}
function load(schema) {
axios
.get(channel.lucentUrl + "/content/" + schema)
.then((response) => {
data = response.data;
})
.catch((error) => console.log(error));
}
function insert(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "insert",
schema: data.schema.name,
});
}
function replace(e) {
e.preventDefault();
dispatch("insert", {
records: selectedRecords,
action: "replace",
});
}
export function open(schema) {
dialogEl.showModal();
load(schema);
}
</script>
<dialog bind:this={dialogEl}>
{#if data.schema}
<div class="dialog-header">
<button
type="button"
class="button"
on:click={insert}
disabled={selectedRecords.length === 0}
>
Insert
</button>
<button
type="button"
class="button"
on:click={replace}
disabled={selectedRecords.length === 0}
>
Replace
</button>
{#if selectedRecords.length > 0}
<span class="">
{selectedRecords.length} records selected
</span>
{/if}
<button
on:click|preventDefault={close}
type="button"
class="button close"
aria-label="Close"
>
<Icon icon="close"></Icon>
</button>
</div>
<div class="dialog-body">
<Index {...data} bind:selected={selectedRecords}></Index>
</div>
{/if}
</dialog>
+25 -21
View File
@@ -1,10 +1,10 @@
<script> <script>
import {createEventDispatcher, getContext} from "svelte"; import { createEventDispatcher, getContext } from "svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const channel = getContext("channel"); const channel = getContext("channel");
export let schema; export let recordId;
let mimeTypes = ""; let mimeTypes = "";
let files = []; let files = [];
let isLoading = false; let isLoading = false;
@@ -17,39 +17,43 @@
files = e.target.files ? [...e.target.files] : []; files = e.target.files ? [...e.target.files] : [];
let formData = new FormData(); let formData = new FormData();
formData.append("schema", schema.name); formData.append("recordId", recordId);
Array.from(files).forEach(function (file) { Array.from(files).forEach(function (file) {
formData.append("files[]", file); formData.append("files[]", file);
}); });
dispatch("beforeUpload", files); dispatch("beforeUpload", files);
axios fetch(channel.lucentUrl + "/files/upload", {
.post(channel.lucentUrl + "/files/upload", formData, { method: "POST",
headers: { body: formData,
"Content-Type": "multipart/form-data", headers: {
}, "X-CSRF-TOKEN": document.querySelector(
}) 'meta[name="csrf-token"]',
.then((response) => { ).content,
if (response.data.error) { },
dispatch("uploadError", response.data.error); })
.then((response) => response.json())
.then((data) => {
if (data.error) {
dispatch("uploadError", data.error);
} else { } else {
dispatch("uploadComplete", response.data); dispatch("uploadComplete", data);
} }
isLoading = false; isLoading = false;
}) })
.catch((error) => { .catch((error) => {
isLoading = false; isLoading = false;
console.log(error.response.data); console.log(error);
}); });
} }
</script> </script>
<fieldset class="upload-button" disabled={isLoading}> <fieldset class="upload-button" disabled={isLoading}>
<label class="button primary btn-spinner "> <label class="button primary btn-spinner">
<span <span
class="spinner-border spinner-border-sm" class="spinner-border spinner-border-sm"
role="status" role="status"
aria-hidden="true" aria-hidden="true"
/> />
Upload file Upload file
<input <input
+44 -45
View File
@@ -1,31 +1,24 @@
<script> <script>
import {sortByField} from "../../edges/sortEdges"; import { sortByField } from "../../edges/sortEdges";
import Sortable from "../../libs/Sortable.svelte"; import Sortable from "../../libs/Sortable.svelte";
import PreviewFile from "../previews/PreviewFile.svelte"; import PreviewFile from "../previews/PreviewFile.svelte";
import Dropdown from "../../common/Dropdown.svelte"; import Dropdown from "../../common/Dropdown.svelte";
import Dialog from "../../dialog/Dialog.svelte"; import Dialog from "../../dialog/Dialog.svelte";
import {insertEdges} from "./reference.js"; import { insertEdges } from "./reference.js";
import {getContext} from "svelte"; import { getContext } from "svelte";
import FileDialog from "../../dialog/FileDialog.svelte";
import Uploader from "../../files/Uploader.svelte";
const channel = getContext("channel"); const channel = getContext("channel");
export let field; export let field;
export let record; export let record;
export let graph export let graph;
let browseModal; let browseModal;
$: references = graph?.edges
.filter((edge) => edge.field === field.name)
.map((edge) => {
return graph.records.find((increc) => increc.id === edge.target && record.id === edge.source);
}).filter((rec) => (rec?.id ? true : false)) ?? [];
let collections = channel.schemas.filter((aschema) =>
field.collections.includes(aschema.name)
);
function removeReference(e) { function removeReference(e) {
e.preventDefault(); e.preventDefault();
graph.edges = graph.edges.filter( graph.edges = graph.edges.filter(
(edge) => !(edge.target === e.detail && edge.field === field.name) (edge) => !(edge.target === e.detail && edge.field === field.name),
); );
} }
@@ -35,50 +28,56 @@
} }
async function reorder(e) { async function reorder(e) {
graph.edges = await sortByField(
graph.edges = await sortByField(e.detail.source, e.detail.target, graph.edges, field.name, references); e.detail.source,
e.detail.target,
graph.edges,
field.name,
references,
);
} }
function insert(e) { function insert(e) {
e.preventDefault(); e.preventDefault();
browseModal.close(); browseModal.close();
graph = insertEdges(graph, record, e.detail.records, field.name, e.detail.action); graph = insertEdges(
graph,
record,
e.detail.records,
field.name,
e.detail.action,
);
}
function uploadComplete(e) {
records = e.detail;
} }
</script> </script>
<div class="mb-0"> <div class="mb-0">
{#if field.collections.length === 1} <!-- <button
<button class="button"
class="button" on:click={(e) => openFileModal(e, collections[0].name)}
on:click={(e) => openBrowseModal(e, collections[0].name)} >
> Browse
Browse </button> -->
</button>
{:else} <div>
<Dropdown> <Uploader recordId={record.id} on:uploadComplete={uploadComplete} />
<div slot="button"> </div>
Browse
</div>
{#each collections as collection}
<!-- {`${channelurl}/content/${collection.name}?parent=${record.id}&parentfield=${field.name}`} -->
<a
class="dropdown-item"
on:click={(e) => openBrowseModal(e, collection.name)}
href="/">{collection.label}</a
>
{/each}
</Dropdown>
{/if}
</div> </div>
{#if references.length > 0} <!-- {#if references.length > 0}
<Sortable sortableClass="mt-3" on:update={reorder}> <Sortable sortableClass="mt-3" on:update={reorder}>
{#each references as reference (reference.id)} {#each references as reference (reference.id)}
<!--This div helps the sorting thing--> <!--This div helps the sorting thing-->
<div> <!-- <div>
<PreviewFile record={reference} hasDelete={true} on:remove={removeReference}></PreviewFile> <PreviewFile
record={reference}
hasDelete={true}
on:remove={removeReference}
></PreviewFile>
</div> </div>
{/each} {/each}
</Sortable> </Sortable>
{/if} {/if} -->
<Dialog bind:this={browseModal} on:insert={insert}></Dialog> <!-- <FileDialog bind:this={browseModal} on:insert={insert}></FileDialog> -->
+13 -22
View File
@@ -11,7 +11,6 @@ final class Channel
{ {
public string $lucentUrl; public string $lucentUrl;
public string $filesUrl; public string $filesUrl;
public array $disks;
public string $previewTargetUrl; public string $previewTargetUrl;
/** /**
@@ -19,37 +18,29 @@ final class Channel
* @param Collection<UserCommand> $commands * @param Collection<UserCommand> $commands
*/ */
function __construct( function __construct(
public string $name, public string $name,
public string $url, public string $url,
public string $previewTarget, public string $previewTarget,
public Collection $commands, public Collection $commands,
public Collection $schemas, public Collection $schemas,
public array $imageFilters, public array $imageFilters,
public array $roles, public array $roles,
) ) {
{
$this->lucentUrl = $url . "/lucent"; $this->lucentUrl = $url . "/lucent";
$this->filesUrl = $this->makeFilesUrl(); $this->filesUrl = $this->makeFilesUrl();
$this->disks = $this->getDisksFromSchemas();
$this->previewTargetUrl = $url . "/" . $previewTarget; $this->previewTargetUrl = $url . "/" . $previewTarget;
} }
private function makeFilesUrl(): string private function makeFilesUrl(): string
{ {
return match (config("filesystems.disks.lucent.driver")) { return match (config("filesystems.disks.lucent.driver")) {
"s3" => config("filesystems.disks.lucent.endpoint") . "/" . config("filesystems.disks.lucent.bucket"), "s3" => config("filesystems.disks.lucent.endpoint") .
"local" => $this->url . "/storage" . config("filesystems.disks.lucent.endpoint"), "/" .
default => "" 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\Channel\Data\UserCommand;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
use Lucent\Schema\Schema; use Lucent\Data\Schema;
use Lucent\Schema\SchemaService; use Lucent\Schema\SchemaService;
use PhpOption\Option; use PhpOption\Option;
+19 -16
View File
@@ -4,22 +4,18 @@ namespace Lucent\Commands;
use DirectoryIterator; use DirectoryIterator;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Lucent\Schema\Schema; use Lucent\Data\Schema;
use Lucent\Schema\SchemaService; use Lucent\Schema\SchemaService;
use Lucent\Schema\Type;
class CompileSchemas extends Command 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) public function handle(SchemaService $schemaService)
{ {
$configDir = base_path(config("lucent.schemas_path"));
$configDir = base_path(config('lucent.schemas_path'));
$schemasDirIterator = new DirectoryIterator($configDir); $schemasDirIterator = new DirectoryIterator($configDir);
$schemas = []; $schemas = [];
@@ -28,31 +24,39 @@ class CompileSchemas extends Command
continue; continue;
} }
$schemaJson = file_get_contents($configDir . "/" . $file->getFilename()); $schemaJson = file_get_contents(
$configDir . "/" . $file->getFilename(),
);
$schema = json_decode($schemaJson, true); $schema = json_decode($schemaJson, true);
if (empty($schema)) { if (empty($schema)) {
$this->error("Invalid JSON " . $file->getFilename()); $this->error("Invalid JSON " . $file->getFilename());
return 0; return 0;
} }
$schemas[] = $schema; $schemas[] = $schema;
} }
$schemas = collect($schemas)->sortBy("label")->values(); $schemas = collect($schemas)->sortBy("label")->values();
$roles = $schemas $roles = $schemas
->map([$schemaService, 'fromArray']) ->map([$schemaService, "fromArray"])
->whereIn("type", [Type::COLLECTION, Type::FILES]) ->reduce(
->reduce(fn($carry, Schema $schema) => array_merge( fn($carry, Schema $schema) => array_merge(
$carry, $carry,
$schema->read, $schema->read,
$schema->write, $schema->write,
config("lucent.canInvite") ?? [], config("lucent.canInvite") ?? [],
config("lucent.canBuild") ?? [], config("lucent.canBuild") ?? [],
), []); ),
[],
);
$json = [ $json = [
"schemas" => $schemas->toArray(), "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"))) { if (!file_exists(storage_path("lucent"))) {
@@ -63,5 +67,4 @@ class CompileSchemas extends Command
$this->info("Lucent Schemas were updated"); $this->info("Lucent Schemas were updated");
} }
} }
+20 -17
View File
@@ -15,44 +15,47 @@ use Lucent\Schema\Type;
class RebuildThumbnails extends Command 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 function __construct(
public Query $query, public Query $query,
public FileService $fileService, public FileService $fileService,
) ) {
{
parent::__construct(); parent::__construct();
} }
public function handle(ChannelService $channelService): int public function handle(ChannelService $channelService): int
{ {
$channelService->channel->schemas $channelService->channel->schemas
->filter(fn(Schema $schema) => get_class($schema) === FilesSchema::class) ->filter(
->map([$this, 'rebuildThumbnails']); fn(Schema $schema) => get_class($schema) === FilesSchema::class,
)
->map([$this, "rebuildThumbnails"]);
return 0; return 0;
} }
public function rebuildThumbnails(FilesSchema $schema): void public function rebuildThumbnails(FilesSchema $schema): void
{ {
$this->info("Rebuilding thumbnails for " . $schema->name);
$this->info("Rebuilding thumbnails for ". $schema->name); $records = $this->query
$records = $this->query->limit(0)->filter(["schema" => $schema->name])->run()->records; ->limit(0)
->filter(["schema" => $schema->name])
->run()->records;
$disk = $this->fileService->loadDisk($schema->disk); $disk = $this->fileService->loadDisk($schema->disk);
foreach ($records as $record) { foreach ($records as $record) {
try{ try {
$this->fileService->createTemplates(
$this->fileService->createTemplates($disk, $record->_file->path); $disk,
$record->_file->path,
);
} catch (Exception $e) { } 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->tableUsers();
$this->tableRecords(); $this->tableRecords();
$this->tableFiles();
$this->tableRevisions(); $this->tableRevisions();
$this->tableSessions(); $this->tableSessions();
$this->tableCommandLogs(); $this->tableCommandLogs();
@@ -73,11 +74,8 @@ class SetupDatabase extends Command
$table->string("status"); $table->string("status");
$table->jsonb("data"); $table->jsonb("data");
$table->jsonb("_sys"); $table->jsonb("_sys");
$table->jsonb("_file");
$table->text("search")->default(""); $table->text("search")->default("");
// $table->index(["schema", "_sys->updatedAt", "status"]); // $table->index(["schema", "_sys->updatedAt", "status"]);
$table->index("search"); $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 private function tableRevisions(): void
{ {
$schema = Database::make()->getSchemaBuilder(); $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 Intervention\Image\ImageManager;
use Lucent\Channel\ChannelService; use Lucent\Channel\ChannelService;
use Lucent\Database\Database; use Lucent\Database\Database;
use Lucent\Id\Id;
use Lucent\LucentException; use Lucent\LucentException;
use Lucent\Record\FileData as RecordFile; use Lucent\Data\File as DataFile;
use Lucent\Record\QueryRecord; use Lucent\Record\QueryRecord;
use Lucent\Schema\FilesSchema; use Lucent\Schema\FilesSchema;
use Lucent\Schema\Schema;
use Spatie\ImageOptimizer\OptimizerChainFactory; use Spatie\ImageOptimizer\OptimizerChainFactory;
class FileService class FileService
{ {
public function __construct( public function __construct(
public ChannelService $channelService, public ChannelService $channelService,
public ImageManager $imageManager, public ImageManager $imageManager,
public Logger $logger public Logger $logger,
) ) {}
{
}
public function getPath(QueryRecord $file): string 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); $pathinfo = pathinfo($url);
$contents = file_get_contents($url); $contents = file_get_contents($url);
if ($contents === false) { if ($contents === false) {
throw new LucentException("Failed to upload file from url"); throw new LucentException("Failed to upload file from url");
} }
$file = '/tmp/' . $pathinfo['basename']; $file = "/tmp/" . $pathinfo["basename"];
file_put_contents($file, $contents); file_put_contents($file, $contents);
$uploadedFile = new UploadedFile($file, $pathinfo['basename']); $uploadedFile = new UploadedFile($file, $pathinfo["basename"]);
return $this->upload($schema, $uploadedFile); 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); $originalName = pathinfo(
$extension = pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION); $file->getClientOriginalName(),
PATHINFO_FILENAME,
);
$extension = pathinfo(
$file->getClientOriginalName(),
PATHINFO_EXTENSION,
);
$originalFilename = $file->getClientOriginalName(); $originalFilename = $file->getClientOriginalName();
$filename = $this->createFileName($originalName, $extension); $filename = $this->createFileName($originalName, $extension);
$mimetype = $file->getMimeType(); $mimetype = $file->getMimeType();
@@ -58,72 +65,74 @@ class FileService
$optimizerChain->setTimeout(30)->optimize($file->getPathName()); $optimizerChain->setTimeout(30)->optimize($file->getPathName());
$checksum = sha1_file($file); $checksum = sha1_file($file);
$recordId = $this->checkDuplicate($schema->name, $checksum, $file->getSize());
if (!empty($recordId)) { $disk = $this->loadDisk();
return new FileUploadResult( $path = "files/" . $recordId . "/" . $filename;
recordFile: null,
duplicateId: $recordId,
isDuplicate: true
);
}
$disk = $this->loadDisk($schema);
$path = $schema->path . "/" . $filename;
$res = $disk->put( $res = $disk->put(
$path, $path,
file_get_contents($file), file_get_contents($file),
// 'public' // now managed by aws policy // 'public' // now managed by aws policy
); );
if ($res === false) { if ($res === false) {
throw new LucentException("File $filename not uploaded"); throw new LucentException("File $filename not uploaded");
} }
if($this->isImage($mimetype)){ if ($this->isImage($mimetype)) {
$this->createTemplates($disk, $path); $this->createTemplates($disk, $path);
} }
[$width, $height] = $this->isImage($mimetype)
list($width, $height) = $this->isImage($mimetype) ? getimagesize($file) : [0, 0]; ? getimagesize($file)
$recordFile = new RecordFile( : [0, 0];
return new DataFile(
id: Id::new(),
recordId: $recordId,
originalName: $originalFilename, originalName: $originalFilename,
mime: $mimetype, mime: $mimetype,
path: $path, path: $path,
disk: $schema->disk,
size: $file->getSize(), size: $file->getSize(),
width: $width, width: $width,
height: $height, height: $height,
checksum: $checksum checksum: $checksum,
);
return new FileUploadResult(
recordFile: $recordFile,
duplicateId: "",
isDuplicate: false
); );
} }
private function createFileName(string $originalName, string $extension): string private function createFileName(
{ string $originalName,
return Str::slug($originalName, '-') . '-' . uniqid() . '.' . $extension; string $extension,
): string {
return Str::slug($originalName, "-") .
"-" .
uniqid() .
"." .
$extension;
} }
private function isImage(string $mimetype): bool 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); 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 private function checkDuplicate(
{ string $schemaName,
string $checksum,
$record = Database::make()->table("lucent_records") int $filesize,
): string {
$record = Database::make()
->table("lucent_records")
->where("schema", $schemaName) ->where("schema", $schemaName)
->where("_file->checksum", $checksum) ->where("_file->checksum", $checksum)
->where("_file->size", $filesize) ->where("_file->size", $filesize)
@@ -137,14 +146,14 @@ class FileService
$originalImage = $this->imageManager->make($disk->get($path)); $originalImage = $this->imageManager->make($disk->get($path));
foreach (config("lucent.imageFilters") as $preset => $filterClass) { foreach (config("lucent.imageFilters") as $preset => $filterClass) {
$imageClone = clone $originalImage; $imageClone = clone $originalImage;
$image = $imageClone->filter(new $filterClass); $image = $imageClone->filter(new $filterClass());
$templateUri = "/templates/" . $preset . "/" . $path; $templateUri = "/templates/" . $preset . "/" . $path;
$disk->put($templateUri, $image->encode('webp', 75)); $disk->put($templateUri, $image->encode("webp", 75));
} }
$thumbDir = "thumbs/" . $path; $thumbDir = "thumbs/" . $path;
$image = $originalImage->fit(300, 300); $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\fail;
use function Lucent\Response\ok; use function Lucent\Response\ok;
class FileController extends Controller class FileController extends Controller
{ {
public function __construct( public function __construct(
private readonly ChannelService $channelService, private readonly ChannelService $channelService,
private readonly RecordService $recordService, private readonly RecordService $recordService,
private readonly FileService $fileService, private readonly FileService $fileService,
private readonly Query $query private readonly Query $query,
) ) {}
{
}
public function fromDisk(Request $request, string $disk) public function fromDisk(Request $request, string $disk)
{ {
@@ -38,7 +33,7 @@ class FileController extends Controller
public function thumb(Request $request, string $disk) public function thumb(Request $request, string $disk)
{ {
$imagePath = "thumbs/".$request->route("any"); $imagePath = "thumbs/" . $request->route("any");
$disk = $this->fileService->loadDisk($disk); $disk = $this->fileService->loadDisk($disk);
return response()->file($disk->path($imagePath)); return response()->file($disk->path($imagePath));
} }
@@ -49,34 +44,21 @@ class FileController extends Controller
return $disk->download($request->input("path")); return $disk->download($request->input("path"));
} }
public function upload(Request $request) public function upload(Request $request)
{ {
$validator = Validator::make($request->all(), [ $validator = Validator::make($request->all(), [
'files.*' => 'required|file|max:100000', "files.*" => "required|file|max:100000",
]); ]);
if ($validator->fails()) { if ($validator->fails()) {
return fail($validator->errors()->first()); return fail($validator->errors()->first());
} }
$schema = $this->channelService->channel->schemas->firstWhere("name", $request->input("schema")); $recordId = $request->input("recordId");
$files = $request->file('files'); $files = $request->file("files");
foreach ($files as $file) { return ok(
$this->recordService->createFromUploadedFile($file, new RecordInputData( collect($files)
schemaName: $schema->name, ->map(fn($file) => $this->fileService->upload($recordId, $file))
status: Status::PUBLISHED, ->toArray(),
), []); );
}
$graph = $this->query
->filter([
"schema" => $schema->name
])
->limit(15)
->skip(0)
->sort("-_sys.updatedAt")
->run();
return ok($graph->records->toArray());
} }
} }
+32 -46
View File
@@ -2,59 +2,39 @@
namespace Lucent\Schema; namespace Lucent\Schema;
use Lucent\Data\Schema;
use Lucent\LucentException; use Lucent\LucentException;
use Lucent\Primitive\Collection; use Lucent\Primitive\Collection;
class SchemaService class SchemaService
{ {
public function __construct() {}
public function __construct()
{
}
public function fromArray(array $schemaArr): Schema public function fromArray(array $schemaArr): Schema
{ {
return new Schema(
return match ($schemaArr["type"]) { name: $schemaArr["name"],
"collection" => new CollectionSchema( label: $schemaArr["label"],
name: $schemaArr["name"], visible: $schemaArr["visible"] ?? [],
label: $schemaArr["label"], groups: $schemaArr["groups"] ?? [],
visible: $schemaArr["visible"] ?? [], fields: Collection::make($schemaArr["fields"])->map([
groups: $schemaArr["groups"] ?? [], $this,
fields: (new Collection($schemaArr["fields"]))->map([$this, 'mapFields']), "mapFields",
isEntry: $schemaArr["isEntry"] ?? false, ]),
color: $schemaArr["color"] ?? "", isEntry: $schemaArr["isEntry"] ?? false,
sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt", color: $schemaArr["color"] ?? "",
cardTitle: $schemaArr["titleTemplate"] ?? $schemaArr["cardTitle"] ?? null, sortBy: $schemaArr["sortBy"] ?? "-_sys.updatedAt",
cardImage: $schemaArr["cardImage"] ?? null, cardTitle: $schemaArr["titleTemplate"] ??
revisions: $schemaArr["revisions"] ?? 0, ($schemaArr["cardTitle"] ?? null),
read: $schemaArr["read"] ?? [], cardImage: $schemaArr["cardImage"] ?? null,
write: $schemaArr["write"] ?? [], revisions: $schemaArr["revisions"] ?? 0,
), read: $schemaArr["read"] ?? [],
"files" => new FilesSchema( write: $schemaArr["write"] ?? [],
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"] ?? [],
)
};
} }
public function mapFields(array $field): FieldInterface public function mapFields(array $field): FieldInterface
{ {
$schemaFields = [ $schemaFields = [
\Lucent\Schema\Ui\Checkbox::class, \Lucent\Schema\Ui\Checkbox::class,
\Lucent\Schema\Ui\Color::class, \Lucent\Schema\Ui\Color::class,
@@ -70,16 +50,22 @@ class SchemaService
\Lucent\Schema\Ui\Text::class, \Lucent\Schema\Ui\Text::class,
\Lucent\Schema\Ui\Textarea::class, \Lucent\Schema\Ui\Textarea::class,
]; ];
$ui = collect($schemaFields)->filter(function ($className) use ($field) { $ui = collect($schemaFields)
return str_ends_with(strtolower($className), "\\" . strtolower($field["ui"])); ->filter(function ($className) use ($field) {
})->first(); return str_ends_with(
strtolower($className),
"\\" . strtolower($field["ui"]),
);
})
->first();
if (empty($ui)) { if (empty($ui)) {
throw new LucentException("Field UI " . $field["ui"] . " not found"); throw new LucentException(
"Field UI " . $field["ui"] . " not found",
);
} }
unset($field["ui"]); unset($field["ui"]);
return new $ui(...$field); return new $ui(...$field);
} }
} }