feat: improve attachment headers (#13709)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Safer, consistent file downloads with automatic attachment headers and
filenames.
- Smarter MIME detection for uploads (avatars, workspace blobs, Copilot
files/transcripts).
  - Sensible default buffer limit when reading uploads.

- **Bug Fixes**
- Prevents risky content from rendering inline by forcing downloads and
adding no‑sniff protection.
- More accurate content types when original metadata is missing or
incorrect.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-10-09 16:04:18 +08:00
committed by GitHub
parent bf72833f05
commit 1b859a37c5
18 changed files with 143 additions and 33 deletions
@@ -31,6 +31,7 @@ import {
EventBus,
type FileUpload,
RequestMutex,
sniffMime,
Throttle,
TooManyRequest,
UserFriendlyError,
@@ -671,7 +672,11 @@ export class CopilotContextResolver {
const { filename, mimetype } = content;
await this.storage.put(user.id, session.workspaceId, blobId, buffer);
const file = await session.addFile(blobId, filename, mimetype);
const file = await session.addFile(
blobId,
filename,
sniffMime(buffer, mimetype) || mimetype
);
await this.jobs.addFileEmbeddingQueue({
userId: user.id,
@@ -32,6 +32,7 @@ import {
} from 'rxjs';
import {
applyAttachHeaders,
BlobNotFound,
CallMetric,
Config,
@@ -795,6 +796,10 @@ export class CopilotController implements BeforeApplicationShutdown {
} else {
this.logger.warn(`Blob ${workspaceId}/${key} has no metadata`);
}
applyAttachHeaders(res, {
contentType: metadata?.contentType,
filename: key,
});
res.setHeader('cache-control', 'public, max-age=2592000, immutable');
body.pipe(res);
@@ -30,6 +30,7 @@ import {
Paginated,
PaginationInput,
RequestMutex,
sniffMime,
Throttle,
TooManyRequest,
UserFriendlyError,
@@ -806,7 +807,10 @@ export class CopilotResolver {
filename,
uploaded.buffer
);
attachments.push({ attachment, mimeType: blob.mimetype });
attachments.push({
attachment,
mimeType: sniffMime(uploaded.buffer, blob.mimetype) || blob.mimetype,
});
}
}
@@ -12,6 +12,7 @@ import {
NoCopilotProviderAvailable,
OnEvent,
OnJob,
sniffMime,
} from '../../../base';
import { Models } from '../../../models';
import { PromptService } from '../prompt';
@@ -85,7 +86,10 @@ export class CopilotTranscriptionService {
`${blobId}-${idx}`,
buffer
);
infos.push({ url, mimeType: blob.mimetype });
infos.push({
url,
mimeType: sniffMime(buffer, blob.mimetype) || blob.mimetype,
});
}
const model = await this.getModel(userId);
@@ -2,7 +2,12 @@ import { createHash } from 'node:crypto';
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { FileUpload, JobQueue, PaginationInput } from '../../../base';
import {
FileUpload,
JobQueue,
PaginationInput,
sniffMime,
} from '../../../base';
import { ServerFeature, ServerService } from '../../../core';
import { Models } from '../../../models';
import { CopilotStorage } from '../storage';
@@ -64,7 +69,7 @@ export class CopilotWorkspaceService implements OnApplicationBootstrap {
const file = await this.models.copilotWorkspace.addFile(workspaceId, {
fileName,
blobId,
mimeType: content.mimetype,
mimeType: sniffMime(buffer, content.mimetype) || content.mimetype,
size: buffer.length,
});
return { blobId, file };