smoonb 1.0.7 → 1.0.9
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 +43 -0
- package/README.md +27 -7
- package/README.pt-BR.md +27 -7
- package/package.json +4 -3
- package/src/commands/backup/steps/05-auth-settings.js +6 -2
- 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/auth.js +65 -0
- package/src/commands/restore/dump.js +284 -0
- package/src/commands/restore/index.js +8 -127
- package/src/commands/restore/management.js +96 -0
- package/src/commands/restore/pg.js +83 -0
- package/src/commands/restore/runner.js +140 -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 +15 -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 +803 -797
- package/src/i18n/locales/pt-BR.json +802 -796
- package/src/utils/auth-secrets.js +23 -0
- package/src/utils/supabaseCli.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ 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.9] - 2026-10-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Treat SMTP passwords as write-only credentials. Never replay the protected SMTP value returned by the Management API from a legacy backup as the provider password.
|
|
12
|
+
- Ask for the real SMTP password with hidden input before restoring any selected components. Fail before remote mutations if it is missing. Keep the entered password in memory only.
|
|
13
|
+
- Verify readable Auth settings independently from password submission. Continue after the API accepts the supplied password, but report `applied_with_unverified_secrets` and require an authentication email test instead of claiming password or email-delivery verification.
|
|
14
|
+
- Omit the protected SMTP value from new Auth snapshots and explicitly record that the provider password is required during restore. Existing backups remain usable.
|
|
15
|
+
|
|
16
|
+
### Recovery from 1.0.8
|
|
17
|
+
|
|
18
|
+
- If restore stopped at `Configuration verification failed: smtp_pass`, inspect its `report.json`. When Database and Edge Functions are already verified, run restore with those two components disabled and select Auth, Storage, Database Settings and Realtime as needed. Supply the real provider SMTP password when prompted; no new backup or destination project is required for this specific failure.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
|
|
22
|
+
- Cover legacy/new snapshots, hidden input, missing-password preflight, protected-value rejection, HTTP failures, readable-setting mismatches, partial verification reporting and continuation without reloading the database.
|
|
23
|
+
|
|
24
|
+
## [1.0.8] - 2026-10-10
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- 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.
|
|
29
|
+
- Remove extra destination object grants before replaying source ACLs, preventing unintended anonymous/authenticated access to restricted functions and tables.
|
|
30
|
+
- Apply and verify Auth configuration through the Management API, including SMTP and redirect configuration.
|
|
31
|
+
- Remap source Supabase hostnames in application data and text objects when selected; preserve unrelated IDs and paths.
|
|
32
|
+
- 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.
|
|
33
|
+
- 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.
|
|
34
|
+
- Persist stage results and failures; never report unsuccessful or unverified components as restored.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Apply and verify Realtime snapshots through the API; capture fresh Realtime settings for new backups instead of reusing interactive recordings.
|
|
39
|
+
- Verify database settings and fail explicitly for incompatible platform settings rather than silently skipping them.
|
|
40
|
+
- 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.
|
|
41
|
+
- Preserve Function JWT configuration in new backups. Legacy backups default to requiring JWT.
|
|
42
|
+
|
|
43
|
+
### Tests
|
|
44
|
+
|
|
45
|
+
- Add regression coverage for configuration readback, failure reporting, URL mapping, archive preservation, isolated Functions, and PostgreSQL arrays.
|
|
46
|
+
- 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).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
7
50
|
## [1.0.7] - 2026-10-10
|
|
8
51
|
|
|
9
52
|
### Fixed
|
package/README.md
CHANGED
|
@@ -296,14 +296,32 @@ 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
|
+
Readable Auth and Realtime settings are applied through the Management API and verified by reading them back. The **SMTP password cannot be exported**: Supabase returns a protected value that cannot be used as the password in another project. Starting with 1.0.9, smoonb requests the real provider password with hidden input **before restoring any components**, including when using legacy backups. It stays in memory only and is sent to the destination Supabase project. The report distinguishes API password acceptance from readback verification (`applied_with_unverified_secrets`). Test an authentication email on the destination; API acceptance does not validate the password with the email provider.
|
|
317
|
+
|
|
318
|
+
If a 1.0.8 restore stopped at `Configuration verification failed: smtp_pass` and its `report.json` confirms Database and Edge Functions, reuse the same backup and project: run `npx smoonb@1.0.9 restore`, answer **no** for Database and Edge Functions, and **yes** for the pending Auth, Storage, Database Settings and Realtime components. Enter the provider SMTP password when prompted. This specific failure does not require another backup or a database reload.
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
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.
|
|
323
|
+
|
|
324
|
+
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
325
|
|
|
308
326
|
**Supported file formats:**
|
|
309
327
|
- ✅ `.backup.gz` (compressed) - Automatically decompresses before restoring
|
|
@@ -503,7 +521,7 @@ restore/
|
|
|
503
521
|
npx smoonb restore
|
|
504
522
|
```
|
|
505
523
|
(Select desired backup, select components, execute restoration.)
|
|
506
|
-
6. **
|
|
524
|
+
6. **Review the result**: inspect `backups/restore-*/report.json` and test login, email and application workflows on the destination.
|
|
507
525
|
|
|
508
526
|
## 🎨 User Experience
|
|
509
527
|
|
|
@@ -602,7 +620,7 @@ npx smoonb restore --skip-supabase-version-check
|
|
|
602
620
|
### .backup.gz file cannot be restored
|
|
603
621
|
|
|
604
622
|
smoonb automatically supports:
|
|
605
|
-
- ✅ `.backup.gz` -
|
|
623
|
+
- ✅ `.backup.gz` - Reads compressed content without changing the original archive
|
|
606
624
|
- ✅ `.backup` - Restores directly
|
|
607
625
|
|
|
608
626
|
If there are problems:
|
|
@@ -661,3 +679,5 @@ Contributions are welcome. [www.smoonb.com/#price](https://www.smoonb.com/#price
|
|
|
661
679
|
**Developed by:** Goalmoon Tecnologia LTDA
|
|
662
680
|
**Website:** https://www.smoonb.com
|
|
663
681
|
**GitHub:** https://github.com/almmello/smoonb
|
|
682
|
+
|
|
683
|
+
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,32 @@ 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
|
+
As configurações legíveis de Auth e Realtime são aplicadas pela Management API e conferidas por nova leitura. A **senha SMTP não é exportável**: o Supabase devolve um valor protegido, que não pode ser usado como senha em outro projeto. A partir da versão 1.0.9, o smoonb solicita a senha real do provedor em campo oculto **antes de restaurar os componentes**, inclusive ao usar backups antigos. Ela permanece somente na memória e é enviada ao Supabase de destino. O relatório distingue senha aceita pela API de configurações verificadas por leitura (`applied_with_unverified_secrets`). Teste o envio de e-mail de autenticação no destino; aceitar uma senha pela API não comprova sua validade junto ao provedor.
|
|
317
|
+
|
|
318
|
+
Se uma restauração 1.0.8 parou em `Configuration verification failed: smtp_pass` e o `report.json` confirmou Database e Edge Functions, use o mesmo backup e projeto: execute `npx smoonb@1.0.9 restore`, responda **não** para Database e Edge Functions e **sim** para Auth, Storage, Database Settings e Realtime que ficaram pendentes. Informe a senha SMTP do provedor quando solicitada. Não é necessário refazer o backup nem recarregar o banco nesse caso.
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
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.
|
|
323
|
+
|
|
324
|
+
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
325
|
|
|
308
326
|
**Formato de arquivos suportados:**
|
|
309
327
|
- ✅ `.backup.gz` (compactado) - Descompacta automaticamente antes de restaurar
|
|
@@ -503,7 +521,7 @@ restore/
|
|
|
503
521
|
npx smoonb restore
|
|
504
522
|
```
|
|
505
523
|
(Seleciona backup desejado, componentes a restaurar, executa restauração.)
|
|
506
|
-
6. **
|
|
524
|
+
6. **Conferir o resultado**: leia `backups/restore-*/report.json` e teste login, e-mail e os fluxos da aplicação no destino.
|
|
507
525
|
|
|
508
526
|
## 🎨 Experiência do Usuário
|
|
509
527
|
|
|
@@ -602,7 +620,7 @@ npx smoonb restore --skip-supabase-version-check
|
|
|
602
620
|
### Arquivo .backup.gz não pode ser restaurado
|
|
603
621
|
|
|
604
622
|
O smoonb suporta automaticamente:
|
|
605
|
-
- ✅ `.backup.gz` -
|
|
623
|
+
- ✅ `.backup.gz` - Lê o conteúdo compactado sem modificar o arquivo original
|
|
606
624
|
- ✅ `.backup` - Restaura diretamente
|
|
607
625
|
|
|
608
626
|
Se houver problemas:
|
|
@@ -661,3 +679,5 @@ Contribuições são bem-vindas. [www.smoonb.com/#price](https://www.smoonb.com/
|
|
|
661
679
|
**Desenvolvido por:** Goalmoon Tecnologia LTDA
|
|
662
680
|
**Website:** https://www.smoonb.com
|
|
663
681
|
**GitHub:** https://github.com/almmello/smoonb
|
|
682
|
+
|
|
683
|
+
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.9",
|
|
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",
|
|
@@ -2,6 +2,7 @@ const chalk = require('chalk');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { writeJson } = require('../../../utils/fsx');
|
|
4
4
|
const { t } = require('../../../i18n');
|
|
5
|
+
const { authSnapshot } = require('../../../utils/auth-secrets');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Etapa 4: Backup Auth Settings via Management API
|
|
@@ -26,15 +27,18 @@ module.exports = async ({ projectId, accessToken, backupDir }) => {
|
|
|
26
27
|
|
|
27
28
|
const authSettings = await authResponse.json();
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
const snapshot = authSnapshot(authSettings);
|
|
31
|
+
// Never store the opaque SMTP readback as a recoverable password.
|
|
30
32
|
const authSettingsPath = path.join(backupDir, 'auth-settings.json');
|
|
31
33
|
await writeJson(authSettingsPath, {
|
|
32
34
|
project_id: projectId,
|
|
33
35
|
timestamp: new Date().toISOString(),
|
|
34
|
-
|
|
36
|
+
...snapshot
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
console.log(chalk.green(`✅ Auth Settings exportadas: ${path.basename(authSettingsPath)}`));
|
|
40
|
+
if (snapshot.secrets.smtp_pass.requires_input)
|
|
41
|
+
console.log(chalk.yellow(getT('backup.smtp.passwordNotExported')));
|
|
38
42
|
return { success: true };
|
|
39
43
|
|
|
40
44
|
} catch (error) {
|
|
@@ -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
|
+
};
|