mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
remove api route for presigned s3 urls
This commit is contained in:
parent
2ef59f414d
commit
9cbcce39bb
3 changed files with 16 additions and 65 deletions
|
|
@ -1,16 +1,13 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
import { requireAuth } from '@/app/lib/auth';
|
||||||
import crypto from 'crypto';
|
|
||||||
import { tempBinaryCache } from '@/src/application/services/temp-binary-cache';
|
import { tempBinaryCache } from '@/src/application/services/temp-binary-cache';
|
||||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||||
import { UsageTracker, getCustomerForUserId, logUsage as libLogUsage } from '@/app/lib/billing';
|
import { UsageTracker, getCustomerForUserId, logUsage as libLogUsage } from '@/app/lib/billing';
|
||||||
import { requireAuth } from '@/app/lib/auth';
|
|
||||||
import { USE_AUTH, USE_BILLING } from '@/app/lib/feature_flags';
|
import { USE_AUTH, USE_BILLING } from '@/app/lib/feature_flags';
|
||||||
|
|
||||||
// POST /api/uploaded-images
|
// POST /api/tmp-images/upload
|
||||||
// Accepts an image file (multipart/form-data, field name: "file")
|
// Accepts an image file (multipart/form-data, field name: "file")
|
||||||
// Stores it either in S3 (if configured) under uploaded_images/<a>/<b>/<uuid>.<ext>
|
// Stores it in the in-memory temp cache and returns a temporary URL.
|
||||||
// or in the in-memory temp cache. Returns a JSON with a URL that the agent can fetch.
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Require authentication if enabled
|
// Require authentication if enabled
|
||||||
|
|
@ -18,7 +15,7 @@ export async function POST(request: NextRequest) {
|
||||||
if (USE_AUTH) {
|
if (USE_AUTH) {
|
||||||
try {
|
try {
|
||||||
currentUser = await requireAuth();
|
currentUser = await requireAuth();
|
||||||
} catch (_) {
|
} catch {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +51,7 @@ export async function POST(request: NextRequest) {
|
||||||
const response: any = result.response as any;
|
const response: any = result.response as any;
|
||||||
descriptionMarkdown = response?.text?.() || null;
|
descriptionMarkdown = response?.text?.() || null;
|
||||||
|
|
||||||
// Track usage similar to agents-runtime
|
// Track usage similar to rag-worker
|
||||||
try {
|
try {
|
||||||
const inputTokens = response?.usageMetadata?.promptTokenCount || 0;
|
const inputTokens = response?.usageMetadata?.promptTokenCount || 0;
|
||||||
const outputTokens = response?.usageMetadata?.candidatesTokenCount || 0;
|
const outputTokens = response?.usageMetadata?.candidatesTokenCount || 0;
|
||||||
|
|
@ -63,9 +60,9 @@ export async function POST(request: NextRequest) {
|
||||||
modelName: 'gemini-2.5-flash',
|
modelName: 'gemini-2.5-flash',
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
context: 'uploaded_images.upload_with_description',
|
context: 'tmp_images.upload_with_description',
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch {
|
||||||
// ignore usage tracking errors
|
// ignore usage tracking errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,56 +70,11 @@ export async function POST(request: NextRequest) {
|
||||||
console.warn('Gemini description failed', e);
|
console.warn('Gemini description failed', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If S3 configured, upload there
|
// Store in temp cache and return temp URL
|
||||||
const s3Bucket = process.env.RAG_UPLOADS_S3_BUCKET || '';
|
|
||||||
if (s3Bucket) {
|
|
||||||
const s3Region = process.env.RAG_UPLOADS_S3_REGION || 'us-east-1';
|
|
||||||
const s3 = new S3Client({
|
|
||||||
region: s3Region,
|
|
||||||
credentials: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY ? {
|
|
||||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
|
|
||||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
|
|
||||||
} : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ext = mime === 'image/jpeg' ? '.jpg' : mime === 'image/webp' ? '.webp' : mime === 'image/png' ? '.png' : '.bin';
|
|
||||||
const imageId = crypto.randomUUID();
|
|
||||||
const last2 = imageId.slice(-2).padStart(2, '0');
|
|
||||||
const dirA = last2.charAt(0);
|
|
||||||
const dirB = last2.charAt(1);
|
|
||||||
const key = `uploaded_images/${dirA}/${dirB}/${imageId}${ext}`;
|
|
||||||
|
|
||||||
await s3.send(new PutObjectCommand({
|
|
||||||
Bucket: s3Bucket,
|
|
||||||
Key: key,
|
|
||||||
Body: buf,
|
|
||||||
ContentType: mime,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const url = `/api/uploaded-images/${imageId}${ext}`;
|
|
||||||
|
|
||||||
// Log usage to billing similar to rag-worker
|
|
||||||
try {
|
|
||||||
if (USE_BILLING && currentUser) {
|
|
||||||
const customer = await getCustomerForUserId(currentUser.id);
|
|
||||||
if (customer) {
|
|
||||||
const items = usageTracker.flush();
|
|
||||||
if (items.length > 0) {
|
|
||||||
await libLogUsage(customer.id, { items });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// ignore billing logging errors
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({ url, storage: 's3', id: `${imageId}${ext}`, mimeType: mime, description: descriptionMarkdown });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise store in temp cache and return temp URL
|
|
||||||
const ttlSec = 10 * 60; // 10 minutes
|
const ttlSec = 10 * 60; // 10 minutes
|
||||||
const id = tempBinaryCache.put(buf, mime, ttlSec * 1000);
|
const id = tempBinaryCache.put(buf, mime, ttlSec * 1000);
|
||||||
const url = `/api/tmp-images/${id}`;
|
const url = `/api/tmp-images/${id}`;
|
||||||
|
|
||||||
// Log usage to billing similar to rag-worker
|
// Log usage to billing similar to rag-worker
|
||||||
try {
|
try {
|
||||||
if (USE_BILLING && currentUser) {
|
if (USE_BILLING && currentUser) {
|
||||||
|
|
@ -134,13 +86,14 @@ export async function POST(request: NextRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch {
|
||||||
// ignore billing logging errors
|
// ignore billing logging errors
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ url, storage: 'temp', id, mimeType: mime, expiresInSec: ttlSec, description: descriptionMarkdown });
|
return NextResponse.json({ url, storage: 'temp', id, mimeType: mime, expiresInSec: ttlSec, description: descriptionMarkdown });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('upload image error', e);
|
console.error('tmp image upload error', e);
|
||||||
return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
|
return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { S3Client, GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
|
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { requireAuth } from '@/app/lib/auth';
|
import { requireAuth } from '@/app/lib/auth';
|
||||||
|
|
||||||
// Serves uploaded images from S3 by UUID-only path: /api/uploaded-images/{id}
|
// Serves uploaded images from S3 at path: /api/uploaded-images/{uuid}.{ext}
|
||||||
// Reconstructs the S3 key using the same sharding logic as image upload.
|
// Reconstructs the S3 key using the same sharding logic as image upload.
|
||||||
export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) {
|
export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) {
|
||||||
// Require authentication (handles guest mode internally when USE_AUTH is disabled)
|
// Require authentication (handles guest mode internally when USE_AUTH is disabled)
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,6 @@ export function ComposeBoxPlayground({
|
||||||
}
|
}
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
uploadAbortRef.current = controller;
|
uploadAbortRef.current = controller;
|
||||||
let usedFallback = false;
|
|
||||||
try {
|
try {
|
||||||
// 1) Request a presigned S3 upload URL via server action
|
// 1) Request a presigned S3 upload URL via server action
|
||||||
const { getUploadUrlForImage } = await import('@/app/actions/uploaded-images.actions');
|
const { getUploadUrlForImage } = await import('@/app/actions/uploaded-images.actions');
|
||||||
|
|
@ -126,12 +125,11 @@ export function ComposeBoxPlayground({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// On local, S3 may be unconfigured. Fallback to legacy temp upload endpoint.
|
|
||||||
if (err?.name === 'AbortError') throw err;
|
if (err?.name === 'AbortError') throw err;
|
||||||
usedFallback = true;
|
// Fallback to temp in-memory upload for local/dev without S3
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', file);
|
form.append('file', file);
|
||||||
const res = await fetch('/api/uploaded-images', {
|
const res = await fetch('/api/tmp-images/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: form,
|
body: form,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue