Skip to content

Keeping media in a bucket

HiLMS keeps uploaded files on its own server until an administrator says otherwise. This guide moves them to an S3-compatible bucket instead. The panel does most of the work: Settings → Media and storage → Set up a bucket opens a step-by-step guide that asks for one thing at a time, in the chosen provider’s own words, hands out the exact CORS rule for the site, and checks the bucket before it saves anything. This document says what happens behind each step, what each provider needs, how to move the files already uploaded, and how to check the result end to end.

Cloudflare R2 is the provider this guide recommends: it has a free tier and charges nothing for traffic out of the bucket, which is what a site serving video pays for elsewhere. The guide also knows Amazon S3, Backblaze B2, Hetzner Object Storage and DigitalOcean Spaces, and takes any other S3-compatible service (a self-hosted MinIO, Wasabi) as Other S3-compatible.

Providers change their consoles, their prices and their free allowances. The steps below name what to look for rather than promising exact button positions, and no number in this guide is a price or a limit: check the provider’s current terms before relying on a free tier.

What Why
A private course-material bucket Every file a lesson shows as course material. It is never public: a student’s link goes through HiLMS, which checks access to the lesson and only then redirects to an address the bucket signs for five minutes
Optionally, a public bucket of its own, with a public address Course covers, avatars, the logo, page pictures and stored fonts. Without one, public files simply stay on this server
One key reaching those buckets, allowed to read and write objects HiLMS writes, reads, lists and deletes files, and asks the course-material bucket to sign addresses
A CORS rule on each bucket for the site’s address A video in a bucket does not play without it (below)

The public bucket must never be the course-material bucket. R2 and B2 open a whole bucket to anyone once it has a public address, so a shared bucket would publish every lesson file with the covers. The guide refuses the same name twice, and HiLMS refuses it again whatever the server’s environment says. Both buckets sit under one account and one endpoint, because the guide has one key for both. HiLMS keeps course material under lessons/ and public files under public/, so a bucket may be shared with nothing else but need not be empty.

A lesson’s video is played in CORS mode (crossorigin="anonymous") whenever its file or one of its caption tracks lives in a bucket. The browser asks HiLMS for the video, HiLMS checks access and redirects to the bucket, and the bucket’s answer must then say that the site’s address may read it. Without that rule the video does not play at all, caption tracks are thrown away, the panel’s upload field cannot preview a stored file, and a style’s fonts kept in the public bucket are refused, so the text falls back to the system face behind them.

The guide’s Allow your site step shows the rule with the site’s own address filled in, a Copy button, and where to paste it in the chosen provider’s console; its last step checks that the bucket really answers with it. The panel lives on the same address as the site (/admin), so one rule covers both. A staging copy or a developer’s local site goes into the same list of allowed origins.

Sign in as an administrator, open Settings (Ustawienia) at the foot of the panel’s navigation and choose Media and storage (Media i przechowywanie). Under Where new uploads go press Set up a bucket. The guide has six steps; nothing is saved until the last.

  1. Provider — who keeps the files. If there is no account anywhere yet, Cloudflare R2 is the easiest start.
  2. Connect — what the provider needs to be found (R2: the Account ID and the jurisdiction; Amazon S3: the region; B2: the region shown in the bucket’s endpoint; Hetzner: the location; Spaces: the region; Other: the endpoint, the region and the addressing), then the key and its secret, named the way the provider names them, with where the console shows them. Every named provider needs both: without a key the S3 client would go looking for a cloud server’s own credentials. The secret is stored encrypted and never shown again; when changing a setup, a blank secret keeps the stored one.
  3. Course material — the private bucket’s name, with the provider’s steps to create it.
  4. Public files — optional. Without it public files stay on this server, which suits most sites. With it: the public bucket and the address it is read from. Amazon S3, B2, Hetzner and Spaces read a public bucket from an address of a fixed shape, which the guide fills in (type a CDN’s address instead if one sits in front); R2 and Other need the address typed. R2’s r2.dev address is meant for trying things out and is limited by Cloudflare: a live site connects its own domain to the bucket.
  5. Allow your site — the CORS rule (above), for each bucket in use.
  6. Check and save — the site writes a 19-byte test file to each bucket, reads it back, asks the course-material bucket to sign an address and opens that address from outside as a player would, reads the CORS answer, makes sure the course-material bucket does not open without a signature, reads the public bucket through its public address, and deletes the test files. Each check is listed in words: passed, or failed with the reason and the fix in the provider’s own terms.
    • A failure that must be fixed — the bucket cannot be written, read or asked to sign, or the course-material bucket opens to anybody — keeps Save off. Fix it in the provider’s console and press Check again.
    • A failure that may be saved anyway — the CORS rule, or the public address — is explained with what it will break, and Save asks for a tick in “Save anyway — I understand that …”.

