diff --git a/admin/src/components/ActionButtons/index.jsx b/admin/src/components/ActionButtons/index.jsx index 3cc94f1..b69f18d 100644 --- a/admin/src/components/ActionButtons/index.jsx +++ b/admin/src/components/ActionButtons/index.jsx @@ -8,7 +8,7 @@ import { getFetchClient, useNotification } from '@strapi/strapi/admin'; import { useIntl } from 'react-intl'; import ConfirmModal from '../ConfirmModal'; -import { exportAllConfig, importAllConfig } from '../../state/actions/Config'; +import { exportAllConfig, importAllConfig, downloadZip } from '../../state/actions/Config'; const ActionButtons = () => { const { post, get } = getFetchClient(); @@ -40,6 +40,7 @@ const ActionButtons = () => { {!isEmpty(partialDiff) && ( {Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"} )} + ); }; @@ -52,6 +53,9 @@ const ActionButtonsStyling = styled.div` > button { margin-right: 10px; } + > button:last-of-type { + margin-left: auto; + } `; export default ActionButtons; diff --git a/admin/src/helpers/blob.js b/admin/src/helpers/blob.js new file mode 100644 index 0000000..f555c5c --- /dev/null +++ b/admin/src/helpers/blob.js @@ -0,0 +1,9 @@ +export function b64toBlob(dataURI, type) { + const byteString = atob(dataURI); + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + return new Blob([ab], { type }); +} diff --git a/admin/src/state/actions/Config.js b/admin/src/state/actions/Config.js index ef1115e..73dbb0d 100644 --- a/admin/src/state/actions/Config.js +++ b/admin/src/state/actions/Config.js @@ -3,6 +3,8 @@ * Main actions * */ +import { saveAs } from 'file-saver'; +import { b64toBlob } from '../../helpers/blob'; export function getAllConfigDiff(toggleNotification, formatMessage, get) { return async function(dispatch) { @@ -50,6 +52,23 @@ export function exportAllConfig(partialDiff, toggleNotification, formatMessage, }; } +export function downloadZip(toggleNotification, formatMessage, post, get) { + return async function(dispatch) { + dispatch(setLoadingState(true)); + try { + const { message, base64Data, name } = (await get('/config-sync/zip')).data; + toggleNotification({ type: 'success', message }); + if (base64Data) { + saveAs(b64toBlob(base64Data, 'application/zip'), name, { type: 'application/zip' }); + } + dispatch(setLoadingState(false)); + } catch (err) { + toggleNotification({ type: 'warning', message: formatMessage({ id: 'notification.error' }) }); + dispatch(setLoadingState(false)); + } + }; +} + export function importAllConfig(partialDiff, force, toggleNotification, formatMessage, post, get) { return async function(dispatch) { dispatch(setLoadingState(true)); diff --git a/admin/src/translations/en.json b/admin/src/translations/en.json index f20500f..a585ed1 100644 --- a/admin/src/translations/en.json +++ b/admin/src/translations/en.json @@ -28,6 +28,7 @@ "ConfigDiff.Database": "Database", "Buttons.Export": "Export", + "Buttons.DownloadConfig": "Download Config", "Buttons.Import": "Import", "FirstExport.Message": "Looks like this is your first time using config-sync for this project.", diff --git a/package.json b/package.json index 09489c2..537b71f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "chalk": "^4.1.2", "cli-table": "^0.3.6", "commander": "^8.3.0", + "file-saver": "^2.0.5", "git-diff": "^2.0.6", "immutable": "^3.8.2", "inquirer": "^8.2.0", diff --git a/server/config/type.js b/server/config/type.js index 0ae0a53..cb4fab3 100644 --- a/server/config/type.js +++ b/server/config/type.js @@ -173,7 +173,7 @@ const ConfigType = class ConfigType { * @param {string} configName - The name of the config file. * @returns {void} */ - exportSingle = async (configName) => { + exportSingle = async (configName) => { const formattedDiff = await strapi.plugin('config-sync').service('main').getFormattedDiff(this.configPrefix); // Check if the config should be excluded. @@ -193,6 +193,17 @@ const ConfigType = class ConfigType { } } + + /** + * Zip config files + * + * @param {string} configName - The name of the zip archive. + * @returns {void} + */ + zipConfig = async () => { + return strapi.plugin('config-sync').service('main').zipConfigFiles(); + } + /** * Get all role-permissions config from the db. * @@ -243,7 +254,7 @@ const ConfigType = class ConfigType { * * @returns {void} */ - importAll = async () => { + importAll = async () => { // The main.importAllConfig service will loop the core-store.importSingle service. await strapi.plugin('config-sync').service('main').importAllConfig(this.configPrefix); } @@ -253,7 +264,7 @@ const ConfigType = class ConfigType { * * @returns {void} */ - exportAll = async () => { + exportAll = async () => { // The main.importAllConfig service will loop the core-store.importSingle service. await strapi.plugin('config-sync').service('main').exportAllConfig(this.configPrefix); } diff --git a/server/controllers/config.js b/server/controllers/config.js index 3c8000f..da1c2a8 100644 --- a/server/controllers/config.js +++ b/server/controllers/config.js @@ -84,6 +84,19 @@ module.exports = { return strapi.plugin('config-sync').service('main').getFormattedDiff(); }, + zipConfig: async (ctx) => { + // Check for existance of the config file sync dir. + if (!fs.existsSync(strapi.config.get('plugin.config-sync.syncDir'))) { + ctx.send({ + message: 'No config files were found.', + }); + + return; + } + + return strapi.plugin('config-sync').service('main').zipConfigFiles(); + }, + /** * Get the current Strapi env. * @returns {string} The current Strapi environment. diff --git a/server/routes/admin.js b/server/routes/admin.js index 7422b45..f5fe7a1 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -27,6 +27,14 @@ module.exports = { policies: [], }, }, + { + method: "GET", + path: "/zip", + handler: "config.zipConfig", + config: { + policies: [], + }, + }, { method: "GET", path: "/app-env", diff --git a/server/services/main.js b/server/services/main.js index 1486795..cd11ff6 100644 --- a/server/services/main.js +++ b/server/services/main.js @@ -3,6 +3,7 @@ const { isEmpty } = require('lodash'); const fs = require('fs'); const util = require('util'); +const childProcess = require("child_process"); const difference = require('../utils/getObjectDiff'); const { logMessage } = require('../utils'); @@ -54,7 +55,7 @@ module.exports = () => ({ * @param {string} configName - The name of the config file. * @returns {void} */ - deleteConfigFile: async (configName) => { + deleteConfigFile: async (configName) => { // Check if the config should be excluded. const shouldExclude = !isEmpty(strapi.config.get('plugin::config-sync.excludedConfig').filter((option) => configName.startsWith(option))); if (shouldExclude) return; @@ -65,6 +66,23 @@ module.exports = () => ({ fs.unlinkSync(`${strapi.config.get('plugin::config-sync.syncDir')}${configName}.json`); }, + /** + * Zip config files. + * + * @param {string} configName - The name of the config file. + * @returns {void} + */ + zipConfigFiles: async () => { + const fileName = `config-${new Date().toJSON()}.zip`; + childProcess.execSync(`zip -r ${fileName} *`, { + cwd: strapi.config.get('plugin.config-sync.syncDir'), + }); + const fullFilePath = `${strapi.config.get('plugin.config-sync.syncDir')}${fileName}`; + const base64Data = fs.readFileSync(fullFilePath, { encoding: 'base64' }); + fs.unlinkSync(fullFilePath); + return { base64Data, name: fileName, message: 'Success' }; + }, + /** * Read from a config file. * @@ -191,7 +209,7 @@ module.exports = () => ({ * @param {object} onSuccess - Success callback to run on each single successfull import. * @returns {void} */ - exportAllConfig: async (configType = null, onSuccess) => { + exportAllConfig: async (configType = null, onSuccess) => { const fileConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromFiles(); const databaseConfig = await strapi.plugin('config-sync').service('main').getAllConfigFromDatabase(); diff --git a/yarn.lock b/yarn.lock index 7868e39..08c351d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6610,6 +6610,11 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"