Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ jobs:
echo "ERROR: db:push needs an interactive rename decision; land it as a versioned migration instead of relying on push." >&2
exit 1
fi
bun run ./scripts/apply-dev-workspace-file-size-cutover.ts
else
echo "Applying versioned migrations (db:migrate)"
bun run ./scripts/migrate.ts
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/app/api/files/uploads/finalizers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
registerUploadedWorkspaceFile,
type WorkspaceFileRecord,
} from '@/lib/uploads/contexts/workspace'
import { type StorageContext, toLegacyWorkspaceFileSize } from '@/lib/uploads/shared/types'
import { getWorkspaceFileSize, type StorageContext } from '@/lib/uploads/shared/types'
import { UploadSessionError, type UploadSessionRecord } from '@/lib/uploads/upload-session/service'
import { toV2File } from '@/app/api/v2/files/utils'

Expand Down Expand Up @@ -364,7 +364,6 @@ async function insertOrLoadFileMetadata(
originalName: input.originalName,
displayName: input.originalName,
contentType: input.contentType,
size: toLegacyWorkspaceFileSize(input.size),
sizeBytes: input.size,
deletedAt: null,
uploadedAt: now,
Expand Down Expand Up @@ -396,7 +395,7 @@ async function findFileMetadataByKey(key: string): Promise<FileMetadataRecord |
}

function assertMatchingMetadata(existing: FileMetadataRecord, input: FinalizedMetadataInput): void {
const existingSize = existing.sizeBytes ?? existing.size
const existingSize = getWorkspaceFileSize(existing)
if (
existing.key !== input.key ||
existing.userId !== input.userId ||
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/mothership/local-files/stage/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ vi.mock('@sim/db/schema', () => ({
displayName: 'workspaceFiles.displayName',
originalName: 'workspaceFiles.originalName',
contentType: 'workspaceFiles.contentType',
size: 'workspaceFiles.size',
sizeBytes: 'workspaceFiles.sizeBytes',
deletedAt: 'workspaceFiles.deletedAt',
},
}))
Expand Down Expand Up @@ -95,7 +95,7 @@ describe('POST /api/mothership/local-files/stage', () => {
displayName: null,
originalName: 'report.pdf',
contentType: 'application/pdf',
size: 42,
sizeBytes: 42,
},
])
mockWhere.mockReturnValue({ limit: mockLimit })
Expand Down Expand Up @@ -139,7 +139,7 @@ describe('POST /api/mothership/local-files/stage', () => {
displayName: 'report (2).pdf',
originalName: 'report.pdf',
contentType: 'application/pdf',
size: 42,
sizeBytes: 42,
},
])
const response = await POST(request())
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/mothership/local-files/stage/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
trackChatUpload,
WorkspaceFileKeyOwnershipError,
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { getWorkspaceFileSize } from '@/lib/uploads/shared/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('StageLocalFileUploadAPI')
Expand Down Expand Up @@ -52,7 +53,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
displayName: workspaceFiles.displayName,
originalName: workspaceFiles.originalName,
contentType: workspaceFiles.contentType,
size: workspaceFiles.size,
sizeBytes: workspaceFiles.sizeBytes,
})
.from(workspaceFiles)
.where(
Expand Down Expand Up @@ -87,7 +88,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
key,
file.originalName,
file.contentType,
file.size
getWorkspaceFileSize(file)
)
).displayName

