Skip to content

Commit 0f57409

Browse files
tea-artistunofn
andauthored
[sync] fix: refactor email processing and attachment handling (#1507) (#2824)
Synced from teableio/teable-ee@6177977 Co-authored-by: Uno <uno@teable.ai>
1 parent 9b63a02 commit 0f57409

4 files changed

Lines changed: 39 additions & 54 deletions

File tree

apps/nestjs-backend/src/cache/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ICacheStore {
3535
[key: `send-mail-rate-limit:${string}`]: boolean;
3636
[key: `oauth:token-rate:${string}:${string}`]: number;
3737
[key: `automation:email:rate:${string}:${number}`]: number;
38+
[key: `automation:email-att:${string}`]: string[];
3839
// Distributed lock keys
3940
[key: `lock:${string}`]: string;
4041
[key: `import:result:manifest:${string}`]: {

apps/nestjs-backend/src/features/attachments/attachments.service.ts

Lines changed: 12 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { AttachmentsStorageService } from './attachments-storage.service';
3333
import StorageAdapter from './plugins/adapter';
3434
import type { LocalStorage } from './plugins/local';
3535
import { InjectStorageAdapter } from './plugins/storage';
36-
import { getExtensionPreview } from './utils';
36+
import { getExtensionPreview, getSafeUploadContentType } from './utils';
3737
@Injectable()
3838
export class AttachmentsService {
3939
private logger = new Logger(AttachmentsService.name);
@@ -281,55 +281,6 @@ export class AttachmentsService {
281281
return await this.notifyToAttachmentItem(token, filename);
282282
}
283283

284-
async uploadFromLocalFile(filePath: string, filename: string): Promise<IAttachmentItem> {
285-
const MAX_FILE_SIZE = this.thresholdConfig.maxOpenapiAttachmentUploadSize;
286-
const stat = await fs.promises.stat(filePath);
287-
const contentLength = stat.size;
288-
289-
if (contentLength > MAX_FILE_SIZE) {
290-
const maxSize = (MAX_FILE_SIZE / (1024 * 1024)).toFixed(2);
291-
throw new CustomHttpException(
292-
`File size exceeds the maximum limit of ${maxSize} MB`,
293-
HttpErrorCode.VALIDATION_ERROR,
294-
{
295-
localization: {
296-
i18nKey: 'httpErrors.attachment.fileSizeExceedsMaximumLimit',
297-
context: {
298-
maxSize: `${maxSize}MB`,
299-
},
300-
},
301-
}
302-
);
303-
}
304-
305-
const contentType = mimeTypes.lookup(filename) || 'application/octet-stream';
306-
const { token, url } = await this.signature({
307-
type: UploadType.Table,
308-
contentLength,
309-
contentType,
310-
internal: true,
311-
});
312-
313-
try {
314-
await this.uploadStreamToStorage(
315-
url,
316-
fs.createReadStream(filePath),
317-
contentType,
318-
contentLength
319-
);
320-
return await this.notifyToAttachmentItem(token, filename);
321-
} finally {
322-
await fs.promises.unlink(filePath);
323-
// Clean up temp subdirectory (created by email attachment saver)
324-
const dir = dirname(filePath);
325-
try {
326-
await fs.promises.rmdir(dir);
327-
} catch {
328-
/* directory not empty or already removed */
329-
}
330-
}
331-
}
332-
333284
async uploadFromUrl(
334285
fileUrl: string,
335286
uploadType: UploadType = UploadType.Table
@@ -394,7 +345,10 @@ export class AttachmentsService {
394345
const headResponse = await axios.head(fileUrl);
395346
contentLength =
396347
headResponse.headers['content-length'] && parseInt(headResponse.headers['content-length']);
397-
contentType = mimeTypes.lookup(fileUrl) || headResponse.headers['content-type'];
348+
contentType =
349+
mimeTypes.lookup(fileUrl) ||
350+
headResponse.headers['content-type'] ||
351+
'application/octet-stream';
398352
this.logger.log(
399353
`HEAD request successful. Content-Length: ${contentLength}, Content-Type: ${contentType}`
400354
);
@@ -460,7 +414,7 @@ export class AttachmentsService {
460414
try {
461415
await axios.put(url, stream, {
462416
headers: {
463-
'Content-Type': contentType,
417+
'Content-Type': getSafeUploadContentType(contentType),
464418
'Content-Length': contentLength,
465419
},
466420
});
@@ -473,7 +427,12 @@ export class AttachmentsService {
473427
private getFilenameFromUrl(url: string): string {
474428
const urlParts = new URL(url);
475429
const pathParts = urlParts.pathname.split('/');
476-
return pathParts[pathParts.length - 1] || 'downloaded_file';
430+
const rawFilename = pathParts[pathParts.length - 1] || 'downloaded_file';
431+
try {
432+
return decodeURIComponent(rawFilename);
433+
} catch {
434+
return rawFilename;
435+
}
477436
}
478437

479438
private async downloadFile(

apps/nestjs-backend/src/features/attachments/plugins/local.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { IClsStore } from '../../../types/cls';
1818
import { FileUtils } from '../../../utils';
1919
import { Encryptor } from '../../../utils/encryptor';
2020
import { second } from '../../../utils/second';
21+
import { isBodyParserFallback } from '../utils';
2122
import StorageAdapter from './adapter';
2223
import type { ILocalFileUpload, IObjectMeta, IPresignParams, IRespHeaders } from './types';
2324

@@ -132,7 +133,7 @@ export class LocalStorage implements StorageAdapter {
132133
},
133134
});
134135
}
135-
if (mimetype && mimetype !== contentType) {
136+
if (mimetype && !isBodyParserFallback(mimetype, contentType) && mimetype !== contentType) {
136137
throw new CustomHttpException(
137138
`Not allow upload ${mimetype} file`,
138139
HttpErrorCode.VALIDATION_ERROR,

apps/nestjs-backend/src/features/attachments/utils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
2+
const OCTET_STREAM = 'application/octet-stream';
3+
const JSON_PREFIX = 'application/json';
4+
5+
/**
6+
* Check if a content type would be intercepted by Express body parser (e.g. application/json).
7+
* When uploading internally via localhost, these types cause the stream to be consumed
8+
* before reaching the upload handler, so we need to fall back to application/octet-stream.
9+
*/
10+
export const getSafeUploadContentType = (contentType: string): string => {
11+
if (contentType && contentType.startsWith(JSON_PREFIX)) {
12+
return OCTET_STREAM;
13+
}
14+
return contentType;
15+
};
16+
17+
/**
18+
* Check if a mimetype mismatch is caused by the body parser fallback.
19+
* Returns true if the request used octet-stream as a substitute for a JSON content type.
20+
*/
21+
export const isBodyParserFallback = (mimetype: string, expectedType: string): boolean => {
22+
return mimetype === OCTET_STREAM && expectedType.startsWith(JSON_PREFIX);
23+
};
24+
125
export const getExtensionPreview = (contentType: string) => {
226
const imageExtensions = [
327
'jif',

0 commit comments

Comments
 (0)