Implement move extensions
| @@ -465,7 +465,7 @@ async function connectToApi(baseUrl) { | ||
| 465 | 465 | |
| 466 | 466 | function updateStatus(success) { |
| 467 | 467 | connectedToApi = success; |
| 468 | 468 | const _text = success ? 't`Connected to API'` : 't`Could not connect to API'`; |
| 469 | 469 | const _class = success ? 'success' : 'failure'; |
| 470 | 470 | $('#extensions_status').text(_text); |
| 471 | 471 | $('#extensions_status').attr('class', _class); |
| @@ -723,7 +723,7 @@ async function showExtensionsDetails() { | ||
| 723 | 723 | } |
| 724 | 724 | if (stateChanged) { |
| 725 | 725 | waitingForSave = true; |
| 726 | 726 | const toast = toastr.info('t`The page will be reloaded shortly...'`, 't`Extensions state changed'`); |
| 727 | 727 | await saveSettings(); |
| 728 | 728 | toastr.clear(toast); |
| 729 | 729 | waitingForSave = false; |
| @@ -735,7 +735,7 @@ async function showExtensionsDetails() { | ||
| 735 | 735 | popupPromise = popup.show(); |
| 736 | 736 | checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove()); |
| 737 | 737 | } catch (error) { |
| 738 | 738 | toastr.error('t`Error loading extensions. See browser console for details.'`); |
| 739 | 739 | console.error(error); |
| 740 | 740 | } |
| 741 | 741 | if (popupPromise) { |
| @@ -817,7 +817,7 @@ async function onDeleteClick() { | ||
| 817 | 817 | } |
| 818 | 818 | |
| 819 | 819 | // use callPopup to create a popup for the user to confirm before delete |
| 820 | 820 | const confirmation = await callGenericPopup(t`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {}); |
| 821 | 821 | if (confirmation === POPUP_RESULT.AFFIRMATIVE) { |
| 822 | 822 | await deleteExtension(extensionName); |
| 823 | 823 | } |
| @@ -832,7 +832,57 @@ async function onMoveClick() { | ||
| 832 | 832 | return; |
| 833 | 833 | } |
| 834 | 834 | |
| 835 | - toastr.info('Not implemented yet'); | |
| 835 | + const source = getExtensionType(extensionName); | |
| 836 | + const destination = source === 'global' ? 'local' : 'global'; | |
| 837 | + | |
| 838 | + const confirmationHeader = t`Move extension`; | |
| 839 | + const confirmationText = source == 'global' | |
| 840 | + ? t`Are you sure you want to move ${extensionName} to your local extensions? This will make it available only for you.` | |
| 841 | + : t`Are you sure you want to move ${extensionName} to the global extensions? This will make it available for all users.`; | |
| 842 | + | |
| 843 | + const confirmation = await Popup.show.confirm(confirmationHeader, confirmationText); | |
| 844 | + | |
| 845 | + if (!confirmation) { | |
| 846 | + return; | |
| 847 | + } | |
| 848 | + | |
| 849 | + $(this).find('i').addClass('fa-spin'); | |
| 850 | + await moveExtension(extensionName, source, destination); | |
| 851 | +} | |
| 852 | + | |
| 853 | +/** | |
| 854 | + * Moves an extension via the API. | |
| 855 | + * @param {string} extensionName Extension name | |
| 856 | + * @param {string} source Source type | |
| 857 | + * @param {string} destination Destination type | |
| 858 | + * @returns {Promise<void>} | |
| 859 | + */ | |
| 860 | +async function moveExtension(extensionName, source, destination) { | |
| 861 | + try { | |
| 862 | + const result = await fetch('/api/extensions/move', { | |
| 863 | + method: 'POST', | |
| 864 | + headers: getRequestHeaders(), | |
| 865 | + body: JSON.stringify({ | |
| 866 | + extensionName, | |
| 867 | + source, | |
| 868 | + destination, | |
| 869 | + }), | |
| 870 | + }); | |
| 871 | + | |
| 872 | + if (!result.ok) { | |
| 873 | + const text = await result.text(); | |
| 874 | + toastr.error(text || result.statusText, t`Extension move failed`, { timeOut: 5000 }); | |
| 875 | + console.error('Extension move failed', result.status, result.statusText, text); | |
| 876 | + return; | |
| 877 | + } | |
| 878 | + | |
| 879 | + toastr.success(t`Extension ${extensionName} moved.`); | |
| 880 | + await loadExtensionSettings({}, false, false); | |
| 881 | + await Popup.util.popups.find(popup => popup.content.querySelector('.extensions_info'))?.completeCancelled(); | |
| 882 | + showExtensionsDetails(); | |
| 883 | + } catch (error) { | |
| 884 | + console.error('Error:', error); | |
| 885 | + } | |
| 836 | 886 | } |
| 837 | 887 | |
| 838 | 888 | /** |
| @@ -853,7 +903,7 @@ export async function deleteExtension(extensionName) { | ||
| 853 | 903 | console.error('Error:', error); |
| 854 | 904 | } |
| 855 | 905 | |
| 856 | 906 | toastr.success(t`Extension ${extensionName} deleted`); |
| 857 | 907 | showExtensionsDetails(); |
| 858 | 908 | // reload the page to remove the extension from the list |
| 859 | 909 | location.reload(); |
| @@ -896,7 +946,7 @@ async function getExtensionVersion(extensionName, abortSignal) { | ||
| 896 | 946 | export async function installExtension(url, global) { |
| 897 | 947 | console.debug('Extension installation started', url); |
| 898 | 948 | |
| 899 | 949 | toastr.info('t`Please wait...'`, 't`Installing extension'`); |
| 900 | 950 | |
| 901 | 951 | const request = await fetch('/api/extensions/install', { |
| 902 | 952 | method: 'POST', |
| @@ -909,7 +959,7 @@ export async function installExtension(url, global) { | ||
| 909 | 959 | |
| 910 | 960 | if (!request.ok) { |
| 911 | 961 | const text = await request.text(); |
| 912 | 962 | toastr.warning(text || request.statusText, 't`Extension installation failed'`, { timeOut: 5000 }); |
| 913 | 963 | console.error('Extension installation failed', request.status, request.statusText, text); |
| 914 | 964 | return; |
| 915 | 965 | } |
| @@ -80,6 +80,7 @@ router.post('/install', jsonParser, async (request, response) => { | ||
| 80 | 80 | const { url, global } = request.body; |
| 81 | 81 | |
| 82 | 82 | if (global && !request.user.profile.admin) { |
| 83 | + console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`); | |
| 83 | 84 | return response.status(403).send('Forbidden: No permission to install global extensions.'); |
| 84 | 85 | } |
| 85 | 86 | |
| @@ -123,6 +124,7 @@ router.post('/update', jsonParser, async (request, response) => { | ||
| 123 | 124 | const { extensionName, global } = request.body; |
| 124 | 125 | |
| 125 | 126 | if (global && !request.user.profile.admin) { |
| 127 | + console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`); | |
| 126 | 128 | return response.status(403).send('Forbidden: No permission to update global extensions.'); |
| 127 | 129 | } |
| 128 | 130 | |
| @@ -153,6 +155,50 @@ router.post('/update', jsonParser, async (request, response) => { | ||
| 153 | 155 | } |
| 154 | 156 | }); |
| 155 | 157 | |
| 158 | +router.post('/move', jsonParser, async (request, response) => { | |
| 159 | + try { | |
| 160 | + const { extensionName, source, destination } = request.body; | |
| 161 | + | |
| 162 | + if (!extensionName || !source || !destination) { | |
| 163 | + return response.status(400).send('Bad Request. Not all required parameters are provided.'); | |
| 164 | + } | |
| 165 | + | |
| 166 | + if (!request.user.profile.admin) { | |
| 167 | + console.warn(`User ${request.user.profile.handle} does not have permission to move extensions.`); | |
| 168 | + return response.status(403).send('Forbidden: No permission to move extensions.'); | |
| 169 | + } | |
| 170 | + | |
| 171 | + const sourceDirectory = source === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; | |
| 172 | + const destinationDirectory = destination === 'global' ? PUBLIC_DIRECTORIES.globalExtensions : request.user.directories.extensions; | |
| 173 | + const sourcePath = path.join(sourceDirectory, sanitize(extensionName)); | |
| 174 | + const destinationPath = path.join(destinationDirectory, sanitize(extensionName)); | |
| 175 | + | |
| 176 | + if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isDirectory()) { | |
| 177 | + console.error(`Source directory does not exist at ${sourcePath}`); | |
| 178 | + return response.status(404).send('Source directory does not exist.'); | |
| 179 | + } | |
| 180 | + | |
| 181 | + if (fs.existsSync(destinationPath)) { | |
| 182 | + console.error(`Destination directory already exists at ${destinationPath}`); | |
| 183 | + return response.status(409).send('Destination directory already exists.'); | |
| 184 | + } | |
| 185 | + | |
| 186 | + if (source === destination) { | |
| 187 | + console.error('Source and destination directories are the same'); | |
| 188 | + return response.status(409).send('Source and destination directories are the same.'); | |
| 189 | + } | |
| 190 | + | |
| 191 | + fs.cpSync(sourcePath, destinationPath, { recursive: true, force: true }); | |
| 192 | + fs.rmSync(sourcePath, { recursive: true, force: true }); | |
| 193 | + console.log(`Extension has been moved from ${sourcePath} to ${destinationPath}`); | |
| 194 | + | |
| 195 | + return response.sendStatus(204); | |
| 196 | + } catch (error) { | |
| 197 | + console.log('Moving extension failed', error); | |
| 198 | + return response.status(500).send('Internal Server Error. Try again later.'); | |
| 199 | + } | |
| 200 | +}); | |
| 201 | + | |
| 156 | 202 | /** |
| 157 | 203 | * HTTP POST handler function to get the current git commit hash and branch name for a given extension. |
| 158 | 204 | * It checks whether the repository is up-to-date with the remote, and returns the status along with |
| @@ -211,6 +257,7 @@ router.post('/delete', jsonParser, async (request, response) => { | ||
| 211 | 257 | const { extensionName, global } = request.body; |
| 212 | 258 | |
| 213 | 259 | if (global && !request.user.profile.admin) { |
| 260 | + console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`); | |
| 214 | 261 | return response.status(403).send('Forbidden: No permission to delete global extensions.'); |
| 215 | 262 | } |
| 216 | 263 | |