Skip to content

Commit 770c1f5

Browse files
committed
fix(files): re-derive image dimensions on content swap instead of clearing
Clearing width/height to NULL on a content swap reopened the width IS NULL backfill path, so a late fire-and-forget PATCH for the previous image could write its stale size onto the new content. Instead, measure the new bytes' intrinsic dimensions server-side (image-size, headers only) and store those (or null for a non-image), so the row always matches the current content and a stale backfill can't apply.
1 parent 1c026d5 commit 770c1f5

1 file changed

Lines changed: 26 additions & 5 deletions

File tree

apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { createLogger } from '@sim/logger'
1010
import { getErrorMessage, getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors'
1111
import { generateShortId } from '@sim/utils/id'
1212
import { and, eq, isNotNull, isNull, or, sql } from 'drizzle-orm'
13+
import { imageSize } from 'image-size'
1314
import type { ShareRecord } from '@/lib/api/contracts/public-shares'
1415
import {
1516
decrementStorageUsageForBillingContextInTx,
@@ -888,6 +889,23 @@ export async function updateWorkspaceFileDimensions(
888889
return updated.length > 0
889890
}
890891

892+
/**
893+
* Best-effort intrinsic dimensions of an image buffer (reads headers only, no full decode). Returns null
894+
* for non-images and unparseable bytes — callers then store null and let the lazy client backfill fill it.
895+
*/
896+
function measureImageDimensions(
897+
content: Buffer,
898+
contentType: string
899+
): { width: number; height: number } | null {
900+
if (!contentType.startsWith('image/')) return null
901+
try {
902+
const { width, height } = imageSize(content)
903+
return width && height ? { width, height } : null
904+
} catch {
905+
return null
906+
}
907+
}
908+
891909
/**
892910
* Look up a single active workspace file by its original name.
893911
* Returns the record if found, or null if no matching file exists.
@@ -1234,6 +1252,10 @@ export async function updateWorkspaceFileContent(
12341252
const storageBillingContext = await resolveStorageBillingContext(workspaceId)
12351253
const nextContentType = contentType || fileRecord.type
12361254
const nextStorageKey = generateWorkspaceFileKey(workspaceId, fileRecord.name)
1255+
// Re-derive intrinsic dimensions from the NEW bytes so the row never carries the previous image's size
1256+
// — and so a late fire-and-forget backfill PATCH for the OLD image (guarded on `width IS NULL`) can't
1257+
// resurrect stale dimensions, since these stay non-null for an image.
1258+
const nextDimensions = measureImageDimensions(content, nextContentType)
12371259

12381260
try {
12391261
const metadata: Record<string, string> = {
@@ -1312,11 +1334,10 @@ export async function updateWorkspaceFileContent(
13121334
key: uploadResult.key,
13131335
size: content.length,
13141336
contentType: nextContentType,
1315-
// Content is being replaced, so any stored intrinsic dimensions no longer describe it. Clear
1316-
// them (they re-backfill on next view via the `width IS NULL` path) so the editor never
1317-
// reserves a stale aspect ratio for the new bytes.
1318-
width: null,
1319-
height: null,
1337+
// Replaced content gets its OWN intrinsic dimensions (or null for a non-image), so the editor
1338+
// never reserves the previous image's aspect ratio and a late stale backfill can't apply.
1339+
width: nextDimensions?.width ?? null,
1340+
height: nextDimensions?.height ?? null,
13201341
updatedAt: now,
13211342
contentUpdatedAt,
13221343
})

0 commit comments

Comments
 (0)