@hed-hog/core 0.0.400 → 0.0.402
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/ai/ai.controller.d.ts +2 -0
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.service.d.ts +86 -2
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +304 -23
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/document-kit/document-kit.module.d.ts +3 -0
- package/dist/document-kit/document-kit.module.d.ts.map +1 -0
- package/dist/document-kit/document-kit.module.js +24 -0
- package/dist/document-kit/document-kit.module.js.map +1 -0
- package/dist/document-kit/document-kit.registry.d.ts +55 -0
- package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
- package/dist/document-kit/document-kit.registry.js +91 -0
- package/dist/document-kit/document-kit.registry.js.map +1 -0
- package/dist/document-kit/document-kit.service.d.ts +49 -0
- package/dist/document-kit/document-kit.service.d.ts.map +1 -0
- package/dist/document-kit/document-kit.service.js +135 -0
- package/dist/document-kit/document-kit.service.js.map +1 -0
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +136 -13
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +297 -27
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
- package/dist/file/mcp-file-source.schema.js +8 -5
- package/dist/file/mcp-file-source.schema.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +26 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
- package/dist/integration-profile/testers/ai.testers.js +46 -8
- package/dist/integration-profile/testers/ai.testers.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +2 -0
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
- package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
- package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
- package/dist/meeting/meeting-provider.registry.d.ts +72 -0
- package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
- package/dist/meeting/meeting-provider.registry.js.map +1 -1
- package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
- package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
- package/dist/oauth/oauth-token-broker.service.js +105 -3
- package/dist/oauth/oauth-token-broker.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
- package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-bar.js +117 -0
- package/dist/pdf/document/charts/chart-bar.js.map +1 -0
- package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
- package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-donut.js +89 -0
- package/dist/pdf/document/charts/chart-donut.js.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.js +60 -0
- package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
- package/dist/pdf/document/charts/chart-line.d.ts +29 -0
- package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-line.js +89 -0
- package/dist/pdf/document/charts/chart-line.js.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.js +110 -0
- package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
- package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
- package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-radar.js +83 -0
- package/dist/pdf/document/charts/chart-radar.js.map +1 -0
- package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
- package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-scale.js +118 -0
- package/dist/pdf/document/charts/chart-scale.js.map +1 -0
- package/dist/pdf/document/charts/index.d.ts +8 -0
- package/dist/pdf/document/charts/index.d.ts.map +1 -0
- package/dist/pdf/document/charts/index.js +24 -0
- package/dist/pdf/document/charts/index.js.map +1 -0
- package/dist/pdf/document/index.d.ts +8 -0
- package/dist/pdf/document/index.d.ts.map +1 -0
- package/dist/pdf/document/index.js +24 -0
- package/dist/pdf/document/index.js.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.js +36 -0
- package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
- package/dist/pdf/document/pdf-document.css.d.ts +20 -0
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.css.js +327 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.js +226 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
- package/dist/pdf/document/pdf-document.service.d.ts +27 -0
- package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.service.js +81 -0
- package/dist/pdf/document/pdf-document.service.js.map +1 -0
- package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
- package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.theme.js +105 -0
- package/dist/pdf/document/pdf-document.theme.js.map +1 -0
- package/dist/pdf/document/pdf-document.types.d.ts +164 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.types.js +18 -0
- package/dist/pdf/document/pdf-document.types.js.map +1 -0
- package/dist/pdf/pdf.module.d.ts.map +1 -1
- package/dist/pdf/pdf.module.js +3 -2
- package/dist/pdf/pdf.module.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +75 -1
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +167 -63
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
- package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
- package/dist/pdf/templates/commercial-proposal.template.js +276 -0
- package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
- package/dist/setting/favicon.controller.d.ts +26 -0
- package/dist/setting/favicon.controller.d.ts.map +1 -0
- package/dist/setting/favicon.controller.js +130 -0
- package/dist/setting/favicon.controller.js.map +1 -0
- package/dist/setting/setting.module.d.ts.map +1 -1
- package/dist/setting/setting.module.js +2 -1
- package/dist/setting/setting.module.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/data/setting_group.yaml +52 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +81 -2
- package/hedhog/frontend/messages/pt.json +81 -2
- package/hedhog/table/file.yaml +3 -1
- package/package.json +5 -5
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/ai/ai-attachment.fragments.spec.ts +56 -3
- package/src/ai/ai.service.responses.spec.ts +429 -0
- package/src/ai/ai.service.ts +409 -27
- package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
- package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
- package/src/core.module.ts +3 -0
- package/src/document-kit/document-kit.module.ts +12 -0
- package/src/document-kit/document-kit.registry.ts +125 -0
- package/src/document-kit/document-kit.service.spec.ts +156 -0
- package/src/document-kit/document-kit.service.ts +173 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.spec.ts +168 -1
- package/src/file/file.service.ts +435 -36
- package/src/file/mcp-file-source.schema.ts +8 -5
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/index.ts +5 -0
- package/src/integration-profile/integration-profile.secrets.ts +26 -0
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/ai.testers.ts +70 -5
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
- package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/mcp/mcp.module.ts +2 -0
- package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
- package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
- package/src/meeting/meeting-provider.registry.ts +80 -0
- package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
- package/src/oauth/oauth-token-broker.service.ts +129 -2
- package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
- package/src/pdf/document/charts/chart-bar.ts +217 -0
- package/src/pdf/document/charts/chart-donut.ts +163 -0
- package/src/pdf/document/charts/chart-gauge.ts +94 -0
- package/src/pdf/document/charts/chart-line.ts +159 -0
- package/src/pdf/document/charts/chart-primitives.ts +188 -0
- package/src/pdf/document/charts/chart-radar.ts +136 -0
- package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
- package/src/pdf/document/charts/chart-scale.ts +141 -0
- package/src/pdf/document/charts/charts.spec.ts +157 -0
- package/src/pdf/document/charts/index.ts +7 -0
- package/src/pdf/document/index.ts +7 -0
- package/src/pdf/document/pdf-document.chrome.ts +38 -0
- package/src/pdf/document/pdf-document.css.ts +326 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
- package/src/pdf/document/pdf-document.renderer.ts +317 -0
- package/src/pdf/document/pdf-document.service.ts +77 -0
- package/src/pdf/document/pdf-document.theme.ts +146 -0
- package/src/pdf/document/pdf-document.types.ts +149 -0
- package/src/pdf/pdf.module.ts +3 -2
- package/src/pdf/pdf.service.spec.ts +131 -0
- package/src/pdf/pdf.service.ts +256 -82
- package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
- package/src/pdf/templates/commercial-proposal.template.ts +400 -0
- package/src/setting/favicon.controller.spec.ts +92 -0
- package/src/setting/favicon.controller.ts +135 -0
- package/src/setting/setting.module.ts +2 -1
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
- package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
package/src/file/file.service.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DeleteObjectCommand,
|
|
2
3
|
GetBucketNotificationConfigurationCommand,
|
|
3
4
|
GetObjectCommand,
|
|
4
5
|
HeadBucketCommand,
|
|
@@ -25,13 +26,15 @@ import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
|
25
26
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
26
27
|
import {
|
|
27
28
|
BadRequestException,
|
|
29
|
+
ConflictException,
|
|
28
30
|
ForbiddenException,
|
|
29
31
|
forwardRef,
|
|
30
32
|
Inject,
|
|
31
33
|
Injectable,
|
|
32
34
|
Logger,
|
|
33
35
|
NotFoundException,
|
|
34
|
-
OnModuleInit
|
|
36
|
+
OnModuleInit,
|
|
37
|
+
ServiceUnavailableException
|
|
35
38
|
} from '@nestjs/common';
|
|
36
39
|
import { JwtService } from '@nestjs/jwt';
|
|
37
40
|
import { createHash } from 'crypto';
|
|
@@ -56,10 +59,15 @@ import {
|
|
|
56
59
|
} from './image-optimization.service';
|
|
57
60
|
import {
|
|
58
61
|
AbstractProvider,
|
|
62
|
+
MultipartUploadPart,
|
|
63
|
+
supportsDirectUpload,
|
|
59
64
|
supportsServerSideCopy,
|
|
60
65
|
} from './provider/abstract.provider';
|
|
61
66
|
import { ProviderFactory } from './provider/provider.factory';
|
|
62
67
|
|
|
68
|
+
/** Códigos de erro do S3/Spaces que indicam indisponibilidade passageira, não arquivo ruim. */
|
|
69
|
+
const STORAGE_UNAVAILABLE_CODES = ['SlowDown', 'ServiceUnavailable', 'InternalError', 'RequestTimeout'];
|
|
70
|
+
|
|
63
71
|
type ResolvedProvider = {
|
|
64
72
|
provider: AbstractProvider;
|
|
65
73
|
providerId: number;
|
|
@@ -78,6 +86,49 @@ const PROVIDER_CACHE_TTL_MS = Math.max(
|
|
|
78
86
|
Number(process.env.FILE_PROVIDER_CACHE_TTL_MS ?? 30_000),
|
|
79
87
|
);
|
|
80
88
|
|
|
89
|
+
/** Upload direto em partes: mínimo de 16 MiB por parte e até 9000 partes (regra do Studio). */
|
|
90
|
+
export const DIRECT_UPLOAD_MIN_PART_BYTES = 16 * 1024 * 1024;
|
|
91
|
+
export const DIRECT_UPLOAD_MAX_PARTS = 9000;
|
|
92
|
+
/** Validade do token e das URLs de parte: um vídeo grande em conexão lenta leva horas. */
|
|
93
|
+
export const DIRECT_UPLOAD_DEFAULT_TTL_SECONDS = 86_400;
|
|
94
|
+
const DIRECT_UPLOAD_TOKEN_TYPE = 'file-direct-upload';
|
|
95
|
+
/**
|
|
96
|
+
* `file.size` é BIGINT no banco, e o PrismaService o devolve como number: o teto real é o
|
|
97
|
+
* maior inteiro exato de um number (~9 PB).
|
|
98
|
+
*/
|
|
99
|
+
const FILE_SIZE_COLUMN_MAX = Number.MAX_SAFE_INTEGER;
|
|
100
|
+
|
|
101
|
+
export type DirectUploadFileInfo = {
|
|
102
|
+
originalname: string;
|
|
103
|
+
mimetype: string;
|
|
104
|
+
size: number;
|
|
105
|
+
isPrivate?: boolean;
|
|
106
|
+
createdByUserId?: number | null;
|
|
107
|
+
/** Mesmo sentido do `uploadFromPath`: o chamador já conferiu o tipo pelas regras dele. */
|
|
108
|
+
skipMimeValidation?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* O chamador já conferiu o tamanho pelas regras dele: pula a `storage-max-size`. O teto
|
|
111
|
+
* da coluna `file.size` continua valendo.
|
|
112
|
+
*/
|
|
113
|
+
skipSizeValidation?: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type CompleteDirectUploadInput = DirectUploadFileInfo & {
|
|
117
|
+
key: string;
|
|
118
|
+
uploadId: string;
|
|
119
|
+
destination: string;
|
|
120
|
+
parts: MultipartUploadPart[];
|
|
121
|
+
/**
|
|
122
|
+
* Confere os primeiros `bytes` do objeto gravado (ex.: sniff do tipo real). Se `check`
|
|
123
|
+
* lançar, o objeto é apagado e o erro sobe como veio; se devolver um mimetype, é ele
|
|
124
|
+
* que vai para a linha `file` (o tipo real, no lugar do declarado).
|
|
125
|
+
*/
|
|
126
|
+
verifyHead?: {
|
|
127
|
+
bytes: number;
|
|
128
|
+
check: (head: Buffer) => string | void | Promise<string | void>;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
81
132
|
/** Raw `?w=`/`?f=`/`?q=` query values for the public image endpoint. */
|
|
82
133
|
export type ImageVariantRequest = {
|
|
83
134
|
width?: number | string;
|
|
@@ -304,16 +355,37 @@ export class FileService implements OnModuleInit {
|
|
|
304
355
|
return file;
|
|
305
356
|
}
|
|
306
357
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
358
|
+
/**
|
|
359
|
+
* Traduz a falha do provider de storage para a resposta HTTP certa.
|
|
360
|
+
*
|
|
361
|
+
* 5xx e throttle do S3/Spaces são indisponibilidade passageira, não defeito:
|
|
362
|
+
* viram 503 com `code: STORAGE_UNAVAILABLE`, que o HttpExceptionFilter da API
|
|
363
|
+
* responde com `Retry-After` e reporta deduplicado. Sem isso, um 503 do Spaces
|
|
364
|
+
* com corpo que não é XML (ex.: `upstream connect error` do balanceador)
|
|
365
|
+
* estourava no parse do SDK como `char 'u' is not expected` e virava 500.
|
|
366
|
+
*
|
|
367
|
+
* O status vem de `$metadata.httpStatusCode`, que o SDK carimba mesmo quando a
|
|
368
|
+
* desserialização do erro falha; `$response.statusCode` cobre o resto.
|
|
369
|
+
*/
|
|
370
|
+
private mapStorageError(error: any, fileId: number): never {
|
|
371
|
+
const status = error?.$metadata?.httpStatusCode ?? error?.$response?.statusCode;
|
|
372
|
+
const codes = [error?.name, error?.Code];
|
|
312
373
|
|
|
313
|
-
if (
|
|
374
|
+
if (codes.includes('NoSuchKey') || status === 404) {
|
|
314
375
|
throw new NotFoundException(`File not found in storage: ${fileId}`);
|
|
315
376
|
}
|
|
316
377
|
|
|
378
|
+
const unavailable =
|
|
379
|
+
(typeof status === 'number' && status >= 500) ||
|
|
380
|
+
codes.some((c) => STORAGE_UNAVAILABLE_CODES.includes(c));
|
|
381
|
+
|
|
382
|
+
if (unavailable) {
|
|
383
|
+
throw new ServiceUnavailableException(
|
|
384
|
+
{ message: 'Storage temporarily unavailable', code: 'STORAGE_UNAVAILABLE' },
|
|
385
|
+
{ cause: error },
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
317
389
|
throw error;
|
|
318
390
|
}
|
|
319
391
|
|
|
@@ -338,11 +410,15 @@ export class FileService implements OnModuleInit {
|
|
|
338
410
|
const provider = await this.getProvider();
|
|
339
411
|
const providerPath = this.resolveProviderPath(file);
|
|
340
412
|
|
|
413
|
+
// O HEAD só serve para descobrir o tamanho quando a tabela não o tem. Feito
|
|
414
|
+
// sempre, dobrava as requisições ao storage em cada segmento HLS.
|
|
341
415
|
let metadata: any;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
416
|
+
if (file.size == null) {
|
|
417
|
+
try {
|
|
418
|
+
metadata = await provider.metaData(providerPath);
|
|
419
|
+
} catch {
|
|
420
|
+
metadata = undefined;
|
|
421
|
+
}
|
|
346
422
|
}
|
|
347
423
|
|
|
348
424
|
try {
|
|
@@ -353,7 +429,7 @@ export class FileService implements OnModuleInit {
|
|
|
353
429
|
size: file.size ?? this.getFileSizeFromMetadata(metadata),
|
|
354
430
|
};
|
|
355
431
|
} catch (error) {
|
|
356
|
-
this.
|
|
432
|
+
this.mapStorageError(error, fileId);
|
|
357
433
|
}
|
|
358
434
|
}
|
|
359
435
|
|
|
@@ -369,16 +445,21 @@ export class FileService implements OnModuleInit {
|
|
|
369
445
|
|
|
370
446
|
const provider = await this.getProvider();
|
|
371
447
|
const providerPath = this.resolveProviderPath(file);
|
|
372
|
-
const totalSize
|
|
448
|
+
const totalSize = Number(file.size ?? 0);
|
|
373
449
|
|
|
374
450
|
const resolvedStart = start ?? 0;
|
|
375
451
|
const resolvedEnd = end !== undefined ? end : totalSize > 0 ? totalSize - 1 : undefined;
|
|
376
452
|
const hasRange = start !== undefined;
|
|
377
453
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
454
|
+
let stream: Awaited<ReturnType<AbstractProvider['readStream']>>;
|
|
455
|
+
try {
|
|
456
|
+
stream = await provider.readStream(
|
|
457
|
+
providerPath,
|
|
458
|
+
hasRange ? { start: resolvedStart, end: resolvedEnd } : undefined,
|
|
459
|
+
);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
this.mapStorageError(error, fileId);
|
|
462
|
+
}
|
|
382
463
|
|
|
383
464
|
const contentLength =
|
|
384
465
|
resolvedEnd !== undefined ? resolvedEnd - resolvedStart + 1 : totalSize;
|
|
@@ -461,16 +542,7 @@ export class FileService implements OnModuleInit {
|
|
|
461
542
|
try {
|
|
462
543
|
providerBuffer = await provider.buffer(providerPath);
|
|
463
544
|
} catch (error: any) {
|
|
464
|
-
|
|
465
|
-
error?.name === 'NoSuchKey' ||
|
|
466
|
-
error?.Code === 'NoSuchKey' ||
|
|
467
|
-
error?.$metadata?.httpStatusCode === 404;
|
|
468
|
-
|
|
469
|
-
if (missingInStorage) {
|
|
470
|
-
throw new NotFoundException(`File not found in storage: ${fileId}`);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
throw error;
|
|
545
|
+
this.mapStorageError(error, fileId);
|
|
474
546
|
}
|
|
475
547
|
|
|
476
548
|
const buffer = Buffer.from(providerBuffer);
|
|
@@ -549,6 +621,10 @@ export class FileService implements OnModuleInit {
|
|
|
549
621
|
// Quem enviou. É o que permite recusar um `fileId` de outro usuário nas tools MCP
|
|
550
622
|
// (ver `assertFileUsable`); fica nulo nos caminhos internos sem usuário.
|
|
551
623
|
createdByUserId?: number | null;
|
|
624
|
+
// Grava a imagem byte a byte, sem a conversão para WebP nem o redimensionamento.
|
|
625
|
+
// Para quem entrega o arquivo a um consumidor externo que exige o formato
|
|
626
|
+
// original — o Instagram recusa WebP. O chamador já normalizou o arquivo.
|
|
627
|
+
skipImageOptimization?: boolean;
|
|
552
628
|
},
|
|
553
629
|
) {
|
|
554
630
|
|
|
@@ -585,12 +661,18 @@ export class FileService implements OnModuleInit {
|
|
|
585
661
|
fileBuffer.buffer,
|
|
586
662
|
);
|
|
587
663
|
|
|
588
|
-
const optimized =
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
664
|
+
const optimized = options?.skipImageOptimization
|
|
665
|
+
? {
|
|
666
|
+
buffer: fileBuffer.buffer,
|
|
667
|
+
mimetype: effectiveMimetype,
|
|
668
|
+
originalname: normalizedFilename,
|
|
669
|
+
}
|
|
670
|
+
: await this.imageOptimizationService.optimizeBuffer(
|
|
671
|
+
fileBuffer.buffer,
|
|
672
|
+
effectiveMimetype,
|
|
673
|
+
normalizedFilename,
|
|
674
|
+
{ maxWidth: resize?.maxWidth, maxHeight: resize?.maxHeight },
|
|
675
|
+
);
|
|
594
676
|
|
|
595
677
|
const normalizedFile = {
|
|
596
678
|
...fileBuffer,
|
|
@@ -635,6 +717,8 @@ export class FileService implements OnModuleInit {
|
|
|
635
717
|
createdByUserId?: number | null;
|
|
636
718
|
// Mesmo par maxWidth/maxHeight que `upload` aceita — só tem efeito em image/*.
|
|
637
719
|
resize?: { maxWidth?: number; maxHeight?: number };
|
|
720
|
+
// Mesma opção de `upload`: a imagem segue o caminho de streaming, intacta.
|
|
721
|
+
skipImageOptimization?: boolean;
|
|
638
722
|
},
|
|
639
723
|
) {
|
|
640
724
|
if (!filePath) {
|
|
@@ -684,8 +768,9 @@ export class FileService implements OnModuleInit {
|
|
|
684
768
|
// streaming direto do disco via provider.uploadFromPath. Isso evita carregar
|
|
685
769
|
// o arquivo inteiro na RAM (fs.readFile estoura o limite de Buffer / causa OOM)
|
|
686
770
|
// e habilita upload multipart no S3. A otimização de imagem só se aplica a
|
|
687
|
-
// image/*, então não há perda nesse caminho.
|
|
688
|
-
|
|
771
|
+
// image/*, então não há perda nesse caminho. `skipImageOptimization` manda a
|
|
772
|
+
// imagem pelo mesmo caminho: o chamador quer os bytes exatamente como estão.
|
|
773
|
+
if (!mimetype.startsWith('image/') || metadata?.skipImageOptimization) {
|
|
689
774
|
const url = await provider.uploadFromPath(destination, filePath, {
|
|
690
775
|
filename: originalname,
|
|
691
776
|
mimetype,
|
|
@@ -745,6 +830,292 @@ export class FileService implements OnModuleInit {
|
|
|
745
830
|
return file;
|
|
746
831
|
}
|
|
747
832
|
|
|
833
|
+
/**
|
|
834
|
+
* O storage ativo aceita upload direto em partes assinadas (S3 e Spaces)? Sem provider
|
|
835
|
+
* configurado, ou com um que não implementa, é `false` — quem oferece o caminho cai no
|
|
836
|
+
* upload pela API.
|
|
837
|
+
*/
|
|
838
|
+
async supportsDirectUpload(): Promise<boolean> {
|
|
839
|
+
try {
|
|
840
|
+
return supportsDirectUpload(await this.getProvider());
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private async getDirectUploadProvider() {
|
|
847
|
+
const provider = await this.getProvider();
|
|
848
|
+
if (!supportsDirectUpload(provider)) {
|
|
849
|
+
throw new ConflictException('The file storage does not support direct uploads');
|
|
850
|
+
}
|
|
851
|
+
return provider;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** `key` gravada pelo start: `${destination}/${uuid.ext}`, sem subpasta extra. */
|
|
855
|
+
private assertDirectUploadKey(key: string, destination: string): string {
|
|
856
|
+
const prefix = `${destination}/`;
|
|
857
|
+
const storedFilename = typeof key === 'string' && key.startsWith(prefix)
|
|
858
|
+
? key.slice(prefix.length)
|
|
859
|
+
: '';
|
|
860
|
+
|
|
861
|
+
if (!storedFilename || storedFilename.includes('/')) {
|
|
862
|
+
throw new BadRequestException('Invalid direct upload key');
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return storedFilename;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Abre um upload multipart direto no storage. A chave segue o mesmo formato que o
|
|
870
|
+
* `uploadFromPath` grava (`file.location` = destination, `file.path` = uuid.ext do
|
|
871
|
+
* `getFilename` do provider), então o arquivo final é servido como qualquer outro.
|
|
872
|
+
*
|
|
873
|
+
* Nada vai para o banco aqui: o estado entre as chamadas fica com quem chamou (token
|
|
874
|
+
* assinado, ver `signDirectUploadToken`) e a linha `file` só nasce no complete.
|
|
875
|
+
* Tamanho da parte com a regra do Studio: no mínimo 16 MiB e no máximo 9000 partes.
|
|
876
|
+
*/
|
|
877
|
+
async startDirectUpload(destination: string, info: DirectUploadFileInfo) {
|
|
878
|
+
if (!destination) {
|
|
879
|
+
throw new BadRequestException(`No destination provided`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const size = Number(info?.size);
|
|
883
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
884
|
+
throw new BadRequestException(`No size provided`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Acima disso o tamanho não cabe exato num number, e a linha não teria como ser lida.
|
|
888
|
+
if (size > FILE_SIZE_COLUMN_MAX) {
|
|
889
|
+
throw new BadRequestException(`File too large: ${size} bytes`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const mimetype = String(info?.mimetype ?? '').trim();
|
|
893
|
+
if (!info?.skipMimeValidation && !(await this.acceptMimetypes(mimetype))) {
|
|
894
|
+
throw new BadRequestException(`Invalid file type: ${mimetype}`);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!info?.skipSizeValidation && !(await this.maxFileSize(size))) {
|
|
898
|
+
throw new BadRequestException(`File too large: ${size} bytes`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const originalname = this.normalizeUploadedFilename(
|
|
902
|
+
String(info?.originalname ?? '').trim() || 'file',
|
|
903
|
+
);
|
|
904
|
+
const provider = await this.getDirectUploadProvider();
|
|
905
|
+
const key = `${destination}/${provider.getFilename(originalname)}`;
|
|
906
|
+
const uploadId = await provider.createMultipartUpload(key, mimetype || 'application/octet-stream');
|
|
907
|
+
|
|
908
|
+
const partSize = Math.max(
|
|
909
|
+
DIRECT_UPLOAD_MIN_PART_BYTES,
|
|
910
|
+
Math.ceil(size / DIRECT_UPLOAD_MAX_PARTS),
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
key,
|
|
915
|
+
uploadId,
|
|
916
|
+
partSize,
|
|
917
|
+
partCount: Math.max(1, Math.ceil(size / partSize)),
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** URLs assinadas de PUT para as partes pedidas (1-based, sem repetir). */
|
|
922
|
+
async signDirectUploadParts(
|
|
923
|
+
key: string,
|
|
924
|
+
uploadId: string,
|
|
925
|
+
partNumbers: number[],
|
|
926
|
+
ttlSeconds = DIRECT_UPLOAD_DEFAULT_TTL_SECONDS,
|
|
927
|
+
) {
|
|
928
|
+
if (!key || !uploadId) {
|
|
929
|
+
throw new BadRequestException('Invalid direct upload');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const numbers = Array.isArray(partNumbers) ? partNumbers.map(Number) : [];
|
|
933
|
+
if (
|
|
934
|
+
numbers.length === 0 ||
|
|
935
|
+
numbers.some((n) => !Number.isInteger(n) || n < 1 || n > DIRECT_UPLOAD_MAX_PARTS)
|
|
936
|
+
) {
|
|
937
|
+
throw new BadRequestException('Invalid part numbers');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const provider = await this.getDirectUploadProvider();
|
|
941
|
+
return provider.signUploadParts(
|
|
942
|
+
key,
|
|
943
|
+
uploadId,
|
|
944
|
+
[...new Set(numbers)],
|
|
945
|
+
Math.max(60, Math.floor(ttlSeconds)),
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** Aborta o multipart (as partes já enviadas somem do bucket). Upload já fechado não é erro. */
|
|
950
|
+
async abortDirectUpload(key: string, uploadId: string): Promise<void> {
|
|
951
|
+
if (!key || !uploadId) {
|
|
952
|
+
throw new BadRequestException('Invalid direct upload');
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const provider = await this.getDirectUploadProvider();
|
|
956
|
+
try {
|
|
957
|
+
await provider.abortMultipartUpload(key, uploadId);
|
|
958
|
+
} catch (error: any) {
|
|
959
|
+
const status = error?.$metadata?.httpStatusCode;
|
|
960
|
+
if (error?.name === 'NoSuchUpload' || error?.Code === 'NoSuchUpload' || status === 404) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Fecha o multipart e registra o arquivo. Confere o que o storage gravou antes de criar
|
|
969
|
+
* a linha — tamanho pelo HeadObject (tem que bater com o declarado no start), teto
|
|
970
|
+
* `storage-max-size`, allowlist `storage-accept-mimetype` e, quando pedido, os primeiros
|
|
971
|
+
* bytes (`verifyHead`) — e apaga o objeto quando algo não confere: um upload recusado
|
|
972
|
+
* não deixa blob órfão no bucket.
|
|
973
|
+
*/
|
|
974
|
+
async completeDirectUpload(input: CompleteDirectUploadInput) {
|
|
975
|
+
const { key, uploadId, destination } = input ?? ({} as CompleteDirectUploadInput);
|
|
976
|
+
|
|
977
|
+
if (!destination) {
|
|
978
|
+
throw new BadRequestException(`No destination provided`);
|
|
979
|
+
}
|
|
980
|
+
if (!uploadId) {
|
|
981
|
+
throw new BadRequestException('Invalid direct upload');
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const storedFilename = this.assertDirectUploadKey(key, destination);
|
|
985
|
+
|
|
986
|
+
const parts = Array.isArray(input.parts) ? input.parts : [];
|
|
987
|
+
if (
|
|
988
|
+
parts.length === 0 ||
|
|
989
|
+
parts.some(
|
|
990
|
+
(part) =>
|
|
991
|
+
!Number.isInteger(Number(part?.partNumber)) ||
|
|
992
|
+
Number(part.partNumber) < 1 ||
|
|
993
|
+
typeof part?.etag !== 'string' ||
|
|
994
|
+
!part.etag.trim(),
|
|
995
|
+
)
|
|
996
|
+
) {
|
|
997
|
+
throw new BadRequestException('Invalid upload parts');
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const provider = await this.getDirectUploadProvider();
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
await provider.completeMultipartUpload(
|
|
1004
|
+
key,
|
|
1005
|
+
uploadId,
|
|
1006
|
+
parts.map((part) => ({ partNumber: Number(part.partNumber), etag: part.etag.trim() })),
|
|
1007
|
+
);
|
|
1008
|
+
} catch (error: any) {
|
|
1009
|
+
const status = error?.$metadata?.httpStatusCode;
|
|
1010
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
1011
|
+
// Parte faltando, ETag errado, parte pequena demais: o multipart não tem conserto.
|
|
1012
|
+
await this.abortDirectUpload(key, uploadId).catch(() => undefined);
|
|
1013
|
+
throw new BadRequestException('The uploaded parts could not be assembled');
|
|
1014
|
+
}
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const discard = async () => {
|
|
1019
|
+
await provider.delete(key).catch((error) =>
|
|
1020
|
+
this.logger.warn(`Failed to delete rejected direct upload ${key}: ${error?.message}`),
|
|
1021
|
+
);
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
const declaredSize = Number(input.size);
|
|
1025
|
+
const storedSize = this.getFileSizeFromMetadata(await provider.metaData(key));
|
|
1026
|
+
|
|
1027
|
+
if (storedSize === undefined || storedSize !== declaredSize) {
|
|
1028
|
+
await discard();
|
|
1029
|
+
throw new BadRequestException(
|
|
1030
|
+
`Stored size (${storedSize ?? 'unknown'} bytes) differs from the declared size (${declaredSize} bytes)`,
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (
|
|
1035
|
+
storedSize > FILE_SIZE_COLUMN_MAX ||
|
|
1036
|
+
(!input.skipSizeValidation && !(await this.maxFileSize(storedSize)))
|
|
1037
|
+
) {
|
|
1038
|
+
await discard();
|
|
1039
|
+
throw new BadRequestException(`File too large: ${storedSize} bytes`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
let mimetype = String(input.mimetype ?? '').trim();
|
|
1043
|
+
|
|
1044
|
+
if (input.verifyHead) {
|
|
1045
|
+
try {
|
|
1046
|
+
const verified = await input.verifyHead.check(
|
|
1047
|
+
await provider.readHead(key, input.verifyHead.bytes),
|
|
1048
|
+
);
|
|
1049
|
+
if (typeof verified === 'string' && verified.trim()) {
|
|
1050
|
+
mimetype = verified.trim();
|
|
1051
|
+
}
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
await discard();
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (!input.skipMimeValidation && !(await this.acceptMimetypes(mimetype))) {
|
|
1059
|
+
await discard();
|
|
1060
|
+
throw new BadRequestException(`Invalid file type: ${mimetype}`);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
return await this.prismaService.file.create({
|
|
1065
|
+
data: {
|
|
1066
|
+
filename: this.normalizeUploadedFilename(
|
|
1067
|
+
String(input.originalname ?? '').trim() || storedFilename,
|
|
1068
|
+
),
|
|
1069
|
+
path: storedFilename,
|
|
1070
|
+
provider_id: this.providerId,
|
|
1071
|
+
location: destination,
|
|
1072
|
+
mimetype_id: await this.getMimeType(mimetype),
|
|
1073
|
+
size: storedSize,
|
|
1074
|
+
is_private: input.isPrivate ?? false,
|
|
1075
|
+
created_by_user_id: input.createdByUserId ?? null,
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
await discard();
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Token opaco que carrega o estado de um upload direto entre as chamadas (sem tabela):
|
|
1086
|
+
* JWT HS256 com o JWT_SECRET da API e o tipo `file-direct-upload`, como o token do
|
|
1087
|
+
* `file-upload-ticket`. Quem chama põe nas claims o que precisa amarrar (marca, usuário).
|
|
1088
|
+
*/
|
|
1089
|
+
signDirectUploadToken(
|
|
1090
|
+
claims: Record<string, unknown>,
|
|
1091
|
+
ttlSeconds = DIRECT_UPLOAD_DEFAULT_TTL_SECONDS,
|
|
1092
|
+
): string {
|
|
1093
|
+
return this.jwtService.sign(
|
|
1094
|
+
{ ...claims, t: DIRECT_UPLOAD_TOKEN_TYPE },
|
|
1095
|
+
{
|
|
1096
|
+
secret: process.env.JWT_SECRET,
|
|
1097
|
+
expiresIn: `${Math.max(1, Math.floor(ttlSeconds))}s`,
|
|
1098
|
+
},
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/** Claims do token de upload direto; assinatura inválida, expirado ou de outro tipo = 400. */
|
|
1103
|
+
readDirectUploadToken<T extends Record<string, unknown>>(token: string): T {
|
|
1104
|
+
let payload: any;
|
|
1105
|
+
|
|
1106
|
+
try {
|
|
1107
|
+
payload = this.jwtService.verify(String(token ?? ''), { secret: process.env.JWT_SECRET });
|
|
1108
|
+
} catch {
|
|
1109
|
+
throw new BadRequestException('Invalid or expired upload token');
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (payload?.t !== DIRECT_UPLOAD_TOKEN_TYPE) {
|
|
1113
|
+
throw new BadRequestException('Token is not a direct upload token');
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return payload as T;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
748
1119
|
/**
|
|
749
1120
|
* Mimetypes de imagem que `optimizeBuffer` devolve intactos por decisão de projeto
|
|
750
1121
|
* (vetor não vira bitmap, GIF animado não vira quadro único). Enfileirá-los para
|
|
@@ -843,7 +1214,7 @@ export class FileService implements OnModuleInit {
|
|
|
843
1214
|
}> {
|
|
844
1215
|
const { file, buffer } = await this.getBuffer(fileId);
|
|
845
1216
|
const currentMimetype = file.file_mimetype.name;
|
|
846
|
-
const oldSize = file.size ?? buffer.length;
|
|
1217
|
+
const oldSize = Number(file.size ?? buffer.length);
|
|
847
1218
|
|
|
848
1219
|
if (!currentMimetype.startsWith('image/')) {
|
|
849
1220
|
return { changed: false, oldSize, newSize: oldSize, mimetypeChanged: false };
|
|
@@ -1976,6 +2347,34 @@ export class FileService implements OnModuleInit {
|
|
|
1976
2347
|
});
|
|
1977
2348
|
}
|
|
1978
2349
|
|
|
2350
|
+
/**
|
|
2351
|
+
* Apaga um objeto de bucket avulso, com as credenciais de quem o configurou.
|
|
2352
|
+
*
|
|
2353
|
+
* Mesmo motivo do `headS3Object` e do `signS3Object`: o bucket e de outra
|
|
2354
|
+
* library (o Studio guarda os brutos no perfil de armazenamento dele), e nao o
|
|
2355
|
+
* destino ativo do core. O S3 responde 204 tambem para key inexistente, entao
|
|
2356
|
+
* apagar duas vezes nao e erro.
|
|
2357
|
+
*/
|
|
2358
|
+
async deleteS3Object(params: {
|
|
2359
|
+
accessKeyId: string;
|
|
2360
|
+
secretAccessKey: string;
|
|
2361
|
+
sessionToken?: string;
|
|
2362
|
+
region: string;
|
|
2363
|
+
bucket: string;
|
|
2364
|
+
key: string;
|
|
2365
|
+
}): Promise<void> {
|
|
2366
|
+
const s3 = new S3Client({
|
|
2367
|
+
region: params.region,
|
|
2368
|
+
credentials: {
|
|
2369
|
+
accessKeyId: params.accessKeyId,
|
|
2370
|
+
secretAccessKey: params.secretAccessKey,
|
|
2371
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
2372
|
+
},
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
await s3.send(new DeleteObjectCommand({ Bucket: params.bucket, Key: params.key }));
|
|
2376
|
+
}
|
|
2377
|
+
|
|
1979
2378
|
async headS3Bucket(params: {
|
|
1980
2379
|
accessKeyId: string;
|
|
1981
2380
|
secretAccessKey: string;
|
|
@@ -35,7 +35,7 @@ export const FILE_SOURCE_SCHEMA_PROPERTIES: Record<string, any> = {
|
|
|
35
35
|
uploadId: {
|
|
36
36
|
type: 'number',
|
|
37
37
|
description:
|
|
38
|
-
'ID returned by core.files.create-upload-url or core.files.upload.init
|
|
38
|
+
'ID returned by core.files.create-upload-url (after the PUT) or core.files.upload.init (after every chunk was sent with core.files.upload.append). The upload is completed automatically, so core.files.upload.complete is optional here.',
|
|
39
39
|
},
|
|
40
40
|
fileUrl: {
|
|
41
41
|
type: 'string',
|
|
@@ -45,7 +45,7 @@ export const FILE_SOURCE_SCHEMA_PROPERTIES: Record<string, any> = {
|
|
|
45
45
|
fileBase64: {
|
|
46
46
|
type: 'string',
|
|
47
47
|
description:
|
|
48
|
-
'Base64-encoded content, sent inline. Only for small files (about 256kb): base64 of anything larger overflows the model output limit. Requires fileName and mimeType.',
|
|
48
|
+
'Base64-encoded content, sent inline. Only for small files (about 256kb): base64 of anything larger overflows the model output limit; use core.files.upload.init for larger files instead of giving up. Requires fileName and mimeType.',
|
|
49
49
|
},
|
|
50
50
|
fileName: {
|
|
51
51
|
type: 'string',
|
|
@@ -72,9 +72,12 @@ export const FILE_SOURCE_SCHEMA_PROPERTIES: Record<string, any> = {
|
|
|
72
72
|
* `storage-max-size`, e é ele que aparece na mensagem de erro.
|
|
73
73
|
*/
|
|
74
74
|
export const FILE_SOURCE_DESCRIPTION = [
|
|
75
|
-
'Exactly one file source is required: fileId, uploadId, fileUrl or fileBase64.',
|
|
76
|
-
'
|
|
77
|
-
'
|
|
75
|
+
'Exactly one file source is required: fileId, uploadId, fileUrl or fileBase64. Pick the first that fits:',
|
|
76
|
+
'(1) file already in storage: fileId.',
|
|
77
|
+
'(2) file publicly reachable on the internet: fileUrl.',
|
|
78
|
+
'(3) client can make HTTP requests to the Hub API host (e.g. a local CLI agent): core.files.create-upload-url, PUT the bytes to the returned url, then pass the uploadId (or the fileId from core.files.upload.complete). Fastest.',
|
|
79
|
+
'(4) client runs in a network-restricted sandbox (e.g. claude.ai), or the PUT fails with host_not_allowed, 403 or a network error: core.files.upload.init, then core.files.upload.append for each chunk (index 0..totalChunks-1, chunkBase64 of at most chunkBytes raw bytes as returned by init), then pass the uploadId (or the fileId from core.files.upload.complete). Nothing leaves the MCP connection, and it works up to the storage max size; generate the chunks programmatically in the sandbox (split the file, print the base64 of each chunk) instead of typing base64 by hand.',
|
|
80
|
+
'(5) small file (about 256kb or less): fileBase64 inline.',
|
|
78
81
|
'Every path enforces the storage-accept-mimetype and storage-max-size settings (10mb by default).',
|
|
79
82
|
].join(' ');
|
|
80
83
|
|