#fetch image

1 messages · Page 1 of 1 (latest)

rugged flint
#

pls create me an action to import an image, i want this image to be resized and converted in webp using sharp js. also pls the fetch with the image data.
also pls add verification to the image

fading ploverBOT
#

Hi @rugged flint. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Below is a minimal example of how to:

  • Fetch an image from a URL in an action (Node runtime)
  • Validate it (MIME type + basic size check)
  • Resize it and convert to WebP using sharp
  • Store the processed image in Convex file storage

This uses the sharp pattern from the Discord thread and Convex’s file-storage APIs. [Actions runtime; Sharp example]

Note: You must:

  • Add "use node"; at the top of the file (actions only).
  • Mark sharp as an external package in convex.json:
    {
      "node": {
        "externalPackages": ["sharp"]
      }
    }
    
  • Install a sharp version that works in Convex (0.33.5 is known to work). [Sharp example]
// convex/images.ts
"use node";

import { action, internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import sharp from "sharp";

// 1) Action: fetch, verify, resize, convert to webp, store
export const importAndProcessImage = action({
  args: {
    url: v.string(),
    maxWidth: v.optional(v.number()),   // e.g. 1200
    maxHeight: v.optional(v.number()),  // e.g. 1200
  },
  handler: async (ctx, { url, maxWidth = 1200, maxHeight = 1200 }) => {
    // Fetch the image
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
    }
#

// Basic content-type verification
const contentType = response.headers.get("content-type") ?? "";
const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/gif"];
if (!allowedTypes.some((t) => contentType.startsWith(t))) {
throw new Error(Unsupported image type: ${contentType});
}

// Get raw bytes
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);

// Extra verification via sharp: ensure it's a valid image and not huge
const metadata = await sharp(buffer).metadata();
if (!metadata.width || !metadata.height) {
  throw new Error("Invalid image: missing dimensions");
}
const maxPixels = 8000 * 8000; // example safety limit
if (metadata.width * metadata.height > maxPixels) {
  throw new Error("Image too large");
}

// Resize and convert to webp
const processedBuffer = await sharp(buffer)
  .resize({
    width: maxWidth,
    height: maxHeight,
    fit: "inside", // keep aspect ratio, fit within box
    withoutEnlargement: true,
  })
  .webp({ quality: 80 })
  .toBuffer();

// Store processed image in Convex storage
const storageId = await ctx.storage.store(
  new Blob([processedBuffer], { type: "image/webp" })
);

// Optionally write to your own table
await ctx.runMutation(internal.images.saveProcessedImage, {
  storageId,
  originalUrl: url,
  width: metadata.width,
  height: metadata.height,
  contentType: "image/webp",
});

return { storageId };

},
});

#

// 2) Internal mutation: save metadata in your table
export const saveProcessedImage = internalMutation({
args: {
storageId: v.id("_storage"),
originalUrl: v.string(),
width: v.number(),
height: v.number(),
contentType: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert("images", {
storageId: args.storageId,
originalUrl: args.originalUrl,
width: args.width,
height: args.height,
contentType: args.contentType,
createdAt: Date.now(),
});
},
});


This gives you:

- `importAndProcessImage` action you can call from the client (via `useAction`) with an image URL.
- Verification of content type and dimensions.
- Resized, WebP-converted image stored in Convex storage, plus metadata in your `images` table.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@rugged flint Kapa.ai is still learning and improving, please let me know how I did by reacting below.

rugged flint
#

@fading plover is not an url, i want the fetch to get the blob

fading ploverBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

You can take a Blob directly as an argument to the action (e.g. coming from an HTTP action or another server-side step), then process and store it. Convex storage expects a Blob, and ctx.storage.store takes a Blob directly. [Storage store; Storing files in actions]

Below is an action that:

  • Accepts a Blob (no URL)
  • Verifies it’s an image
  • Uses sharp to resize + convert to WebP
  • Stores the processed image in Convex storage
// convex/images.ts
"use node";

import { action } from "./_generated/server";
import { v } from "convex/values";
import sharp from "sharp";

