From 761024cc7437723e1a4928b2d04b72de64c01aec Mon Sep 17 00:00:00 2001 From: Alexander Engel Date: Thu, 15 Jun 2023 16:33:56 -0700 Subject: [PATCH 1/5] feat: added option to download zip --- admin/src/components/ActionButtons/index.js | 3 +- admin/src/state/actions/Config.js | 27 +++++++++++++--- server/config/type.js | 19 ++++++++--- server/controllers/config.js | 13 ++++++++ server/routes/admin.js | 8 +++++ server/services/main.js | 36 ++++++++++++++++++--- 6 files changed, 93 insertions(+), 13 deletions(-) diff --git a/admin/src/components/ActionButtons/index.js b/admin/src/components/ActionButtons/index.js index 4fa188b..4a30c07 100644 --- a/admin/src/components/ActionButtons/index.js +++ b/admin/src/components/ActionButtons/index.js @@ -7,7 +7,7 @@ import { Map } from 'immutable'; import { useNotification } from '@strapi/helper-plugin'; import ConfirmModal from '../ConfirmModal'; -import { exportAllConfig, importAllConfig } from '../../state/actions/Config'; +import { downloadZip, exportAllConfig, importAllConfig } from '../../state/actions/Config'; const ActionButtons = () => { const dispatch = useDispatch(); @@ -30,6 +30,7 @@ const ActionButtons = () => { + {!isEmpty(partialDiff) && (

{Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"}

)} diff --git a/admin/src/state/actions/Config.js b/admin/src/state/actions/Config.js index 5922bfe..a9c6b64 100644 --- a/admin/src/state/actions/Config.js +++ b/admin/src/state/actions/Config.js @@ -7,7 +7,7 @@ import { request } from '@strapi/helper-plugin'; export function getAllConfigDiff(toggleNotification) { - return async function(dispatch) { + return async function (dispatch) { dispatch(setLoadingState(true)); try { const configDiff = await request('/config-sync/diff', { method: 'GET' }); @@ -38,7 +38,7 @@ export function setConfigPartialDiffInState(config) { } export function exportAllConfig(partialDiff, toggleNotification) { - return async function(dispatch) { + return async function (dispatch) { dispatch(setLoadingState(true)); try { const { message } = await request('/config-sync/export', { @@ -55,8 +55,27 @@ export function exportAllConfig(partialDiff, toggleNotification) { }; } +export function downloadZip(toggleNotification) { + return async function (dispatch) { + dispatch(setLoadingState(true)); + try { + const { message, url } = await request('/config-sync/zip', { + method: 'GET' + }); + toggleNotification({ type: 'success', message }); + if (url) { + window.location = url; + } + dispatch(setLoadingState(false)); + } catch (err) { + toggleNotification({ type: 'warning', message: { id: 'notification.error' } }); + dispatch(setLoadingState(false)); + } + }; +} + export function importAllConfig(partialDiff, force, toggleNotification) { - return async function(dispatch) { + return async function (dispatch) { dispatch(setLoadingState(true)); try { const { message } = await request('/config-sync/import', { @@ -85,7 +104,7 @@ export function setLoadingState(value) { } export function getAppEnv(toggleNotification) { - return async function(dispatch) { + return async function (dispatch) { try { const envVars = await request('/config-sync/app-env', { method: 'GET', diff --git a/server/config/type.js b/server/config/type.js index 74d15ca..343bcb1 100644 --- a/server/config/type.js +++ b/server/config/type.js @@ -166,7 +166,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. @@ -186,12 +186,23 @@ 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. * * @returns {object} Object with key value pairs of configs. */ - getAllFromDatabase = async () => { + getAllFromDatabase = async () => { const AllConfig = await noLimit(strapi.query(this.queryString), {}); const configs = {}; @@ -234,7 +245,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); } @@ -244,7 +255,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 4de75ee..ca06968 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 c603f62..3dc5935 100644 --- a/server/services/main.js +++ b/server/services/main.js @@ -5,6 +5,7 @@ const fs = require('fs'); const util = require('util'); const difference = require('../utils/getObjectDiff'); const { logMessage } = require('../utils'); +const child_process = require("child_process"); /** * Main services for config import/export. @@ -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,33 @@ 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` + child_process.execSync(`zip -r ${fileName} *`, { + cwd: strapi.config.get('plugin.config-sync.syncDir') + }); + const fullFilePath = `${strapi.config.get('plugin.config-sync.syncDir')}${fileName}` + const stats = fs.statSync(fullFilePath); + + const result = await strapi.plugins.upload.services.upload.upload({ + data: {}, //mandatory declare the data(can be empty), otherwise it will give you an undefined error. This parameters will be used to relate the file with a collection. + files: { + path: fullFilePath, + name: `configs/${fileName}`, + type: 'application/zip', // mime type of the file + size: stats.size, + }, + }); + fs.unlinkSync(fullFilePath); + return { url: result[0].url, message: 'Success' }; + }, + /** * Read from a config file. * @@ -191,7 +219,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(); @@ -242,8 +270,8 @@ module.exports = () => ({ * * @returns {void} */ - exportSingleConfig: async (configName, onSuccess) => { - // Check if the config should be excluded. + exportSingleConfig: async (configName, onSuccess) => { + // 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; From 38563dbe6d5e9b3a620c533ad4ce4085529e02d1 Mon Sep 17 00:00:00 2001 From: Alexander Engel Date: Mon, 14 Oct 2024 11:58:53 -0700 Subject: [PATCH 2/5] fix: do not store config file --- admin/src/state/actions/Config.js | 16 +++++++++++++--- package.json | 1 + server/services/main.js | 14 ++------------ yarn.lock | 5 +++++ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/admin/src/state/actions/Config.js b/admin/src/state/actions/Config.js index a9c6b64..7232366 100644 --- a/admin/src/state/actions/Config.js +++ b/admin/src/state/actions/Config.js @@ -5,6 +5,7 @@ */ import { request } from '@strapi/helper-plugin'; +import { saveAs } from 'file-saver'; export function getAllConfigDiff(toggleNotification) { return async function (dispatch) { @@ -59,12 +60,21 @@ export function downloadZip(toggleNotification) { return async function (dispatch) { dispatch(setLoadingState(true)); try { - const { message, url } = await request('/config-sync/zip', { + const { message, base64Data, name } = await request('/config-sync/zip', { method: 'GET' }); toggleNotification({ type: 'success', message }); - if (url) { - window.location = url; + if (base64Data) { + function b64toBlob(dataURI) { + 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: 'image/jpeg' }); + } + saveAs(b64toBlob(base64Data), name, { type: 'application/zip' }) } dispatch(setLoadingState(false)); } catch (err) { diff --git a/package.json b/package.json index 02e9002..2b401f7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,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/services/main.js b/server/services/main.js index 3dc5935..4e9fa96 100644 --- a/server/services/main.js +++ b/server/services/main.js @@ -78,19 +78,9 @@ module.exports = () => ({ cwd: strapi.config.get('plugin.config-sync.syncDir') }); const fullFilePath = `${strapi.config.get('plugin.config-sync.syncDir')}${fileName}` - const stats = fs.statSync(fullFilePath); - - const result = await strapi.plugins.upload.services.upload.upload({ - data: {}, //mandatory declare the data(can be empty), otherwise it will give you an undefined error. This parameters will be used to relate the file with a collection. - files: { - path: fullFilePath, - name: `configs/${fileName}`, - type: 'application/zip', // mime type of the file - size: stats.size, - }, - }); + const base64Data = fs.readFileSync(fullFilePath, { encoding: 'base64' }); fs.unlinkSync(fullFilePath); - return { url: result[0].url, message: 'Success' }; + return { base64Data, name: fileName, message: 'Success' }; }, /** diff --git a/yarn.lock b/yarn.lock index 49c0c71..35648a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2660,6 +2660,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@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" From 6e6b6cfba7cb7da686fa02c2ff274fbbd92616bd Mon Sep 17 00:00:00 2001 From: Alexander Engel Date: Mon, 14 Oct 2024 12:07:17 -0700 Subject: [PATCH 3/5] fix: moved button to new file --- admin/src/components/ActionButtons/index.jsx | 3 ++- admin/src/translations/en.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/admin/src/components/ActionButtons/index.jsx b/admin/src/components/ActionButtons/index.jsx index 3cc94f1..9c17c88 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(); @@ -37,6 +37,7 @@ const ActionButtons = () => { )} onSubmit={(force) => dispatch(exportAllConfig(partialDiff, toggleNotification, formatMessage, post, get))} /> + {!isEmpty(partialDiff) && ( {Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"} )} 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.", From 3a71083a0508e957c725943d76ed4139b1aeab57 Mon Sep 17 00:00:00 2001 From: Alexander Engel Date: Mon, 14 Oct 2024 12:27:22 -0700 Subject: [PATCH 4/5] chore: fixed linting, moved helper function --- admin/src/helpers/blob.js | 9 +++++++++ admin/src/state/actions/Config.js | 14 +++----------- server/services/main.js | 10 +++++----- 3 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 admin/src/helpers/blob.js 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 4be0ac6..1259709 100644 --- a/admin/src/state/actions/Config.js +++ b/admin/src/state/actions/Config.js @@ -4,6 +4,7 @@ * */ import { saveAs } from 'file-saver'; +import { b64toBlob } from '../../helpers/blob'; export function getAllConfigDiff(toggleNotification, formatMessage, get) { return async function(dispatch) { @@ -52,22 +53,13 @@ export function exportAllConfig(partialDiff, toggleNotification, formatMessage, } export function downloadZip(toggleNotification, formatMessage, post, get) { - return async function (dispatch) { + return async function(dispatch) { dispatch(setLoadingState(true)); try { const { message, base64Data, name } = await get('/config-sync/zip'); toggleNotification({ type: 'success', message }); if (base64Data) { - function b64toBlob(dataURI) { - 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: 'image/jpeg' }); - } - saveAs(b64toBlob(base64Data), name, { type: 'application/zip' }) + saveAs(b64toBlob(base64Data, 'application/zip'), name, { type: 'application/zip' }); } dispatch(setLoadingState(false)); } catch (err) { diff --git a/server/services/main.js b/server/services/main.js index 4941940..cd11ff6 100644 --- a/server/services/main.js +++ b/server/services/main.js @@ -3,9 +3,9 @@ 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'); -const child_process = require("child_process"); /** * Main services for config import/export. @@ -73,11 +73,11 @@ module.exports = () => ({ * @returns {void} */ zipConfigFiles: async () => { - const fileName = `config-${new Date().toJSON()}.zip` - child_process.execSync(`zip -r ${fileName} *`, { - cwd: strapi.config.get('plugin.config-sync.syncDir') + 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 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' }; From 9911e78b505668214a82e35a888c499a4caba571 Mon Sep 17 00:00:00 2001 From: Tim Schipper Date: Wed, 16 Oct 2024 09:55:19 +0200 Subject: [PATCH 5/5] feat: add response.data and fix styling for layout with download zip button --- admin/src/components/ActionButtons/index.jsx | 5 ++++- admin/src/state/actions/Config.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/admin/src/components/ActionButtons/index.jsx b/admin/src/components/ActionButtons/index.jsx index 9c17c88..b69f18d 100644 --- a/admin/src/components/ActionButtons/index.jsx +++ b/admin/src/components/ActionButtons/index.jsx @@ -37,10 +37,10 @@ const ActionButtons = () => { )} onSubmit={(force) => dispatch(exportAllConfig(partialDiff, toggleNotification, formatMessage, post, get))} /> - {!isEmpty(partialDiff) && ( {Object.keys(partialDiff).length} {Object.keys(partialDiff).length === 1 ? "config change" : "config changes"} )} +
); }; @@ -53,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/state/actions/Config.js b/admin/src/state/actions/Config.js index 1259709..73dbb0d 100644 --- a/admin/src/state/actions/Config.js +++ b/admin/src/state/actions/Config.js @@ -56,7 +56,7 @@ export function downloadZip(toggleNotification, formatMessage, post, get) { return async function(dispatch) { dispatch(setLoadingState(true)); try { - const { message, base64Data, name } = await get('/config-sync/zip'); + 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' });