Skip to content
Open
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
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,55 @@ Nested properties should use dot notation, for example, `database.server`.
# Start an SSH session as the site user
spinupwp sites:ssh <site_id>

# Connect a Git repository
spinupwp sites:git:connect <site_id> --repo=<repo> --branch=<branch> --push-to-deploy

# Connect a Git repository using a unique deploy key for the site
spinupwp sites:git:connect <site_id> --repo=<repo> --branch=<branch> \
--private-key=~/.ssh/deploy_key --public-key=~/.ssh/deploy_key.pub

# Update Git settings
spinupwp sites:git:update <site_id> --branch=production --push-to-deploy=false

# Disconnect Git
spinupwp sites:git:disconnect <site_id>

# Enable, update, or disable the page cache
spinupwp sites:page-cache:enable <site_id>
spinupwp sites:page-cache:update <site_id> --duration=1 --duration-unit=h --path-exclusion=/cart/ --path-exclusion=/checkout/
spinupwp sites:page-cache:disable <site_id>

# Update Nginx settings
spinupwp sites:nginx <site_id> --uploads-directory-protected=true --xmlrpc-protected=true

# Enable, update, or disable the server-level WP cron
spinupwp sites:cron:enable <site_id> --interval=5
spinupwp sites:cron:update <site_id> --interval=15
spinupwp sites:cron:disable <site_id>

# Enable, update, or disable basic auth
spinupwp sites:basic-auth:enable <site_id> --username=<username> --password=<password>
spinupwp sites:basic-auth:update <site_id> --username=<username>
spinupwp sites:basic-auth:disable <site_id>

# Manage path redirects
spinupwp sites:redirects:list <site_id>
spinupwp sites:redirects:add <site_id> --from=/old-path --to=/new-path --type=permanent
spinupwp sites:redirects:delete <site_id> --from=/old-path --to=/new-path

# Update backup settings
spinupwp sites:backups:settings <site_id> --storage-provider-id=1 --bucket=<bucket> --region=nyc3 --exclude-path=node_modules

# Update the backup schedule (daily at 2am and 2pm, monthly on the first Monday at 4am)
spinupwp sites:backups:schedule <site_id> --daily-times=2,14 --daily-database=true --daily-files=true \
--daily-retention=30 --monthly-days=first:1 --monthly-time=4

# Disable weekly backups
spinupwp sites:backups:schedule <site_id> --weekly-days=none

# Update the site user's SSH authentication
spinupwp sites:site-user <site_id> --authentication=publickey --ssh-key-id=1 --ssh-key-id=2

