🛠 ᴺᴱᵂ ᴾᴸᵁᴳᴵᴺ Wasabi Storage Dropzone & Utilities (Image Resize & Compression, MultiUploader, Folder support & No Filesize Limit!)

Using the Dropzone, the MIME type is automatically detected by the browser while uploading to Wasabi. If you have a need to override it, please revert back to me privately to better understand your use-case.

@redvivi I just installed the plugin and I’m doing some tests to see if it suits my needs. The dropzone uploader works fine, however, backend actions like getting file metadata and generating presigned URLs don’t work when an http referer access policy is assigned like shown below. I need to get rid of the policy for them to work. Any idea how to get around this? How do I alter the policy to allow the access needed for the backend actions? I don’t want it to be possible to open even short duration presigned URLs outside of the app which is why I need this policy. I know http referer is quite easy to get around but it’s still an extra layer of security.

1 Like

HTTP referer is not the one you think in backend operations, as it is called from Bubble’s servers :slight_smile:
You might want to allow only IP addresses used by Bubble’s clusters - see IP address range used by Bubble, or alternatively Bubble support.

I’m away from computer but I have it setup where I allowed a specific Wasabi user to have full S3 access, then the Bubble app uses that user’s API key. So the backend actions have the s3:PutObject and all other permissions needed.

1 Like

Hi @redvivi,

Is the compression executed in the browser or by the server? I’m asking this because I’m currently facing issues with another Wasabi plugin, where my app crashes when too many and too large images get compressed since it uses too much computing power for the browser.

Thanks in advance.

Browser-side. It may crash because of the resulting compressed files, stored in memory, are exceeding the allowable limit of the browser’s JS.

All plugins on the marketplace using in-browser compression will suffer from this, unfortunately.

The solution is to limit the number of files or using a specialized service compressing assets on server side.

1 Like

Currently experiencing problems, is this due to the Amazon/Bubble situation?

Yes it is.

1 Like

@tylerboodman @redvivi Thanks for the input guys. I ended up exploring S3 permission definition in a bit more detail and found what works for my use case. My main use case is presigned url generation (based on S3 docs I thought the minimum duration was 1 minute but the plugin allows durations as short as 1 second which is great) and I needed to accomplish these goals:

  1. Prevent any access to my Wasabi bucket’s files apart from the cases described below
  2. Allow the Bubble plugin to access my Wasabi bucket
  3. Allow the files to be accessed from my website (e.g., for embedders or opening the links)
  4. Allow the Google Drive embedder to access the files

This is achieved by the following conditions:

  1. Allow the relevant IP addresses that Bubble uses in the backend which are listed on IP Ranges - Documentation - OctoPerf under US West (Oregon)
  2. Allow requests that use my domain as http referer
  3. Allow Google Drive embedder’s user agent

Note - none of these are bulletproof against someone who knows what they’re doing but they’re good enough to prevent unauthorised access and sharing of privilged resources by general users.

In case anyone is after a similar solution, below is my Wasabi bucket’s access policy that achieves the above. It denies access to the bucket’s files unless at least 1 of the above described conditions is satisfied. Keep in mind that it only allows the GetObject action due to my described use case but the policy can easily be adapted to include more actions.

