Skip to content

Commit abb0333

Browse files
authored
feat: backup command fixes (#85)
2 parents dd08233 + 0f463ea commit abb0333

File tree

7 files changed

+112
-40
lines changed

7 files changed

+112
-40
lines changed

.changeset/bright-teams-decide.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@calycode/types": minor
3+
"@calycode/utils": minor
4+
"@calycode/core": minor
5+
"@calycode/cli": minor
6+
---
7+
8+
feat: fixing and wrapping up backup exporting command

packages/cli/src/commands/backups.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ async function restorationWizard({ instance, workspace, sourceBackup, forceConfi
9696
}
9797
}
9898

99+
async function exportWizard({ instance, workspace, branch, core, doLog, output }) {
100+
attachCliEventHandlers('export-backup', core, arguments);
101+
102+
const { instanceConfig, workspaceConfig, branchConfig, context } = await resolveConfigs({
103+
cliContext: { instance, workspace, branch },
104+
core,
105+
});
106+
107+
// Resolve output dir
108+
const outputDir = output
109+
? output
110+
: replacePlaceholders(instanceConfig.backups.output, {
111+
'@': await findProjectRoot(),
112+
instance: instanceConfig.name,
113+
workspace: workspaceConfig.name,
114+
branch: branchConfig.label,
115+
});
116+
117+
const outputObject = await core.exportBackup({ ...context, outputDir });
118+
119+
printOutputDir(doLog, outputObject.outputDir);
120+
}
121+
99122
// [ ] Add potentially context awareness like in the other commands
100123
function registerExportBackupCommand(program, core) {
101124
const cmd = program
@@ -107,13 +130,14 @@ function registerExportBackupCommand(program, core) {
107130

108131
cmd.action(
109132
withErrorHandler(async (options) => {
110-
attachCliEventHandlers('export-backup', core, options);
111-
const outputObject = await core.exportBackup({
112-
branch: options.branch,
133+
await exportWizard({
113134
instance: options.instance,
114135
workspace: options.workspace,
136+
branch: options.branch,
137+
core: core,
138+
doLog: options.printOutputDir,
139+
output: options.output,
115140
});
116-
printOutputDir(options.printOutput, outputObject.outputDir);
117141
})
118142
);
119143
}

packages/cli/src/node-config-storage.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,21 +237,25 @@ export const nodeConfigStorage: ConfigStorage = {
237237
async writeFile(filePath, data) {
238238
await fs.promises.writeFile(filePath, data);
239239
},
240-
async streamToFile(
241-
destinationPath: string,
242-
source: ReadableStream | NodeJS.ReadableStream
243-
): Promise<void> {
244-
const dest = fs.createWriteStream(destinationPath, { mode: 0o600 });
240+
241+
async streamToFile({
242+
path,
243+
stream,
244+
}: {
245+
path: string;
246+
stream: ReadableStream | NodeJS.ReadableStream;
247+
}): Promise<void> {
248+
const dest = fs.createWriteStream(path, { mode: 0o600 });
245249
let nodeStream: NodeJS.ReadableStream;
246250

247251
// Convert if necessary
248-
if (typeof (source as any).pipe === 'function') {
252+
if (typeof (stream as any).pipe === 'function') {
249253
// already a NodeJS stream
250-
nodeStream = source as NodeJS.ReadableStream;
254+
nodeStream = stream as NodeJS.ReadableStream;
251255
} else {
252256
// WHATWG stream (from fetch in Node 18+)
253257
// Can only use fromWeb if available in the environment
254-
nodeStream = Readable.fromWeb(source as any);
258+
nodeStream = Readable.fromWeb(stream as any);
255259
}
256260

257261
await new Promise<void>((resolve, reject) => {
@@ -261,9 +265,11 @@ export const nodeConfigStorage: ConfigStorage = {
261265
nodeStream.on('error', (err) => reject(err));
262266
});
263267
},
268+
264269
async readFile(filePath) {
265270
return await fs.promises.readFile(filePath); // returns Buffer
266271
},
272+
267273
async exists(filePath) {
268274
try {
269275
await fs.promises.access(filePath);

packages/core/src/implementations/backups.ts

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { replacePlaceholders, joinPath } from '@calycode/utils';
1+
import { replacePlaceholders, joinPath, dirname } from '@calycode/utils';
22

33
/**
44
* Exports a backup and emits events for CLI/UI.
55
*/
6-
async function exportBackupImplementation({ instance, workspace, branch, core }) {
6+
async function exportBackupImplementation({ outputDir, instance, workspace, branch, core }) {
77
core.emit('start', {
88
name: 'export-backup',
99
payload: { instance, workspace, branch },
@@ -35,43 +35,70 @@ async function exportBackupImplementation({ instance, workspace, branch, core })
3535
percent: 15,
3636
});
3737

38-
// Resolve output dir
39-
const outputDir = replacePlaceholders(instanceConfig.backups.output, {
40-
instance: instanceConfig.name,
41-
workspace: workspaceConfig.name,
42-
branch: branchConfig.label,
38+
core.emit('progress', {
39+
name: 'export-backup',
40+
message: 'Requesting backup from Xano API...',
41+
percent: 40,
4342
});
4443

45-
await core.storage.mkdir(outputDir, { recursive: true });
46-
44+
const startTime = Date.now();
4745
core.emit('progress', {
4846
name: 'export-backup',
4947
message: 'Requesting backup from Xano API...',
5048
percent: 40,
5149
});
5250

53-
const backupStreamRequest = await fetch(
54-
`${instanceConfig.url}/api:meta/workspace/${workspaceConfig.id}/export`,
55-
{
56-
method: 'POST',
57-
headers: {
58-
Authorization: `Bearer ${await core.loadToken(instanceConfig.name)}`,
59-
'Content-Type': 'application/json',
60-
},
61-
body: JSON.stringify({ branch: branchConfig.label }),
62-
}
63-
);
51+
let backupStreamRequest;
52+
try {
53+
backupStreamRequest = await fetch(
54+
`${instanceConfig.url}/api:meta/workspace/${workspaceConfig.id}/export`,
55+
{
56+
method: 'POST',
57+
headers: {
58+
Authorization: `Bearer ${await core.loadToken(instanceConfig.name)}`,
59+
'Content-Type': 'application/json',
60+
},
61+
body: JSON.stringify({ branch: branchConfig.label }),
62+
}
63+
);
64+
} catch (err) {
65+
core.emit('error', {
66+
error: err,
67+
message: 'Fetch failed',
68+
step: 'fetch',
69+
elapsed: Date.now() - startTime,
70+
});
71+
throw err;
72+
}
6473

65-
core.emit('progress', {
66-
name: 'export-backup',
67-
message: 'Saving backup file...',
68-
percent: 80,
74+
core.emit('info', {
75+
message: 'Response headers received',
76+
headers: backupStreamRequest.headers,
77+
status: backupStreamRequest.status,
78+
elapsed: Date.now() - startTime,
6979
});
7080

7181
const now = new Date();
7282
const ts = now.toISOString().replace(/[:.]/g, '-');
7383
const backupPath = joinPath(outputDir, `backup-${ts}.tar.gz`);
74-
await core.storage.streamToFile(backupStreamRequest.body, backupPath);
84+
85+
await core.storage.mkdir(outputDir, { recursive: true });
86+
try {
87+
await core.storage.streamToFile({ path: backupPath, stream: backupStreamRequest.body });
88+
core.emit('info', {
89+
message: 'Streaming complete',
90+
backupPath,
91+
elapsed: Date.now() - startTime,
92+
});
93+
} catch (err) {
94+
core.emit('error', {
95+
error: err,
96+
message: 'Streaming to file failed',
97+
step: 'streamToFile',
98+
elapsed: Date.now() - startTime,
99+
});
100+
throw err;
101+
}
75102

76103
core.emit('progress', {
77104
name: 'export-backup',

packages/core/src/implementations/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export async function setupInstanceImplementation(
8181
output: '{@}/{workspace}/{branch}/codegen/{api_group_normalized_name}',
8282
},
8383
backups: {
84-
output: '{@}/{workspace}{branch}/backups',
84+
output: '{@}/{workspace}/{branch}/backups',
8585
},
8686
registry: {
8787
output: '{@}/registry',

packages/core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,9 @@ export class Caly extends TypedEmitter<EventMap> {
179179
* });
180180
* ```
181181
*/
182-
async exportBackup({ instance, workspace, branch }): Promise<Record<string, string>> {
182+
async exportBackup({ instance, workspace, branch, outputDir }): Promise<Record<string, string>> {
183183
return exportBackupImplementation({
184+
outputDir,
184185
instance,
185186
workspace,
186187
branch,

packages/types/src/storage/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ export interface ConfigStorage {
4343
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
4444
readdir(path: string): Promise<string[]>;
4545
writeFile(path: string, data: string | Uint8Array): Promise<void>;
46-
streamToFile(path: string, stream: ReadableStream | NodeJS.ReadableStream): Promise<void>;
46+
streamToFile({
47+
path,
48+
stream,
49+
}: {
50+
path: string;
51+
stream: ReadableStream | NodeJS.ReadableStream;
52+
}): Promise<void>;
4753
readFile(path: string): Promise<string | Uint8Array>;
4854
exists(path: string): Promise<boolean>;
4955

0 commit comments

Comments
 (0)