diff --git a/CHANGELOG.md b/CHANGELOG.md index 1152fad..9888d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,4 +14,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Changed -- Fixed composer 2 issue \ No newline at end of file +- Fixed composer 2 issue + +## 1.4.0 - 2020-12-08 + +### Added + +- Added generic s3-compliant provider (issue #31 on craft-remote-sync) +- Added TTR to queue jobs (issue #38 on craft-remote-sync) + +### Changed + +- Bumped version number for parity between sync & backup plugins +- Added support for transfering files to and from all volume backends, not just local +- Fixed filename regex (issue #26 on craft-remote-sync) +- Moved shared utilities JS and CSS to core module +- Updated the formatting for file table \ No newline at end of file diff --git a/src/assets/RemoteCoreUtility/RemoteCoreUtilityAsset.php b/src/assets/RemoteCoreUtility/RemoteCoreUtilityAsset.php new file mode 100644 index 0000000..8939834 --- /dev/null +++ b/src/assets/RemoteCoreUtility/RemoteCoreUtilityAsset.php @@ -0,0 +1,29 @@ +sourcePath = __DIR__ . '/dist'; + + $this->depends = [ + CpAsset::class, + ]; + + $this->js = [ + 'js/RemoteCoreUtility.js' + ]; + + $this->css = [ + 'css/RemoteCoreUtility.css', + ]; + + parent::init(); + } +} diff --git a/src/assets/RemoteCoreUtility/dist/css/RemoteCoreUtility.css b/src/assets/RemoteCoreUtility/dist/css/RemoteCoreUtility.css new file mode 100644 index 0000000..639f8f1 --- /dev/null +++ b/src/assets/RemoteCoreUtility/dist/css/RemoteCoreUtility.css @@ -0,0 +1,123 @@ +.rb-utilities { + position: relative; + } + + .rb-utilities-section { + position: relative; + } + + .rb-utilities-section:nth-child(2) { + margin-top: 25px; + } + + .rb-utilities-table { + margin-bottom: 10px; + } + + /* Hide Craft's default hover background on table rows */ + table.data.rb-utilities-table tbody tr:not(.disabled):hover th, + table.data.rb-utilities-table tbody tr:not(.disabled):hover td { + background-color: transparent; + } + + /* Hide template rows and collapsed rows */ + .rb-utilities-table tr.default-row, + .rb-utilities-table.rb-utilities-table--collapsed + tr:not(.default-row):nth-child(n + 4) { + display: none; + } + + .rb-utilities-table tbody tr td:first-child { + display: flex; + align-items: center; + padding-left: 0; + } + + /* Row */ + .rb-utilities-row { + + } + + /* "Latest" bubble */ + .rb-utilities-bubble { + border: 1px solid #9aa5b1; + margin: 2px 0 0 10px; + padding: 3px 6px; + border-radius: 3px; + font-size: 11px; + line-height: 1; + color: #9aa5b1; + } + + /* Add green circle to the title */ + .rb-utilities-table tbody tr:not(.default-row) td:first-child:before { + display: block; + height: 10px; + width: 10px; + content: ""; + border-radius: 50%; + background-color: #78c678; + margin-right: 10px; + } + + /* "Show All" row */ + .rb-utilities-table tbody tr.show-all-row td { + padding-top: 0; + padding-bottom: 0; + } + .rb-utilities-table tr.show-all-row td a { + font-size: 12px; + } + + .rb-utilities-submit { + display: flex; + justify-content: flex-start; + align-items: center; + position: relative; + } + + /* Small status spinner */ + .rb-utilities-submit .utility-status { + width: 100px; + margin-left: 20px; + display: flex; + justify-content: flex-start; + align-items: center; + } + + .rb-utilities-submit .utility-status .progressbar { + width: 20%; + left: auto; + top: auto; + } + + .rb-utilities-form select { + min-width: 200px; + } + + .rb-utilities-overlay { + cursor: wait; + background-color: rgba(255, 255, 255, 0.75); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + .rb-utilities-guard { + cursor: not-allowed; + background-color: rgba(255, 255, 255, 0.75); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + \ No newline at end of file diff --git a/src/assets/RemoteCoreUtility/dist/js/RemoteCoreUtility.js b/src/assets/RemoteCoreUtility/dist/js/RemoteCoreUtility.js new file mode 100644 index 0000000..d89b430 --- /dev/null +++ b/src/assets/RemoteCoreUtility/dist/js/RemoteCoreUtility.js @@ -0,0 +1,237 @@ +(function ($) { + Craft.RemoteCoreUtility = Garnish.Base.extend({ + init: function (id) { + this.$element = $("#" + id); + + if (this.$element.length <= 0) { + return; + } + + this.$form = $("form", this.$element); + this.$table = $("table", this.$element); + this.$tbody = $("tbody", this.$table); + this.$showAllRow = $(".show-all-row", this.$tbody); + this.$submit = $("input.submit", this.$form); + this.$loadingOverlay = $(".rb-utilities-overlay", this.$element); + + this.label = this.$element.attr("data-label"); + this.labelPlural = this.$element.attr("data-label-plural"); + this.pullMessage = this.$element.attr("data-pull-message"); + this.deleteMessage = this.$element.attr("data-delete-message"); + + this.listActionUrl = this.$table.attr("data-list-action"); + this.pushActionUrl = this.$table.attr("data-push-action"); + this.pullActionUrl = this.$table.attr("data-pull-action"); + this.deleteActionUrl = this.$table.attr("data-delete-action"); + + this.csrfToken = this.$form.find('input[name="CRAFT_CSRF_TOKEN"]').val(); + + this.$form.on("submit", this.push.bind(this)); + + // Show all rows + this.$showAllRow.find("a").on( + "click", + function (e) { + this.$showAllRow.hide(); + this.$table.removeClass("rb-utilities-table--collapsed"); + e.preventDefault(); + }.bind(this) + ); + + this.list(); + }, + + clearTable: function () { + this.$tbody + .find("tr") + .filter(function (i, row) { + return !$(row).hasClass("default-row"); + }) + .remove(); + }, + + showLoading: function () { + this.$loadingOverlay.fadeIn(); + }, + + hideLoading: function () { + this.$loadingOverlay.fadeOut(); + }, + + hideTableNoResults: function () { + this.$tbody.find(".no-results-row").hide(); + }, + + showTableNoResults: function () { + this.$tbody.find(".no-results-row").show(); + }, + + hideTableErrors: function () { + this.$tbody.find(".errors-row").hide(); + }, + + showTableErrors: function () { + this.$tbody.find(".errors-row").show(); + }, + + updateTable: function (options, error) { + if (error) { + this.showTableErrors(); + return false; + } + + if (options.length <= 0) { + this.showTableNoResults(); + return false; + } + + // Backups are ordered newest to oldest ([0] = most recent) but we + // prepend them instead of append them to make it easier to style + for (var i = options.length - 1; i >= 0; i--) { + var $row = this.$tbody + .find(".template-row") + .clone() + .removeClass("template-row default-row"); + + var $td = $row.find("td:first"); + $td.addClass("rb-utilities-row"); + + // Add date + var $span = $("").text(options[i].text).attr("title", options[i].title); + $td.append($span); + + // Add "latest" bubble + if (i === 0) { + $td.append($("", { + class: "rb-utilities-bubble" + }).text("latest")); + } else { + $row.removeClass("first"); + } + + // Add "Pull" button + var $pullButton = $row.find(".pull-button"); + if ($pullButton.length > 0) { + this.addListener( + $row.find(".pull-button"), + "click", + this.pull.bind(this, options[i].filename) + ); + } + + // Add "Delete" button + var $deleteButton = $row.find(".delete-button"); + if ($deleteButton.length > 0) { + this.addListener( + $row.find(".delete-button"), + "click", + this.delete.bind(this, options[i].filename) + ); + } + + this.$tbody.prepend($row); + } + + if (options.length > 3) { + this.$showAllRow.show(); + } + + return true; + }, + + /** + * Push a database/volume + */ + push: function (ev) { + if (ev) { + ev.preventDefault(); + } + this.post(this.pushActionUrl); + }, + + /** + * Pull a database/volume + */ + pull: function (filename, ev) { + if (ev) { + ev.preventDefault(); + } + var yes = confirm(this.pullMessage); + if (yes) { + this.post(this.pullActionUrl, { + filename: filename, + }); + } + }, + + /** + * Delete a database/volume + */ + delete: function (filename, ev) { + if (ev) { + ev.preventDefault(); + } + var yes = confirm(this.deleteMessage); + if (yes) { + this.post(this.deleteActionUrl, { + filename: filename, + }); + } + }, + + /** + * Get and list database/volumes + */ + list: function () { + this.clearTable(); + this.showLoading(); + $.get({ + url: Craft.getActionUrl(this.listActionUrl), + dataType: "json", + success: function (response) { + if (response["success"]) { + this.updateTable(response["options"]); + } else { + var message = "Error fetching files"; + if (response["error"]) { + message = response["error"]; + } + this.updateTable([], message); + Craft.cp.displayError(message); + } + }.bind(this), + complete: function () { + this.hideLoading(); + }.bind(this), + error: function (error) { + this.updateTable([], true); + Craft.cp.displayError("Error fetching files"); + }.bind(this), + }); + }, + + post: function (action, data = {}) { + var postData = Object.assign(data, { + CRAFT_CSRF_TOKEN: this.csrfToken, + }); + var url = Craft.getActionUrl(action); + this.showLoading(); + Craft.postActionRequest( + url, + postData, + function (response) { + if (response["success"]) { + window.location.reload(); + } else { + var message = "Error fetching files"; + if (response["error"]) { + message = response["error"]; + } + this.updateTable([], message); + Craft.cp.displayError(message); + } + }.bind(this) + ); + }, + }); +})(jQuery); diff --git a/src/helpers/RemoteFile.php b/src/helpers/RemoteFile.php index d817a5a..f1c8cbb 100644 --- a/src/helpers/RemoteFile.php +++ b/src/helpers/RemoteFile.php @@ -2,6 +2,7 @@ namespace weareferal\remotecore\helpers; +use weareferal\remotecore\RemoteCoreModule; use weareferal\remotecore\helpers\TimeHelper; /** @@ -14,7 +15,6 @@ class RemoteFile { public $filename; public $datetime; - public $label; public $env; // Regex to capture/match: @@ -24,25 +24,15 @@ class RemoteFile // - Random string // - Version // - Extension - private static $regex = '/^(?:[a-zA-Z0-9\-]+)\_(?:([a-zA-Z0-9\-]+)\_)?(\d{6}\_\d{6})\_(?:[a-zA-Z0-9]+)\_(?:[v0-9\.]+)\.(?:\w{2,10})$/'; + private static $regex = '/^(?:[a-zA-Z0-9\-]+)\_(?:([a-zA-Z0-9\-]+)\_)?(\d{6}\_\d{6})\_(?:[a-zA-Z0-9]+)\_(?:[va-zA-Z0-9\.\-]+)\.(?:\w{2,10})$/'; - public function __construct($_filename) + public function __construct($filename) { // Extract values from filename - preg_match(RemoteFile::$regex, $_filename, $matches); - $env = $matches[1]; - $date = $matches[2]; - $datetime = date_create_from_format('ymd_Gis', $date); - $timesince = TimeHelper::time_since($datetime->getTimestamp()); - $label = $datetime->format('Y-m-d H:i:s'); - if ($env) { - $label = $label . ' (' . $env . ')'; - } - $this->filename = $_filename; - $this->datetime = $datetime; - $this->label = $label; - $this->timesince = $timesince; - $this->env = $env; + preg_match(RemoteFile::$regex, $filename, $matches); + $this->filename = $filename; + $this->datetime = date_create_from_format('ymd_Gis', $matches[2]); + $this->env = $matches[1]; } public static function createArray($filenames) { @@ -59,12 +49,16 @@ public static function createArray($filenames) { return array_reverse($files); } - public static function toHTMLOptions($array) { + public static function toHTMLOptions($array, $format="Y-m-d H:i:s") { $options = []; foreach ($array as $i => $file) { + $timesince = TimeHelper::time_since($file->datetime->getTimestamp()); + $title = $file->filename . " (" . $timesince . " ago)"; + $text = $file->datetime->format($format); $options[$i] = [ - "label" => $file->label, - "value" => $file->filename + "text" => $text, + "title" => $title, + "filename" => $file->filename ]; } return $options; diff --git a/src/models/Settings.php b/src/models/Settings.php index c9b3a10..7dee00b 100644 --- a/src/models/Settings.php +++ b/src/models/Settings.php @@ -49,16 +49,30 @@ abstract class Settings extends Model public $doSpacesName; public $doSpacesPath; + // Other S3 compliant configuration settings + public $otherS3AccessKey; + public $otherS3SecretKey; + public $otherS3Endpoint; + public $otherS3RegionName; + public $otherS3BucketName; + public $otherS3BucketPath; + // Use the build-in Craft queue to handle all operations. This is useful // when you have large database of volume backups that need to be run where // a regular non-async request might timeout public $useQueue = false; + // https://github.com/yiisoft/yii2-queue/blob/master/docs/guide/retryable.md#retryablejobinterface + public $queueTtr = "300"; // Show/hide either databases or volume backups in the utilities panel. // This is useful for cleaning up the interface public $hideDatabases = false; public $hideVolumes = false; + public $displayDateFormat = "Y-m-d H:i:s"; + + + public function rules(): array { return [ @@ -103,6 +117,14 @@ public function rules(): array return $model->cloudProvider == 'do' & $model->enabled == 1; } ], + [ + ['otherS3AccessKey', 'otherS3SecretKey', 'otherS3BucketName', 'otherS3RegionName', 'otherS3Endpoint'], + 'required', + 'when' => function ($model) { + return $model->cloudProvider == 'other-s3' & $model->enabled == 1; + } + ], + [ [ 'cloudProvider', @@ -111,6 +133,7 @@ public function rules(): array 'googleClientId', 'googleClientSecret', 'googleProjectName', 'googleAuthRedirect', 'googleDriveFolderId', 'dropboxAppKey', 'dropboxSecretKey', 'dropboxAccessToken', 'dropboxFolder', 'doAccessKey', 'doSecretKey', 'doSpacesName', 'doRegionName', 'doSpacesPath', + 'displayDateFormat', 'queueTtr' ], 'string' ], diff --git a/src/services/ProviderFactory.php b/src/services/ProviderFactory.php index da257c1..79e79bb 100644 --- a/src/services/ProviderFactory.php +++ b/src/services/ProviderFactory.php @@ -6,6 +6,7 @@ use weareferal\remotecore\services\providers\DropboxProvider; use weareferal\remotecore\services\providers\GoogleDriveProvider; use weareferal\remotecore\services\providers\DigitalOceanProvider; +use weareferal\remotecore\services\providers\OtherS3Provider; use Craft; use craft\base\Component; @@ -36,6 +37,9 @@ public function create($plugin) { case "do": $ProviderClass = DigitalOceanProvider::class; break; + case "other-s3": + $ProviderClass = OtherS3Provider::class; + break; } return new $ProviderClass($plugin); } diff --git a/src/services/ProviderService.php b/src/services/ProviderService.php index 3843592..b7b1c8d 100644 --- a/src/services/ProviderService.php +++ b/src/services/ProviderService.php @@ -128,20 +128,36 @@ public function pushDatabase() */ public function pushVolumes(): string { + Craft::debug("Pushing volumes (zip and push)", "remote-core"); + + $time1 = microtime(true); + + // Copy volume files to tmp folder and zip it up + $zipFilename = $this->createFilename(); + $tmpDir = $this->copyVolumeFiles(); + $zipPath = $this->createVolumesZip($zipFilename, $tmpDir); + $this->rmDir($tmpDir); + Craft::debug("- time to create volume zip:" . (string) (microtime(true) - $time1) . " seconds", "remote-core"); + Craft::debug('- new volume zip path:' . $zipPath, 'remote-core'); + + // Push zip to remote destination + $time2 = microtime(true); + $this->push($zipPath); + Craft::debug("- time to push volume zip: " . (string) (microtime(true) - $time2) . " seconds", "remote-core"); + + // Keep or delete the created zip file $settings = $this->getSettings(); - $filename = $this->createFilename(); - $path = $this->createVolumesZip($filename); - - Craft::debug('New volume zip path:' . $path, 'remote-core'); - - $this->push($path); - if (! property_exists($settings, 'keepLocal') || ! $settings->keepLocal) { - Craft::debug('Deleting local database sql file:' . $path, 'remote-core'); - unlink($path); + Craft::debug('- deleting tmp local volume file:' . $zipPath, 'remote-core'); + if (file_exists($zipPath)) { + unlink($zipPath); + } else { + Craft::debug('- file does not exist: ' . $zipPath, 'remote-core'); + } } + Craft::debug("- total time to create and push volume zip: " . (string) (microtime(true) - $time1) . " seconds", "remote-core"); - return $filename; + return $zipFilename; } /** @@ -180,16 +196,18 @@ public function pullDatabase($filename) */ public function pullVolume($filename) { - // Before pulling volumes, backup the local + // Before pulling volumes, create an emergency backup $settings = $this->getSettings(); if (property_exists($settings, 'keepEmergencyBackup') && $settings->keepEmergencyBackup) { - $this->createVolumesZip("emergency-backup"); + $tmpDir = $this->copyVolumeFiles(); + $zipPath = $this->createVolumesZip("emergency-backup", $tmpDir); + $this->rmDir($tmpDir); } - $path = $this->getLocalDir() . DIRECTORY_SEPARATOR . $filename; - $this->pull($filename, $path); - $this->restoreVolumesZip($path); - unlink($path); + $zipPath = $this->getLocalDir() . DIRECTORY_SEPARATOR . $filename; + $this->pull($filename, $zipPath); + $this->restoreVolumesZip($zipPath); + unlink($zipPath); } /** @@ -218,6 +236,46 @@ public function deleteVolume($filename) $this->delete($filename); } + /** + * Copy volume files from their source to the local temp folder + * + * @return string $path to the temporary directory containing the volumes + */ + private function copyVolumeFiles(): string + { + Craft::debug("Copying volume files to temp directory", "remote-core"); + + $volumes = Craft::$app->getVolumes()->getAllVolumes(); + $tmpDirName = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . strtolower(StringHelper::randomString(10)); + Craft::debug("-- tmp path: " . $tmpDirName, "remote-core"); + + if (count($volumes) <= 0) { + Craft::debug("-- no volumes configured, skipping zipping", "remote-core"); + return null; + } + + $time = microtime(true); + foreach ($volumes as $volume) { + $fileUris = $volume->getFileList('/', true); // all files in the volume + $tmpPath = $tmpDirName . DIRECTORY_SEPARATOR . $volume->handle; + if (! file_exists($tmpPath)) { + mkdir($tmpPath, 0777, true); + } + foreach ($fileUris as $fileUri=>$file) { + $localPath = $tmpPath . DIRECTORY_SEPARATOR . $file['path']; + if($file['type'] == "file") { + $volume->saveFileLocally($fileUri, $localPath); + } else { + mkdir($localPath, 0777); + } + } + + } + Craft::debug("- time to copy volume files to local temp folder:" . (string) (microtime(true) - $time) . " seconds", "remote-core"); + + return $tmpDirName; + } + /** * Create volumes zip * @@ -227,27 +285,22 @@ public function deleteVolume($filename) * @return string $path the temporary path to the new zip file * @since 1.0.0 */ - private function createVolumesZip($filename): string + private function createVolumesZip($filename, $tmpDir): string { $path = $this->getLocalDir() . DIRECTORY_SEPARATOR . $filename . '.zip'; + + Craft::debug("Creating zip from tmp volume files", "remote-core"); + Craft::debug("-- tmp dir: " . $tmpDir, "remote-core"); + Craft::debug("-- zip path: " . $path, "remote-core"); + if (file_exists($path)) { + Craft::debug("-- old zip file exists, deleting...", "remote-core"); unlink($path); } - $volumes = Craft::$app->getVolumes()->getAllVolumes(); - $tmpDirName = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . strtolower(StringHelper::randomString(10)); + Craft::debug("-- recursively zipping tmp directory", "remote-core"); + ZipHelper::recursiveZip($tmpDir, $path); - if (count($volumes) <= 0) { - return null; - } - - foreach ($volumes as $volume) { - $tmpPath = $tmpDirName . DIRECTORY_SEPARATOR . $volume->handle; - FileHelper::copyDirectory($volume->rootPath, $tmpPath); - } - - ZipHelper::recursiveZip($tmpDirName, $path); - FileHelper::clearDirectory(Craft::$app->getPath()->getTempPath()); return $path; } @@ -260,24 +313,41 @@ private function createVolumesZip($filename): string * @param string $path the path to the zip file to restore * @since 1.0.0 */ - private function restoreVolumesZip($path) + private function restoreVolumesZip($zipPath) { $volumes = Craft::$app->getVolumes()->getAllVolumes(); - $tmpDirName = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . strtolower(StringHelper::randomString(10)); + $tmpDir = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . strtolower(StringHelper::randomString(10)); + + Craft::debug("Restoring volume files", "remote-core"); + Craft::debug("-- tmp dir: " . $tmpDir, "remote-core"); + Craft::debug("-- zip path: " . $zipPath, "remote-core"); - ZipHelper::unzip($path, $tmpDirName); + // Unzip files to temp folder + ZipHelper::unzip($zipPath, $tmpDir); - $folders = array_diff(scandir($tmpDirName), array('.', '..')); - foreach ($folders as $folder) { + // Copy all files to the volume + $dirs = array_diff(scandir($tmpDir), array('.', '..')); + foreach ($dirs as $dir) { + Craft::debug("-- unzipped folder: " . $dir, "remote-core"); foreach ($volumes as $volume) { - if ($folder == $volume->handle) { - $dest = $tmpDirName . DIRECTORY_SEPARATOR . $folder; - if (!file_exists($volume->rootPath)) { - FileHelper::createDirectory($volume->rootPath); - } else { - FileHelper::clearDirectory($volume->rootPath); + if ($dir == $volume->handle) { + // Send to volume backend + $absDir = $tmpDir . DIRECTORY_SEPARATOR . $dir; + $files = FileHelper::findFiles($absDir); + foreach ($files as $file) { + Craft::debug("-- " . $file, "remote-core"); + if (is_file($file)) { + $relPath = str_replace($tmpDir . DIRECTORY_SEPARATOR . $volume->handle, '', $file); + $stream = fopen($file, 'r'); + if ($volume->fileExists($relPath)) { + $volume->updateFileByStream($relPath, $stream, []); + } else { + $volume->createFileByStream($relPath, $stream, []); + } + fclose($stream); + } + } - FileHelper::copyDirectory($dest, $volume->rootPath); } } } @@ -371,6 +441,11 @@ protected function getSettings() return $this->plugin->getSettings(); } - + private function rmDir($dir) { + FileHelper::clearDirectory($dir); + if (file_exists($dir)) { + rmdir($dir); + } + } } diff --git a/src/services/providers/OtherS3Provider.php b/src/services/providers/OtherS3Provider.php new file mode 100644 index 0000000..0f8dee0 --- /dev/null +++ b/src/services/providers/OtherS3Provider.php @@ -0,0 +1,38 @@ +plugin->settings->otherS3Endpoint); + } + + protected function getAccessKey(): ?string { + return Craft::parseEnv($this->plugin->settings->otherS3AccessKey); + } + + protected function getSecretKey(): ?string { + return Craft::parseEnv($this->plugin->settings->otherS3SecretKey); + } + + protected function getRegionName(): ?string { + return Craft::parseEnv($this->plugin->settings->otherS3RegionName); + } + + protected function getBucketName(): ?string { + return Craft::parseEnv($this->plugin->settings->otherS3BucketName); + } + + protected function getBucketPath(): ?string { + return Craft::parseEnv($this->plugin->settings->otherS3BucketPath); + } +} diff --git a/src/templates/settings.twig b/src/templates/settings.twig index bb06b5c..7533145 100644 --- a/src/templates/settings.twig +++ b/src/templates/settings.twig @@ -23,7 +23,8 @@ { label: 'Backblaze B2', value: 'b2' }, { label: 'Google Drive', value: 'google' }, { label: 'Dropbox', value: 'dropbox' }, - { label: 'Digital Ocean Spaces', value: 'do' } + { label: 'Digital Ocean Spaces', value: 'do' }, + { label: 'Other S3-Compliant Provider', value: 'other-s3' } ], value: settings.cloudProvider, required: true, @@ -377,6 +378,90 @@ }) }} +{# Other S3 compliant #} +
+ {{ forms.autosuggestField({ + label: "Access Key"|t('remote-core'), + instructions: "The access key associated with your account"|t('remote-core'), + name: 'otherS3AccessKey', + id: 'otherS3AccessKey', + suggestEnvVars: true, + suggestAliases: true, + value: settings.otherS3AccessKey, + required: (settings.cloudProvider == 'other-s3'), + type: 'password', + errors: settings.getErrors('otherS3AccessKey'), + warning: configWarning('otherS3AccessKey', pluginHandle) + }) }} + + {{ forms.autosuggestField({ + label: "Secret Key", + instructions: "The secret key associated with your account"|t('remote-core'), + name: 'otherS3SecretKey', + id: 'otherS3SecretKey', + suggestEnvVars: true, + suggestAliases: true, + value: settings.otherS3SecretKey, + required: (settings.cloudProvider == 'other-s3'), + type: 'password', + errors: settings.getErrors('otherS3SecretKey'), + warning: configWarning('otherS3SecretKey', pluginHandle) + }) }} + + {{ forms.autosuggestField({ + label: "Endpoint"|t('remote-core'), + instructions: "The endpoint for the S3-compliant provider"|t('remote-core'), + name: 'otherS3Endpoint', + id: 'otherS3Endpoint', + suggestEnvVars: true, + suggestAliases: true, + value: settings.otherS3Endpoint, + required: (settings.cloudProvider == 'other-s3'), + errors: settings.getErrors('otherS3Endpoint'), + warning: configWarning('otherS3Endpoint', pluginHandle) + }) }} + + {{ forms.autosuggestField({ + label: "Bucket Name"|t('remote-core'), + instructions: "The name of the bucket you want to send backups to"|t('remote-core'), + name: 'otherS3BucketName', + id: 'otherS3BucketName', + suggestEnvVars: true, + suggestAliases: true, + value: settings.otherS3BucketName, + placeholder: "my-craft-backups", + required: (settings.cloudProvider == 'other-s3'), + errors: settings.getErrors('otherS3BucketName'), + warning: configWarning('otherS3BucketName', pluginHandle) + }) }} + + {{ forms.autosuggestField({ + label: "Region Name"|t('remote-core'), + instructions: "The region your bucket is in"|t('remote-core'), + name: 'otherS3RegionName', + id: 'otherS3RegionName', + suggestEnvVars: true, + suggestAliases: true, + value: settings.otherS3RegionName, + required: (settings.cloudProvider == 'other-s3'), + errors: settings.getErrors('otherS3RegionName'), + warning: configWarning('otherS3RegionName', pluginHandle) + }) }} + + {{ forms.autosuggestField({ + label: "Bucket Path"|t('remote-core'), + instructions: "A path within your bucket to prefix your backups with. Do not include a leading or trailing slash"|t('remote-core'), + name: 'otherS3BucketPath', + id: 'otherS3BucketPath', + suggestEnvVars: true, + suggestAliases: true, + value: settings.otherS3BucketPath, + placeholder: "craft-backups/my-site", + errors: settings.getErrors('otherS3BucketPath'), + warning: configWarning('otherS3BucketPath', pluginHandle) + }) }} +
+
{{ forms.lightswitchField({ @@ -389,6 +474,17 @@ warning: configWarning('useQueue', pluginHandle) }) }} +{{ forms.textField({ + label: "Queue 'Time To Reserve'"|t('remote-core'), + type: "text", + instructions: "Max time for job execution"|t('remote-core'), + name: 'queueTtr', + id: 'queueTtr', + value: settings.queueTtr, + errors: settings.getErrors('queueTtr'), + warning: configWarning('queueTtr', 'remote-core') +}) }} +
{% block customSettings %}{% endblock %} @@ -413,4 +509,17 @@ on: settings.hideVolumes, errors: settings.getErrors('hideVolumes'), warning: configWarning('hideVolumes', pluginHandle) +}) }} + +
+ +{{ forms.textField({ + label: "Display Date Format"|t('remote-core'), + type: "text", + instructions: "The date format to use to display files in the utilities section"|t('remote-core'), + name: 'displayDateFormat', + id: 'displayDateFormat', + value: settings.displayDateFormat, + errors: settings.getErrors('displayDateFormat'), + warning: configWarning('displayDateFormat', 'remote-core') }) }} \ No newline at end of file