Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/bright-teams-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@calycode/types": minor
"@calycode/utils": minor
"@calycode/core": minor
"@calycode/cli": minor
---

feat: fixing and wrapping up backup exporting command
32 changes: 28 additions & 4 deletions packages/cli/src/commands/backups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,29 @@ async function restorationWizard({ instance, workspace, sourceBackup, forceConfi
}
}

async function exportWizard({ instance, workspace, branch, core, doLog, output }) {
attachCliEventHandlers('export-backup', core, arguments);

const { instanceConfig, workspaceConfig, branchConfig, context } = await resolveConfigs({
cliContext: { instance, workspace, branch },
core,
});

// Resolve output dir
const outputDir = output
? output
: replacePlaceholders(instanceConfig.backups.output, {
'@': await findProjectRoot(),
instance: instanceConfig.name,
workspace: workspaceConfig.name,
branch: branchConfig.label,
});

const outputObject = await core.exportBackup({ ...context, outputDir });

printOutputDir(doLog, outputObject.outputDir);
}

// [ ] Add potentially context awareness like in the other commands
function registerExportBackupCommand(program, core) {
const cmd = program
Expand All @@ -107,13 +130,14 @@ function registerExportBackupCommand(program, core) {

cmd.action(
withErrorHandler(async (options) => {
attachCliEventHandlers('export-backup', core, options);
const outputObject = await core.exportBackup({
branch: options.branch,
await exportWizard({
instance: options.instance,
workspace: options.workspace,
branch: options.branch,
core: core,
doLog: options.printOutputDir,
output: options.output,
});
printOutputDir(options.printOutput, outputObject.outputDir);
})
);
}
Expand Down
22 changes: 14 additions & 8 deletions packages/cli/src/node-config-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,21 +237,25 @@ export const nodeConfigStorage: ConfigStorage = {
async writeFile(filePath, data) {
await fs.promises.writeFile(filePath, data);
},
async streamToFile(
destinationPath: string,
source: ReadableStream | NodeJS.ReadableStream
): Promise<void> {
const dest = fs.createWriteStream(destinationPath, { mode: 0o600 });

async streamToFile({
path,
stream,
}: {
path: string;
stream: ReadableStream | NodeJS.ReadableStream;
}): Promise<void> {
const dest = fs.createWriteStream(path, { mode: 0o600 });
let nodeStream: NodeJS.ReadableStream;

// Convert if necessary
if (typeof (source as any).pipe === 'function') {
if (typeof (stream as any).pipe === 'function') {
// already a NodeJS stream
nodeStream = source as NodeJS.ReadableStream;
nodeStream = stream as NodeJS.ReadableStream;
} else {
// WHATWG stream (from fetch in Node 18+)
// Can only use fromWeb if available in the environment
nodeStream = Readable.fromWeb(source as any);
nodeStream = Readable.fromWeb(stream as any);
}

await new Promise<void>((resolve, reject) => {
Expand All @@ -261,9 +265,11 @@ export const nodeConfigStorage: ConfigStorage = {
nodeStream.on('error', (err) => reject(err));
});
},

async readFile(filePath) {
return await fs.promises.readFile(filePath); // returns Buffer
},

async exists(filePath) {
try {
await fs.promises.access(filePath);
Expand Down
77 changes: 52 additions & 25 deletions packages/core/src/implementations/backups.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { replacePlaceholders, joinPath } from '@calycode/utils';
import { replacePlaceholders, joinPath, dirname } from '@calycode/utils';

/**
* Exports a backup and emits events for CLI/UI.
*/
async function exportBackupImplementation({ instance, workspace, branch, core }) {
async function exportBackupImplementation({ outputDir, instance, workspace, branch, core }) {
core.emit('start', {
name: 'export-backup',
payload: { instance, workspace, branch },
Expand Down Expand Up @@ -35,43 +35,70 @@ async function exportBackupImplementation({ instance, workspace, branch, core })
percent: 15,
});

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

await core.storage.mkdir(outputDir, { recursive: true });

const startTime = Date.now();
core.emit('progress', {
name: 'export-backup',
message: 'Requesting backup from Xano API...',
percent: 40,
});

const backupStreamRequest = await fetch(
`${instanceConfig.url}/api:meta/workspace/${workspaceConfig.id}/export`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${await core.loadToken(instanceConfig.name)}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ branch: branchConfig.label }),
}
);
let backupStreamRequest;
try {
backupStreamRequest = await fetch(
`${instanceConfig.url}/api:meta/workspace/${workspaceConfig.id}/export`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${await core.loadToken(instanceConfig.name)}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ branch: branchConfig.label }),
}
);
} catch (err) {
core.emit('error', {
error: err,
message: 'Fetch failed',
step: 'fetch',
elapsed: Date.now() - startTime,
});
throw err;
}

core.emit('progress', {
name: 'export-backup',
message: 'Saving backup file...',
percent: 80,
core.emit('info', {
message: 'Response headers received',
headers: backupStreamRequest.headers,
status: backupStreamRequest.status,
elapsed: Date.now() - startTime,
});

const now = new Date();
const ts = now.toISOString().replace(/[:.]/g, '-');
const backupPath = joinPath(outputDir, `backup-${ts}.tar.gz`);
await core.storage.streamToFile(backupStreamRequest.body, backupPath);

await core.storage.mkdir(outputDir, { recursive: true });
try {
await core.storage.streamToFile({ path: backupPath, stream: backupStreamRequest.body });
core.emit('info', {
message: 'Streaming complete',
backupPath,
elapsed: Date.now() - startTime,
});
} catch (err) {
core.emit('error', {
error: err,
message: 'Streaming to file failed',
step: 'streamToFile',
elapsed: Date.now() - startTime,
});
throw err;
}

core.emit('progress', {
name: 'export-backup',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/implementations/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export async function setupInstanceImplementation(
output: '{@}/{workspace}/{branch}/codegen/{api_group_normalized_name}',
},
backups: {
output: '{@}/{workspace}{branch}/backups',
output: '{@}/{workspace}/{branch}/backups',
},
registry: {
output: '{@}/registry',
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ export class Caly extends TypedEmitter<EventMap> {
* });
* ```
*/
async exportBackup({ instance, workspace, branch }): Promise<Record<string, string>> {
async exportBackup({ instance, workspace, branch, outputDir }): Promise<Record<string, string>> {
return exportBackupImplementation({
outputDir,
instance,
workspace,
branch,
Expand Down
8 changes: 7 additions & 1 deletion packages/types/src/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ export interface ConfigStorage {
mkdir(path: string, options?: { recursive?: boolean }): Promise<void>;
readdir(path: string): Promise<string[]>;
writeFile(path: string, data: string | Uint8Array): Promise<void>;
streamToFile(path: string, stream: ReadableStream | NodeJS.ReadableStream): Promise<void>;
streamToFile({
path,
stream,
}: {
path: string;
stream: ReadableStream | NodeJS.ReadableStream;
}): Promise<void>;
readFile(path: string): Promise<string | Uint8Array>;
exists(path: string): Promise<boolean>;

Expand Down