Saving checks once more on the server, stores the setup and, from the guide opened with Set up a bucket, sends new uploads to the bucket. The summary then says where new uploads go and when the bucket was last checked; Check again repeats the check on the saved setup, Change… reopens the guide (where new uploads go stays as it is), and Stop using the bucket sends new uploads to this server again and keeps the setup, so files already in the bucket keep working and can come back.

Cloudflare R2. A free Cloudflare account is enough, but Cloudflare asks for a payment method before R2 can be used, even on the free tier. The Account ID is on the R2 overview page. Create the buckets with Create bucket (location Automatic; a jurisdiction such as EU changes the endpoint, and every bucket the site uses must be in that same jurisdiction). The key is an API token from Manage API tokens with Object Read & Write, limited to the site’s buckets; Cloudflare shows its Access Key ID and Secret Access Key once. Public access for the public bucket is in its Settings → Public access (R2.dev subdomain, or a custom domain on Cloudflare in the same account), and the CORS rule under Settings → CORS policy.

Amazon S3. The region is the one the buckets were created in. The key belongs to an IAM user whose policy allows reading, writing, listing and deleting objects in the two buckets. A public bucket needs “Block all public access” turned off and a bucket policy allowing everybody s3:GetObject on public/*; HiLMS sends no per-file permission, because a new bucket refuses one. The CORS rule goes under Permissions → Cross-origin resource sharing (CORS).

Backblaze B2. The region is the middle part of the bucket’s endpoint (s3.us-west-004.backblazeb2.com → us-west-004). Buckets are private or public as a whole (“Files in Bucket are”), and B2 may ask for a verified e-mail address before it allows a public one. The key is an Application Key with Read and Write access; B2 calls its parts keyID and applicationKey, and the guide does too. B2’s console sets CORS rules for the S3-compatible API; the guide gives the origin to enter.

Hetzner Object Storage. The location is fsn1, nbg1 or hel1. A public bucket gets the visibility Public in the console. Hetzner’s console has no CORS editor, so the guide prints the command that sets the rule with the AWS command-line tool.

DigitalOcean Spaces. The region is the Space’s datacentre (fra1, ams3, …). Public files are made readable one by one as HiLMS writes them. The CORS rule goes in the Space’s settings.

Other S3-compatible. The endpoint, the region and path-style addressing (only for a provider that cannot serve the bucket as a sub-domain, such as a self-hosted MinIO) are typed. A field left blank uses the server’s own MEDIA_S3_* value from .env (requirements.md).

Moving is optional. Every file’s record says which disk it lives on, so after a switch the files already uploaded keep being served from this server, with no time limit, while new uploads go to the bucket. Moving them frees this server’s disk, or empties a provider being left.

Where your files are shows how many files, and how much, lie on this server and in the bucket, and how many wait to go where new uploads go. The button says exactly what it does — Move 16 files (8.6 MB) to Cloudflare R2, or …back to this server — and appears only when something waits. Confirming starts a background move: each file is copied, checked to have arrived whole, and only then removed from where it was, one file at a time, so students keep watching and downloading throughout. The section shows “Moving… 7 of 16 files done” while it runs and keeps the button locked, then lists any file that could not be moved with the reason, and offers Try the N remaining …. A file that could not be moved stays where it was and keeps working.

The moves run on a queue of their own, media-moves, with one worker and up to an hour per file, so two moves never overlap and a large video has the time it needs. Horizon must be running; when it is not, the section says that moves wait for the queue worker. For a library of very large videos on a slow uplink, php artisan hilms:media:relocate moves everything from the command line with no time limit (--dry-run lists what would move).

A deployment never moves files: hilms:upgrade only says how many wait.

A record names its disk, not its bucket. So while any file lives in a bucket, the guide keeps that bucket’s identity — the provider, the account or location, the endpoint and the bucket’s name — fixed, and says how many files hold it there; the server refuses a changed identity even if a browser sends one. The key, the secret and the public address may change at any time (the check proves the new ones), which is how a key is rotated or a custom domain replaces r2.dev.

To change provider or bucket: Stop using the bucket, move the files back to this server, then set up the new bucket and move them there. Forget the bucket clears the setup, keys included, and is offered only once no file lives in either bucket.

Files kept in a bucket are not in HiLMS’s nightly backup: the provider keeps them. A backup of the database and the server’s own files can itself go to a bucket through the BACKUP_S3_* keys in .env — use a separate, private bucket for that, never the public one.

Run these once after the move, and again after any change to the buckets, the key or the CORS rules. Each says what to do and what you should see. You need an administrator, one student enrolled in a course, and a second browser or a private window.

  1. The check. On Media and storage, press Check again. Expected: “Checked just now: everything works”, and the Health page shows MediaStorage as reachable.
  2. Upload a lesson video. Edit a lesson, add a Video block with Uploaded file (Wgrany plik), press Upload new (Prześlij nowy) beside Video file (Plik wideo), upload an MP4, save the lesson. Expected: the upload finishes, the block’s preview plays it, and in the provider’s console the course-material bucket holds a file under lessons/<number>/.
  3. Play and seek. Sign in as the enrolled student and open that lesson. Expected: the video plays; dragging the position to the middle and to the end plays from there. The browser’s developer tools (Network) show the request to /media/… answered with a 302 and the next request, to the bucket’s address, with 206 Partial Content.
  4. A long pause. Pause the video for more than five minutes, then drag the position to a part that has not loaded yet. Expected: the video carries on from there, at the speed it was set to. The Network tab shows a 403 from the bucket’s expired address and then a new request to /media/…: the player asks HiLMS again, which checks access and signs a new address.
  5. A stranger gets nothing. Copy the video’s address from the developer tools (the src of the <source> inside <video>, starting with /media/…?lesson=…) and, from a terminal where no browser headers are sent, run curl -s -o /dev/null -w '%{http_code}\n' '<that address>'. Expected: 403, because nobody is signed in.
  6. The link does not open in an address bar. As the student, paste the same address into a new tab. Expected: a 403 page, with no redirect to the bucket in the Network tab.
  7. Public files through their public address. With a public bucket, give a course a new cover with Upload new and save. Expected: on the catalogue and the course page the picture’s address starts with the public address, followed by public/, and opening it in a private window shows the picture. Without a public bucket the address stays on the site’s own /storage/…, which is correct.
  8. Captions from the bucket. In the media library, open the video from step 2, add a WebVTT caption track under Captions (Napisy), save. Expected: as the student, the player offers the captions and shows them.
  9. Three browsers. Repeat steps 3, 4 and 8 in Chrome, Firefox and Safari.
  10. Without the CORS rule it fails, and the check says so. Remove the CORS policy from the course-material bucket, wait a minute, and press Check again. Expected: the CORS check fails with the provider’s fix, and the student’s video no longer plays (reload with the cache emptied). Put the policy back; the check passes and the video plays.
  11. Move the files both ways. Stop using the bucket, then press Move … back to this server and wait for the section to finish. Expected: the lesson still plays (now with no redirect in the Network tab) and the bucket’s lessons/ is empty. Use the bucket again, move the files there once more, and the lesson plays from the bucket again.
  12. A backup to a bucket. Create a third, private bucket with the same key reaching it, and set BACKUP_S3_ENABLED=true with BACKUP_S3_KEY, BACKUP_S3_SECRET, BACKUP_S3_BUCKET, BACKUP_S3_ENDPOINT and BACKUP_S3_REGION in .env (auto for R2). Apply .env (docker compose up -d in Docker), then run php artisan backup:run. Expected: the command ends without an error and the backup bucket holds a zip in a directory named after the application.

If a step fails, the check on the page and the log (storage/logs) carry the reason; upgrade.md has the same move in the context of an upgrade.

HiLMS is MIT-licensed. No replicants were harmed in the writing of these books.