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
466466function updateStatus(success) {
467467 connectedToApi = success;
468468 const _text = success ? 't`Connected to API'` : 't`Could not connect to API'`;
469469 const _class = success ? 'success' : 'failure';
470470 $('#extensions_status').text(_text);
471471 $('#extensions_status').attr('class', _class);
@@ -723,7 +723,7 @@ async function showExtensionsDetails() {
723723 }
724724 if (stateChanged) {
725725 waitingForSave = true;
726726 const toast = toastr.info('t`The page will be reloaded shortly...'`, 't`Extensions state changed'`);
727727 await saveSettings();
728728 toastr.clear(toast);
729729 waitingForSave = false;
@@ -735,7 +735,7 @@ async function showExtensionsDetails() {
735735 popupPromise = popup.show();
736736 checkForUpdatesManual(abortController.signal).finally(() => htmlLoading.remove());
737737 } catch (error) {
738738 toastr.error('t`Error loading extensions. See browser console for details.'`);
739739 console.error(error);
740740 }
741741 if (popupPromise) {
@@ -817,7 +817,7 @@ async function onDeleteClick() {
817817 }
818818
819819 // use callPopup to create a popup for the user to confirm before delete
820820 const confirmation = await callGenericPopup(t`Are you sure you want to delete ${extensionName}?`, POPUP_TYPE.CONFIRM, '', {});
821821 if (confirmation === POPUP_RESULT.AFFIRMATIVE) {
822822 await deleteExtension(extensionName);
823823 }
@@ -832,7 +832,57 @@ async function onMoveClick() {
832832 return;
833833 }
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+ */
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+ }
836886}
837887
838888/**
@@ -853,7 +903,7 @@ export async function deleteExtension(extensionName) {
853903 console.error('Error:', error);
854904 }
855905
856906 toastr.success(t`Extension ${extensionName} deleted`);
857907 showExtensionsDetails();
858908 // reload the page to remove the extension from the list
859909 location.reload();
@@ -896,7 +946,7 @@ async function getExtensionVersion(extensionName, abortSignal) {
896946export async function installExtension(url, global) {
897947 console.debug('Extension installation started', url);
898948
899949 toastr.info('t`Please wait...'`, 't`Installing extension'`);
900950
901951 const request = await fetch('/api/extensions/install', {
902952 method: 'POST',
@@ -909,7 +959,7 @@ export async function installExtension(url, global) {
909959
910960 if (!request.ok) {
911961 const text = await request.text();
912962 toastr.warning(text || request.statusText, 't`Extension installation failed'`, { timeOut: 5000 });
913963 console.error('Extension installation failed', request.status, request.statusText, text);
914964 return;
915965 }
src/endpoints/extensions.js+47 -0
@@ -80,6 +80,7 @@ router.post('/install', jsonParser, async (request, response) => {
8080 const { url, global } = request.body;
8181
8282 if (global && !request.user.profile.admin) {
83+ console.warn(`User ${request.user.profile.handle} does not have permission to install global extensions.`);
8384 return response.status(403).send('Forbidden: No permission to install global extensions.');
8485 }
8586
@@ -123,6 +124,7 @@ router.post('/update', jsonParser, async (request, response) => {
123124 const { extensionName, global } = request.body;
124125
125126 if (global && !request.user.profile.admin) {
127+ console.warn(`User ${request.user.profile.handle} does not have permission to update global extensions.`);
126128 return response.status(403).send('Forbidden: No permission to update global extensions.');
127129 }
128130
@@ -153,6 +155,50 @@ router.post('/update', jsonParser, async (request, response) => {
153155 }
154156});
155157
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+
156202/**
157203 * HTTP POST handler function to get the current git commit hash and branch name for a given extension.
158204 * 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) => {
211257 const { extensionName, global } = request.body;
212258
213259 if (global && !request.user.profile.admin) {
260+ console.warn(`User ${request.user.profile.handle} does not have permission to delete global extensions.`);
214261 return response.status(403).send('Forbidden: No permission to delete global extensions.');
215262 }
216263