История коммитов

.
feat(storage): serve files from disks that are not public
A disk without a base URL now has a way to reach the browser: FileStore::openStream() and the /file/{id} route stream it, and the URL a module renders switches to that route on its own — moving a directory of attachments onto a private disk changes nothing in the modules.

That is what a private disk is for. On the public disk anybody who has the address has the file forever, whatever the site decides afterwards; on a private one the paths cannot be guessed or listed, and a file stops being reachable the moment its row is deleted. Who may see a given attachment is still not asked — three modules relate files to their records in three different ways, so there is nothing to ask yet.
.
refactor(files): split the file registry from the disk
FileStorage took Johncms\Http\Request although it outlives one, returned Models\File|Models\File[] depending on a boolean flag, and File built the path, wrote to the disk and inserted the row at once, reaching for the container from inside a domain class. The row it inserted always said 'local' regardless of the disk it had written to, so a second disk would have made every row point at the wrong one.

FileStore now owns both halves, and the order they run in is deliberate: storing writes the file first and removes it again if the row cannot be inserted; deleting removes the row first and only logs a disk that refuses, because a row pointing at nothing is worse than a file nobody points at. Deleting is idempotent, which is what lets guestbook drop DeleteAttachedFilesService — its loop, its try/catch and its logging are deleteMany().

Modules get StoredFileDTO instead of the model: the CKEditor endpoints build their JSON from it, and the URL comes from the disk rather than from UPLOAD_PATH concatenation. Models\File becomes Johncms\Files\StoredFile, a detail of the package behind FileRepositoryInterface, and forum stops querying the core table through it: AttachUploadedFilesToMessageUseCase asks FileStore::filterIdsInDirectory().

The extension a file is stored under is now reduced to letters and digits, and League\Flysystem\FilesystemException no longer appears in any module.
.
refactor(storage): introduce the storage port and the disk registry
Johncms\Files\Filesystem handed out League\Flysystem\Filesystem itself, so every caller programmed against the library, and its switch had a single default branch: any driver name but a local directory was silently treated as one. It also ignored the 'default' key of the configuration and defaulted to a hard-coded disk name.

StorageInterface is now the port, FlysystemStorage the only class that names the library, and StorageRegistry resolves disks by name for the callers that learn it at runtime. Drivers come from the StorageDriver enum through StorageFactory, following CacheDriver/CachePoolFactory; an unknown one is refused where the settings are read.

Fixes the permissions of what is stored: the local adapter applies its permission map only when the write tells it the visibility, and it creates directories with plain mkdir(), which subtracts umask. Files are now written with the visibility of the disk, and the directories a write creates get it afterwards, so a strict umask no longer produces files the web server cannot read.

The configuration moves from storages/type/root_dir to disks/driver/root, gaining url, visibility and permissions per disk.
.
chore(deps): upgrade league/flysystem to 3.x
The local adapter moved into league/flysystem-local, which the core package requires, so no code change was needed: namespaces and signatures are unchanged.
.
feat(images): add cropping, padding, stretching and watermarking to the image processor
.
docs(images): document the new operations and bump the documentation submodule
.
docs(images): add the image processing guide for agents and bump the docs submodule
.
refactor(images): upgrade intervention/image to v4 behind an image processor port
.
docs: bump the documentation submodule
.
chore: ignore .DS_Store