Uploads carry the name the file came with (#83)

An upload sends its file name next to the raw bytes, and the server keeps
it in a sidecar next to the content-addressed file. Both pickers caption
every tile and row with it, and an asset made there is named after the
file instead of "Texture" / "Font". Last upload of the same content wins.

The API's own set_error_handler ignores error_reporting(), so @-silenced
warnings still throw: a missing sidecar took the whole listing down with a
500. Existence is checked instead of suppressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DeveloperandClaude Opus 5 committed 2026-09-13 20:32:45 +02:00
1 parent 86b5363a01
commit 99c5d2ad78
21 files changed
+347 -61

No files matched your search

+15 -5
View File
@@ -46,10 +46,13 @@ final class FontStore
* libmagic versions (`font/sfnt`, `application/vnd.ms-opentype`, plain `application/octet-stream`),
* so a second opinion from it would reject valid files rather than catch bad ones.
*
* @return array{file:string,bytes:int}
* `$originalName` is kept beside the file as a caption (see OriginalName): the hash says
* nothing, and the name inside the font is deliberately never read (see `gm-font-` families).
*
* @return array{file:string,name:string,bytes:int}
* @throws InvalidFontException when the bytes are not a supported font
*/
public function store(string $bytes): array
public function store(string $bytes, string $originalName = ''): array
{
$extension = self::SIGNATURES[substr($bytes, 0, 4)] ?? '';
if ($extension === '') {
@@ -65,26 +68,29 @@ final class FontStore
if (!is_file($path)) {
$this->writeFile($path, $bytes);
}
OriginalName::write($this->dir, $file, $originalName);
return ['file' => $file, 'bytes' => strlen($bytes)];
return ['file' => $file, 'name' => OriginalName::read($this->dir, $file), 'bytes' => strlen($bytes)];
}
/**
* Every stored file, newest first – what the font picker offers. Unlike an image there is
* nothing to read out of the header here: the picker shows each face by rendering with it.
*
* @return list<array{file:string,bytes:int,updatedAt:string}>
* @return list<array{file:string,name:string,bytes:int,updatedAt:string}>
*/
public function list(): array
{
$items = [];
foreach (glob($this->dir . '/*.*') ?: [] as $path) {
$file = basename($path);
// Sidecars share the directory and are skipped: `{hash}.ttf.json` is not a valid name.
if (!self::isValidName($file)) {
continue;
}
$items[] = [
'file' => $file,
'name' => OriginalName::read($this->dir, $file),
'bytes' => (int) filesize($path),
'updatedAt' => gmdate('c', (int) filemtime($path)),
];
@@ -99,7 +105,11 @@ final class FontStore
return false;
}
$path = $this->dir . '/' . $file;
return is_file($path) && unlink($path);
if (!is_file($path) || !unlink($path)) {
return false;
}
OriginalName::delete($this->dir, $file);
return true;
}
/** Temp file plus rename, so a reader never sees a half-written font (see TextureStore). */