{
  "Id": "Policy1722457766584",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1722457618992",
      "Effect": "Deny",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::[BUCKET NAME]/*",
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": [
            "139.56.16.0/23",
            [US West (Oregon) AWS IP addresses],
            "99.77.186.0/24"
          ]
        },
        "StringNotLike": {
          "aws:Referer": [
            "https://[DOMAIN NAME]/",
            "https://[DOMAIN NAME]/*",
            "https://console.wasabisys.com/",
            "https://console.wasabisys.com/*"
          ],
          "aws:UserAgent": "Mozilla/5.0 (compatible; Google AppsViewer; http://drive.google.com)"
        }
      }
    }
  ]
}

Also, a couple of notes on the plugin:

  • It would be useful to have a specific error message when the plugin doesn’t gain the required access - it now gives a generic one so it can take a while to figure out what the cause is
  • I think the guide is a bit outdated - for example, I don’t see a cross-origin resource sharing section in my Wasabi bucket permissions tab which is needed for step 1) of the instructions on the plugin page
1 Like

Hi @redvivi,

I’m trying to show the thumbnail preview, but even with a one on one copy from your demo editor, it is not showing up. How can I solve this?

Thanks in advance.

Send me in DM your app editor with write access, specify the page where the uploader is installed, along with the sample file you are trying to preview.

Hi when i upload photos from safari at Iphone, and resize and compress photos idk why but they rotate. Checked it form desctop Chrome — all works good. Just as i know when we make resize procces at brouser side it can lose orientation, can you add some setting Do not Rotate etc.

Checked — it happend only with photos that i made on iphone…

@redvivi yes after all my testing — this problem is only with photos made on phone (in my case on Iphone) if for example i made a screenshot of photo it resize and upload normally, but if you take photo it turn on 90° and if you download this file and upload it again it make another turn on on 90°. But base64 not turning at 90° … but if you download this base and get jpg and upload it with resize it turn on 90° again… could you help please how to solve this problem?

I have identified the issue. The definitive fix requires quite some work and isn’t easy.

Basically you have the “natural” orientation of the image (e.g. the pixel’s bytes are presented). You read them and the image is displayed as it is, that’s one thing.

One of the other hand, photographic devices may store orientation information in EXIF data at capture, which is then used to display it accordingly on such devices, which may differ from the pixel bytes presentation.

Can you try v1.27.1 @sashashru1234 ? I have tried a long shot fix…

2 Likes

Hi @redvivi! Thank you for answer!

i’ve update plugin, not working. May be i don’t see some option that i should turn on?

And i tested. Photos that i upload via chorme from mac — they comressed and rezided but still turning at 90. Buttt from Iphone Safari — compression and resizing disabled i’ve tried it 6 times with differend photos, it’s upload just original size with normal orientation. But base64 working normaly in both devices

Here is example https://wnote.b-cdn.net/IMG_3665%20(1).jpeg

There is no option - I deactivated the rotation detection but obviously that’s not sufficient.

Hi @redvivi

I’ve talk a lot yesterday with chatgbt about that issue

It give me some solve option, i just share may be it could help to find rigth way.

EXIF.js set-up

<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>

Var1

function handleFileUpload(file) {
  const reader = new FileReader();

  reader.onload = function(e) {
    const img = new Image();
    img.onload = function() {
      // Создаем canvas для обработки изображения
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');

      EXIF.getData(img, function() {
        const orientation = EXIF.getTag(this, "Orientation");

        // Настройка canvas с правильной ориентацией
        switch (orientation) {
          case 2: // Flip horizontal
            ctx.transform(-1, 0, 0, 1, canvas.width, 0);
            break;
          case 3: // Rotate 180 degrees
            ctx.transform(-1, 0, 0, -1, canvas.width, canvas.height);
            break;
          case 4: // Flip vertical
            ctx.transform(1, 0, 0, -1, 0, canvas.height);
            break;
          case 5: // Rotate 90 degrees counterclockwise
            ctx.transform(0, 1, 1, 0, 0, 0);
            break;
          case 6: // Rotate 90 degrees clockwise
            ctx.transform(0, 1, -1, 0, canvas.height, 0);
            break;
          case 7: // Rotate 270 degrees clockwise
            ctx.transform(0, -1, -1, 0, canvas.height, canvas.width);
            break;
          case 8: // Rotate 270 degrees counterclockwise
            ctx.transform(0, -1, 1, 0, 0, canvas.width);
            break;
          default: // No transformation needed
            ctx.transform(1, 0, 0, 1, 0, 0);
        }

        // Рисуем изображение на canvas
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);

        // Преобразуем canvas в файл и передаем дальше
        canvas.toBlob(function(blob) {
          // Ваш код для загрузки файла
          console.log(blob); // здесь можно продолжить с загрузкой изображения
        }, 'image/jpeg', 0.7); // качество 0.7
      });
    };
    img.src = e.target.result;
  };

  reader.readAsDataURL(file);
}

var2

Dropzone.options.myDropzone = {
  url: '/upload', // путь к серверу, на который вы отправляете изображения
  maxFilesize: 10, // максимальный размер файла (в MB)
  acceptedFiles: 'image/*', // только изображения
  init: function() {
    this.on("addedfile", function(file) {
      // Проверка ориентации изображения при добавлении файла
      fixOrientation(file, function(fixedFile) {
        // После коррекции ориентации, заменяем файл в Dropzone
        const dataTransfer = new DataTransfer();
        dataTransfer.items.add(fixedFile);
        file = dataTransfer.files[0];
        this.emit('thumbnail', file, URL.createObjectURL(fixedFile)); // Генерация миниатюры
      });
    });
  }
};

// Функция для обработки ориентации изображения
function fixOrientation(file, callback) {
  const reader = new FileReader();

  reader.onload = function(event) {
    const img = new Image();
    img.onload = function() {
      // Создаем canvas для исправления ориентации
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d");

      // Читаем EXIF данные для ориентации
      EXIF.getData(img, function() {
        const orientation = EXIF.getTag(this, "Orientation");

        let width = img.width;
        let height = img.height;

        // Обрезаем или ресайзим изображение, если оно больше нужных размеров
        const maxWidth = 1280;
        const maxHeight = 720;

        if (width > maxWidth) {
          height = Math.round((maxWidth / width) * height);
          width = maxWidth;
        }

        if (height > maxHeight) {
          width = Math.round((maxHeight / height) * width);
          height = maxHeight;
        }

        canvas.width = width;
        canvas.height = height;

        // Применяем ориентацию
        switch (orientation) {
          case 2: // Flip horizontal
            ctx.transform(-1, 0, 0, 1, canvas.width, 0);
            break;
          case 3: // Rotate 180 degrees
            ctx.transform(-1, 0, 0, -1, canvas.width, canvas.height);
            break;
          case 4: // Flip vertical
            ctx.transform(1, 0, 0, -1, 0, canvas.height);
            break;
          case 5: // Rotate 90 degrees counterclockwise
            ctx.transform(0, 1, 1, 0, 0, 0);
            break;
          case 6: // Rotate 90 degrees clockwise
            ctx.transform(0, 1, -1, 0, canvas.height, 0);
            break;
          case 7: // Rotate 270 degrees clockwise
            ctx.transform(0, -1, -1, 0, canvas.height, canvas.width);
            break;
          case 8: // Rotate 270 degrees counterclockwise
            ctx.transform(0, -1, 1, 0, 0, canvas.width);
            break;
          default:
            ctx.transform(1, 0, 0, 1, 0, 0); // Без изменений
        }

        // Рисуем изображение на canvas
        ctx.drawImage(img, 0, 0, width, height);

        // Преобразуем canvas в изображение
        canvas.toBlob(function(blob) {
          callback(blob);
        }, "image/jpeg", 0.7); // качество 0.7
      });
    };
    img.src = event.target.result;
  };

  reader.readAsDataURL(file);
}

var3

// Функция для обработки изображения
function processImage(file, maxWidth, maxHeight, quality, callback) {
    const reader = new FileReader();
    
    reader.onload = function(event) {
        const img = new Image();
        img.onload = function() {
            // Создаем canvas для обработки
            const canvas = document.createElement("canvas");
            const ctx = canvas.getContext("2d");

            // Получаем ориентацию изображения с помощью EXIF
            EXIF.getData(img, function() {
                const orientation = EXIF.getTag(this, "Orientation");

                let width = img.width;
                let height = img.height;

                // Масштабируем изображение, если оно превышает максимальные размеры
                if (width > maxWidth) {
                    height = Math.round((maxWidth / width) * height);
                    width = maxWidth;
                }
                if (height > maxHeight) {
                    width = Math.round((maxHeight / height) * width);
                    height = maxHeight;
                }

                // Настроим canvas для рисования с правильной ориентацией
                canvas.width = width;
                canvas.height = height;

                // Учитываем ориентацию, чтобы изображение не переворачивалось
                switch (orientation) {
                    case 2: // Flipped horizontally
                        ctx.transform(-1, 0, 0, 1, canvas.width, 0);
                        break;
                    case 3: // 180 degrees
                        ctx.transform(-1, 0, 0, -1, canvas.width, canvas.height);
                        break;
                    case 4: // Flipped vertically
                        ctx.transform(1, 0, 0, -1, 0, canvas.height);
                        break;
                    case 5: // 90 degrees rotated
                        ctx.transform(0, 1, 1, 0, 0, 0);
                        break;
                    case 6: // 90 degrees rotated
                        ctx.transform(0, 1, -1, 0, canvas.height, 0);
                        break;
                    case 7: // 270 degrees rotated
                        ctx.transform(0, -1, -1, 0, canvas.height, canvas.width);
                        break;
                    case 8: // 270 degrees rotated
                        ctx.transform(0, -1, 1, 0, 0, canvas.width);
                        break;
                    default:
                        ctx.transform(1, 0, 0, 1, 0, 0);
                }

                // Рисуем изображение на canvas с учетом ориентации
                ctx.drawImage(img, 0, 0, width, height);

                // Получаем изображение с качеством 0.7
                canvas.toBlob(function(blob) {
                    callback(blob);
                }, "image/jpeg", quality);
            });
        };

        img.src = event.target.result;
    };

    reader.readAsDataURL(file);
}

// Пример использования

// Получаем файл (например, через Dropzone)
var file = event.target.files[0];

// Максимальные размеры и качество
const maxWidth = 1280;
const maxHeight = 720;
const quality = 0.7;

// Вызовем функцию обработки изображения
processImage(file, maxWidth, maxHeight, quality, function(processedFile) {
    console.log("Обработанный файл готов:", processedFile);
    // Добавляем обработанный файл в Dropzone или другой элемент
    dropzoneInstance.addFile(processedFile);
});

I am not software dude, just interested in this function works. Thank you

My guy (or gal), no offense, but @redvivi has the majority of the AI plugins on the market. I’m sure he’s tried to use AI to help in this situation.

When it comes to AI helping you with a solution, or finding a solution, it can help you from point A to point Z, sure. But when it comes to point C, D, G, O, Q…. That’s when things get fuzzy. It’s not a simple “hey, this is giving me this issue, how can it be fixed?”. The AI doesn’t understand how each of his functions works, how Bubble works, and how the libraries work, all in a simple prompt or even with some context. Give him time, and I’m sure he’ll find a probable solution.

If you feel comfortable with AI developing plugins for you, you should just give it a try on your own :wink:

2 Likes

Of course, I’m not rushing, I’m just trying to be helpful :v:

1 Like

I have good news)

@redvivi fixed it and now when you want to upload a photo from your phone, nothing will be reversed and the compression will work perfectly! I uploaded 6 and 12 photos at a time, everything is fine

Thank you for your work

2 Likes