Expand Down
28 changes: 24 additions & 4 deletions apps/sim/background/cleanup-soft-deletes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe('cleanup soft deletes', () => {
key: 'workspace/ws-1/file-failed',
workspaceId: 'ws-1',
context: 'workspace',
size: 11,
sizeBytes: 11,
},
])
mockDeleteFiles.mockResolvedValueOnce({
Expand All @@ -148,14 +148,14 @@ describe('cleanup soft deletes', () => {
key: 'workspace/ws-1/file-deleted',
workspaceId: 'ws-1',
context: 'workspace',
size: 7,
sizeBytes: 7,
},
{
id: 'file-restored',
key: 'workspace/ws-1/file-restored',
workspaceId: 'ws-1',
context: 'workspace',
size: 13,
sizeBytes: 13,
},
])
mockDeleteFiles.mockResolvedValueOnce({ deleted: 2, failed: [] })
Expand Down Expand Up @@ -184,7 +184,7 @@ describe('cleanup soft deletes', () => {
key: 'mothership/chat-file',
workspaceId: 'ws-1',
context: 'mothership',
size: 17,
sizeBytes: 17,
},
])
mockDeleteFiles.mockResolvedValueOnce({ deleted: 1, failed: [] })
Expand All @@ -198,6 +198,26 @@ describe('cleanup soft deletes', () => {
expect(mockDecrementStorageUsageForBillingContextInTx).not.toHaveBeenCalled()
})

it('fails before deleting storage when canonical size metadata is missing', async () => {
mockSelectRowsByIdChunks
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
id: 'file-missing-size',
key: 'workspace/ws-1/file-missing-size',
workspaceId: 'ws-1',
context: 'workspace',
sizeBytes: null,
},
])

await expect(runCleanupSoftDeletes(basePayload)).rejects.toThrow(
'Workspace file is missing canonical size_bytes metadata'
)
expect(mockDeleteFiles).not.toHaveBeenCalled()
})

