funifier-mcp 0.2.25 → 0.2.27
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/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +5 -2
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/core/api-client.d.ts +21 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +154 -1
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/constants.d.ts +14 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +14 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/types/Folder.d.ts +16 -0
- package/dist/core/types/Folder.d.ts.map +1 -0
- package/dist/core/types/Folder.js +3 -0
- package/dist/core/types/Folder.js.map +1 -0
- package/dist/core/types/FolderContent.d.ts +10 -0
- package/dist/core/types/FolderContent.d.ts.map +1 -0
- package/dist/core/types/FolderContent.js +3 -0
- package/dist/core/types/FolderContent.js.map +1 -0
- package/dist/core/types/FolderContentType.d.ts +10 -0
- package/dist/core/types/FolderContentType.d.ts.map +1 -0
- package/dist/core/types/FolderContentType.js +3 -0
- package/dist/core/types/FolderContentType.js.map +1 -0
- package/dist/core/types/FolderLog.d.ts +11 -0
- package/dist/core/types/FolderLog.d.ts.map +1 -0
- package/dist/core/types/FolderLog.js +3 -0
- package/dist/core/types/FolderLog.js.map +1 -0
- package/dist/core/types/index.d.ts +4 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +4 -0
- package/dist/core/types/index.js.map +1 -1
- package/dist/mcp/bundle.js +121 -87
- package/dist/mcp/check-update.d.ts +2 -0
- package/dist/mcp/check-update.d.ts.map +1 -0
- package/dist/mcp/check-update.js +44 -0
- package/dist/mcp/check-update.js.map +1 -0
- package/dist/mcp/index.js +5 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/_char-guard.js +1 -1
- package/dist/mcp/tools/_char-guard.js.map +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
- package/dist/mcp/tools/_fetch-current.js +12 -0
- package/dist/mcp/tools/_fetch-current.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +33 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts +4 -0
- package/dist/mcp/tools/folder.d.ts.map +1 -0
- package/dist/mcp/tools/folder.js +68 -0
- package/dist/mcp/tools/folder.js.map +1 -0
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +5 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +26 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +192 -1
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/datasource-funifier-docs/.search-index.json +0 -17318
- package/datasource-funifier-docs/.skills-map.json +0 -73
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,155 +1,528 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `upload`
|
|
2
2
|
|
|
3
|
+
**Acesso Studio:** sem rota/página dedicada no backend — o módulo é consumido via API REST (ex.: seletores de imagem/arquivo do Studio chamam `/v3/upload`).
|
|
3
4
|
**API Endpoint:** `/v3/upload`
|
|
5
|
+
**Coleção MongoDB:** `upload` (schemaless — mapeada para `HashMap.class` em `Entity.UPLOAD`, sem entidade tipada)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
> Engenharia reversa baseada exclusivamente no código-fonte de `funifier-service` (commit `830037e`). Classe principal: `com.funifier.rest.v3.rest.UploadRest` (605 linhas). Os exemplos de resposta no Javadoc da própria classe estão **desatualizados** (vide seção 7) — esta documentação descreve o comportamento real do runtime.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Visão Geral
|
|
12
|
+
|
|
13
|
+
O módulo `upload` é o ponto de entrada para **persistência de arquivos binários** (imagens, documentos, qualquer mídia) em armazenamento de objetos em nuvem, mantendo um **registro de metadados** de cada arquivo na coleção MongoDB `upload`.
|
|
14
|
+
|
|
15
|
+
Papel arquitetural:
|
|
16
|
+
|
|
17
|
+
- **Dois artefatos por upload**: (1) o arquivo físico enviado ao provedor de nuvem (S3 / Azure Blob / Google Cloud Storage) e (2) um documento de metadados na coleção `upload`. Os dois são gravados em sequência dentro do mesmo handler; não há transação entre eles.
|
|
18
|
+
- **Abstração multi-cloud**: o provedor é escolhido em tempo de execução por `Configuration.getCloud()` (`"azure"`, `"google"`, ou qualquer outro valor → Amazon S3, que é o default). A escolha é **por servidor/configuração**, não por requisição.
|
|
19
|
+
- **Multi-tenant por apiKey**: todo arquivo é armazenado sob o prefixo `games/{apiKey}/{session}`. O `apiKey` identifica a gamificação dona do arquivo e também determina qual banco MongoDB recebe o registro (`FrontController.getInstance(apiKey)`).
|
|
20
|
+
- **Schemaless**: `Entity.UPLOAD` mapeia para `HashMap.class` — não existe POJO/entidade tipada. O documento gravado é exatamente o `Map` montado no handler. Não há validação de schema; qualquer campo dentro de `extra` é aceito e persistido verbatim.
|
|
21
|
+
|
|
22
|
+
Problemas que resolve:
|
|
23
|
+
|
|
24
|
+
- Upload de avatares, logos, capas, documentos e mídia genérica usada por outros módulos (player, catalog, challenge, etc.) através da URL pública retornada.
|
|
25
|
+
- Geração automática de **thumbnails** e **transformações de imagem** (resize / mudança de formato) no endpoint `/image`.
|
|
26
|
+
- Catálogo consultável dos arquivos enviados por gamificação (endpoint `GET /v3/upload`).
|
|
27
|
+
|
|
28
|
+
Relação com outros módulos:
|
|
29
|
+
|
|
30
|
+
- **`trigger`** — o endpoint `/file` dispara as triggers `before_create` e `after_create` da entidade `upload` (`Trigger.ENTITY_UPLOAD`). **O endpoint `/image` NÃO dispara triggers** (vide seção 6).
|
|
31
|
+
- **`player`** — a URL retornada é tipicamente copiada para `Player.image` (`{ small, medium, original }`). Não há referência forte (FK); é apenas cópia de string de URL.
|
|
32
|
+
- **AI (`AIRest`)** — o módulo de geração de imagens por IA (`AIRest.java:2234`) também grava na coleção `upload`: baixa a imagem da URL da OpenAI, reenvia via `S3Service.create(..., session="image")` e salva um documento `upload`. Esse caminho usa **S3 diretamente** (ignora `getCloud()`).
|
|
33
|
+
- **`static`** — módulo distinto (coleção `static`); usa o mesmo `S3Service`/`StaticManager` mas **não** compartilha a coleção `upload`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Arquitetura e Fluxos
|
|
38
|
+
|
|
39
|
+
### 2.1 Classes envolvidas
|
|
40
|
+
|
|
41
|
+
| Classe | Papel |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `com.funifier.rest.v3.rest.UploadRest` | Controller REST v3 (`/v3/upload`) — todos os 5 endpoints |
|
|
44
|
+
| `com.funifier.engine.aws.service.s3.S3Service` | Provedor **default** (Amazon S3) — `create` / `delete` / `deleteURL` |
|
|
45
|
+
| `com.funifier.engine.cloud.azure.AzureService` | Provedor Azure Blob Storage (quando `cloud="azure"`) |
|
|
46
|
+
| `com.funifier.engine.cloud.google.GoogleService` | Provedor Google Cloud Storage (quando `cloud="google"`) |
|
|
47
|
+
| `com.funifier.controller.Configuration` | `getCloud()` resolve o provedor ativo |
|
|
48
|
+
| `com.funifier.engine.guid.Guid` | `newShortGuid()` → `new org.bson.types.ObjectId().toString()` (hex de 24 chars) |
|
|
49
|
+
| `com.funifier.engine.integration.trigger.Trigger` | Constantes `ENTITY_UPLOAD`, `EVENT_BEFORE_CREATE`, `EVENT_AFTER_CREATE` |
|
|
50
|
+
| `com.funifier.rest.v3.AuthBean` | Resolução de credenciais (`getApiKey()`) |
|
|
51
|
+
| `net.coobird.thumbnailator.Thumbnails` | Biblioteca de resize/conversão de imagem (endpoint `/image`) |
|
|
52
|
+
|
|
53
|
+
Não há `Service`/`Repository`/`Manager`/`Scheduler` dedicado ao módulo `upload`. A persistência MongoDB é feita diretamente via Jongo: `manager.getJongoConnection().getCollection(Entity.UPLOAD.collection)`.
|
|
54
|
+
|
|
55
|
+
### 2.2 Pipeline principal — `POST /v3/upload/file` (`uploadFile`)
|
|
56
|
+
|
|
57
|
+
Sequência real de execução (linhas 106-214 de `UploadRest.java`):
|
|
58
|
+
|
|
59
|
+
1. **[Auth/Tenant]** `FrontController.getInstance(authBean.getApiKey()).getManagerFactory()` — resolve o tenant **pelo apiKey** (sem `.check()` de access_token; vide seção 8).
|
|
60
|
+
2. **[Validação]** lê a parte `extra` do multipart. Se ausente/vazia → `400 BAD_REQUEST` com `{"message":"Field extra is required..."}`.
|
|
61
|
+
3. **[Sanitização silenciosa]** `ex = ex.substring(0, ex.lastIndexOf("}")+1)` — **trunca tudo após a última `}`** antes de fazer parse JSON (vide seção 5).
|
|
62
|
+
4. **[Default]** `session = extra.session ?? "upload"`.
|
|
63
|
+
5. **[Loop por arquivo]** para cada `InputPart` da parte `file`:
|
|
64
|
+
1. `getFileName(header)` extrai o nome do `Content-Disposition` (fallback `"unknown"`).
|
|
65
|
+
2. lê os bytes via `IOUtils.toByteArray`.
|
|
66
|
+
3. monta o `Map upload` com `_id` (novo `ObjectId`), `extra`, `size`, `filename` (nome original), `content_type`, `time` (`new Date()`), e `bytes`.
|
|
67
|
+
4. **[Trigger BEFORE]** `triggerManager.execute(guid, upload, ENTITY_UPLOAD, EVENT_BEFORE_CREATE, ...)` — a trigger pode **mutar** `upload.bytes` e `upload.filename`; após ela o código relê `bytes`/`filename` e recalcula `size`.
|
|
68
|
+
5. **[Upload nuvem]** conforme `getCloud()`: `S3Service.create` | `AzureService.create` | `GoogleService.create` → retorna `{filename_unique, bucket, extension, url}`, mesclado via `upload.putAll(after)`.
|
|
69
|
+
6. **[Limpeza]** `upload.remove("bytes")` — os bytes **não** são persistidos no Mongo.
|
|
70
|
+
7. **[Persistência]** `getCollection("upload").save(upload)`.
|
|
71
|
+
8. **[Trigger AFTER]** `triggerManager.execute(..., EVENT_AFTER_CREATE, ...)`.
|
|
72
|
+
9. adiciona ao array `uploads`.
|
|
73
|
+
6. **[Resposta]** `201 CREATED` com `{"uploads":[...], "status":"OK"}`.
|
|
74
|
+
|
|
75
|
+
```mermaid
|
|
76
|
+
flowchart TD
|
|
77
|
+
A["POST /v3/upload/file"] --> B{"extra presente?"}
|
|
78
|
+
B -->|"não"| E400["400 Field extra is required"]
|
|
79
|
+
B -->|"sim"| C["parse extra (trunca após última }) + session ?? upload"]
|
|
80
|
+
C --> D["para cada file part"]
|
|
81
|
+
D --> F["lê bytes + filename + content_type"]
|
|
82
|
+
F --> G["monta Map: _id, extra, size, filename, content_type, time, bytes"]
|
|
83
|
+
G --> H["trigger before_create (pode mutar bytes/filename)"]
|
|
84
|
+
H --> I{"getCloud()"}
|
|
85
|
+
I -->|"azure"| J1["AzureService.create"]
|
|
86
|
+
I -->|"google"| J2["GoogleService.create"]
|
|
87
|
+
I -->|"default"| J3["S3Service.create"]
|
|
88
|
+
J1 --> K["putAll(filename_unique, bucket, extension, url)"]
|
|
89
|
+
J2 --> K
|
|
90
|
+
J3 --> K
|
|
91
|
+
K --> L["remove(bytes)"]
|
|
92
|
+
L --> M["save() na coleção upload"]
|
|
93
|
+
M --> N["trigger after_create"]
|
|
94
|
+
N --> D
|
|
95
|
+
D --> O["201 CREATED uploads + status OK"]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **Síncrono.** Todo o pipeline roda na thread da requisição. **Sem transação**: se o `save()` no Mongo falhar após o upload na nuvem, o arquivo físico fica órfão (e vice-versa). `IOException` no loop é capturada e apenas logada (`printStackTrace`) — a resposta ainda retorna `201` com os uploads que conseguiram completar.
|
|
99
|
+
|
|
100
|
+
### 2.3 Pipeline `POST /v3/upload/image` (`uploadImage`)
|
|
101
|
+
|
|
102
|
+
Difere de `/file` (linhas 250-420):
|
|
103
|
+
|
|
104
|
+
1. `session = extra.session ?? "image"` (default diferente).
|
|
105
|
+
2. lê `extra.thumbnails` (lista) e `extra.transform` (lista) — ambos opcionais.
|
|
106
|
+
3. **[Transform]** se `transform` tem itens, aplica em sequência sobre os bytes originais via Thumbnailator: estágio `"size"` (`{width,height}`) e estágio `"outputFormat"` (`{format}`). Outros estágios são ignorados.
|
|
107
|
+
4. grava o documento da **versão original** com `thumb="original"` (o arquivo físico é prefixado `original_` antes do guid da nuvem).
|
|
108
|
+
5. **[Thumbnails]** para cada item de `thumbnails` (`{name,width,height}`): redimensiona, faz upload (prefixo `{name}_`) e grava **um documento `upload` separado** com `thumb={name}`.
|
|
109
|
+
6. **NÃO dispara triggers.** **NÃO** adiciona/remove campo `bytes`.
|
|
110
|
+
7. resposta `201 CREATED` com `uploads` contendo o original **+ um item por thumbnail**.
|
|
111
|
+
|
|
112
|
+
```mermaid
|
|
113
|
+
flowchart TD
|
|
114
|
+
A["POST /v3/upload/image"] --> B{"extra presente?"}
|
|
115
|
+
B -->|"não"| E["400 Field extra is required"]
|
|
116
|
+
B -->|"sim"| C["session ?? image; lê thumbnails[] e transform[]"]
|
|
117
|
+
C --> D{"transform tem itens?"}
|
|
118
|
+
D -->|"sim"| T["aplica size / outputFormat (Thumbnailator)"]
|
|
119
|
+
D -->|"não"| U["bytes inalterados"]
|
|
120
|
+
T --> V["upload original (thumb=original, prefixo original_)"]
|
|
121
|
+
U --> V
|
|
122
|
+
V --> W["save() original"]
|
|
123
|
+
W --> X{"thumbnails tem itens?"}
|
|
124
|
+
X -->|"sim"| Y["para cada thumb: resize + upload (prefixo nome_) + save()"]
|
|
125
|
+
X -->|"não"| Z["fim do loop"]
|
|
126
|
+
Y --> Z
|
|
127
|
+
Z --> R["201 CREATED uploads[original + N thumbs]"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 2.4 Interação entre módulos (upload de arquivo)
|
|
131
|
+
|
|
132
|
+
```mermaid
|
|
133
|
+
sequenceDiagram
|
|
134
|
+
participant C as Cliente
|
|
135
|
+
participant R as UploadRest
|
|
136
|
+
participant T as TriggerManager
|
|
137
|
+
participant S as Cloud (S3/Azure/Google)
|
|
138
|
+
participant M as MongoDB (coleção upload)
|
|
139
|
+
C->>R: POST /v3/upload/file (multipart: file, extra)
|
|
140
|
+
R->>R: valida extra, monta Map
|
|
141
|
+
R->>T: execute(before_create) [só /file]
|
|
142
|
+
T-->>R: upload (bytes/filename possivelmente mutados)
|
|
143
|
+
R->>S: create(bytes, filename, apiKey, session)
|
|
144
|
+
S-->>R: {filename_unique, bucket, extension, url}
|
|
145
|
+
R->>M: save(upload sem bytes)
|
|
146
|
+
R->>T: execute(after_create) [só /file]
|
|
147
|
+
R-->>C: 201 {uploads, status}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2.5 Pipeline de exclusão e consulta
|
|
151
|
+
|
|
152
|
+
- **`DELETE /v3/upload/{id}`** (`delete`, linhas 437-466): `findOne({_id})` → `remove({_id})` → lê `filename_unique` e `bucket` → **se** `url` contém o `apiKey`, apaga o arquivo físico no provedor → **sempre** chama `playerManager.delete(id)` no final (efeito colateral, vide seção 5).
|
|
153
|
+
- **`DELETE /v3/upload`** (corpo JSON, `delete`, linhas 482-504): **se** `url` contém o `apiKey`, apaga o arquivo no provedor e remove **todos** os documentos com aquele `url` (`remove({url:#})`).
|
|
154
|
+
- **`GET /v3/upload`** (`findAll`, linhas 529-590): monta aggregation MongoDB com `$match` (injetando `q` cru), filtro de data em `time`, `$sort` e `$limit`. Vide seção 4.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 3. Estrutura dos Objetos
|
|
159
|
+
|
|
160
|
+
### 3.1 Documento `upload` — coleção `upload`
|
|
161
|
+
|
|
162
|
+
Não há entidade tipada. Os campos abaixo são os efetivamente gravados pelo código (variam por endpoint).
|
|
163
|
+
|
|
164
|
+
| Campo | Tipo | Padrão | Origem | Descrição |
|
|
165
|
+
|---|---|---|---|---|
|
|
166
|
+
| `_id` | String | auto | `Guid.newShortGuid()` | Hex de `ObjectId` (24 chars). Gerado no handler. **≠** do guid usado no `filename_unique`. |
|
|
167
|
+
| `extra` | Object | — (obrigatório) | request | JSON arbitrário enviado na parte `extra`. Persistido verbatim em **todos** os documentos (inclusive em cada thumbnail). |
|
|
168
|
+
| `size` | int | — | runtime | Tamanho em bytes após triggers/transformações. |
|
|
169
|
+
| `filename` | String | `"unknown"` | `Content-Disposition` | **Nome original** do arquivo (não o nome único na nuvem). |
|
|
170
|
+
| `content_type` | String | — | header `Content-Type` da parte | MIME do arquivo enviado. |
|
|
171
|
+
| `time` | Date | `new Date()` | runtime | Momento do upload. Serializado como epoch em millis no JSON. |
|
|
172
|
+
| `filename_unique` | String | — | provedor (`create`) | Nome único no storage. **S3/Azure:** `{guid}_{filename}`. **Google:** caminho completo `games/{apiKey}/{session}/{guid}_{filename}`. |
|
|
173
|
+
| `bucket` | String | — | provedor | **S3:** `{bucketRegion}/games/{apiKey}/{session}`. **Azure:** `games/{apiKey}/{session}`. **Google:** apenas o nome do bucket. |
|
|
174
|
+
| `extension` | String | — | provedor | `"." + filename.split("\\.")[1]` — **segundo** token (vide seção 5, bug de extensão). |
|
|
175
|
+
| `url` | String | — | provedor | URL pública direta do arquivo. |
|
|
176
|
+
| `thumb` | String | ausente em `/file` | runtime (`/image`) | `"original"` ou o `name` do thumbnail. **Só existe nos documentos de `/image`.** |
|
|
177
|
+
| `exception` | Object | ausente | provedor (em falha) | Quando o upload na nuvem falha, `create()` retorna `{exception:{...}}` e o documento é salvo **mesmo assim**, sem `url`/`bucket` (vide seção 5). |
|
|
8
178
|
|
|
9
|
-
|
|
179
|
+
### 3.2 Campos NÃO persistidos / computados
|
|
10
180
|
|
|
11
|
-
-
|
|
12
|
-
- Para enviar documentos para treinamentos
|
|
13
|
-
- Para adicionar recursos visuais a desafios e itens
|
|
181
|
+
- **`bytes`** (`byte[]`) — adicionado ao `Map` apenas no fluxo `/file` para passar pela trigger e pelo provedor; **removido com `upload.remove("bytes")` antes do `save()`**. Nunca chega ao Mongo. (No `/image` o campo `bytes` nunca é adicionado ao `Map`.)
|
|
14
182
|
|
|
15
|
-
|
|
183
|
+
### 3.3 Campos legados / desatualizados (apenas no Javadoc, NÃO no runtime)
|
|
16
184
|
|
|
17
|
-
|
|
18
|
-
**Método:** POST
|
|
19
|
-
**Endpoint:** `/v3/upload/image`
|
|
20
|
-
**Content-Type:** multipart/form-data
|
|
185
|
+
O Javadoc de `uploadFile`/`uploadImage` mostra campos que **o código não grava**:
|
|
21
186
|
|
|
22
|
-
**
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
187
|
+
- `filename_original` — **não existe** no runtime; o nome original fica em `filename`.
|
|
188
|
+
- `bucket: "funifier/games/..."` — formato **antigo**; o código atual usa `{bucketRegion}/games/...` (S3) — vide comentários `//OLD VERSION` / `//NEW VERSION : COM BUCKET` em `S3Service`.
|
|
189
|
+
- O bloco comentado de thumbnail em `uploadFile` (linhas 157-164) é **código morto** (geração de thumb 160×160 no `/file`), desabilitado. O `/file` não gera thumbnails.
|
|
25
190
|
|
|
26
|
-
|
|
27
|
-
- `Authorization: {token}`
|
|
28
|
-
- `Content-Type: undefined` (gerenciado pelo FormData)
|
|
191
|
+
### 3.4 Estrutura de `extra` no endpoint `/image`
|
|
29
192
|
|
|
30
|
-
|
|
193
|
+
`extra` pode conter, além de `session`:
|
|
194
|
+
|
|
195
|
+
| Campo de `extra` | Tipo | Significado |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `thumbnails` | `[{name, width, height}]` | Cada item gera um documento/arquivo adicional redimensionado. `name` vira o valor de `thumb`. |
|
|
198
|
+
| `transform` | `[{stage, ...}]` | Pipeline aplicado **à imagem original**. Estágios reconhecidos: `{"stage":"size","width":W,"height":H}` e `{"stage":"outputFormat","format":"png"}`. Demais estágios são ignorados. |
|
|
199
|
+
|
|
200
|
+
> `width`/`height` são lidos com cast direto `(int) map.get("width")` — exige inteiro JSON. Valor fracionário (ex.: `160.0`) gera `ClassCastException` → `500` (vide seção 5).
|
|
201
|
+
|
|
202
|
+
### 3.5 Enum de provedor (`Configuration.getCloud()`)
|
|
203
|
+
|
|
204
|
+
| Valor de `cloud` | Provedor | Observação |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| `"azure"` | `AzureService` | — |
|
|
207
|
+
| `"google"` | `GoogleService` | — |
|
|
208
|
+
| qualquer outro / vazio | `S3Service` (Amazon) | `getCloud()` retorna `"amazon"` como default quando vazio. |
|
|
209
|
+
|
|
210
|
+
### 3.6 Ciclo de vida
|
|
211
|
+
|
|
212
|
+
O documento `upload` **não possui campo de status/estado**; é criado e (eventualmente) removido. Não há máquina de estados — por isso nenhum `stateDiagram` é aplicável. As únicas variações de "tipo" de documento são via `thumb` (`original` vs. nome do thumbnail) no fluxo `/image`.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 4. Endpoints
|
|
217
|
+
|
|
218
|
+
> **Atenção:** o `@Produces` no nível dos métodos de upload é `text/plain`, embora o corpo seja JSON. Os métodos de consulta/exclusão usam `application/json`.
|
|
219
|
+
|
|
220
|
+
### 4.1 `POST /v3/upload/file`
|
|
221
|
+
|
|
222
|
+
| Aspecto | Detalhe |
|
|
223
|
+
|---|---|
|
|
224
|
+
| Finalidade | Upload de **qualquer tipo de arquivo** (1 ou N arquivos). |
|
|
225
|
+
| Autenticação | Header `Authorization` (Bearer/Basic/Studio/Account) ou `X-Api-Key` / `?api_key` — resolve apiKey. |
|
|
226
|
+
| Content-Type | `multipart/form-data` |
|
|
227
|
+
| Dispara triggers | **Sim** (`before_create` + `after_create`). |
|
|
228
|
+
| Status sucesso | `201 CREATED` |
|
|
229
|
+
|
|
230
|
+
**Partes do multipart:**
|
|
231
|
+
|
|
232
|
+
| Parte | Obrigatória | Descrição |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `file` | Sim | Conteúdo binário. Pode repetir para múltiplos arquivos. |
|
|
235
|
+
| `extra` | **Sim** | String JSON. Ausência → `400`. `session` define o subdiretório (default `"upload"`). |
|
|
236
|
+
|
|
237
|
+
**Comportamento real:** parse de `extra` trunca tudo após a última `}`. Falha de upload na nuvem **não** retorna erro HTTP — grava documento com `exception`. `IOException` no loop é silenciada.
|
|
238
|
+
|
|
239
|
+
**Exemplo (S3 / default), `extra = {"session":"images","name":"arquivo"}`, arquivo `funifier.png`:**
|
|
31
240
|
|
|
32
|
-
**Exemplo de Resposta:**
|
|
33
241
|
```json
|
|
34
242
|
{
|
|
35
243
|
"uploads": [
|
|
36
244
|
{
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"url": "https://s3.amazonaws.com/funifier/games/{apiKey}/images/{id}_original_profile-pic.jpg"
|
|
245
|
+
"_id": "5926104c8517667bbdf63c33",
|
|
246
|
+
"extra": { "session": "images", "name": "arquivo" },
|
|
247
|
+
"size": 3622,
|
|
248
|
+
"filename": "funifier.png",
|
|
249
|
+
"content_type": "image/png",
|
|
250
|
+
"time": 1495666764893,
|
|
251
|
+
"filename_unique": "5926104c8517667bbdf63c32_funifier.png",
|
|
252
|
+
"bucket": "funifier-na1/games/57bde3635e1c92c50b3ce6ff/images",
|
|
253
|
+
"extension": ".png",
|
|
254
|
+
"url": "https://s3.amazonaws.com/funifier-na1/games/57bde3635e1c92c50b3ce6ff/images/5926104c8517667bbdf63c32_funifier.png"
|
|
48
255
|
}
|
|
49
256
|
],
|
|
50
257
|
"status": "OK"
|
|
51
258
|
}
|
|
52
259
|
```
|
|
53
260
|
|
|
54
|
-
###
|
|
55
|
-
**Método:** DELETE
|
|
56
|
-
**Endpoint:** `/v3/upload/{id}`
|
|
261
|
+
### 4.2 `POST /v3/upload/image`
|
|
57
262
|
|
|
58
|
-
|
|
263
|
+
| Aspecto | Detalhe |
|
|
264
|
+
|---|---|
|
|
265
|
+
| Finalidade | Upload de imagem com transformação e geração de thumbnails. |
|
|
266
|
+
| Autenticação | igual a 4.1 |
|
|
267
|
+
| Content-Type | `multipart/form-data` |
|
|
268
|
+
| Dispara triggers | **Não** |
|
|
269
|
+
| Status sucesso | `201 CREATED` |
|
|
59
270
|
|
|
60
|
-
**
|
|
61
|
-
- `Authorization: {token}`
|
|
271
|
+
**Comportamento real:** `session` default `"image"`. Gera **N+1 documentos** (1 original + N thumbnails). O campo `filename` permanece o nome original; o nome físico recebe prefixo `original_`/`{thumbName}_` mais o guid do provedor.
|
|
62
272
|
|
|
63
|
-
**
|
|
64
|
-
|
|
65
|
-
|
|
273
|
+
**Request (cURL):**
|
|
274
|
+
```bash
|
|
275
|
+
curl -X POST "{server}/v3/upload/image" \
|
|
276
|
+
-H "Authorization: Bearer {token}" \
|
|
277
|
+
-F 'file=@./funifier.png;type=image/png' \
|
|
278
|
+
-F 'extra={"session":"images","transform":[{"stage":"size","width":640,"height":640},{"stage":"outputFormat","format":"png"}],"thumbnails":[{"name":"small","width":160,"height":160},{"name":"medium","width":260,"height":260}]}'
|
|
66
279
|
```
|
|
67
|
-
|
|
68
|
-
|
|
280
|
+
|
|
281
|
+
**Response (resumido):**
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"uploads": [
|
|
285
|
+
{ "_id": "...", "thumb": "original", "filename": "funifier.png", "filename_unique": "..._original_funifier.png", "url": "...", "extension": ".png", "size": 50123 },
|
|
286
|
+
{ "_id": "...", "thumb": "small", "filename": "funifier.png", "filename_unique": "..._small_funifier.png", "url": "...", "size": 5559 },
|
|
287
|
+
{ "_id": "...", "thumb": "medium", "filename": "funifier.png", "filename_unique": "..._medium_funifier.png", "url": "...", "size": 9871 }
|
|
288
|
+
],
|
|
289
|
+
"status": "OK"
|
|
290
|
+
}
|
|
69
291
|
```
|
|
70
292
|
|
|
71
|
-
|
|
293
|
+
### 4.3 `DELETE /v3/upload/{id}`
|
|
294
|
+
|
|
295
|
+
| Aspecto | Detalhe |
|
|
296
|
+
|---|---|
|
|
297
|
+
| Finalidade | Remover registro por `_id` e (condicionalmente) o arquivo físico. |
|
|
298
|
+
| Path param | `id` — `_id` do documento `upload`. |
|
|
299
|
+
| Status sucesso | `204 NO_CONTENT` |
|
|
300
|
+
|
|
301
|
+
**Comportamento real:**
|
|
302
|
+
- O documento é **sempre** removido do Mongo se existir (`remove({_id})`), **antes** de qualquer checagem de propriedade.
|
|
303
|
+
- O arquivo físico só é apagado **se** `url != null && url.length > 10 && url.contains(apiKey)`.
|
|
304
|
+
- **Efeito colateral:** ao final, `playerManager.delete(id)` é chamado **incondicionalmente** (vide seção 5).
|
|
305
|
+
- Sempre retorna `204`, mesmo se o `id` não existir.
|
|
306
|
+
|
|
307
|
+
### 4.4 `DELETE /v3/upload` (corpo JSON)
|
|
308
|
+
|
|
309
|
+
| Aspecto | Detalhe |
|
|
310
|
+
|---|---|
|
|
311
|
+
| Finalidade | Remover arquivo e registros por `url`. |
|
|
312
|
+
| Content-Type | `application/json` |
|
|
313
|
+
| Body | `{ "url": "https://.../arquivo.ext" }` |
|
|
314
|
+
| Status sucesso | `204 NO_CONTENT` |
|
|
315
|
+
|
|
316
|
+
**Comportamento real:** só age **se** `url.contains(apiKey)`. Apaga o arquivo no provedor e remove **todos** os documentos com aquele `url` exato. Se a URL não contém o apiKey, **nada acontece** (e ainda assim retorna `204`).
|
|
317
|
+
|
|
318
|
+
### 4.5 `GET /v3/upload`
|
|
319
|
+
|
|
320
|
+
| Aspecto | Detalhe |
|
|
321
|
+
|---|---|
|
|
322
|
+
| Finalidade | Listar/consultar documentos `upload` do tenant. |
|
|
323
|
+
| Status sucesso | `200 OK` |
|
|
324
|
+
|
|
325
|
+
**Query params:**
|
|
326
|
+
|
|
327
|
+
| Param | Tipo | Default | Descrição |
|
|
328
|
+
|---|---|---|---|
|
|
329
|
+
| `q` | String (fragmento Mongo) | — | **Injetado cru** no `$match` (vide seção 8). Ex.: `extra.session:"images"`. |
|
|
330
|
+
| `published_min` | String | — | Limite inferior em `time` (RFC 3339 ou keyword `-1d`, `-30m`, etc.). Inclusivo (`$gte`). |
|
|
331
|
+
| `published_max` | String | — | Limite superior em `time`. (`$lte`). |
|
|
332
|
+
| `orderby` | String | — | Campo de ordenação. **Injetado cru** no `$sort`. |
|
|
333
|
+
| `reverse` | boolean | `false` | `true` → ordem decrescente (`-1`). |
|
|
334
|
+
| `max_results` | int | `100` | `$limit`. Valores `<= 0` são forçados para `100`. |
|
|
335
|
+
|
|
336
|
+
**Pipeline gerado (real):**
|
|
337
|
+
```
|
|
338
|
+
{ $match : { _id: {$exists:true} [, <q>] [, time: {$gte:#, $lte:#}] } }
|
|
339
|
+
[ { $sort : { <orderby> : 1|-1 } } ]
|
|
340
|
+
[ { $limit : <max_results> } ]
|
|
341
|
+
```
|
|
342
|
+
Campos nulos são removidos da resposta (`JsonUtil.toJsonRemoveNullFields`).
|
|
72
343
|
|
|
73
344
|
---
|
|
74
345
|
|
|
75
|
-
|
|
76
|
-
**Método:** DELETE
|
|
77
|
-
**Endpoint:** `/v3/upload`
|
|
78
|
-
**Content-Type:** application/json
|
|
346
|
+
## 5. Regras de Negócio (no código, fora do schema)
|
|
79
347
|
|
|
80
|
-
|
|
348
|
+
- **`extra` é obrigatório** em `/file` e `/image`. Ausência → `400`.
|
|
349
|
+
- **Truncamento silencioso de `extra`**: `ex.substring(0, ex.lastIndexOf("}")+1)` descarta qualquer conteúdo após a última `}`. Se a string não contém `}`, `lastIndexOf` retorna `-1` → `substring(0,0)` → string vazia → parse vazio. Conteúdo extra (ex.: múltiplos objetos concatenados) é silenciosamente cortado.
|
|
350
|
+
- **`session` define o caminho** de armazenamento (`games/{apiKey}/{session}`). Defaults divergentes: `"upload"` (`/file`) vs `"image"` (`/image`).
|
|
351
|
+
- **Bug de extensão**: `"." + filename.split("\\.")[1]` usa o **segundo** token, não o último. `arquivo.tar.gz` → `extension = ".tar"`. **Arquivo sem ponto** → `ArrayIndexOutOfBoundsException` não tratada → `500`.
|
|
352
|
+
- **Falha de upload é silenciosa**: em erro de S3/Azure/Google, `create()` retorna `{exception:{...}}` (sem `url`), e o handler **mescla e salva o documento mesmo assim**, retornando `201`. Resultado: documento `upload` sem `url` e com campo `exception`.
|
|
353
|
+
- **Sem transação / sem rollback**: arquivo na nuvem e documento Mongo são independentes. Falha parcial deixa órfãos.
|
|
354
|
+
- **Propriedade por URL**: exclusão física só ocorre quando `url.contains(apiKey)`. Como a URL contém `games/{apiKey}/`, isso normalmente é verdadeiro para arquivos próprios; URLs de outras gamificações não são apagadas.
|
|
355
|
+
- **Efeito colateral em `DELETE /{id}`**: `playerManager.delete(id)` é chamado com o `_id` do upload. Como `_id` de upload é um `ObjectId` aleatório, normalmente **não** corresponde a nenhum player (no-op), mas é uma chamada não óbvia e potencialmente destrutiva se houver colisão de id. Comportamento legado.
|
|
356
|
+
- **`findAll` sem escopo de usuário/tenant interno**: a query roda no banco da própria gamificação (resolvido por apiKey), retornando **todos** os uploads daquela gamificação — não há filtro por usuário, sessão ou propriedade além do que vier em `q`.
|
|
357
|
+
- **`extra` redundante**: o objeto `extra` inteiro (inclusive `thumbnails`/`transform`) é persistido em cada documento gerado, incluindo cada thumbnail.
|
|
81
358
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 6. Comportamentos Automáticos
|
|
362
|
+
|
|
363
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
364
|
+
|---|---|---|---|
|
|
365
|
+
| Geração de `_id` | toda criação | `ObjectId` hex novo por documento | Sim (`_id`) |
|
|
366
|
+
| Trigger `before_create` (entidade `upload`) | **só** `POST /file` | Pode mutar `bytes` e `filename` antes do upload | Indireta (afeta arquivo/`size`) |
|
|
367
|
+
| Trigger `after_create` (entidade `upload`) | **só** `POST /file` | Pós-processamento (integrações, logs) | Conforme a trigger |
|
|
368
|
+
| Upload na nuvem | toda criação | Cria arquivo físico **público para leitura** | S3/Azure/Google |
|
|
369
|
+
| Transformação de imagem | `/image` com `transform` | Resize / mudança de formato da original | Sim (arquivo transformado) |
|
|
370
|
+
| Geração de thumbnails | `/image` com `thumbnails` | 1 arquivo + 1 documento por thumbnail | Sim (N documentos) |
|
|
371
|
+
| Índice `extra.session` | startup (`ConnectionPool`) | Cria índice na coleção `upload` | Sim (índice MongoDB) |
|
|
372
|
+
| Remoção de player | `DELETE /{id}` | `playerManager.delete(id)` incondicional | Sim (coleção `player`, se colidir) |
|
|
373
|
+
|
|
374
|
+
```mermaid
|
|
375
|
+
flowchart LR
|
|
376
|
+
subgraph file["POST /file"]
|
|
377
|
+
A1["before_create"] --> A2["upload nuvem"] --> A3["save()"] --> A4["after_create"]
|
|
378
|
+
end
|
|
379
|
+
subgraph image["POST /image"]
|
|
380
|
+
B1["transform (opcional)"] --> B2["upload original + save()"] --> B3["thumbnails: resize + upload + save() (N)"]
|
|
381
|
+
end
|
|
85
382
|
```
|
|
86
383
|
|
|
87
|
-
**
|
|
384
|
+
> **Diferença crítica:** triggers de `upload` (`before_create`/`after_create`) **só** existem no `/file`. Qualquer automação dependente dessas triggers **não** roda para imagens enviadas via `/image`.
|
|
88
385
|
|
|
89
386
|
---
|
|
90
387
|
|
|
388
|
+
## 7. Suportado vs NÃO Suportado
|
|
91
389
|
|
|
92
|
-
###
|
|
390
|
+
### ✅ Suportado
|
|
93
391
|
|
|
94
|
-
|
|
392
|
+
- Upload de arquivo genérico (1 ou N) via `POST /v3/upload/file`, com triggers.
|
|
393
|
+
- Upload de imagem com `transform` (`size`, `outputFormat`) e `thumbnails` via `POST /v3/upload/image`.
|
|
394
|
+
- Armazenamento multi-cloud (S3 default, Azure, Google) selecionado por `Configuration.getCloud()`.
|
|
395
|
+
- Exclusão por `_id` (`DELETE /v3/upload/{id}`) e por `url` (`DELETE /v3/upload`).
|
|
396
|
+
- Consulta com `q` (Mongo), filtro de data (`published_min`/`published_max`), `orderby`, `reverse`, `max_results`.
|
|
397
|
+
- URLs públicas de leitura para download direto.
|
|
398
|
+
- Índice em `extra.session`.
|
|
95
399
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
400
|
+
### ❌ NÃO Suportado / Pegadinhas
|
|
401
|
+
|
|
402
|
+
- **`/image` não dispara triggers** (`before_create`/`after_create`) — apenas `/file`.
|
|
403
|
+
- **`filename_original`** — campo do Javadoc que o runtime **não grava**. Use `filename`.
|
|
404
|
+
- **Bloco de thumbnail no `/file`** (linhas 157-164) — **código morto** comentado; `/file` não gera thumbnails.
|
|
405
|
+
- **Validação de schema** — inexistente; coleção é `HashMap` schemaless.
|
|
406
|
+
- **Atualização (PUT/PATCH)** de um upload — **não há** endpoint de update; só create/delete/query.
|
|
407
|
+
- **Endpoint `DELETE /v3/upload/image`** mencionado em docs antigas — **não existe** no código. A exclusão por URL é `DELETE /v3/upload` (sem sufixo `/image`).
|
|
408
|
+
- **Rollback transacional** entre arquivo físico e documento Mongo — inexistente.
|
|
409
|
+
- **Filtro automático por usuário/propriedade** em `GET /v3/upload` — inexistente.
|
|
410
|
+
- **Tratamento de erro de upload** — falha de nuvem não vira erro HTTP; vira documento com `exception`.
|
|
411
|
+
- **Arquivos sem extensão** — quebram (`500`) por `filename.split("\\.")[1]`.
|
|
412
|
+
- **Acesso privado / ACL restrita** — arquivos S3/Google são gravados com leitura pública para `AllUsers`; não há opção de upload privado neste fluxo.
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## 8. Segurança e Permissões
|
|
417
|
+
|
|
418
|
+
- **Autenticação por apiKey, sem validação de access_token no handler.** Os handlers chamam `FrontController.getInstance(authBean.getApiKey()).getManagerFactory()` diretamente — **não** chamam `authBean.getManagerFactory()` (que executaria `.check(accessToken)`) nem verificam escopo/permissão. A resolução do tenant depende apenas de extrair o apiKey das credenciais (`AuthBean.getApiKey()`: Bearer/Basic/Studio/Account/`X-Api-Key`/`?api_key`). Se existir validação de token, ela depende de filtro global de request **não presente nesta classe**. Documentado como observado, sem afirmar exploração.
|
|
419
|
+
- **Isolamento multi-tenant** é feito por **banco-por-gamificação** (cada apiKey → conexão MongoDB própria via `FrontController`) e por prefixo de storage `games/{apiKey}/{session}`. Não há campo de tenant filtrado nas queries.
|
|
420
|
+
- **Arquivos públicos por padrão:**
|
|
421
|
+
- S3: `acl.grantPermission(GroupGrantee.AllUsers, Permission.Read)` em todo `putObject`.
|
|
422
|
+
- Google: `Acl.of(Acl.User.ofAllUsers(), Acl.Role.READER)`.
|
|
423
|
+
- Azure: o container `funifier` é criado sem acesso público explícito (a linha `BlobContainerPublicAccessType.BLOB` está **comentada**) — a visibilidade depende da configuração default do container.
|
|
424
|
+
- **Superfície de injeção (`GET /v3/upload`):** os parâmetros `q` e `orderby` são **concatenados crus** na string da aggregation (`query.append(", " + q)` e `"{$sort : {" + orderby + " : #}}"`). Isso permite manipular o `$match`/`$sort` com operadores MongoDB arbitrários (NoSQL injection dentro do escopo do banco do próprio tenant). Não há sanitização. O escopo de dano é limitado ao banco da gamificação autenticada, mas qualquer campo/operador pode ser injetado.
|
|
425
|
+
- **Exclusão guardada por substring de URL:** `url.contains(apiKey)` é uma checagem fraca de propriedade — baseada em substring da URL, não em verificação de ACL/dono real.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## 9. Observabilidade e Troubleshooting
|
|
430
|
+
|
|
431
|
+
**Diagnóstico — o módulo está funcionando?**
|
|
432
|
+
- Confirme o provedor ativo: valor de `cloud` em `Configuration` (`getCloud()` → `amazon`/`azure`/`google`). Logs de startup imprimem `=== FUNIFIER_CLOUD_PROVIDER [...] ===`.
|
|
433
|
+
- Faça um upload de teste e verifique se o `url` retornado é acessível publicamente (GET na URL).
|
|
434
|
+
|
|
435
|
+
**Queries úteis (banco da gamificação):**
|
|
436
|
+
```
|
|
437
|
+
GET /v3/upload?q=extra.session:"images"&orderby=time&reverse=true&max_results=10
|
|
438
|
+
GET /v3/upload?published_min=-1d&orderby=time&reverse=true
|
|
439
|
+
GET /v3/upload/<id> # (via q, pois não há rota GET /{id}: use q=_id:"<id>")
|
|
106
440
|
```
|
|
441
|
+
Índice disponível: **apenas `extra.session`** (`ConnectionPool.java:164`). **`time` NÃO é indexado**, embora `findAll` filtre e ordene por ele — consultas por data em coleções grandes podem ser lentas (collection scan).
|
|
442
|
+
|
|
443
|
+
**Erros comuns e causas:**
|
|
107
444
|
|
|
108
|
-
|
|
445
|
+
| Sintoma | Causa provável |
|
|
446
|
+
|---|---|
|
|
447
|
+
| `400 Field extra is required` | parte `extra` ausente ou vazia no multipart. |
|
|
448
|
+
| Documento salvo com campo `exception` e sem `url` | falha no provedor de nuvem (credenciais, região, rede). Upload "passou" (201) mas o arquivo não existe. |
|
|
449
|
+
| `500` ao enviar arquivo | nome de arquivo sem ponto/extensão (`split("\\.")[1]` estoura) ou `width`/`height` fracionário em `transform`/`thumbnails`. |
|
|
450
|
+
| `extension` errada (ex.: `.tar`) | nome com múltiplos pontos — usa o **segundo** token. |
|
|
451
|
+
| Triggers de upload não rodaram | upload foi via `/image` (não dispara triggers) e não via `/file`. |
|
|
452
|
+
| Arquivo não foi apagado no `DELETE` | `url` não contém o `apiKey`, ou `url` ausente/curta no documento. |
|
|
453
|
+
| Player sumiu após deletar upload | efeito colateral `playerManager.delete(id)` (colisão de id). |
|
|
109
454
|
|
|
110
|
-
|
|
111
|
-
**Método:** POST
|
|
112
|
-
**Endpoint:** `/v3/upload/file`
|
|
113
|
-
**Content-Type:** multipart/form-data
|
|
455
|
+
**O que verificar quando algo não funciona:** credenciais do provedor; valor de `cloud`; se a `url` foi realmente preenchida no documento (indica sucesso do upload físico); se o `_id` consultado existe; se `q` está sintaticamente válido como fragmento Mongo.
|
|
114
456
|
|
|
115
|
-
|
|
457
|
+
---
|
|
116
458
|
|
|
117
|
-
|
|
118
|
-
- `file`: arquivo binário (pode repetir para vários)
|
|
119
|
-
- `extra`: string JSON obrigatória (ex: `{"session": "docs"}`)
|
|
459
|
+
## 10. Exemplos Práticos
|
|
120
460
|
|
|
121
|
-
|
|
461
|
+
### 10.1 Mínimo funcional (arquivo genérico)
|
|
122
462
|
```bash
|
|
123
463
|
curl -X POST "{server}/v3/upload/file" \
|
|
124
464
|
-H "Authorization: Bearer {token}" \
|
|
125
465
|
-F 'file=@./documento.pdf;type=application/pdf' \
|
|
126
|
-
-F 'extra={"session":"docs"
|
|
466
|
+
-F 'extra={"session":"docs"}'
|
|
127
467
|
```
|
|
128
468
|
|
|
129
|
-
###
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
469
|
+
### 10.2 Avançado (imagem com transform + thumbnails)
|
|
470
|
+
```bash
|
|
471
|
+
curl -X POST "{server}/v3/upload/image" \
|
|
472
|
+
-H "Authorization: Bearer {token}" \
|
|
473
|
+
-F 'file=@./avatar.png;type=image/png' \
|
|
474
|
+
-F 'extra={
|
|
475
|
+
"session":"avatars",
|
|
476
|
+
"transform":[
|
|
477
|
+
{"stage":"size","width":640,"height":640},
|
|
478
|
+
{"stage":"outputFormat","format":"png"}
|
|
479
|
+
],
|
|
480
|
+
"thumbnails":[
|
|
481
|
+
{"name":"small","width":160,"height":160},
|
|
482
|
+
{"name":"medium","width":260,"height":260}
|
|
483
|
+
]
|
|
484
|
+
}'
|
|
485
|
+
```
|
|
486
|
+
Resultado: 3 documentos (`thumb`: `original`, `small`, `medium`), cada um com `url` próprio. Para usar como avatar do player:
|
|
134
487
|
```json
|
|
135
|
-
|
|
488
|
+
POST /v3/player
|
|
489
|
+
{
|
|
490
|
+
"_id": "player_id",
|
|
491
|
+
"image": {
|
|
492
|
+
"small": { "url": "URL_DO_THUMB_small" },
|
|
493
|
+
"medium": { "url": "URL_DO_THUMB_medium" },
|
|
494
|
+
"original": { "url": "URL_DO_ORIGINAL" }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
136
497
|
```
|
|
137
498
|
|
|
138
|
-
###
|
|
139
|
-
|
|
140
|
-
|
|
499
|
+
### 10.3 Anti-pattern (o que NÃO fazer)
|
|
500
|
+
```bash
|
|
501
|
+
# RUIM: nome de arquivo sem extensão → 500 (ArrayIndexOutOfBounds em split("\\.")[1])
|
|
502
|
+
curl ... -F 'file=@./arquivo_sem_extensao'
|
|
503
|
+
|
|
504
|
+
# RUIM: width fracionário → ClassCastException → 500
|
|
505
|
+
-F 'extra={"transform":[{"stage":"size","width":640.0,"height":640.0}]}'
|
|
141
506
|
|
|
142
|
-
|
|
507
|
+
# RUIM: assumir que o upload falhou retorna erro HTTP.
|
|
508
|
+
# Se a nuvem falhar, a resposta ainda é 201 e o documento é salvo com "exception" e sem "url".
|
|
509
|
+
# SEMPRE valide a presença de uploads[i].url antes de usar.
|
|
143
510
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- Integrar com outros módulos
|
|
511
|
+
# RUIM: depender de trigger before/after_create ao usar /image — ela NÃO dispara.
|
|
512
|
+
# Use /file se precisar de triggers.
|
|
513
|
+
```
|
|
148
514
|
|
|
149
|
-
|
|
515
|
+
---
|
|
150
516
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- [ ]
|
|
154
|
-
- [ ]
|
|
155
|
-
- [ ]
|
|
517
|
+
## Checklist de Configuração
|
|
518
|
+
|
|
519
|
+
- [ ] Parte `extra` presente e com JSON válido (sem ela → `400`).
|
|
520
|
+
- [ ] `extra` termina na `}` correta (conteúdo após a última `}` é descartado silenciosamente).
|
|
521
|
+
- [ ] Nome do arquivo **possui extensão** com um único ponto (evita `500` e `extension` errada).
|
|
522
|
+
- [ ] `session` definido conscientemente (define o caminho `games/{apiKey}/{session}`; default `upload`/`image`).
|
|
523
|
+
- [ ] Em `/image`, `width`/`height` são **inteiros** (não use `640.0`).
|
|
524
|
+
- [ ] Validar `uploads[i].url` na resposta antes de usar (presença de `exception` indica falha silenciosa).
|
|
525
|
+
- [ ] Se precisar de automação por trigger, usar `/file` (o `/image` não dispara triggers).
|
|
526
|
+
- [ ] Provedor de nuvem (`Configuration.cloud`) e credenciais configurados; do contrário documentos são salvos com `exception`.
|
|
527
|
+
- [ ] Ciente de que arquivos ficam **públicos para leitura** (S3/Google).
|
|
528
|
+
- [ ] Em `GET /v3/upload`, tratar `q`/`orderby` como entrada confiável (concatenação crua — risco de NoSQL injection).
|