PHPackages                             ultradev/openmage-mediasync-cloudflare-r2 - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. ultradev/openmage-mediasync-cloudflare-r2

ActiveMagento-module[File &amp; Storage](/categories/file-storage)

ultradev/openmage-mediasync-cloudflare-r2
=========================================

Sync Magento/OpenMage media files with Cloudflare R2 and serve via CDN

09↓92.3%PHP

Since Jun 5Pushed 1mo agoCompare

[ Source](https://github.com/LuizSantos22/openmage-mediasync-cloudflare-r2)[ Packagist](https://packagist.org/packages/ultradev/openmage-mediasync-cloudflare-r2)[ RSS](/packages/ultradev-openmage-mediasync-cloudflare-r2/feed)WikiDiscussions main Synced today

READMEChangelogDependenciesVersions (1)Used By (0)

OpenMage Media Sync — Cloudflare R2
===================================

[](#openmage-media-sync--cloudflare-r2)

Synchronize OpenMage media files with a Cloudflare R2 bucket and serve them via CDN for improved performance.

---

🚀 Overview
----------

[](#-overview)

This module syncs product and category images from your OpenMage installation to a Cloudflare R2 bucket. A Cloudflare Worker intercepts media requests and serves images directly from R2, while CSS, JS and other assets continue to be served from your origin server (also cached by Cloudflare CDN).

This approach avoids replacing Magento's native storage system and keeps local media intact.

---

✨ Features
----------

[](#-features)

- Sync product images on product save
- Sync category images on category save
- Sync CMS block/page media on save
- Sync minified JS/CSS bundles from fballiano/openmage-cssjs-minify to R2 automatically
- CLI script to bulk-sync all existing media files
- Compatible with Cloudflare R2 (S3-compatible API)
- Secure credential storage (encrypted in admin)
- Retry mechanism for uploads (3 attempts)
- Avoids unnecessary uploads (local control file — zero R2 API calls on normal requests)
- Skips cache, CSS, JS, and other unnecessary folders automatically
- Blocks accidental upload of server config files (.htaccess, .env, etc.)

---

📦 Requirements
--------------

[](#-requirements)

- OpenMage / Magento 1.9+
- PHP 8.1+
- Composer
- Cloudflare account with R2 enabled
- Domain managed by Cloudflare (for Worker custom domain)

---

📥 Installation
--------------

[](#-installation)

### Option 1 — Composer (recommended)

[](#option-1--composer-recommended)

composer require ultradev/openmage-mediasync-cloudflare-r2

### Option 2 — Manual

[](#option-2--manual)

Copy files into your Magento root:

app/code/community/UltraDev/MediaSync app/etc/modules/UltraDev\_MediaSync.xml shell/ultradev\_media\_sync.php

Then clear cache:

rm -rf var/cache/\* var/session/\*

---

⚙️ Configuration
----------------

[](#️-configuration)

Go to: System &gt; Configuration &gt; UltraDev &gt; UltraDev Media Sync

Fill in the fields:

FieldDescriptionEnableEnable/disable the moduleBucketYour R2 bucket name (e.g. my-store-media)EndpointR2 S3-compatible endpoint (e.g. https://&lt;ACCOUNT\_ID&gt;.r2.cloudflarestorage.com)Access KeyR2 API token Access Key IDSecret KeyR2 API token Secret Access Key (stored encrypted)Sync FBMinify bundles to R2Enable automatic sync of media/fbminify/ to R2 (requires fballiano/openmage-cssjs-minify)---

⚡ Optional: Full JS/CSS Optimization via CDN (fbminify + OpenMage merge)
------------------------------------------------------------------------

[](#-optional-full-jscss-optimization-via-cdn-fbminify--openmage-merge)

This module natively integrates with fballiano/openmage-cssjs-minify to serve minified JS/CSS bundles directly from Cloudflare R2, dramatically reducing render-blocking requests.

### How it works — the full chain

[](#how-it-works--the-full-chain)

OpenMage native merge (Developer settings) ↓ media/js/*.js + media/css/*.css (consolidated bundles — many files merged into few) ↓ fballiano/openmage-cssjs-minify (fbminify) intercepts HTML before it's sent to the browser, minifies each bundle and saves to media/fbminify/ ↓ media/fbminify/*.js + media/fbminify/*.css (minified, production-ready bundles) ↓ UltraDev MediaSync (this module) detects new files in media/fbminify/ and uploads to R2 ↓ cdn.yourdomain.com/media/fbminify/*.js cdn.yourdomain.com/media/fbminify/*.css (served via Cloudflare CDN with long cache)

### Why enable OpenMage native merge with fbminify?

[](#why-enable-openmage-native-merge-with-fbminify)

The fbminify author recommends not enabling OpenMage's native JS/CSS merge because, without a CDN, consolidating into a few large files can be slower than many small files loaded in parallel via HTTP/2.

However, when this R2 module is active, the equation changes significantly:

ScenarioFiles in Served fromResultNo merge, no CDN60+ individual filesOrigin serverSlowMerge only, no CDN2–4 large bundlesOrigin serverMediumNo merge, with CDN60+ individual filesCloudflareMediumMerge + CDN (this setup)2–4 minified bundlesCloudflareFast### Setup

[](#setup)

#### Step 1 — Install fbminify

[](#step-1--install-fbminify)

composer require fballiano/openmage-cssjs-minify

#### Step 2 — Enable OpenMage native merge

[](#step-2--enable-openmage-native-merge)

Go to: Admin → System → Configuration → Developer

- JavaScript Settings → Merge JavaScript Files: Yes
- CSS Settings → Merge CSS Files: Yes

OpenMage will consolidate all JS files into bundles saved at media/js/*.js and CSS files at media/css/*.css.

> These files in media/js/ and media/css/ are the input for fbminify — do not delete them.

#### Step 3 — Enable FBMinify sync in this module

[](#step-3--enable-fbminify-sync-in-this-module)

Go to: Admin → System → Configuration → UltraDev → UltraDev Media Sync

- FBMinify Sync → Sync FBMinify bundles to R2: Yes

#### Step 4 — Trigger first sync

[](#step-4--trigger-first-sync)

Visit your store in an incognito window. fbminify will generate the minified bundles in media/fbminify/ and this module will automatically upload them to R2.

Confirm in the log:

tail -f var/log/ultradev\_mediasync.log

You should see entries like:

R2 Uploaded: media/fbminify/abc123-1780264396.js R2 Uploaded: media/fbminify/def456-1780264396.css

And in the page source:

&lt;script src="[https://cdn.yourdomain.com/media/fbminify/abc123-1780264396.js"&gt;&lt;/script&gt;](https://cdn.yourdomain.com/media/fbminify/abc123-1780264396.js">)### Sync control file

[](#sync-control-file)

To avoid R2 API calls on every page request, the module maintains a local control file at var/fbminify\_synced.json. This file records which files have already been uploaded — only a local file read is performed on each request, no R2 API calls unless a new file is detected.

When you flush the OpenMage cache from admin, the module automatically:

1. Deletes all media/fbminify/ files from R2
2. Deletes var/fbminify\_synced.json
3. Re-sync happens on the next store visit

### Manual re-sync (if needed)

[](#manual-re-sync-if-needed)

Only required if you manually deleted media/fbminify/ from R2 without going through the admin cache flush:

rm -f var/fbminify\_synced.json

Then visit the store in an incognito window.

---

☁️ Cloudflare R2 Setup
----------------------

[](#️-cloudflare-r2-setup)

### 1. Create the bucket

[](#1-create-the-bucket)

- Go to R2 Object Storage → Create bucket
- Note the S3 API endpoint from bucket Settings

### 2. Create API credentials

[](#2-create-api-credentials)

- Go to R2 Object Storage → Manage R2 API Tokens
- Click Create Account API Token
- Set Permission: Object Read &amp; Write
- Set Specify bucket: your bucket name
- Copy the Access Key ID and Secret Access Key (secret shown only once)

> ⚠️ Do NOT use public-read ACL — R2 does not support it.

---

🔀 Cloudflare Worker Setup (required for CDN serving)
----------------------------------------------------

[](#-cloudflare-worker-setup-required-for-cdn-serving)

> ⚠️ Important: Simply setting Base Media URL to a custom domain linked directly to the R2 bucket will break your site's CSS/JS, because OpenMage uses the same base media URL for all assets including stylesheets. The correct approach is to use a Cloudflare Worker as a smart proxy.

### How it works

[](#how-it-works)

The Worker intercepts requests to your CDN subdomain:

- Requests to /media/catalog/, /media/wysiwyg/, /media/header/, /media/fbminify/ → served from R2
- OpenMage cache URLs (e.g. /media/catalog/product/cache/1/image/600x/.../file.jpg) → Worker strips the cache path and fetches the original image from R2
- Everything else (CSS, JS, skin files) → proxied from your origin server

### Step 1 — Create the Worker

[](#step-1--create-the-worker)

1. Go to Workers &amp; Pages → Create
2. Select Start with Hello World
3. Name it (e.g. ultradev-media-proxy)
4. Replace the code with the Worker script below
5. Click Deploy

**Worker script:**

export default { async fetch(request, env, ctx) { const url = new URL(request.url); const path = url.pathname;

```
if (
  path.startsWith('/media/catalog/') ||
  path.startsWith('/media/wysiwyg/') ||
  path.startsWith('/media/header/') ||
  path.startsWith('/media/fbminify/')
) {
  let key = path.replace(/^\/media\//, 'media/');

  const cacheMatch = path.match(
    /\/media\/catalog\/product\/cache\/[^/]+\/[^/]+\/[^/]+\/[^/]+\/(.+)$/
  );
  if (cacheMatch) {
    key = 'media/catalog/product/' + cacheMatch[1];
  }

  const object = await env.R2_BUCKET.get(key);

  if (!object) {
    return new Response('Not found', { status: 404 });
  }

  const headers = new Headers();
  object.writeHttpMetadata(headers);
  headers.set('Cache-Control', 'public, max-age=31536000');
  headers.set('Access-Control-Allow-Origin', '*');
  return new Response(object.body, { headers });
}

const response = await fetch(
  'https://yourdomain.com' + path + url.search,
  request
);
const newHeaders = new Headers(response.headers);
newHeaders.set('Access-Control-Allow-Origin', '*');
return new Response(response.body, {
  status: response.status,
  headers: newHeaders,
});

```

} };

> Replace yourdomain.com with your actual store domain.

### Step 2 — Bind R2 bucket to the Worker

[](#step-2--bind-r2-bucket-to-the-worker)

1. Go to Worker Settings → Domains &amp; Routes → + Add → R2 bucket
2. Set Variable name: R2\_BUCKET
3. Select your bucket
4. Click Deploy

### Step 3 — Add custom domain to the Worker

[](#step-3--add-custom-domain-to-the-worker)

1. Go to Worker Settings → Domains &amp; Routes → + Add → Custom domain
2. Enter your CDN subdomain (e.g. cdn.yourdomain.com)
3. Confirm

> If you get "domain already in use", go to your R2 bucket → Settings → Custom Domains → remove the domain there first, then add it to the Worker.

---

🌐 Configure OpenMage Base Media URL
-----------------------------------

[](#-configure-openmage-base-media-url)

Go to: System &gt; Configuration &gt; General &gt; Web

Set both Base URL for Media Files (HTTP and HTTPS) to:

> Note the /media/ at the end — required because the sync script uploads files with the media/ prefix as the R2 object key.

> Base URL for Skin Files should remain pointing to your origin server ({{unsecure\_base\_url}}skin/) — skin files are not synced to R2.

---

🔄 Usage
-------

[](#-usage)

### Automatic Sync

[](#automatic-sync)

Triggered automatically on:

- Product save (catalog\_product\_save\_after)
- Category save (catalog\_category\_save\_after)
- CMS block save (cms\_block\_save\_after)
- CMS page save (cms\_page\_save\_after)
- Every frontend request — detects new fbminify bundles and uploads them (http\_response\_send\_before)
- Admin cache flush — removes fbminify files from R2 and resets sync control (adminhtml\_cache\_flush\_all, adminhtml\_cache\_flush\_system)

### Manual Bulk Sync (CLI)

[](#manual-bulk-sync-cli)

To sync all existing media files (first-time setup):

php shell/ultradev\_media\_sync.php

### Folders skipped automatically

[](#folders-skipped-automatically)

FolderReasoncache/Regenerated dynamically by OpenMagecss/ and css\_secure/Intermediate CSS — fbminify handles thisjs/Intermediate JS bundles — input for fbminify, not served directlytmp/Temporary uploadscustomer/Customer avatarsdownloadable/Digital download filesxmlconnect/Legacy mobile app assetstheme/Theme filesheader/Admin logo### Files skipped by name

[](#files-skipped-by-name)

.htaccess, .htpasswd, php.ini, .env — server config files that must never be exposed via CDN.

---

⚠️ Important Notes
------------------

[](#️-important-notes)

- This module does not replace Magento's native storage system
- OpenMage still reads from local /media — R2 is used only for CDN delivery
- The media/js/ and media/css/ folders are used internally by OpenMage and fbminify — do not delete them and do not add them to R2
- When clearing OpenMage cache, also purge Cloudflare cache (Caching → Purge Everything) so CDN fetches fresh assets from origin

---

🛠 Troubleshooting
-----------------

[](#-troubleshooting)

### Files not uploading

[](#files-not-uploading)

Check the log:

tail -f var/log/ultradev\_mediasync.log

Verify credentials and endpoint in admin configuration.

### fbminify bundles not appearing in R2

[](#fbminify-bundles-not-appearing-in-r2)

Delete the sync control file and visit the store in an incognito window:

rm -f var/fbminify\_synced.json

### Class not found (AWS SDK)

[](#class-not-found-aws-sdk)

composer install

### Images showing 404 after setup

[](#images-showing-404-after-setup)

- Confirm files were synced to R2 (check bucket objects)
- Confirm R2 Binding R2\_BUCKET is set in Worker settings
- Confirm Worker custom domain matches the Base Media URL configured in OpenMage

### Site CSS broken after changing Base Media URL

[](#site-css-broken-after-changing-base-media-url)

You likely set the CDN URL directly on the R2 bucket custom domain instead of using the Worker. Follow the Worker setup instructions above.

### CORS errors for fonts

[](#cors-errors-for-fonts)

The Worker automatically adds Access-Control-Allow-Origin: \* to all responses. If you still see CORS errors, redeploy the Worker with the latest code above.

---

📄 License
---------

[](#-license)

Proprietary — UltraDev

---

👨‍💻 Author
----------

[](#‍-author)

UltraDev

###  Health Score

21

—

LowBetter than 18% of packages

Maintenance61

Regular maintenance activity

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/83614706?v=4)[LuizSantos22](/maintainers/LuizSantos22)[@LuizSantos22](https://github.com/LuizSantos22)

---

Top Contributors

[![LuizSantos22](https://avatars.githubusercontent.com/u/83614706?v=4)](https://github.com/LuizSantos22 "LuizSantos22 (39 commits)")

### Embed Badge

![Health badge](/badges/ultradev-openmage-mediasync-cloudflare-r2/health.svg)

```
[![Health](https://phpackages.com/badges/ultradev-openmage-mediasync-cloudflare-r2/health.svg)](https://phpackages.com/packages/ultradev-openmage-mediasync-cloudflare-r2)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
