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"