Implement move extensions

83965fb611b84e769c62c8428366941b45cda218

Cohee <18619528+Cohee1207@users.noreply.github.com>

2 files changed, +105 -8Ignore whitespace
public/scripts/extensions.js+58 -8
@@ -465,7 +465,7 @@ async function connectToApi(baseUrl) {
465465
466function updateStatus(success) {466function updateStatus(success) {
467 connectedToApi = success;467 connectedToApi = success;
468 const _text = success ? 'Connected to API' : 'Could not connect to API';468 const _text = success ? t`Connected to API` : t`Could not connect to API`;
469 const _class = success ? 'success' : 'failure';469 const _class = success ? 'success' : 'failure';
470 $('#extensions_status').text(_text);470 $('#extensions_status').text(_text);
471 $('#extensions_status').attr('class', _class);471 $('#extensions_status').attr('class', _class);
@@ -723,7 +723,7 @@ async function showExtensionsDetails() {
723 }723 }
724 if (stateChanged) {724 if (stateChanged) {
725 waitingForSave = true;725 waitingForSave = true;
726 const toast = toastr.info('The page will be reloaded shortly...', 'Extensions state changed');726 const toast = toastr.info(t`The page will be reloaded shortly...`, t`Extensions state changed`);
727 await saveSettings();727 await saveSettings();
728 toastr.clear(toast);728 toastr.clear(toast);
729 waitingForSave = false;729 waitingForSave = false;
@@ -735,7 +735,7 @@ async function showExtensionsDetails() {
735 popupPromise = popup.show();735 popupPromise = popup.show();
736 checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove());736 checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove());
737 } catch (error) {737 } catch (error) {
738 toastr.error('Error loading extensions. See browser console for details.');738 toastr.error(t`Error loading extensions. See browser console for details.`);
739 console.error(error);739 console.error(error);
740 }740 }
741 if (popupPromise) {741 if (popupPromise) {
@@ -817,7 +817,7 @@ async function onDeleteClick() {
817 }817 }
818818
819 // use callPopup to create a popup for the user to confirm before delete819 // use callPopup to create a popup for the user to confirm before delete
820 const confirmation = await callGenericPopup(`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {});820 const confirmation = await callGenericPopup(t`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {});
821 if (confirmation === POPUP_RESULT.AFFIRMATIVE) {821 if (confirmation === POPUP_RESULT.AFFIRMATIVE) {
822 await deleteExtension(extensionName);822 await deleteExtension(extensionName);
823 }823 }
@@ -832,7 +832,57 @@ async function onMoveClick() {
832 return;832 return;
833 }833 }
834834
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 */
860async 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}
837887
838/**888/**
@@ -853,7 +903,7 @@ export async function deleteExtension(extensionName) {
853 console.error('Error:', error);903 console.error('Error:', error);
854 }904 }
855905
856 toastr.success(`Extension ${extensionName} deleted`);906 toastr.success(t`Extension ${extensionName} deleted`);
857 showExtensionsDetails();907 showExtensionsDetails();
858 // reload the page to remove the extension from the list908 // reload the page to remove the extension from the list
859 location.reload();909 location.reload();
@@ -896,7 +946,7 @@ async function getExtensionVersion(extensionName, abortSignal) {
896export async function installExtension(url, global) {946export async function installExtension(url, global) {
897 console.debug('Extension installation started', url);947 console.debug('Extension installation started', url);
898948
899 toastr.info('Please wait...', 'Installing extension');949 toastr.info(t`Please wait...`, t`Installing extension`);
900950
901 const request = await fetch('/api/extensions/install', {951 const request = await fetch('/api/extensions/install', {
902 method: 'POST',952 method: 'POST',
@@ -909,7 +959,7 @@ export async function installExtension(url, global) {
909959
910 if (!request.ok) {960 if (!request.ok) {
911 const text = await request.text();961 const text = await request.text();
912 toastr.warning(text || request.statusText, 'Extension installation failed', { timeOut: 5000 });962 toastr.warning(text || request.statusText, t`Extension installation failed`, { timeOut: 5000 });
913 console.error('Extension installation failed', request.status, request.statusText, text);963 console.error('Extension installation failed', request.status, request.statusText, text);
914 return;964 return;
915 }965 }
src/endpoints/extensions.js+47 -0
@@ -80,6 +80,7 @@ router.post('/install', jsonParser, async (request, response) => {
80 const { url, global } = request.body;80 const { url, global } = request.body;
8181
82 if (global && !request.user.profile.admin) {82 if (global && !request.user.profile.admin) {
83 console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
83 return response.status(403).send('Forbidden: No permission to install global extensions.');84 return response.status(403).send('Forbidden: No permission to install global extensions.');
84 }85 }
8586
@@ -123,6 +124,7 @@ router.post('/update', jsonParser, async (request, response) => {
123 const { extensionName, global } = request.body;124 const { extensionName, global } = request.body;
124125
125 if (global && !request.user.profile.admin) {126 if (global && !request.user.profile.admin) {
127 console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
126 return response.status(403).send('Forbidden: No permission to update global extensions.');128 return response.status(403).send('Forbidden: No permission to update global extensions.');
127 }129 }
128130
@@ -153,6 +155,50 @@ router.post('/update', jsonParser, async (request, response) => {
153 }155 }
154});156});
155157
158router.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 * HTTP POST handler function to get the current git commit hash and branch name for a given extension.203 * HTTP POST handler function to get the current git commit hash and branch name for a given extension.
158 * It checks whether the repository is up-to-date with the remote, and returns the status along with204 * 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 const { extensionName, global } = request.body;257 const { extensionName, global } = request.body;
212258
213 if (global && !request.user.profile.admin) {259 if (global && !request.user.profile.admin) {
260 console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
214 return response.status(403).send('Forbidden: No permission to delete global extensions.');261 return response.status(403).send('Forbidden: No permission to delete global extensions.');
215 }262 }
216263