Skip to content
Draft
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
31 changes: 30 additions & 1 deletion __tests__/commands/export-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ describe( 'commands/ExportSQLCommand', () => {
const downloadMock = jest.spyOn( downloadFileModule, 'downloadFile' ).mockResolvedValue();

await exportCommand.run();
expect( stepSuccessSpy ).toHaveBeenCalledWith( 'prepare' );
expect( stepSuccessSpy ).toHaveBeenCalledWith( 'prepare', expect.any( Array ) );
expect( stepSuccessSpy ).toHaveBeenCalledWith( 'create' );
expect( stepSuccessSpy ).toHaveBeenCalledWith( 'confirmEnoughStorage' );
expect( stepSuccessSpy ).toHaveBeenCalledWith( 'downloadLink' );
Expand All @@ -265,6 +265,35 @@ describe( 'commands/ExportSQLCommand', () => {
expect.any( Function )
);
} );

it( 'should return download URL instead of downloading when skipDownload is true', async () => {
const exportCommandWithUrl = new ExportSQLCommand( app, env, { skipDownload: true } );
const stepSuccessSpyUrl = jest.spyOn( exportCommandWithUrl.progressTracker, 'stepSuccess' );
const stepSkippedSpy = jest.spyOn( exportCommandWithUrl.progressTracker, 'stepSkipped' );
const confirmEnoughStorageSpyUrl = jest.spyOn( exportCommandWithUrl, 'confirmEnoughStorage' );
const downloadMock = jest.spyOn( downloadFileModule, 'downloadFile' ).mockResolvedValue();
const consoleLogSpy = jest.spyOn( console, 'log' ).mockImplementation();

confirmEnoughStorageSpyUrl.mockResolvedValue( { continue: true, isPromptShown: false } );

await exportCommandWithUrl.run();

// Should skip confirmEnoughStorage and download steps
expect( stepSkippedSpy ).toHaveBeenCalledWith( 'confirmEnoughStorage' );
expect( stepSkippedSpy ).toHaveBeenCalledWith( 'download' );

// Should not call confirmEnoughStorage or download
expect( confirmEnoughStorageSpyUrl ).not.toHaveBeenCalled();
expect( downloadMock ).not.toHaveBeenCalled();

// Should output the download URL
expect( consoleLogSpy ).toHaveBeenCalledWith( 'Download URL: https://test-backup.sql.gz' );

stepSuccessSpyUrl.mockRestore();
stepSkippedSpy.mockRestore();
confirmEnoughStorageSpyUrl.mockRestore();
consoleLogSpy.mockRestore();
} );
} );

