Dynamic Image Transformation for Private Files

The origin of this post is a feature request:

https://community.xano.com/feature-requests/post/dynamic-image-transformation-for-private-files-nO8oIQFVKlzMpbx

It is also connected to this thread:
https://community.xano.com/ask-the-community/post/image-transformation-on-gcs-signed-urls-or-private-files-Ug4XgJTfrV0Sd0F



Well as this seems to be a significant feature for our application we have found and implemented a workaround which might not be feasible for all, but I thought it would be beneficial to leave it here. (!warning, requires some experience with Cloudflare, but ChatGPT can help in case that's needed)

Cloudflare image transformations are quite powerful as well and with the help of web workers they can be used for image transformation on signed urls as well. So that is the journey I'd like to take you all with me.

This is not a comprehensive step-by-step guide, but it gives all the required resources that one would need to set it up.

Useful links to Cloudflare documentation pages:
[Image transformation overview](https://developers.cloudflare.com/images/)

[Image transformation via web workers](https://developers.cloudflare.com/images/transform-images/transform-via-workers/)

[Create a web worker and deploy](https://developers.cloudflare.com/workers/)

[Set zone route to a worker](https://developers.cloudflare.com/workers/configuration/routing/routes/)

[Enable image transformations for your zone](https://developers.cloudflare.com/images/get-started/#enable-transformations)

Okay, now that all is read and clear let's also point out why is this not right for all.

Not everyone uses Cloudflare for DNS record manager, but we do. We do that in order to have custom subdomains, and several other goodies, it does require that your 'nameservers' are set to cloudflare and not the original provider of your application's / website's domain provider, so check if you can actually do this first.



Let's assume that based on the above documentation links you have successfully created a 'Web worker' and you have pointed it to your zone. Also you have enabled the image transformation feature.

In the 'edit code' of your worker paste in something like this:

addEventListener("fetch", event => {
  return event.respondWith(handleRequest(event.request, event))
})

/**
 * Fetches an image, resizes it based on the provided template, validates the source, and caches the response.
 * If the image is already in the cache, it returns the cached image.
 * 
 * @param {Request} request - The request object containing the URL of the image and the template for resizing.
 * @param {FetchEvent} event - The fetch event object used to wait until the response is put in the cache.
 * 
 * @returns {Promise<Response>} - A promise that resolves with the response object containing the resized image.
 */
async function handleRequest(request, event) {
  // Check if the image is in the cache.
  let response = await caches.default.match(request)

  // If it is, return the cached image.
  if (response) return response

  // Parse request URL to get access to query string
  let url = new URL(request.url)

  // Cloudflare-specific options are in the cf object.
  let options = { cf: { image: {} } }

  // Define template sizes
  const templates = {
    "tiny": 32,
    "small": 50,
    "med": 160,
    "big": 360,
    "bigger": 600,
    "large": 800,
    "xlarge": 1920
  }

  // Get template from query string
  let tpl = url.searchParams.get("tpl")

  // Check if template exists and set width
  if (tpl && templates[tpl.replace(":box", "")]) {
    options.cf.image.width = templates[tpl.replace(":box", "")]
  }

  // Check if box shape is requested and set fit
  if (tpl && tpl.includes(":box")) {
    options.cf.image.fit = "cover"
    options.cf.image.height = options.cf.image.width
  }

  // Get URL of the original (full size) image to resize.
  const imageURL = url.searchParams.get("image")

  if (!imageURL) return new Response('Missing "image" value', { status: 400 })

  //This whole part is about some validation logic, you can use your custom approach here
  //These are some dummy suggestions.
  try {
    // TODO: further customize validation logic
    const { protocol, hostname, pathname } = new URL(imageURL)

    // Check if the protocol is 'http' or 'https'
    if (protocol !== 'http:' && protocol !== 'https:') {
      return new Response('Invalid protocol. Must be "http" or "https"', { status: 400 })
    }

    // Only accept "image-storage.example.com" images (set it to other hostname if your images are elsewhere)
    if (hostname !== 'image-storage.example.com') {
      return new Response('Access denied.', { status: 403 })
    }

    // Check if the image is from your own specific folder
    if (!pathname.startsWith('/my-super-secure-folder')) {
      return new Response('Access denied.', { status: 403 })
    }

  } catch (err) {
    return new Response('Invalid "image" value', { status: 400 })
  }

  const imageRequest = new Request(imageURL);

  // Fetch the image, resize it, and store it in the cache.
  response = await fetch(imageRequest, options)

  // Clone the response so we can read the body.
  let responseClone = response.clone()

  // Read the body into a variable.
  let body = await responseClone.blob()

  // Create a new response with the same body and status.
  let responseToCache = new Response(body, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  })

  // Set the Cache-Control header.
  responseToCache.headers.set('Cache-Control', 'public, max-age=86400')

  // Put the response in the cache.
  event.waitUntil(caches.default.put(request, responseToCache))

  return response
}

One more step remains, to deploy this.

After deployment you can access your 'route' from your zone with the query parameters as described in the code which will instruct Cloudflare to cache your image for 24h, and will return the transformed images.

This code snippet is in fact just rebuilding Xano's default image transformation syntax with the additional 'image' query parameter.

So let's assume you have a route in your cloudflare zone:
*.example.com/transform-images* pointing to your worker that can have whatever name you set to it.

Then you can do a GET request to this path like below:

https://example.com/transform-images?tpl=med:box&image=${url_encoded_signed_url_to_the_image}

This will return you a transformed image while setting it to the cache.

*One last note, some react-native libraries (or other frameworks) can consider urls with different query parameters the same url, so just add something unique to the path before adding the query, e.g. a UUID for the image:
https://example.com/transform-images/uuid?tpl=med:box&image=${url_encoded_signed_url_to_the_image}

Other
2
1 reply