mirror of
https://github.com/meienberger/runtipi.git
synced 2024-11-25 20:07:58 +03:00
Added option to remove backups when uninstalling an app (#1756)
Co-authored-by: Nicolas Meienberger <47644445+meienberger@users.noreply.github.com>
This commit is contained in:
parent
65c17bba67
commit
58234abe40
@ -8,5 +8,5 @@ export default async () => {
|
||||
["./modules/links/dto/links.dto"]: await import("./modules/links/dto/links.dto"),
|
||||
["./modules/system/dto/system.dto"]: await import("./modules/system/dto/system.dto")
|
||||
};
|
||||
return { "@nestjs/swagger": { "models": [[import("./modules/apps/dto/app-info.dto"), { "AppInfoSimpleDto": {}, "AppInfoDto": {} }], [import("./modules/user/dto/user.dto"), { "UserDto": {} }], [import("./app.dto"), { "UserSettingsDto": {}, "PartialUserSettingsDto": {}, "AppContextDto": {}, "UserContextDto": {}, "AcknowledgeWelcomeBody": {} }], [import("./modules/queue/queue.entity"), { "Queue": { queueNameResponse: { required: true, type: () => String } } }], [import("./modules/auth/dto/auth.dto"), { "LoginBody": {}, "VerifyTotpBody": {}, "LoginDto": {}, "RegisterBody": {}, "RegisterDto": {}, "ChangeUsernameBody": {}, "ChangePasswordBody": {}, "GetTotpUriBody": {}, "GetTotpUriDto": {}, "SetupTotpBody": {}, "DisableTotpBody": {}, "ResetPasswordBody": {}, "ResetPasswordDto": {}, "CheckResetPasswordRequestDto": {} }], [import("./modules/app-lifecycle/dto/app-lifecycle.dto"), { "AppFormBody": {} }], [import("./modules/apps/dto/app.dto"), { "SearchAppsQueryDto": {}, "SearchAppsDto": {}, "UpdateInfoDto": {}, "AppDto": {}, "MyAppsDto": {}, "GuestAppsDto": {}, "AppDetailsDto": {} }], [import("./modules/backups/dto/backups.dto"), { "BackupDto": {}, "RestoreAppBackupDto": {}, "GetAppBackupsDto": {}, "GetAppBackupsQueryDto": {}, "DeleteAppBackupBodyDto": {} }], [import("./modules/links/dto/links.dto"), { "LinkBodyDto": {}, "EditLinkBodyDto": {}, "LinksDto": {} }], [import("./modules/system/dto/system.dto"), { "LoadDto": {} }]], "controllers": [[import("./app.controller"), { "AppController": { "userContext": { type: t["./app.dto"].UserContextDto }, "appContext": { type: t["./app.dto"].AppContextDto }, "updateUserSettings": {}, "acknowledgeWelcome": {}, "getError": {} } }], [import("./modules/auth/auth.controller"), { "AuthController": { "login": { type: t["./modules/auth/dto/auth.dto"].LoginDto }, "verifyTotp": { type: t["./modules/auth/dto/auth.dto"].LoginDto }, "register": { type: t["./modules/auth/dto/auth.dto"].RegisterDto }, "logout": {}, "changeUsername": {}, "changePassword": {}, "getTotpUri": { type: t["./modules/auth/dto/auth.dto"].GetTotpUriDto }, "setupTotp": {}, "disableTotp": {}, "resetPassword": { type: t["./modules/auth/dto/auth.dto"].ResetPasswordDto }, "cancelResetPassword": {}, "checkResetPasswordRequest": { type: t["./modules/auth/dto/auth.dto"].CheckResetPasswordRequestDto } } }], [import("./modules/i18n/i18n.controller"), { "I18nController": { "getTranslation": { type: Object } } }], [import("./core/health/health.controller"), { "HealthController": { "check": { type: Object } } }], [import("./modules/apps/apps.controller"), { "AppsController": { "getInstalledApps": { type: t["./modules/apps/dto/app.dto"].MyAppsDto }, "getGuestApps": { type: t["./modules/apps/dto/app.dto"].GuestAppsDto }, "searchApps": { type: t["./modules/apps/dto/app.dto"].SearchAppsDto }, "getAppDetails": { type: t["./modules/apps/dto/app.dto"].AppDetailsDto }, "getImage": {} } }], [import("./modules/backups/backups.controller"), { "BackupsController": { "backupApp": {}, "restoreAppBackup": {}, "getAppBackups": { type: t["./modules/backups/dto/backups.dto"].GetAppBackupsDto }, "deleteAppBackup": {} } }], [import("./modules/app-lifecycle/app-lifecycle.controller"), { "AppLifecycleController": { "installApp": {}, "startApp": {}, "stopApp": {}, "restartApp": {}, "uninstallApp": {}, "resetApp": {} } }], [import("./modules/links/links.controller"), { "LinksController": { "getLinks": { type: t["./modules/links/dto/links.dto"].LinksDto }, "createLink": {}, "editLink": {}, "deleteLink": {} } }], [import("./modules/system/system.controller"), { "SystemController": { "systemLoad": { type: t["./modules/system/dto/system.dto"].LoadDto }, "downloadLocalCertificate": {} } }]] } };
|
||||
return { "@nestjs/swagger": { "models": [[import("./modules/apps/dto/app-info.dto"), { "AppInfoSimpleDto": {}, "AppInfoDto": {} }], [import("./modules/user/dto/user.dto"), { "UserDto": {} }], [import("./app.dto"), { "UserSettingsDto": {}, "PartialUserSettingsDto": {}, "AppContextDto": {}, "UserContextDto": {}, "AcknowledgeWelcomeBody": {} }], [import("./modules/queue/queue.entity"), { "Queue": { queueNameResponse: { required: true, type: () => String } } }], [import("./modules/auth/dto/auth.dto"), { "LoginBody": {}, "VerifyTotpBody": {}, "LoginDto": {}, "RegisterBody": {}, "RegisterDto": {}, "ChangeUsernameBody": {}, "ChangePasswordBody": {}, "GetTotpUriBody": {}, "GetTotpUriDto": {}, "SetupTotpBody": {}, "DisableTotpBody": {}, "ResetPasswordBody": {}, "ResetPasswordDto": {}, "CheckResetPasswordRequestDto": {} }], [import("./modules/app-lifecycle/dto/app-lifecycle.dto"), { "AppFormBody": {}, "UninstallAppBody": {} }], [import("./modules/apps/dto/app.dto"), { "SearchAppsQueryDto": {}, "SearchAppsDto": {}, "UpdateInfoDto": {}, "AppDto": {}, "MyAppsDto": {}, "GuestAppsDto": {}, "AppDetailsDto": {} }], [import("./modules/backups/dto/backups.dto"), { "BackupDto": {}, "RestoreAppBackupDto": {}, "GetAppBackupsDto": {}, "GetAppBackupsQueryDto": {}, "DeleteAppBackupBodyDto": {} }], [import("./modules/links/dto/links.dto"), { "LinkBodyDto": {}, "EditLinkBodyDto": {}, "LinksDto": {} }], [import("./modules/system/dto/system.dto"), { "LoadDto": {} }]], "controllers": [[import("./app.controller"), { "AppController": { "userContext": { type: t["./app.dto"].UserContextDto }, "appContext": { type: t["./app.dto"].AppContextDto }, "updateUserSettings": {}, "acknowledgeWelcome": {}, "getError": {} } }], [import("./modules/auth/auth.controller"), { "AuthController": { "login": { type: t["./modules/auth/dto/auth.dto"].LoginDto }, "verifyTotp": { type: t["./modules/auth/dto/auth.dto"].LoginDto }, "register": { type: t["./modules/auth/dto/auth.dto"].RegisterDto }, "logout": {}, "changeUsername": {}, "changePassword": {}, "getTotpUri": { type: t["./modules/auth/dto/auth.dto"].GetTotpUriDto }, "setupTotp": {}, "disableTotp": {}, "resetPassword": { type: t["./modules/auth/dto/auth.dto"].ResetPasswordDto }, "cancelResetPassword": {}, "checkResetPasswordRequest": { type: t["./modules/auth/dto/auth.dto"].CheckResetPasswordRequestDto } } }], [import("./modules/i18n/i18n.controller"), { "I18nController": { "getTranslation": { type: Object } } }], [import("./core/health/health.controller"), { "HealthController": { "check": { type: Object } } }], [import("./modules/apps/apps.controller"), { "AppsController": { "getInstalledApps": { type: t["./modules/apps/dto/app.dto"].MyAppsDto }, "getGuestApps": { type: t["./modules/apps/dto/app.dto"].GuestAppsDto }, "searchApps": { type: t["./modules/apps/dto/app.dto"].SearchAppsDto }, "getAppDetails": { type: t["./modules/apps/dto/app.dto"].AppDetailsDto }, "getImage": {} } }], [import("./modules/backups/backups.controller"), { "BackupsController": { "backupApp": {}, "restoreAppBackup": {}, "getAppBackups": { type: t["./modules/backups/dto/backups.dto"].GetAppBackupsDto }, "deleteAppBackup": {} } }], [import("./modules/app-lifecycle/app-lifecycle.controller"), { "AppLifecycleController": { "installApp": {}, "startApp": {}, "stopApp": {}, "restartApp": {}, "uninstallApp": {}, "resetApp": {} } }], [import("./modules/links/links.controller"), { "LinksController": { "getLinks": { type: t["./modules/links/dto/links.dto"].LinksDto }, "createLink": {}, "editLink": {}, "deleteLink": {} } }], [import("./modules/system/system.controller"), { "SystemController": { "systemLoad": { type: t["./modules/system/dto/system.dto"].LoadDto }, "downloadLocalCertificate": {} } }]] } };
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Delete, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '../auth/auth.guard';
|
||||
import { AppLifecycleService } from './app-lifecycle.service';
|
||||
import { AppFormBody, appFormSchema } from './dto/app-lifecycle.dto';
|
||||
import { AppFormBody, UninstallAppBody, appFormSchema } from './dto/app-lifecycle.dto';
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Controller('app-lifecycle')
|
||||
@ -31,8 +31,8 @@ export class AppLifecycleController {
|
||||
}
|
||||
|
||||
@Delete(':id/uninstall')
|
||||
async uninstallApp(@Param('id') id: string) {
|
||||
return this.appLifecycleService.uninstallApp({ appId: id });
|
||||
async uninstallApp(@Param('id') id: string, @Body() body: UninstallAppBody) {
|
||||
return this.appLifecycleService.uninstallApp({ appId: id, removeBackups: body.removeBackups });
|
||||
}
|
||||
|
||||
@Post(':id/reset')
|
||||
|
@ -8,6 +8,7 @@ import { isFQDN } from 'validator';
|
||||
import type { z } from 'zod';
|
||||
import { AppFilesManager } from '../apps/app-files-manager';
|
||||
import { AppsRepository } from '../apps/apps.repository';
|
||||
import { BackupManager } from '../backups/backup.manager';
|
||||
import { type AppEventFormInput, AppEventsQueue, appEventSchema } from '../queue/entities/app-events';
|
||||
import { AppLifecycleCommandFactory } from './app-lifecycle-command.factory';
|
||||
|
||||
@ -21,6 +22,7 @@ export class AppLifecycleService {
|
||||
private readonly config: ConfigurationService,
|
||||
private readonly appFilesManager: AppFilesManager,
|
||||
private readonly socketManager: SocketManager,
|
||||
private readonly backupManager: BackupManager,
|
||||
) {
|
||||
this.logger.debug('Subscribing to app events...');
|
||||
this.appEventsQueue.onEvent(({ eventId, ...data }) => this.invokeCommand(eventId, data));
|
||||
@ -203,8 +205,8 @@ export class AppLifecycleService {
|
||||
/**
|
||||
* Uninstall an app by its ID
|
||||
*/
|
||||
public async uninstallApp(params: { appId: string }) {
|
||||
const { appId } = params;
|
||||
public async uninstallApp(params: { appId: string, removeBackups: boolean }) {
|
||||
const { appId, removeBackups } = params;
|
||||
|
||||
const app = await this.appRepository.getApp(appId);
|
||||
|
||||
@ -212,6 +214,10 @@ export class AppLifecycleService {
|
||||
throw new TranslatableError('APP_ERROR_APP_NOT_FOUND', { id: appId });
|
||||
}
|
||||
|
||||
if (removeBackups) {
|
||||
await this.backupManager.deleteAppBackupsById(appId);
|
||||
}
|
||||
|
||||
await this.appRepository.updateApp(appId, { status: 'uninstalling' });
|
||||
this.socketManager.emit({ type: 'app', event: 'status_change', data: { appId, appStatus: 'uninstalling' } });
|
||||
|
||||
|
@ -13,3 +13,5 @@ export const appFormSchema = z
|
||||
.catchall(z.unknown());
|
||||
|
||||
export class AppFormBody extends createZodDto(appFormSchema) {}
|
||||
|
||||
export class UninstallAppBody extends createZodDto(z.object({ removeBackups: z.boolean() })) {}
|
||||
|
@ -117,6 +117,16 @@ export class BackupManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all backups for an app
|
||||
* @param appId - The app id
|
||||
*/
|
||||
public async deleteAppBackupsById(appId: string): Promise<void> {
|
||||
const backups = await this.listBackupsByAppId(appId);
|
||||
|
||||
await Promise.all(backups.map((backup) => this.deleteBackup(appId, backup.id)))
|
||||
}
|
||||
|
||||
/**
|
||||
* List the backups for an app
|
||||
* @param appId - The app id
|
||||
|
@ -126,6 +126,7 @@
|
||||
"APP_STORE_NO_RESULTS_SUBTITLE": "Try to refine your search",
|
||||
"APP_STORE_SEARCH_PLACEHOLDER": "Search apps",
|
||||
"APP_STORE_TITLE": "App Store",
|
||||
"APP_UNINSTALL_FORM_REMOVE_BACKUPS": "Remove backups",
|
||||
"APP_UNINSTALL_FORM_SUBMIT": "Uninstall",
|
||||
"APP_UNINSTALL_FORM_SUBTITLE": "All data for this app will be lost.",
|
||||
"APP_UNINSTALL_FORM_TITLE": "Uninstall {{name}} ?",
|
||||
|
@ -668,6 +668,16 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UninstallAppBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
@ -2542,6 +2552,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UninstallAppBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"removeBackups": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"removeBackups"
|
||||
]
|
||||
},
|
||||
"RestoreAppBackupDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -758,6 +758,9 @@ export type UninstallAppData = {
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
body: {
|
||||
removeBackups: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type UninstallAppResponse = unknown;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { uninstallAppMutation } from '@/api-client/@tanstack/react-query.gen';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
import { useAppStatus } from '@/modules/app/helpers/use-app-status';
|
||||
import type { AppInfo } from '@/types/app.types';
|
||||
import type { TranslatableError } from '@/types/error.types';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -19,6 +21,8 @@ export const UninstallDialog = ({ info, isOpen, onClose }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setOptimisticStatus } = useAppStatus();
|
||||
|
||||
const [shouldRemoveBackups, setShouldRemoveBackups] = useState(false);
|
||||
|
||||
const uninstallMutation = useMutation({
|
||||
...uninstallAppMutation(),
|
||||
onError: (error: TranslatableError) => {
|
||||
@ -40,9 +44,10 @@ export const UninstallDialog = ({ info, isOpen, onClose }: IProps) => {
|
||||
<IconAlertTriangle className="icon mb-2 text-danger icon-lg" />
|
||||
<h3>{t('APP_UNINSTALL_FORM_WARNING')}</h3>
|
||||
<div className="text-muted">{t('APP_UNINSTALL_FORM_SUBTITLE')}</div>
|
||||
<Switch checked={shouldRemoveBackups} onCheckedChange={setShouldRemoveBackups} label={t('APP_UNINSTALL_FORM_REMOVE_BACKUPS')} className='text-start pt-5'/>
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => uninstallMutation.mutate({ path: { id: info.id } })} intent="danger">
|
||||
<Button onClick={() => uninstallMutation.mutate({ path: { id: info.id }, body: {removeBackups: shouldRemoveBackups} })} intent="danger">
|
||||
{t('APP_UNINSTALL_FORM_SUBMIT')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
Loading…
Reference in New Issue
Block a user