The process of uploading an image to S3 from a form submitted by a user appears to be finishing successfully

Currently, I am working on a Remix app where I have implemented a form to select and upload an image using a simple

<input name="image" type="file" />

The form sends a POST request to a Remix handler. In the handler (refer to the code below), I am attempting to upload this image to Amazon S3 using the v3 SDK. I have experimented with both the PutObjectCommand from client-s3 and the Upload method from lib-storage, but encountered the same outcome...

The upload process seems to be successful as it creates the file in the bucket without any issues. The file size matches exactly what I expect when compared to uploading the file directly into the S3 bucket via the web UI.

However, when trying to open the uploaded file as an "image", it does not display correctly. When viewing the image from the S3 bucket, the browser shows the standard "broken image icon" (similar issue occurs if attempting to open the image in Preview on OSX) - unfortunately, there isn't much information available about what might be wrong. It appears that something may be corrupted or created in a manner that prevents the file from being recognized as an image.


Within the handler, the file data is received as an AsyncIterable<Uint8Array>. My main processing task involves converting this data into a single Uint8Array for the Upload operation (I have already tried setting the body to both buffer and blob with no success).

Could there be an issue with my conversion to a Uint8Array? Is there a more appropriate way to perform this conversion for the upload operation, perhaps using a different data type?

Alternatively, is there a specific configuration that needs to be adjusted regarding how the upload to S3 is being set up?

async ({ name, contentType, data, filename }) => {
  const arrayOfUInt8Array: Uint8Array[] = [];

  let length = 0;
  for await (const x of data)
  {
    arrayOfUInt8Array.push(x);
    length += x.length;
  }

  const uInt8Array = new Uint8Array(length);

  for (const x of arrayOfUInt8Array) {
    uInt8Array.set(x);
  }

  // Tried creating a buffer from the Uint8Array...
  const buff = Buffer.from(uInt8Array);
  // Tried creating a blob from the Uint8Array...
  const blob = new Blob([uInt8Array], { type: contentType });

  const uploadCommand = new Upload({
    client: s3Client,
    params: {
      Bucket: s3Bucket,
      Key: filename,
      Body: uInt8Array,
      ContentType: contentType,
    }
  });

  await uploadCommand.done();

  return '';
},

Answer №1

According to @Botje's comments, the problem stemmed from how the Uint8Array was constructed. The issue was that the source was being constantly overwritten at the beginning, leaving the rest of the array empty.

Instead of using the original code:

  for (const x of arrayOfUInt8Array) {
    uInt8Array.set(x);
  }

I had to make adjustments and use the following code:

  let i = 0;
  let currentIndex = 0;
  for (const x of arrayOfUInt8Array) {
    uInt8Array.set(x, currentIndex)
    currentIndex += arrayOfUInt8Array[i].length
    i += 1
  }

Answer №2

My approach to handling this situation usually involves a two-step strategy:

First, I parse the upload as NodeDiskOnFile using Remix's built-in functionality:

const uploadHandler = unstable_composeUploadHandlers(
    unstable_createFileUploadHandler({
      maxPartSize: 10_000_000,
      file: ({ filename }) => filename,
    }),
    // parse everything else into memory
    unstable_createMemoryUploadHandler()
  );

const formData = await unstable_parseMultipartFormData(
    request,
    uploadHandler
  );

Next, I proceed to upload the files by utilizing presigned URLs:

for (const file of files) {
    const uploadedURL = await uploadFileWithPresignedUrl(file);
    dataWithAWSUrls.uploadedFiles.push(uploadedURL);
  }

Below are the functions that define the structure of presigned URLs:

import {
  GetObjectCommand,
  PutObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { NodeOnDiskFile } from "@remix-run/node";
import axios from "axios";

import slugify from "slugify";
import { slugifyOptions } from "~/constants/constants";
import { env } from "~/env";

const s3Client = new S3Client({
  region: env.AWS_REGION,
  credentials: {
    accessKeyId: env.AWS_ACCESS_KEY_ID,
    secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
  },
});

async function streamToBuffer(
  readable: ReadableStream<Uint8Array>
): Promise<Buffer> {
  const data: Uint8Array[] = [];
  const reader = readable.getReader();

  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      data.push(value);
    }
  } finally {
    reader.releaseLock();
  }

  return Buffer.concat(data.map((chunk) => Buffer.from(chunk)));
}

export async function uploadFileWithPresignedUrl(file: File | NodeOnDiskFile) {
  const buffer = await streamToBuffer(file.stream());

  const slugifiedFileName = slugify(file.name, slugifyOptions);

  const command: any = new PutObjectCommand({
    Bucket: env.AWS_BUCKET_NAME,
    Key: slugifiedFileName,
    Body: buffer,
    ContentType: file.type,
  });

  const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 });

  // Upload file with presigned url
  await axios.put(url, buffer, {
    headers: {
      "Content-Type": file.type,
      "Content-Length": buffer.length,
    },
  });

  const publicUrl = `https://${env.AWS_BUCKET_NAME}.s3.amazonaws.com/${slugifiedFileName}`;

  // Return the url of the uploaded file
  return publicUrl;
}