You can pass any properties of the [Site Schema](https://api.spinupwp.com/?shell#tocS_Site) to the `--fields` flag.
Nested properties should use dot notation, for example, `backups.next_run_time` or `git.branch`.

Expand All @@ -130,11 +179,26 @@ Nested properties should use dot notation, for example, `backups.next_run_time`
--wp-admin-email="<wordpress_admin_user_email>" --wp-admin-user="<wordpress_admin_user_username>" \
--wp-admin-pass="<wordpress_admin_user_password>" --php-version="<php_version>" --page-cache-enabled

Update commands only change the settings you pass. Boolean flags accept `true` or `false`, and repeatable flags such as `--path-exclusion` can be passed multiple times.

A full reference of the available flags is available anytime.

# Display available Arguments and Options for site:create
spinupwp sites:create --help

### SSH Keys

# Get SpinupWP's SSH key
spinupwp ssh-key:get

# List your team's SSH keys
spinupwp ssh-key:list

### Storage Providers

# List your team's storage providers
spinupwp storage-providers:list

## Upgrade

To update the SpinupWP CLI to the latest version, run:
Expand Down
2 changes: 1 addition & 1 deletion app/Commands/BaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ protected function profile(): string
protected function applyValidationLabel(string $key): string
{
if (empty($this->validationLabels) || !array_key_exists($key, $this->validationLabels)) {
return Str::title(str_replace('_', ' ', $key));
return Str::title(str_replace(['_', '.'], ' ', $key));
}

return $this->validationLabels[$key];
Expand Down
45 changes: 45 additions & 0 deletions app/Commands/Concerns/HasSiteIdParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Commands\Concerns;

use SpinupWp\Resources\Site;

trait HasSiteIdParameter
{
protected function selectSite(string $question, ?callable $filter = null, string $noSitesMessage = 'No sites found.'): ?Site
{
$siteId = $this->argument('site_id');

if (empty($siteId)) {
$siteId = $this->askToSelectSite($question, $filter);
}

if (empty($siteId)) {
$this->warn($noSitesMessage);
return null;
}

return $this->spinupwp->getSite((int) $siteId);
}

/**
* @param int|string|null $eventId
*/
protected function displaySiteEvent(Site $site, $eventId, string $queuedMessage, string $savedMessage = ''): void
{
if (is_null($eventId)) {
$this->successfulStep($savedMessage ?: $queuedMessage);
return;
}

$this->successfulStep($queuedMessage);

$this->stepTable([
'Event ID',
'Site',
], [[
$eventId,
$site->domain,
]]);
}
}
2 changes: 1 addition & 1 deletion app/Commands/Concerns/InteractsWithIO.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ protected function toTable($resource): void

$resource->each(function ($item) use (&$rows) {
if (!is_array($item)) {
$item->toArray();
$item = $item->toArray();
}

$row = array_map(function ($value) {
Expand Down
99 changes: 99 additions & 0 deletions app/Commands/Concerns/ParsesOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace App\Commands\Concerns;

use Exception;

trait ParsesOptions
{
protected function stringOption(string $name): ?string
{
$value = $this->option($name);

return is_string($value) ? $value : null;
}

protected function intOption(string $name): ?int
{
$value = $this->stringOption($name);

if (is_null($value)) {
return null;
}

if (!is_numeric($value) || (string) (int) $value !== $value) {
throw new Exception("The --{$name} option must be an integer.");
}

return (int) $value;
}

protected function boolOption(string $name): ?bool
{
$value = $this->stringOption($name);

if (is_null($value)) {
return null;
}

$bool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);

if (is_null($bool)) {
throw new Exception("The --{$name} option must be true or false.");
}

return $bool;
}

/**
* Parse a comma-separated list of integers. Passing "none" returns an empty list.
*/
protected function intListOption(string $name): ?array
{
$value = $this->stringOption($name);

if (is_null($value)) {
return null;
}

if ($value === 'none') {
return [];
}

return array_map(function ($item) use ($name) {
$item = trim($item);

if (!is_numeric($item) || (string) (int) $item !== $item) {
throw new Exception("The --{$name} option must be a comma-separated list of integers.");
}

return (int) $item;
}, explode(',', $value));
}

/**
* Join the values of a repeatable option into a newline-separated string.
*/
protected function multilineOption(string $name): ?string
{
$values = $this->option($name);

if (!is_array($values) || empty($values)) {
return null;
}

return implode("\n", array_filter($values, fn ($value) => is_string($value) && $value !== ''));
}

protected function withoutNulls(array $payload): array
{
return array_filter($payload, fn ($value) => !is_null($value));
}

protected function ensureOptionsProvided(array $payload): void
{
if (empty($payload)) {
throw new Exception("At least one option must be provided. Run '{$this->getName()} --help' to see the available options.");
}
}
}
109 changes: 109 additions & 0 deletions app/Commands/Sites/Backups/ScheduleCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

namespace App\Commands\Sites\Backups;

use App\Commands\BaseCommand;
use App\Commands\Concerns\HasSiteIdParameter;
use App\Commands\Concerns\ParsesOptions;
use Exception;

class ScheduleCommand extends BaseCommand
{
use HasSiteIdParameter;
use ParsesOptions;

protected const WEEKS_OF_MONTH = ['first', 'second', 'third', 'last'];

protected $signature = 'sites:backups:schedule
{site_id? : The site to update}
{--daily-times= : Comma-separated hours (0-23) when daily backups run, or "none" to disable}
{--daily-database= : Back up the database daily (true or false)}
{--daily-files= : Back up site files daily (true or false)}
{--daily-retention= : Days to retain daily backups, or "none"}
{--weekly-days= : Comma-separated weekdays (0 = Sunday to 6 = Saturday) when weekly backups run, or "none" to disable}
{--weekly-time= : The hour (0-23) at which weekly backups run}
{--weekly-database= : Back up the database weekly (true or false)}
{--weekly-files= : Back up site files weekly (true or false)}
{--weekly-retention= : Days to retain weekly backups, or "none"}
{--monthly-days= : Comma-separated week:weekday pairs (e.g. first:1,last:5) when monthly backups run, or "none" to disable}
{--monthly-time= : The hour (0-23) at which monthly backups run}
{--monthly-database= : Back up the database monthly (true or false)}
{--monthly-files= : Back up site files monthly (true or false)}
{--monthly-retention= : Days to retain monthly backups, or "none"}
{--profile= : The SpinupWP configuration profile to use}';

protected $description = 'Update the backup schedule for a site';

protected function action(): int
{
$payload = array_filter([
'daily_schedule' => $this->schedulePayload('daily', [
'time_of_day' => $this->intListOption('daily-times'),
]),
'weekly_schedule' => $this->schedulePayload('weekly', [
'day_of_week' => $this->intListOption('weekly-days'),
'time_of_day' => $this->intOption('weekly-time'),
]),
'monthly_schedule' => $this->schedulePayload('monthly', [
'day_of_month' => $this->monthlyDaysOption(),
'time_of_day' => $this->intOption('monthly-time'),
]),
]);

$this->ensureOptionsProvided($payload);

$site = $this->selectSite('Which site would you like to update the backup schedule for');

if (is_null($site)) {
return self::SUCCESS;
}

$site->updateBackupSchedule($payload);

$this->successfulStep('Backup schedule updated.');

return self::SUCCESS;
}

protected function schedulePayload(string $frequency, array $timing): array
{
$schedule = $this->withoutNulls(array_merge($timing, [
'backup_database' => $this->boolOption("{$frequency}-database"),
'backup_files' => $this->boolOption("{$frequency}-files"),
]));

if ($this->stringOption("{$frequency}-retention") === 'none') {
$schedule['retention_period'] = null;
} elseif (!is_null($retention = $this->intOption("{$frequency}-retention"))) {
$schedule['retention_period'] = $retention;
}

return $schedule;
}

protected function monthlyDaysOption(): ?array
{
$value = $this->stringOption('monthly-days');

if (is_null($value)) {
return null;
}

if ($value === 'none') {
return [];
}

return array_map(function ($pair) {
$parts = explode(':', trim($pair));

if (count($parts) !== 2 || !in_array($parts[0], self::WEEKS_OF_MONTH, true) || !ctype_digit($parts[1])) {
throw new Exception('The --monthly-days option must be a comma-separated list of week:weekday pairs, for example "first:1,last:5".');
}

return [
'week_of_month' => $parts[0],
'day_of_week' => (int) $parts[1],
];
}, explode(',', $value));
}
}
Loading
Loading