smoonb 1.0.7 → 1.0.8
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/CHANGELOG.md +26 -0
- package/README.md +23 -7
- package/README.pt-BR.md +23 -7
- package/package.json +4 -3
- package/src/commands/backup/steps/06-realtime-settings.js +19 -33
- package/src/commands/backup/steps/07-storage.js +7 -12
- package/src/commands/backup/steps/09-edge-functions.js +76 -202
- package/src/commands/restore/auth-fields.json +237 -0
- package/src/commands/restore/dump.js +284 -0
- package/src/commands/restore/index.js +4 -126
- package/src/commands/restore/management.js +74 -0
- package/src/commands/restore/pg.js +83 -0
- package/src/commands/restore/runner.js +117 -0
- package/src/commands/restore/steps/03-database.js +18 -84
- package/src/commands/restore/steps/04-edge-functions.js +95 -122
- package/src/commands/restore/steps/05-auth-settings.js +26 -54
- package/src/commands/restore/steps/06-storage.js +203 -584
- package/src/commands/restore/steps/07-database-settings.js +87 -69
- package/src/commands/restore/steps/08-realtime-settings.js +61 -53
- package/src/i18n/locales/en.json +797 -797
- package/src/i18n/locales/pt-BR.json +796 -796
- package/src/utils/supabaseCli.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [1.0.8] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Restore application and Auth data from the existing full dump in one checked transaction. Fail on SQL errors; compare restored columns and validate foreign keys before commit, including pre-existing Auth constraints.
|
|
12
|
+
- Remove extra destination object grants before replaying source ACLs, preventing unintended anonymous/authenticated access to restricted functions and tables.
|
|
13
|
+
- Apply and verify Auth configuration through the Management API, including SMTP and redirect configuration.
|
|
14
|
+
- Remap source Supabase hostnames in application data and text objects when selected; preserve unrelated IDs and paths.
|
|
15
|
+
- Recover Storage MIME types, cache directives, ownership and user metadata from the full dump even when legacy Storage JSON inventories are empty. Validate ZIP inventory, count empty buckets, and verify every uploaded file by download and SHA-256.
|
|
16
|
+
- Use isolated temporary workspaces for Edge Function backup and deployment; preserve local modified sources and compressed backup archives. Address npm CLI shims on Windows without shell interpolation.
|
|
17
|
+
- Persist stage results and failures; never report unsuccessful or unverified components as restored.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Apply and verify Realtime snapshots through the API; capture fresh Realtime settings for new backups instead of reusing interactive recordings.
|
|
22
|
+
- Verify database settings and fail explicitly for incompatible platform settings rather than silently skipping them.
|
|
23
|
+
- Require Node.js 20 or later. A new, empty destination is required for the database restore; platform-managed schemas and administrator defaults remain destination-managed.
|
|
24
|
+
- Preserve Function JWT configuration in new backups. Legacy backups default to requiring JWT.
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
|
|
28
|
+
- Add regression coverage for configuration readback, failure reporting, URL mapping, archive preservation, isolated Functions, and PostgreSQL arrays.
|
|
29
|
+
- Add an opt-in Docker test of the actual restore step with cyclic references, Auth identities, broad destination defaults, invalid-data rollback, populated-target refusal and Storage metadata/content verification (mock HTTP, real PostgreSQL).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
7
33
|
## [1.0.7] - 2026-10-10
|
|
8
34
|
|
|
9
35
|
### Fixed
|
package/README.md
CHANGED
|
@@ -296,14 +296,28 @@ npx smoonb restore --skip-supabase-version-check
|
|
|
296
296
|
- 🔄 Realtime Settings (if available in backup)
|
|
297
297
|
9. **Detailed summary** - Shows selected backup, target project and components
|
|
298
298
|
10. **Final confirmation** - Confirms before starting
|
|
299
|
-
11. **
|
|
299
|
+
11. **Project URLs** - Confirms whether Supabase URLs in data and files should point to the new project
|
|
300
300
|
12. **Restore execution:**
|
|
301
301
|
- 📊 Database - Restores via `psql` (supports `.backup.gz` and `.backup`)
|
|
302
|
-
- ⚡ Edge Functions -
|
|
303
|
-
- 🔐 Auth Settings -
|
|
304
|
-
- 📦 Storage - Restores buckets and
|
|
302
|
+
- ⚡ Edge Functions - Deploys from a temporary copy, preserving local sources
|
|
303
|
+
- 🔐 Auth Settings - Applies via API and verifies the resulting values
|
|
304
|
+
- 📦 Storage - Restores buckets and ZIP files, preserves metadata and verifies every download
|
|
305
305
|
- 🔧 Database Settings - Restores extensions and settings via SQL
|
|
306
|
-
- 🔄 Realtime Settings -
|
|
306
|
+
- 🔄 Realtime Settings - Applies and verifies the snapshot recorded in the backup
|
|
307
|
+
|
|
308
|
+
### Verified restoration (1.0.8)
|
|
309
|
+
|
|
310
|
+
Use a new, empty destination project and Node.js 20 or later. Existing complete 1.0.7 backups can be reused without another capture. Version 1.0.8 uses the complete dump as one snapshot for application structure, Auth data, migration history, buckets and object metadata. Platform schemas and internal migrations remain at the destination's version.
|
|
311
|
+
|
|
312
|
+
Database loading uses one transaction, stops on errors, suspends triggers only on its connection, compares every restored column and checks foreign keys before commit. Extra destination default grants are cleared before replaying the backup's grants. Failed SQL is never reported as success. Compressed archives and local function sources remain untouched.
|
|
313
|
+
|
|
314
|
+
Optional URL remapping changes only the project's Supabase hostname, including URLs inside JSON and text files. It does not rename files or replace isolated project IDs. Historical URLs may also change; choose preservation when needed. Signed URLs, custom domains, external integrations and application API keys need separate validation. Destination platform keys are not replaced.
|
|
315
|
+
|
|
316
|
+
Auth and Realtime are applied through the Management API and verified by reading them back. Legacy Realtime captures may be stale: restoration reproduces the recorded values and date, not the source project's current settings. New backups capture Realtime directly through the API. Legacy function backups without the JWT flag default to requiring JWT; new backups preserve that flag.
|
|
317
|
+
|
|
318
|
+
Reports are saved after each stage, including failures. External services cannot share the database transaction: earlier stages remain completed if a later stage fails. Do not reload the database into a populated destination. Resolve the reported stage and select only pending components, or use another empty project.
|
|
319
|
+
|
|
320
|
+
Unsupported internal data (such as encrypted Vault secrets without key migration) and destination plan/version incompatibilities stop the operation. Verified status covers the selected components; validate login, email delivery and business workflows before production use.
|
|
307
321
|
|
|
308
322
|
**Supported file formats:**
|
|
309
323
|
- ✅ `.backup.gz` (compressed) - Automatically decompresses before restoring
|
|
@@ -503,7 +517,7 @@ restore/
|
|
|
503
517
|
npx smoonb restore
|
|
504
518
|
```
|
|
505
519
|
(Select desired backup, select components, execute restoration.)
|
|
506
|
-
6. **
|
|
520
|
+
6. **Review the result**: inspect `backups/restore-*/report.json` and test login, email and application workflows on the destination.
|
|
507
521
|
|
|
508
522
|
## 🎨 User Experience
|
|
509
523
|
|
|
@@ -602,7 +616,7 @@ npx smoonb restore --skip-supabase-version-check
|
|
|
602
616
|
### .backup.gz file cannot be restored
|
|
603
617
|
|
|
604
618
|
smoonb automatically supports:
|
|
605
|
-
- ✅ `.backup.gz` -
|
|
619
|
+
- ✅ `.backup.gz` - Reads compressed content without changing the original archive
|
|
606
620
|
- ✅ `.backup` - Restores directly
|
|
607
621
|
|
|
608
622
|
If there are problems:
|
|
@@ -661,3 +675,5 @@ Contributions are welcome. [www.smoonb.com/#price](https://www.smoonb.com/#price
|
|
|
661
675
|
**Developed by:** Goalmoon Tecnologia LTDA
|
|
662
676
|
**Website:** https://www.smoonb.com
|
|
663
677
|
**GitHub:** https://github.com/almmello/smoonb
|
|
678
|
+
|
|
679
|
+
Additional restore-path test: `npm run test:restore:docker`. Uses disposable PostgreSQL, synthetic data and mocked Storage HTTP transport; covers Auth, grants, cycles, rollback, files and metadata.
|
package/README.pt-BR.md
CHANGED
|
@@ -296,14 +296,28 @@ npx smoonb restore --skip-supabase-version-check
|
|
|
296
296
|
- 🔄 Realtime Settings (se disponível no backup)
|
|
297
297
|
9. **Resumo detalhado** - Mostra backup selecionado, projeto destino e componentes
|
|
298
298
|
10. **Confirmação final** - Confirma antes de iniciar
|
|
299
|
-
11. **
|
|
299
|
+
11. **Endereços do projeto** - Confirma se as URLs Supabase nos dados e arquivos devem apontar para o novo projeto
|
|
300
300
|
12. **Execução da restauração:**
|
|
301
301
|
- 📊 Database - Restaura via `psql` (suporta `.backup.gz` e `.backup`)
|
|
302
|
-
- ⚡ Edge Functions -
|
|
303
|
-
- 🔐 Auth Settings -
|
|
304
|
-
- 📦 Storage - Restaura buckets e arquivos do ZIP
|
|
302
|
+
- ⚡ Edge Functions - Faz deploy de uma cópia temporária, preservando as fontes locais
|
|
303
|
+
- 🔐 Auth Settings - Aplica via API e verifica os valores recuperados
|
|
304
|
+
- 📦 Storage - Restaura buckets e arquivos do ZIP, preserva metadados e verifica cada download
|
|
305
305
|
- 🔧 Database Settings - Restaura extensões e configurações via SQL
|
|
306
|
-
- 🔄 Realtime Settings -
|
|
306
|
+
- 🔄 Realtime Settings - Aplica e verifica o snapshot registrado no backup
|
|
307
|
+
|
|
308
|
+
### Restauração verificada (1.0.8)
|
|
309
|
+
|
|
310
|
+
Use um projeto de destino novo e vazio e Node.js 20 ou mais recente. O backup completo existente da versão 1.0.7 pode ser reutilizado, sem nova captura. A versão 1.0.8 usa o dump completo como snapshot único: recupera a estrutura da aplicação, dados de Auth, histórico de migrações, buckets e metadados. Os schemas internos e as migrações da plataforma são mantidos na versão do destino.
|
|
311
|
+
|
|
312
|
+
A carga do banco usa uma transação com interrupção em erro, suspende triggers apenas nessa conexão, compara todas as colunas recuperadas e verifica chaves estrangeiras antes de confirmar. Permissões herdadas dos defaults do destino são removidas antes da reaplicação dos grants do backup. SQL com falha nunca é classificado como sucesso. O arquivo compactado e a pasta local de funções são preservados.
|
|
313
|
+
|
|
314
|
+
O remapeamento opcional troca apenas o domínio Supabase do projeto em URLs, inclusive em JSON e arquivos de texto. Não troca IDs isolados nem nomes de arquivos. URLs históricas também podem ser afetadas; escolha preservar quando isso for necessário. URLs assinadas, domínios personalizados, integrações externas e chaves de API da aplicação precisam de validação específica. As chaves de plataforma do destino não são substituídas.
|
|
315
|
+
|
|
316
|
+
Auth e Realtime são aplicados pela Management API e conferidos por nova leitura. A captura antiga de Realtime pode conter valores desatualizados: a restauração reproduz os valores e a data registrados, não o estado atual do projeto original. Novos backups capturam Realtime diretamente pela API. Em backups antigos sem o sinalizador de JWT de Functions, o padrão seguro é exigir JWT; os novos backups guardam esse sinalizador.
|
|
317
|
+
|
|
318
|
+
O relatório é salvo durante cada etapa, inclusive em falhas. Serviços externos não participam da transação do banco: se uma etapa posterior falhar, as anteriores permanecem registradas como concluídas. Não repita a carga do banco em um destino já preenchido. Resolva a etapa indicada e selecione apenas os componentes pendentes, ou use outro projeto vazio.
|
|
319
|
+
|
|
320
|
+
Dados internos não suportados (por exemplo, Vault com segredos criptografados sem migração da chave) e configurações incompatíveis com o plano/versão do destino interrompem o processo. O status verificado cobre os componentes selecionados; ainda é necessário validar login, envio de e-mail e fluxos de negócio antes de usar o destino em produção.
|
|
307
321
|
|
|
308
322
|
**Formato de arquivos suportados:**
|
|
309
323
|
- ✅ `.backup.gz` (compactado) - Descompacta automaticamente antes de restaurar
|
|
@@ -503,7 +517,7 @@ restore/
|
|
|
503
517
|
npx smoonb restore
|
|
504
518
|
```
|
|
505
519
|
(Seleciona backup desejado, componentes a restaurar, executa restauração.)
|
|
506
|
-
6. **
|
|
520
|
+
6. **Conferir o resultado**: leia `backups/restore-*/report.json` e teste login, e-mail e os fluxos da aplicação no destino.
|
|
507
521
|
|
|
508
522
|
## 🎨 Experiência do Usuário
|
|
509
523
|
|
|
@@ -602,7 +616,7 @@ npx smoonb restore --skip-supabase-version-check
|
|
|
602
616
|
### Arquivo .backup.gz não pode ser restaurado
|
|
603
617
|
|
|
604
618
|
O smoonb suporta automaticamente:
|
|
605
|
-
- ✅ `.backup.gz` -
|
|
619
|
+
- ✅ `.backup.gz` - Lê o conteúdo compactado sem modificar o arquivo original
|
|
606
620
|
- ✅ `.backup` - Restaura diretamente
|
|
607
621
|
|
|
608
622
|
Se houver problemas:
|
|
@@ -661,3 +675,5 @@ Contribuições são bem-vindas. [www.smoonb.com/#price](https://www.smoonb.com/
|
|
|
661
675
|
**Desenvolvido por:** Goalmoon Tecnologia LTDA
|
|
662
676
|
**Website:** https://www.smoonb.com
|
|
663
677
|
**GitHub:** https://github.com/almmello/smoonb
|
|
678
|
+
|
|
679
|
+
Teste adicional do caminho real de restore: `npm run test:restore:docker`. Usa PostgreSQL descartável, dados fictícios e transporte HTTP simulado para Storage; cobre Auth, permissões, ciclos, rollback, arquivos e metadados.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoonb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
5
|
"preferGlobal": false,
|
|
6
6
|
"preventGlobalInstall": true,
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"build": "node scripts/build.js",
|
|
19
19
|
"build:check": "node scripts/build.js",
|
|
20
20
|
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
21
|
-
"test:docker": "node tests/integration/circular-restore.js"
|
|
21
|
+
"test:docker": "node tests/integration/circular-restore.js",
|
|
22
|
+
"test:restore:docker": "node tests/integration/verified-restore.js"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
24
25
|
"supabase",
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
"author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
|
|
34
35
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
35
36
|
"engines": {
|
|
36
|
-
"node": ">=
|
|
37
|
+
"node": ">=20.0.0"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@supabase/supabase-js": "^2.93.0",
|
|
@@ -1,33 +1,19 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const stats = await fs.stat(path.join(backupDir, 'realtime-settings.json'));
|
|
22
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
23
|
-
|
|
24
|
-
console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`));
|
|
25
|
-
|
|
26
|
-
return { success: true, settings, source, reusedFrom };
|
|
27
|
-
} catch (error) {
|
|
28
|
-
const getT = global.smoonbI18n?.t || t;
|
|
29
|
-
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.realtime.error', { message: error.message })}`));
|
|
30
|
-
return { success: false };
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const { management } = require('../../restore/management');
|
|
4
|
+
module.exports = async ({ projectId, accessToken, backupDir }) => {
|
|
5
|
+
try {
|
|
6
|
+
const settings = await management(
|
|
7
|
+
{ targetProjectId: projectId, targetAccessToken: accessToken },
|
|
8
|
+
'config/realtime',
|
|
9
|
+
);
|
|
10
|
+
const captured_at = new Date().toISOString();
|
|
11
|
+
await fs.writeFile(
|
|
12
|
+
path.join(backupDir, 'realtime-settings.json'),
|
|
13
|
+
JSON.stringify({ project_id: projectId, captured_at, settings }, null, 2),
|
|
14
|
+
);
|
|
15
|
+
return { success: true, settings, source: 'management-api', captured_at };
|
|
16
|
+
} catch {
|
|
17
|
+
return { success: false, reason: 'realtime_api_capture_failed' };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -106,18 +106,13 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
106
106
|
try {
|
|
107
107
|
console.log(chalk.white(` - ${getT('backup.steps.storage.processing', { bucketName: bucket.name })}`));
|
|
108
108
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let objects = [];
|
|
117
|
-
if (objectsResponse.ok) {
|
|
118
|
-
objects = await objectsResponse.json();
|
|
119
|
-
}
|
|
120
|
-
|
|
109
|
+
const { readDump, records } = require('../../restore/dump');
|
|
110
|
+
const dumpFile = (await fs.readdir(backupDir)).find(f => /\.backup(?:\.gz)?$/.test(f));
|
|
111
|
+
if (!dumpFile) throw new Error('Database inventory required for Storage metadata');
|
|
112
|
+
const dump = readDump(path.join(backupDir, dumpFile));
|
|
113
|
+
const objects = records(dump.tables.find(t => t.schema === 'storage' && t.name === 'objects'))
|
|
114
|
+
.filter(o => o.bucket_id === bucket.id).map(o => ({ ...o, metadata: JSON.parse(o.metadata || '{}') }));
|
|
115
|
+
|
|
121
116
|
const bucketInfo = {
|
|
122
117
|
id: bucket.id,
|
|
123
118
|
name: bucket.name,
|
|
@@ -1,202 +1,76 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
await
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const functions = await functionsResponse.json();
|
|
78
|
-
|
|
79
|
-
if (!functions || functions.length === 0) {
|
|
80
|
-
console.log(chalk.white(` - ${getT('backup.steps.functions.noneFound')}`));
|
|
81
|
-
await writeJson(path.join(functionsDir, 'README.md'), {
|
|
82
|
-
message: getT('backup.steps.functions.noFunctionsMessage')
|
|
83
|
-
});
|
|
84
|
-
return { success: true, reason: 'no_functions', functions: [] };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
console.log(chalk.white(` - Encontradas ${functions.length} Edge Function(s)`));
|
|
88
|
-
|
|
89
|
-
const downloadedFunctions = [];
|
|
90
|
-
let successCount = 0;
|
|
91
|
-
let errorCount = 0;
|
|
92
|
-
const totalFuncs = functions.length;
|
|
93
|
-
const stepStart = Date.now();
|
|
94
|
-
|
|
95
|
-
// Baixar cada Edge Function via Supabase CLI
|
|
96
|
-
// Nota: O CLI ignora o cwd e sempre baixa para supabase/functions
|
|
97
|
-
for (let idx = 0; idx < functions.length; idx++) {
|
|
98
|
-
const func = functions[idx];
|
|
99
|
-
const current = idx + 1;
|
|
100
|
-
const elapsed = Math.floor((Date.now() - stepStart) / 1000);
|
|
101
|
-
try {
|
|
102
|
-
console.log(chalk.white(` - Baixando ${current}/${totalFuncs}: ${func.name}... (${elapsed}s)`));
|
|
103
|
-
|
|
104
|
-
// Criar diretório da função NO BACKUP
|
|
105
|
-
const functionTargetDir = path.join(functionsDir, func.name);
|
|
106
|
-
await ensureDir(functionTargetDir);
|
|
107
|
-
|
|
108
|
-
// Diretório temporário onde o supabase CLI irá baixar (supabase/functions)
|
|
109
|
-
const tempDownloadDir = path.join(process.cwd(), 'supabase', 'functions', func.name);
|
|
110
|
-
|
|
111
|
-
// Baixar Edge Function via Supabase CLI (sempre vai para supabase/functions)
|
|
112
|
-
const { execSync } = require('child_process');
|
|
113
|
-
|
|
114
|
-
execSync(`supabase functions download ${func.name}`, {
|
|
115
|
-
timeout: 60000,
|
|
116
|
-
stdio: 'pipe'
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// COPIAR arquivos de supabase/functions para o backup
|
|
120
|
-
try {
|
|
121
|
-
const stat = await fs.stat(tempDownloadDir);
|
|
122
|
-
if (stat.isDirectory()) {
|
|
123
|
-
const files = await fs.readdir(tempDownloadDir);
|
|
124
|
-
for (const file of files) {
|
|
125
|
-
const srcPath = path.join(tempDownloadDir, file);
|
|
126
|
-
const dstPath = path.join(functionTargetDir, file);
|
|
127
|
-
|
|
128
|
-
const fileStats = await fs.stat(srcPath);
|
|
129
|
-
if (fileStats.isDirectory()) {
|
|
130
|
-
// Copiar diretórios recursivamente
|
|
131
|
-
await fs.cp(srcPath, dstPath, { recursive: true });
|
|
132
|
-
} else {
|
|
133
|
-
// Copiar arquivos
|
|
134
|
-
await fs.copyFile(srcPath, dstPath);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// Arquivos não foram baixados, continuar
|
|
140
|
-
console.log(chalk.yellow(` ⚠️ Nenhum arquivo encontrado em ${tempDownloadDir}`));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Limpar função baixada temporariamente APENAS se o usuário escolheu limpar após
|
|
144
|
-
// Se não escolheu, preservar (pode ser que já existisse antes)
|
|
145
|
-
if (shouldCleanAfter) {
|
|
146
|
-
try {
|
|
147
|
-
await fs.rm(tempDownloadDir, { recursive: true, force: true });
|
|
148
|
-
} catch {
|
|
149
|
-
// Ignorar erro de limpeza
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// Se shouldCleanAfter = false, manter a função baixada (e qualquer função que já existia)
|
|
153
|
-
|
|
154
|
-
console.log(chalk.green(` ✅ ${func.name} baixada com sucesso`));
|
|
155
|
-
successCount++;
|
|
156
|
-
|
|
157
|
-
downloadedFunctions.push({
|
|
158
|
-
name: func.name,
|
|
159
|
-
slug: func.name,
|
|
160
|
-
version: func.version || 'unknown',
|
|
161
|
-
files: await fs.readdir(functionTargetDir).catch(() => [])
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
} catch (error) {
|
|
165
|
-
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.downloadError', { funcName: func.name, message: error.message })}`));
|
|
166
|
-
errorCount++;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
console.log(chalk.green(`📊 Backup de Edge Functions concluído:`));
|
|
171
|
-
console.log(chalk.green(` ✅ Sucessos: ${successCount}`));
|
|
172
|
-
console.log(chalk.green(` ❌ Erros: ${errorCount}`));
|
|
173
|
-
|
|
174
|
-
// Limpar pasta supabase/functions APÓS o backup apenas se o usuário escolheu
|
|
175
|
-
// Nota: shouldCleanAfter já foi definido acima
|
|
176
|
-
if (shouldCleanAfter) {
|
|
177
|
-
await cleanDir(supabaseFunctionsDir);
|
|
178
|
-
console.log(chalk.white(` - ${getT('backup.steps.functions.cleanAfter')}`));
|
|
179
|
-
} else {
|
|
180
|
-
// Preservar tudo: tanto as funções que já existiam quanto as que foram baixadas
|
|
181
|
-
// As funções baixadas não foram removidas individualmente (linha acima foi ajustada)
|
|
182
|
-
console.log(chalk.white(` - ${getT('backup.steps.functions.preserved')}`));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
success: true,
|
|
187
|
-
reason: 'success',
|
|
188
|
-
functions: downloadedFunctions,
|
|
189
|
-
functions_count: functions.length,
|
|
190
|
-
success_count: successCount,
|
|
191
|
-
error_count: errorCount,
|
|
192
|
-
method: 'docker'
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
} catch (error) {
|
|
196
|
-
const getT = global.smoonbI18n?.t || t;
|
|
197
|
-
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.error', { message: error.message })}`));
|
|
198
|
-
console.log('⏭️ Continuando com outros componentes...');
|
|
199
|
-
return { success: false, reason: 'download_error', error: error.message, functions: [] };
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { runSupabase } = require('../../../utils/supabaseCli');
|
|
5
|
+
const { management } = require('../../restore/management');
|
|
6
|
+
const { copyDirectoryRecursive } = require('../../restore/utils');
|
|
7
|
+
module.exports = async ({ projectId, accessToken, backupDir }) => {
|
|
8
|
+
const root = await fs.mkdtemp(
|
|
9
|
+
path.join(os.tmpdir(), 'smoonb-functions-backup-'),
|
|
10
|
+
);
|
|
11
|
+
try {
|
|
12
|
+
const functions = await management(
|
|
13
|
+
{ targetProjectId: projectId, targetAccessToken: accessToken },
|
|
14
|
+
'functions',
|
|
15
|
+
);
|
|
16
|
+
await fs.mkdir(path.join(root, 'supabase'), { recursive: true });
|
|
17
|
+
await fs.writeFile(
|
|
18
|
+
path.join(root, 'supabase', 'config.toml'),
|
|
19
|
+
'project_id = "smoonb-backup"\n',
|
|
20
|
+
);
|
|
21
|
+
const downloaded = [];
|
|
22
|
+
for (const f of functions) {
|
|
23
|
+
const name = f.slug || f.name;
|
|
24
|
+
if (!/^[-a-zA-Z0-9_]+$/.test(name))
|
|
25
|
+
throw new Error('Invalid function slug');
|
|
26
|
+
runSupabase(
|
|
27
|
+
'supabase',
|
|
28
|
+
[
|
|
29
|
+
'functions',
|
|
30
|
+
'download',
|
|
31
|
+
name,
|
|
32
|
+
'--project-ref',
|
|
33
|
+
projectId,
|
|
34
|
+
'--workdir',
|
|
35
|
+
root,
|
|
36
|
+
],
|
|
37
|
+
{
|
|
38
|
+
cwd: root,
|
|
39
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken },
|
|
40
|
+
stdio: 'pipe',
|
|
41
|
+
timeout: 180000,
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
const destination = path.join(backupDir, 'edge-functions', name);
|
|
46
|
+
await copyDirectoryRecursive(
|
|
47
|
+
path.join(root, 'supabase', 'functions', name),
|
|
48
|
+
destination,
|
|
49
|
+
);
|
|
50
|
+
downloaded.push({
|
|
51
|
+
name,
|
|
52
|
+
slug: name,
|
|
53
|
+
version: f.version,
|
|
54
|
+
verify_jwt: f.verify_jwt,
|
|
55
|
+
files: await fs.readdir(destination),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
reason: 'success',
|
|
61
|
+
functions: downloaded,
|
|
62
|
+
functions_count: downloaded.length,
|
|
63
|
+
success_count: downloaded.length,
|
|
64
|
+
error_count: 0,
|
|
65
|
+
method: 'cli-isolated',
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
reason: 'function_download_failed',
|
|
71
|
+
functions: [],
|
|
72
|
+
};
|
|
73
|
+
} finally {
|
|
74
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
};
|