it('hard-deletes retained documents before deleting an expired knowledge base', async () => {
mockChunkedBatchDelete.mockImplementationOnce(
async (options: {
Expand Down
11 changes: 4 additions & 7 deletions apps/sim/background/cleanup-soft-deletes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type { StorageContext } from '@/lib/uploads'
import { isUsingCloudStorage, StorageService } from '@/lib/uploads'
import { allocateUniqueWorkspaceFileName } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { deleteFileMetadata } from '@/lib/uploads/server/metadata'
import { getWorkspaceFileSize } from '@/lib/uploads/shared/types'
import { deduplicateWorkflowName } from '@/lib/workflows/utils'

const logger = createLogger('CleanupSoftDeletes')
Expand Down Expand Up @@ -113,9 +114,7 @@ async function selectExpiredWorkspaceFiles(
key: workspaceFiles.key,
workspaceId: workspaceFiles.workspaceId,
context: workspaceFiles.context,
size: sql<number>`coalesce(${workspaceFiles.sizeBytes}, ${workspaceFiles.size})`.mapWith(
Number
),
sizeBytes: workspaceFiles.sizeBytes,
})
.from(workspaceFiles)
.where(
Expand All @@ -136,7 +135,7 @@ async function selectExpiredWorkspaceFiles(
key: r.key,
workspaceId: r.workspaceId,
context: r.context as StorageContext,
size: r.size,
size: getWorkspaceFileSize(r),
})),
}
}
Expand Down Expand Up @@ -329,9 +328,7 @@ async function deleteExpiredBillableWorkspaceFileRows(
)
.returning({
id: workspaceFiles.id,
size: sql<number>`coalesce(${workspaceFiles.sizeBytes}, ${workspaceFiles.size})`.mapWith(
Number
),
size: sql<number>`${workspaceFiles.sizeBytes}`.mapWith(Number),
})
if (deletedRows.some(({ size }) => size < 0)) {
throw new Error('Cannot delete workspace files with negative stored-byte metadata')
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/ee/workspace-forking/lib/copy/copy-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ describe('planForkFileCopies', () => {
displayName: null,
contentType: 'text/plain',
size: 4321,
sizeBytes: 4321,
deletedAt: null,
uploadedAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
Expand Down Expand Up @@ -593,6 +594,7 @@ describe('planForkFileCopies', () => {
displayName: null,
contentType: 'text/plain',
size: 4321,
sizeBytes: 4321,
deletedAt: null,
uploadedAt: new Date('2026-01-01'),
updatedAt: new Date('2026-01-01'),
Expand Down
8 changes: 4 additions & 4 deletions apps/sim/ee/workspace-forking/lib/copy/copy-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
headObject,
uploadFile,
} from '@/lib/uploads/core/storage-service'
import type { StorageContext } from '@/lib/uploads/shared/types'
import { getWorkspaceFileSize, type StorageContext } from '@/lib/uploads/shared/types'
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
import { resolveForkFolderMapping } from '@/ee/workspace-forking/lib/copy/copy-workflows'
import {
Expand Down Expand Up @@ -219,7 +219,7 @@ export async function planForkFileCopies(params: {
context: meta.context as StorageContext,
fileName: meta.originalName,
contentType: meta.contentType,
size: meta.size,
size: getWorkspaceFileSize(meta),
targetFileId: childFileId,
displayName: meta.displayName,
userId,
Expand Down Expand Up @@ -341,7 +341,7 @@ export async function executeForkFileBlobCopies(
originalName: targetOriginalName,
displayName: targetDisplayName,
contentType: task.contentType,
size: task.size,
sizeBytes: task.size,
deletedAt: null,
uploadedAt: new Date(),
})
Expand Down Expand Up @@ -389,7 +389,7 @@ export async function executeForkFileBlobCopies(
originalName: targetOriginalName,
displayName: targetDisplayName,
contentType: task.contentType,
size: task.size,
sizeBytes: task.size,
deletedAt: null,
uploadedAt: new Date(),
})
Expand Down
15 changes: 14 additions & 1 deletion apps/sim/ee/workspace-forking/lib/copy/storage-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
} from '@/ee/workspace-forking/lib/copy/storage-quota'
import { ForkError } from '@/ee/workspace-forking/lib/lineage/authz'

function makeExecutor(total: number | string) {
function makeExecutor(total: number | string | null) {
const execute = vi.fn((_query: unknown) => Promise.resolve([{ total }]))
return { executor: { execute } as unknown as DbOrTx, execute }
}
Expand All @@ -71,6 +71,8 @@ describe('sumForkCopyBytes', () => {
const compiled = outerQuery.toSQL()
expect(compiled.sql).toBe('SELECT (? + ?)::bigint AS total')
const [fileBytes, kbBytes] = compiled.params
expect(fileBytes.toSQL().sql).toContain('count(*) FILTER')
expect(fileBytes.toSQL().sql).toContain('IS NULL')
expect(fileBytes.toSQL().params).toContainEqual({
type: 'and',
conditions: [
Expand Down Expand Up @@ -101,6 +103,17 @@ describe('sumForkCopyBytes', () => {
expect(bytes).toBe(1024)
})

it('fails closed when a selected workspace file lacks canonical size metadata', async () => {
const { executor } = makeExecutor(null)

await expect(
sumForkCopyBytes(executor, 'src-ws', { fileIds: ['wf-missing-size'] })
).rejects.toMatchObject({
message: 'Storage calculation is temporarily unavailable',
statusCode: 503,
})
})

it('runs no query for an empty selection', async () => {
const { executor, execute } = makeExecutor(0)

Expand Down
16 changes: 11 additions & 5 deletions apps/sim/ee/workspace-forking/lib/copy/storage-quota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface ForkCopyBytesSelection {
/**
* Byte total a fork/sync copy selection would duplicate into the target: selected
* workspace-file blobs plus the selected knowledge bases' stored document blobs. Sizes
* come from the metadata rows (`workspace_files.size`, `document.file_size`) - no blob
* come from the metadata rows (`workspace_files.size_bytes`, `document.file_size`) - no blob
* reads. Both sums scope to the source workspace with the same filters the copy itself
* applies, so an id that is not actually copyable can only over-count (block), never
* under-count.
Expand All @@ -47,8 +47,11 @@ export async function sumForkCopyBytes(
const fileBytes =
fileSelectors.length === 0
? sql<number>`0`
: sql<number>`(
SELECT coalesce(sum(coalesce(${workspaceFiles.sizeBytes}, ${workspaceFiles.size})), 0)
: sql<number | null>`(
SELECT CASE
WHEN count(*) FILTER (WHERE ${workspaceFiles.sizeBytes} IS NULL) > 0 THEN NULL
ELSE coalesce(sum(${workspaceFiles.sizeBytes}), 0)
END
FROM ${workspaceFiles}
WHERE ${and(
fileSelectors.length === 1 ? fileSelectors[0] : or(...fileSelectors),
Expand All @@ -74,10 +77,13 @@ export async function sumForkCopyBytes(
isNotNull(document.storageKey)
)}
)`
const [row] = await executor.execute<{ total: number | string }>(
const [row] = await executor.execute<{ total: number | string | null }>(
sql`SELECT (${fileBytes} + ${kbBytes})::bigint AS total`
)
return Number(row?.total ?? 0)
if (row?.total == null) {
throw new ForkError('Storage calculation is temporarily unavailable', 503)
}
return Number(row.total)
}

type ForkCreationPayerPolicy = Pick<
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/billing/storage/payer-transfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ vi.mock('@sim/db/schema', () => ({
__table: 'workspaceFiles',
context: 'workspaceFiles.context',
deletedAt: 'workspaceFiles.deletedAt',
size: 'workspaceFiles.size',
sizeBytes: 'workspaceFiles.sizeBytes',
workspaceId: 'workspaceFiles.workspaceId',
},
}))
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/billing/storage/payer-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ async function getExactWorkspaceStorageBytes(tx: DbOrTx, workspaceId: string): P
const [row] = await tx.execute<ExactWorkspaceStorageRow>(sql`
SELECT
COALESCE((
SELECT SUM(coalesce(${workspaceFiles.sizeBytes}, ${workspaceFiles.size}::bigint))
SELECT SUM(${workspaceFiles.sizeBytes})
FROM ${workspaceFiles}
WHERE ${workspaceFiles.workspaceId} = ${workspaceId}
AND ${workspaceFiles.context} = 'workspace'
Expand Down Expand Up @@ -171,7 +171,7 @@ async function getExactWorkspaceStorageBytesBatch(
FROM (
SELECT
${workspaceFiles.workspaceId} AS workspace_id,
SUM(coalesce(${workspaceFiles.sizeBytes}, ${workspaceFiles.size}::bigint)) AS workspace_file_bytes,
SUM(${workspaceFiles.sizeBytes}) AS workspace_file_bytes,
0::bigint AS document_bytes
FROM ${workspaceFiles}
WHERE ${inArray(workspaceFiles.workspaceId, workspaceIds)}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/copilot/chat/fork-chat-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function makeRow(overrides: Partial<ForkableChatFileRow> = {}): ForkableChatFile
displayName: 'cat.png',
contentType: 'image/png',
size: 100,
sizeBytes: 100,
deletedAt: null,
uploadedAt: new Date('2026-06-01T00:00:00.000Z'),
updatedAt: new Date('2026-06-01T00:00:00.000Z'),
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/lib/copilot/chat/fork-chat-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { workspaceFiles } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateShortId } from '@sim/utils/id'
import { omit } from '@sim/utils/object'
import { and, eq, isNull } from 'drizzle-orm'
import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
import type { DbOrTx, DbTransaction } from '@/lib/db/types'
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { copyWorkspaceFileSecretProvenanceInTx } from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
import { downloadFile, uploadFile } from '@/lib/uploads/core/storage-service'
import type { StorageContext } from '@/lib/uploads/shared/types'
import { getWorkspaceFileSize, type StorageContext } from '@/lib/uploads/shared/types'
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'

const logger = createLogger('ForkChatFiles')
Expand Down Expand Up @@ -120,11 +121,12 @@ export async function planChatFileCopies(params: {
const copyId = `wf_${generateShortId()}`
const targetKey = generateWorkspaceFileKey(row.workspaceId, row.originalName)
copyRows.push({
...row,
...omit(row, ['size']),
id: copyId,
key: targetKey,
chatId: newChatId,
userId,
sizeBytes: getWorkspaceFileSize(row),
deletedAt: null,
uploadedAt: now,
updatedAt: now,
Expand Down
Loading
Loading