describe( 'liveBackupCopy', () => {
Expand Down
112 changes: 112 additions & 0 deletions docs/export-sql-skip-download.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Export SQL: Skip Download Option

The `vip export sql` command includes a `--skip-download` option that allows you to retrieve the download URL for a database backup without downloading the file. This is useful when you need the URL for automation workflows, want to download the file later, or need to use a different download method.

## Usage

```bash
vip @<app>.<env> export sql --skip-download
```

## Examples

### Get download URL for the latest backup

```bash
vip @example-app.production export sql --skip-download
```

This command will:
1. Prepare the most recent database backup
2. Generate a download URL
3. Display the URL without downloading the file

### Combine with other options

You can combine `--skip-download` with other export options:

```bash
# Get URL for a fresh backup
vip @example-app.production export sql --generate-backup --skip-download

# Get URL for a partial export
vip @example-app.production export sql --table=wp_posts --skip-download

# Get URL for specific sites in a multisite network
vip @example-app.production export sql --site-id=2,3 --skip-download
```

## Output

When using `--skip-download`, the command displays progress information and outputs the download URL:

```
✓ Preparing for backup download
• Attaching to an existing export for the backup with timestamp 2025-11-14T13:21:07.000Z
✓ Creating backup copy
✓ Requesting download link
• Download URL: https://example.org/sql.gz

Download URL:
https://example.org/sql.gz
```

## Use Cases

### Automation workflows

Store the URL in a variable for use in scripts:

```bash
#!/bin/bash
OUTPUT=$(vip @example-app.production export sql --skip-download 2>&1)
URL=$(echo "$OUTPUT" | grep -o 'https://[^[:space:]]*')
echo "Backup URL: $URL"
# Use $URL in your automation workflow
```

### Download with custom tools

Get the URL and use your preferred download tool:

```bash
# Get the URL
vip @example-app.production export sql --skip-download

# Download with wget
wget -O backup.sql.gz "https://example.org/sql.gz"

# Or with curl
curl -o backup.sql.gz "https://example.org/sql.gz"
```

### Integration with cloud storage

Retrieve the URL and upload directly to cloud storage without saving locally:

```bash
# Get the URL
URL=$(vip @example-app.production export sql --skip-download 2>&1 | grep -o 'https://[^[:space:]]*')

# Upload directly to S3
wget -O - "$URL" | aws s3 cp - s3://my-bucket/backups/backup.sql.gz
```

## Important Notes

- The download URL is temporary and will expire after a period of time
- When using `--skip-download`, the command skips:
- Storage space confirmation prompt
- File download
- The output file path option (`--output`) has no effect when `--skip-download` is used
- The backup must still be prepared and a download URL generated, which may take some time depending on backup size

## Related Commands

- [`vip export sql`](https://docs.wpvip.com/vip-cli/export-sql/) - Download database backups
- [`vip backup db`](https://docs.wpvip.com/vip-cli/backup-db/) - Create database backups

## See Also

- [VIP CLI Export SQL Documentation](https://docs.wpvip.com/vip-cli/export-sql/)
- [Database Backups on WordPress VIP](https://docs.wpvip.com/technical-references/backups/)
9 changes: 7 additions & 2 deletions src/bin/vip-export-sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const examples = [
description:
'Generate a fresh database backup for an environment and download an archived copy of that backup.',
},
{
usage: 'vip @example-app.develop export sql --skip-download',
description: 'Skip downloading the database backup file.',
},
{
usage: 'vip @example-app.develop export sql --table=wp_posts --table=wp_comments',
description:
Expand Down Expand Up @@ -96,12 +100,13 @@ command( {
'generate-backup',
'Generate a fresh database backup and export an archived copy of that backup.'
)
.option( 'skip-download', 'Skip downloading the file.' )
.examples( examples )
.argv(
process.argv,
async (
arg,
{ app, env, output, configFile, table, siteId, wpcliCommand, generateBackup }
{ app, env, output, configFile, table, siteId, wpcliCommand, generateBackup, skipDownload }
) => {
const liveBackupCopyCLIOptions = parseLiveBackupCopyCLIOptions(
configFile,
Expand All @@ -121,7 +126,7 @@ command( {
const exportCommand = new ExportSQLCommand(
app,
env,
{ outputFile: output, generateBackup, liveBackupCopyCLIOptions },
{ outputFile: output, generateBackup, liveBackupCopyCLIOptions, skipDownload },
trackerFn
);
await exportCommand.run();
Expand Down
56 changes: 29 additions & 27 deletions src/commands/export-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
PromptStatus,
} from '../lib/backup-storage-availability/backup-storage-availability';
import * as exit from '../lib/cli/exit';
import { formatBytes, getGlyphForStatus } from '../lib/cli/format';
import { formatBytes } from '../lib/cli/format';
import { ProgressTracker } from '../lib/cli/progress';
import { downloadFile, OnProgressCallback } from '../lib/http/download-file';
import * as liveBackupCopy from '../lib/live-backup-copy';
Expand Down Expand Up @@ -221,6 +221,7 @@ export interface ExportSQLOptions {
generateBackup?: boolean;
liveBackupCopyCLIOptions?: LiveBackupCopyCLIOptions;
showMyDumperWarning?: boolean;
skipDownload?: boolean;
}

/**
Expand All @@ -244,6 +245,8 @@ export class ExportSQLCommand {

private readonly showMyDumperWarning: boolean;

private readonly skipDownload: boolean;

/**
* Creates an instance of SQLExportCommand
*
Expand All @@ -265,14 +268,15 @@ export class ExportSQLCommand {
this.progressTracker = new ProgressTracker( [
{ id: this.steps.PREPARE, name: 'Preparing for backup download' },
{ id: this.steps.CREATE, name: 'Creating backup copy' },
{ id: this.steps.CONFIRM_ENOUGH_STORAGE, name: "Checking if there's enough storage" },
{ id: this.steps.DOWNLOAD_LINK, name: 'Requesting download link' },
{ id: this.steps.CONFIRM_ENOUGH_STORAGE, name: "Checking if there's enough storage" },
{ id: this.steps.DOWNLOAD, name: 'Downloading file' },
] );
this.track = trackerFn;
this.liveBackupCopyCLIOptions = options.liveBackupCopyCLIOptions;
this.showMyDumperWarning =
options.showMyDumperWarning === undefined ? true : options.showMyDumperWarning;
this.skipDownload = options.skipDownload || false;
}

/**
Expand Down Expand Up @@ -412,6 +416,17 @@ export class ExportSQLCommand {
size = Number( bytesWrittenMeta.value );
}

const downloadURLString = `\n${ chalk.green( 'Download URL' ) }:\n${ url }\n\n`;

if ( this.skipDownload ) {
this.progressTracker.stepSkipped( this.steps.CONFIRM_ENOUGH_STORAGE );
this.progressTracker.stepSkipped( this.steps.DOWNLOAD );
this.progressTracker.print();
this.progressTracker.stopPrinting();
console.log( downloadURLString );
return;
}

const storageConfirmed = await this.progressTracker.handleContinuePrompt(
async setPromptShown => {
const status = await this.confirmEnoughStorage( Number( size ) );
Expand Down Expand Up @@ -478,39 +493,26 @@ export class ExportSQLCommand {
exit.withError( `No backup found for site ${ this.app.name }` );
}

if ( ! this.generateBackup ) {
console.log(
`${ getGlyphForStatus( 'success' ) } Latest backup found with timestamp ${
latestBackup.createdAt
}`
);
} else {
console.log(
`${ getGlyphForStatus( 'success' ) } Backup created with timestamp ${
latestBackup.createdAt
}`
);
}
const prepareAdditionalInfo = [];

const showMyDumperWarning =
this.showMyDumperWarning && ( latestBackup.sqlDumpTool ?? envSqlDumpTool ) === 'mydumper';
if ( showMyDumperWarning ) {
console.warn(
chalk.yellow.bold( 'WARNING:' ),
chalk.yellow(
'This is a large or complex database. The backup file for this database is generated with MyDumper. ' +
'The file can only be loaded with MyLoader. ' +
'For more information: https://github.com/mydumper/mydumper'
)
prepareAdditionalInfo.push(
`${ chalk.yellow.bold(
'WARNING:'
) } This is a large or complex database. The backup file for this database is generated with MyDumper. The file can only be loaded with MyLoader. For more information: https://github.com/mydumper/mydumper`
);
}

if ( await this.getExportJob() ) {
console.log(
prepareAdditionalInfo.push(
`Attaching to an existing export for the backup with timestamp ${ latestBackup.createdAt }`
);
} else {
console.log( `Exporting database backup with timestamp ${ latestBackup.createdAt }` );
prepareAdditionalInfo.push(
`Exporting database backup with timestamp ${ latestBackup.createdAt }`
);

try {
await createExportJob( this.app.id, this.env.id, latestBackup.id );
Expand Down Expand Up @@ -545,7 +547,7 @@ export class ExportSQLCommand {
this.isPrepared.bind( this )
);

this.progressTracker.stepSuccess( this.steps.PREPARE );
this.progressTracker.stepSuccess( this.steps.PREPARE, prepareAdditionalInfo );

await pollUntil(
this.getExportJob.bind( this ),
Expand All @@ -555,7 +557,7 @@ export class ExportSQLCommand {
this.progressTracker.stepSuccess( this.steps.CREATE );

const url = await generateDownloadLink( this.app.id, this.env.id, latestBackup.id );
this.progressTracker.stepSuccess( this.steps.DOWNLOAD_LINK );
this.progressTracker.stepSuccess( this.steps.DOWNLOAD_LINK, [ url ] );

return url;
}
Expand Down Expand Up @@ -588,7 +590,7 @@ export class ExportSQLCommand {
} );
this.progressTracker.stepSuccess( this.steps.CREATE );

this.progressTracker.stepSuccess( this.steps.DOWNLOAD_LINK );
this.progressTracker.stepSuccess( this.steps.DOWNLOAD_LINK, [ result.url ] );

return result;
} catch ( err ) {
Expand Down
Loading