#fetch image
1 messages · Page 1 of 1 (latest)
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
sharpas an external package inconvex.json:{ "node": { "externalPackages": ["sharp"] } }- Install a
sharpversion 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.
@fading plover is not an url, i want the fetch to get the blob
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
sharpto 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.
@fading plover on the front end if i want to send the image?
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:
----------------------