How to Add EXIF Metadata (e.g., DateTimeOriginal) to Files Uploaded via Uppy Companion from Google Photos/Drive?

Hi everyone,

I’m using Uppy with Companion running in Docker on my cloud. My goal is to allow users to upload photos from Google Photos and Google Drive. I need to extract EXIF metadata, specifically the DateTimeOriginal, from these files and add it to the file’s metadata before the upload is finalized, similar to how I handle local file uploads.

Currently, for local file uploads, I’m using the file-added event on the client-side with the exifr library to parse EXIF data and then uppyInstance.setFileMeta():

Client-side file-added event:

.on('file-added', (file) => {
  handleFileAdded(
    filesAddedRef,
    maxResolution,
    minResolution,
    file,
    removedImagesRef,
    uppyInstance,
    filesLength
  );
})

Inside handleFileAdded (simplified for EXIF extraction):

async function handleFileAdded(/*...relevant params...,*/ file, /*...,*/ uppyInstance) {
  let dateTaken;
  // For local files, file.data is available
  if (file.data instanceof Blob || file.data instanceof File) {
    try {
      const exifData = await exifr.parse(file.data);
      if (exifData && exifData.DateTimeOriginal) {
        dateTaken = exifData.DateTimeOriginal.toISOString();
      }
    } catch (error) {
      console.error('Failed to parse EXIF:', error);
    }
  }

  uppyInstance.setFileMeta(file.id, {
    // ... other metadata like width, height
    date_taken: dateTaken,
  });
}

The Challenge with Companion:

When files are selected from Google Photos or Google Drive via Companion, the file.data (the actual file blob) isn’t directly available on the client-side before Companion processes the file. My client-side EXIF parsing logic won’t work directly for these remote files.

My Question:

Is there a recommended way to add server-side logic to my Uppy Companion instance to:

  1. Fetch the file from Google Photos/Drive.
  2. Parse its EXIF data (specifically DateTimeOriginal) on the server where Companion is running.
  3. Add this date_taken (and potentially other metadata like dimensions if not already provided by the provider) to the file’s metadata that Uppy eventually sends in the POST message to my backend endpoint?

I’ve looked through the Uppy and Companion documentation but haven’t found a clear example or mention of hooks or customization points within Companion itself for modifying file metadata after fetching from the provider but before sending it to the client or the final upload destination.

Could someone point me towards the best approach for this? For example:

  • Are there specific Companion options or lifecycle hooks I can leverage?
  • Would I need to fork Companion or create a custom provider/plugin?
  • Is there a way for Companion to fetch this metadata via the Google Photos/Drive APIs and include it?

Any guidance or examples would be greatly appreciated!

Thanks!

Hi. Unfortunately there isn’t an easy way to do that, because we stream files from source to destination, so it would be hard to fetch metadata before uploading the file, but I’ve created Companion hooks · Issue #5764 · transloadit/uppy · GitHub for discussing this feature.

Hi
i solved it with the exif reader,
just fetch the image again make blob and parse it,
didn’t find any better way

export async function getImageFromGooglePhotos(
  file: UppyFile<Meta, Record<string, never>>,
  maxResolution: number
): Promise<string | null> {
  if (!file.remote || !file.remote.body) {
    console.error('Invalid file structure: remote or remote.body is missing');
    return null;
  }

  const googleAccessToken = file.remote.body.accessToken;
  let imageUrl = file.remote.body.url as string;

  if (!imageUrl) {
    console.error('Invalid file structure: remote.body.url is missing');
    return null;
  }

  const maxParam = `w${maxResolution}-h${maxResolution}`;
  if (imageUrl.includes('=')) {
    imageUrl = imageUrl.replace(/=[^=]*$/, `=${maxParam}`);
  } else {
    imageUrl += `=${maxParam}`;
  }

  try {
    file.remote.body.url = imageUrl;

    const response = await fetch(imageUrl, {
      headers: {
        Authorization: `Bearer ${googleAccessToken}`,
      },
    });

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const blob = await response.blob();
    const fileUrl = URL.createObjectURL(blob);
    file.preview = fileUrl;
    return fileUrl;
  } catch (error) {
    console.error('Failed to fetch Google Photos image:', error);
    return null;
  }
}

     let dateTaken;
      try {
        const exifData = await exifr.parse(file.preview || file.data);
        if (exifData && exifData.DateTimeOriginal) {
          dateTaken = exifData.DateTimeOriginal.toISOString();
        }
      } catch (error) {
        console.error('Failed to parse:', error);
      }