export const processImageBlob = action({
  args: {
    // In practice you’d pass this Blob from an httpAction or another action.
    // Here we just type it as `any` because Convex validators don’t have a Blob type.
    blob: v.any(),
    maxWidth: v.optional(v.number()),
    maxHeight: v.optional(v.number()),
  },
  handler: async (ctx, { blob, maxWidth = 1200, maxHeight = 1200 }) => {
    if (!(blob instanceof Blob)) {
      throw new Error("Expected a Blob");
    }

    const contentType = blob.type ?? "";
    const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/gif"];
    if (!allowedTypes.some((t) => contentType.startsWith(t))) {
      throw new Error(`Unsupported image type: ${contentType}`);
    }

    // Read Blob into a Buffer for sharp
    const arrayBuffer = await blob.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);

    // Verify via sharp
    const metadata = await sharp(buffer).metadata();
    if (!metadata.width || !metadata.height) {
      throw new Error("Invalid image: missing dimensions");
    }
#

// Resize + convert to webp
const processedBuffer = await sharp(buffer)
.resize({
width: maxWidth,
height: maxHeight,
fit: "inside",
withoutEnlargement: true,
})
.webp({ quality: 80 })
.toBuffer();

// Store processed image as Blob in Convex storage
const storageId = await ctx.storage.store(
  new Blob([processedBuffer], { type: "image/webp" })
);

return { storageId };

},
});


Key points from the docs:

- `ctx.storage.store` takes a `Blob` and returns an `Id<"_storage">`. [[Storage store](<https://docs.convex.dev/api/interfaces/server.StorageActionWriter#store>)]
- Storing generated/fetched files from actions is the recommended pattern. [[Storing files in actions](<https://docs.convex.dev/file-storage/store-files>)]
- Use `"use node";` and mark `sharp` as an external package in `convex.json` when using Node-only libraries. [[Node runtime](<https://docs.convex.dev/functions/runtimes#nodejs-runtime>); [External packages](<https://docs.convex.dev/functions/bundling#external-packages>); [Sharp example](<https://discord.com/channels/1019350475847499849/1360170974103732234>)]
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@rugged flint Kapa.ai is still learning and improving, please let me know how I did by reacting below.

rugged flint
#

@fading plover on the front end if i want to send the image?

fading ploverBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

To send an image from the frontend to Convex, use the upload URL flow: generate a short‑lived URL with a mutation, fetch the file to that URL, then store the returned storageId in your DB. [Upload URLs]

1. Backend: generate upload URL + save metadata

// convex/images.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const generateUploadUrl = mutation({
  args: {},
  handler: async (ctx) => {
    return await ctx.storage.generateUploadUrl();
  },
});

export const saveImage = mutation({
  args: {
    storageId: v.id("_storage"),
    // add any extra metadata you want
  },
  handler: async (ctx, { storageId }) => {
    await ctx.db.insert("images", {
      storageId,
      createdAt: Date.now(),
    });
  },
});

This follows the pattern from the docs. [Generate URL; Save ID]

2. Frontend (React): send the image file

// src/App.tsx
import { FormEvent, useRef, useState } from "react";
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

export default function App() {
  const generateUploadUrl = useMutation(api.images.generateUploadUrl);
  const saveImage = useMutation(api.images.saveImage);

  const imageInput = useRef<HTMLInputElement>(null);
  const [selectedImage, setSelectedImage] = useState<File | null>(null);

  async function handleUpload(e: FormEvent) {
    e.preventDefault();
    if (!selectedImage) return;

    // 1) Get short‑lived upload URL from Convex
    const postUrl = await generateUploadUrl();
#

// 2) POST the file (blob) to that URL
const result = await fetch(postUrl, {
method: "POST",
headers: { "Content-Type": selectedImage.type },
body: selectedImage, // this is the image blob
});

if (!result.ok) {
  console.error("Upload failed");
  return;
}

// 3) Get storageId from Convex
const { storageId } = await result.json();

// 4) Save storageId + metadata in your DB
await saveImage({ storageId });

setSelectedImage(null);
if (imageInput.current) imageInput.current.value = "";

}

return (
<form onSubmit={handleUpload}>
<input
type="file"
accept="image/*"
ref={imageInput}
onChange={(e) => setSelectedImage(e.target.files?.[0] ?? null)}
disabled={selectedImage !== null}
/>
<button type="submit" disabled={!selectedImage}>
Upload
</button>
</form>
);
}


This is the same pattern as the official example for uploading files from a web page. [[Web upload example](<https://docs.convex.dev/file-storage/upload-files#uploading-files-via-upload-urls>)]

---

If you then want to run your **Sharp processing action**, you can:

- Either process the image directly in the action that uses `ctx.storage.store` (if you’re fetching from a URL or another source), or  
- After upload, call an action that reads the file from storage (not covered in the provided docs) and re‑stores the processed version.

The knowledge sources don’t show a full “read from storage then re‑store processed file” example, so I can’t give a precise code snippet for that part.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------