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

.
docs(storage): bump the documentation submodule
Brings in the two storage pages, the reworked image examples and the upgrade notes. Also catches up with the cache and image pages that were already pushed to the documentation repository.
.
feat(storage): add the s3 driver
Built on league/flysystem-aws-s3-v3, which stays optional: it pulls the AWS SDK behind it, and a site keeping its files on its own server has no use for either. Configure the driver without the package and the disk refuses to build with the command to install it, the way CachePoolFactory handles a missing redis extension.

Nothing else changes for the modules: withLocalCopy() downloads to a temporary file when the disk is not on this server, which is what the image processor and getID3 need, and the public address comes from the 'url' of the disk — the bucket, or a CDN in front of it.
.
refactor(mail,library): move their upload directories onto the disk
MailFileService kept its own UPLOAD_PATH constant and chmod'ed every attachment to 0666 — writable by anything running on the server; the mode now comes from the configuration of the disk. LibraryCoverStorage replaces four places that spelled out the three cover directories, and the size is an enum rather than a directory name repeated in each of them.

UserClean, which deletes what a removed user leaves behind, drops its hand-written recursive directory removal for deleteDirectory() — which the port gains here, along with what the library export needed.
.
refactor(forum): move message attachments onto the storage disk
Ten places built 'forum/attach/' by hand, two of them in the admin module, and the model of an attachment opened it with FileInfo to answer whether it is a picture and how big it is. ForumAttachmentStorage owns the directory and answers both questions; the preview endpoint asks it for a local copy instead of assuming the file is on this server.

The name of an attachment is a column of rows that predate the validation this CMS has now, so it goes through basename() before it becomes a path — the old code interpolated it as it came.
.
refactor(album): move album pictures onto the storage disk
Six places built 'users/album/{id}/' by hand — the upload, both deletes, the presenter, the download and the edit form — and the upload created the directory itself with mkdir(0777), which umask then narrowed. AlbumPhotoStorage owns the directory now; the disk creates what it needs with the permissions of the configuration.

"Use this picture as my profile photo" no longer copies between two hand-built paths: it asks UserImages to take the photo from the two files of the album, and copies nothing when either of them is missing — the old code copied whichever half it found and left the profile with mismatched pictures.

The port gains copy(), which that needed.

The installer seeds its demo photos through the same storage, so a fresh site gets them with the same permissions as an upload.
.
refactor(profile): move user pictures onto the storage disk
The avatar and the profile photo were addressed by hand in six places — two use cases writing them, two deleting them, the Twig runtime building the URL of one and a mutator of the user model building the URLs of the other — each with its own UPLOAD_PATH concatenation. Johncms\Users\UserImages owns the names now, and everything goes through the disk.

The port gains the two operations this needed: storeGenerated(), which hands the image processor a path to write to and puts the result on the disk (the counterpart of withLocalCopy), and lastModified(), which the cache-busting parameter of the avatar is built from.

Also drops the avatar_file variable the profile form passed to a template that never read it.
.
docs(storage): document the storage port and the file registry
Explains what FileStore is for, what happens when only half of a store or a delete succeeds, and when to reach for a disk directly. Also states the rule the port exists for: no module names League\Flysystem.
.
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.