export async function getFileWithPresignedUrl(key: string) {
  const command = new GetObjectCommand({
    Bucket: env.AWS_BUCKET_NAME,
    Key: key,
  });
  const signedUrl = getSignedUrl(s3Client, command, { expiresIn: 3600 });
  return signedUrl;
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Building CRUD functionality using the ExpressJS router

I'm completely new to the MEAN stack and I need some guidance on how to manage my routing effectively. Should I use express.Router() or AngularJS controllers (https://docs.angularjs.org/tutorial/step_07)? My current setup includes Express v4.12.0, An ...

Modify the indicator color of the tabs in Material UI v5

https://i.sstatic.net/nhXHL.png I have implemented tabs in my project, but I am unable to change the background indicator: const theme = createTheme({ MuiTabs: { root: { styleOverrides: { indicator: {backgroundColor: "red !i ...

Generate a novel item by organizing the key of an array of objects

I have an array of objects containing various items: [ { CATEGORY:"Fruits" ITEM_AVAIL:true ITEM_NAME:"Apple" ITEM_PRICE:100 ITEM_UNIT:"per_kg" ITEM_UPDATED:Object ORDER_COUNT:0 }, { CATEG ...

Displaying items as objects in search results in Kendo Angular's auto complete feature

Seeking assistance with implementing Kendo Angular's auto complete widget using server filtering. Following the service call, the popup displays [object Object] and the count of these matches the results retrieved from the server. Could someone kindly ...

How can I simulate or manipulate the element's scrollHeight and clientHeight in testing scenarios?

In my JavaScript code, I have a function that checks if an HTML paragraph element, 'el', is a certain size by comparing its scrollHeight and clientHeight properties: function isOverflow(element: string): boolean { const el = document.getEleme ...

Exploring Raycasting with Three.js and WhitestormJS

Having trouble with Raycasting in Three.js and WhitestormJS. Perhaps I haven't grasped the concept properly. The goal is to align the raycaster's direction with the camera, so that when it intersects an element, that element is added to the inte ...

Having trouble with passing post data from jQuery ajax to a php page?

Attempting to retrieve post data using an alert() function, but encountering an issue where the data is not being passed to the PHP page. The result always shows {"success":false,"result":0} The goal is to transmit a password to the PHP page, hash it usin ...

Is there a simple angular directive available for displaying expandable/collapsible text?

I have been exploring the idea of creating an Angular directive that would truncate a paragraph or a div if it exceeds a certain number of characters (for example, 115). I have not been successful in finding a solution that fits my needs. I have come acro ...

Adding 2-dimensional text to a specified location with the help of three.js

let startPosition = new THREE.Vector3(-5,0,0); let targetPosition = new THREE.Vector3(-5,2,0); let directionVector = new THREE.Vector3().sub(targetPos,startPosition); let arrowHelper = newTHREE.ArrowHelper(directionVector.clone().normalize(),startPosition, ...

Troubleshooting Media Query problem in Angular 6

Currently, I am developing a website using Angular 6. One of the components in my project is called our-work.ts. To set its media query globally, I decided to define it in a file named global.scss. Here's an example snippet of what I've written: ...

Interact with a Firebase database table using Node.js

I recently imported data into a table called users in Firebase, and here are the details: { "users" : [ {"id": 1, "fcmToken" : "APA91bHJAzXe384OEYvfk4bKsyS1NQvteph7DwG...", "fName" : "John", "lName" : "Doe", "phone" : "978677086 ...

Tracking your daily nutrition using cronometer with the powerful combination of JavaScript

I'm currently developing a JavaScript cronometer in vueJS. I want the timer to start at 5 minutes (300 seconds) and then continue counting minutes and seconds. Although my cronometer is functioning, I am struggling to get it to start counting minutes ...

Transferring blank documents to a React-Native server

When attempting to send JSON files to the server using NodeJS with multer, I am running into an issue where the files are being sent empty. Currently, I am utilizing React-native-File-System to iterate through all the files located in the specified folder ...

Issue encountered - Attempting to provide an object as input parameter to puppeteer function was unsuccessful

Encountering an error when attempting to pass a simple object into an asynchronous function that utilizes puppeteer. The specific error message is: (node:4000) UnhandledPromiseRejectionWarning: Error: Evaluation failed: ReferenceError: userInfo is not def ...

Customize the appearance of the v-autocomplete component in Vuetify

How can I style the autocomplete component to only show words that begin with the letters I'm typing, rather than all words containing those letters? Also, is there a way to display the typed letters in bold without highlighting them? https://i.sstat ...

Emphasize the designated drop zone in Dragula

Incorporating the Dragula package into my Angular 2 project has greatly enhanced the drag-and-drop functionality. Bundling this feature has been a seamless experience. Visit the ng2-dragula GitHub page for more information. Although the drag-and-drop fun ...

How do I pass a value from a Vue component's prop to an HTML attribute in Vue.js?

I am seeking assistance with displaying a countdown timer in Vue.js using the flip-countdown library. My goal is to pass the value of the 'remaining_time' prop as an attribute in the HTML code. HTML: <flip-countdown deadline=remaining_time&g ...

The JWT authentication appears to be functioning properly when tested on "postman," however, it is encountering issues on the website

When I attempt to verify a user's authentication using tools such as "Postman" or "Insomnia," everything functions correctly (when I set the bearer). However, when I try to log in through the website, I encounter an error here: function authenticateT ...

Dividing JSON File Entries Among Several Files

I am facing a challenge with a file that contains an overwhelming amount of data objects in JSON format. The structure is as follows: { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {}, "geometry": ...

What steps should I take to make my Vue JS delete function operational?

As I work on developing my website, I've encountered some challenges. Being new to coding, I'm struggling with creating a functional delete user button. When I click delete, it redirects me to the delete URL but doesn't remove the entry from ...