diff --git a/src/controller/admin/user/backupController.ts b/src/controller/admin/user/backupController.ts index 069ab4d..25ef6dd 100644 --- a/src/controller/admin/user/backupController.ts +++ b/src/controller/admin/user/backupController.ts @@ -12,7 +12,9 @@ import InternalException from "../../../exceptions/internalException"; export async function getGeneratedBackups(req: Request, res: Response): Promise { let filesInFolder = FileSystemHelper.getFilesInDirectory(`backup`); - res.json(filesInFolder); + let sorted = filesInFolder.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime()); + + res.json(sorted); } /** @@ -33,6 +35,38 @@ export async function downloadBackupFile(req: Request, res: Response): Promise} + */ +export async function getUploadedBackups(req: Request, res: Response): Promise { + let filesInFolder = FileSystemHelper.getFilesInDirectory("uploaded-backup"); + + let sorted = filesInFolder.sort((a, b) => new Date(b.split("_")[0]).getTime() - new Date(a.split("_")[0]).getTime()); + + res.json(sorted); +} + +/** + * @description download uploaded backup file + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function downloadUploadedBackupFile(req: Request, res: Response): Promise { + let filename = req.params.filename; + + let filepath = FileSystemHelper.formatPath("uploaded-backup", filename); + + res.sendFile(filepath, { + headers: { + "Content-Type": "application/json", + }, + }); +} + /** * @description create backup manually * @param req {Request} Express req object @@ -55,8 +89,25 @@ export async function restoreBackupByLocalFile(req: Request, res: Response): Pro let filename = req.body.filename; let partial = req.body.partial; let include = req.body.include; + let overwrite = req.body.overwrite; - await BackupHelper.loadBackup({ filename, include, partial }); + await BackupHelper.loadBackup({ filename, include, partial, overwrite }); + + res.sendStatus(204); +} + +/** + * @description restore uploaded backup by selected + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function restoreBackupByUploadedFile(req: Request, res: Response): Promise { + let filename = req.body.filename; + let partial = req.body.partial; + let include = req.body.include; + + await BackupHelper.loadBackup({ filename, path: "uploaded-backup", include, partial }); res.sendStatus(204); } diff --git a/src/factory/admin/club/member/membership.ts b/src/factory/admin/club/member/membership.ts index cbc171a..72089ae 100644 --- a/src/factory/admin/club/member/membership.ts +++ b/src/factory/admin/club/member/membership.ts @@ -40,7 +40,7 @@ export default abstract class MembershipFactory { return { durationInDays: record.durationInDays, durationInYears: record.durationInYears, - exactDuration: record.exactDuration, + exactDuration: record.exactDuration.toString(), status: record.status, statusId: record.statusId, memberId: record.memberId, diff --git a/src/helpers/backupHelper.ts b/src/helpers/backupHelper.ts index ee649e7..ff69e71 100644 --- a/src/helpers/backupHelper.ts +++ b/src/helpers/backupHelper.ts @@ -4,6 +4,8 @@ import { EntityManager } from "typeorm"; import uniqBy from "lodash.uniqby"; import InternalException from "../exceptions/internalException"; import UserService from "../service/user/userService"; +import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults"; +import DatabaseActionException from "../exceptions/databaseActionException"; export type BackupSection = | "member" @@ -97,7 +99,26 @@ export default abstract class BackupHelper { FileSystemHelper.writeFile(path, filename + ".json", JSON.stringify(json, null, 2)); - // TODO: delete older backups by copies env + let files = FileSystemHelper.getFilesInDirectory("backup", ".json"); + let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime()); + + const filesToDelete = sorted.slice(BACKUP_COPIES); + for (const file of filesToDelete) { + FileSystemHelper.deleteFile("backup", file); + } + } + + static async createBackupOnInterval() { + let files = FileSystemHelper.getFilesInDirectory("backup", ".json"); + let newestFile = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime())[0]; + + let lastBackup = new Date(newestFile.split(".")[0]); + let diffInMs = new Date().getTime() - lastBackup.getTime(); + let diffInDays = diffInMs / (1000 * 60 * 60 * 24); + + if (diffInDays >= BACKUP_INTERVAL) { + await this.createBackup({}); + } } static async loadBackup({ @@ -105,11 +126,13 @@ export default abstract class BackupHelper { path = "/backup", include = [], partial = false, + overwrite = false, }: { filename: string; path?: string; partial?: boolean; include?: Array; + overwrite?: boolean; }): Promise { this.transactionManager = undefined; @@ -127,10 +150,12 @@ export default abstract class BackupHelper { const sections = this.backupSection .filter((bs) => (partial ? include.includes(bs.type) : true)) .sort((a, b) => a.orderOnClear - b.orderOnClear); - for (const section of sections.filter((s) => Object.keys(backup).includes(s.type))) { - let refered = this.backupSectionRefered[section.type]; - for (const ref of refered) { - await this.transactionManager.getRepository(ref).delete({}); + if (!overwrite) { + for (const section of sections.filter((s) => Object.keys(backup).includes(s.type))) { + let refered = this.backupSectionRefered[section.type]; + for (const ref of refered) { + await this.transactionManager.getRepository(ref).delete({}); + } } } @@ -144,7 +169,7 @@ export default abstract class BackupHelper { }) .catch((err) => { this.transactionManager = undefined; - throw new InternalException("failed to restore backup - rolling back actions", err); + throw new DatabaseActionException("BACKUP RESTORE", include.join(", "), err); }); } diff --git a/src/helpers/fileSystemHelper.ts b/src/helpers/fileSystemHelper.ts index db6b95e..f77bef2 100644 --- a/src/helpers/fileSystemHelper.ts +++ b/src/helpers/fileSystemHelper.ts @@ -11,14 +11,17 @@ export abstract class FileSystemHelper { } static readFile(...filePath: string[]) { + this.createFolder(...filePath); return readFileSync(this.formatPath(...filePath), "utf8"); } static readFileasBase64(...filePath: string[]) { + this.createFolder(...filePath); return readFileSync(this.formatPath(...filePath), "base64"); } static readTemplateFile(filePath: string) { + this.createFolder(filePath); return readFileSync(process.cwd() + filePath, "utf8"); } @@ -28,6 +31,13 @@ export abstract class FileSystemHelper { writeFileSync(path, file); } + static deleteFile(...filePath: string[]) { + const path = this.formatPath(...filePath); + if (existsSync(path)) { + unlinkSync(path); + } + } + static formatPath(...args: string[]) { return join(process.cwd(), "files", ...args); } diff --git a/src/index.ts b/src/index.ts index fe2fc80..5aa758a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,5 +40,5 @@ import RefreshCommandHandler from "./command/refreshCommandHandler"; const job = schedule.scheduleJob("0 0 * * *", async () => { console.log(`${new Date().toISOString()}: running Cron`); await RefreshCommandHandler.deleteExpired(); - // TODO: create backup by interval env + await BackupHelper.createBackupOnInterval(); }); diff --git a/src/routes/admin/user/backup.ts b/src/routes/admin/user/backup.ts index d700636..b486a49 100644 --- a/src/routes/admin/user/backup.ts +++ b/src/routes/admin/user/backup.ts @@ -4,17 +4,22 @@ import multer from "multer"; import { createManualBackup, downloadBackupFile, + downloadUploadedBackupFile, getGeneratedBackups, + getUploadedBackups, restoreBackupByLocalFile, + restoreBackupByUploadedFile, uploadBackupFile, } from "../../../controller/admin/user/backupController"; +import { FileSystemHelper } from "../../../helpers/fileSystemHelper"; const storage = multer.diskStorage({ destination: (req, file, cb) => { - cb(null, "files/backup/"); + FileSystemHelper.createFolder("uploaded-backup"); + cb(null, "files/uploaded-backup/"); }, filename: (req, file, cb) => { - cb(null, `${new Date().toISOString().split("T")[0]}-uploaded-${file.originalname}`); + cb(null, `${new Date().toISOString().split("T")[0]}_${file.originalname}`); }, }); @@ -31,14 +36,22 @@ const upload = multer({ var router = express.Router({ mergeParams: true }); -router.get("/", async (req: Request, res: Response) => { +router.get("/generated", async (req: Request, res: Response) => { await getGeneratedBackups(req, res); }); -router.get("/:filename", async (req: Request, res: Response) => { +router.get("/generated/:filename", async (req: Request, res: Response) => { await downloadBackupFile(req, res); }); +router.get("/uploaded", async (req: Request, res: Response) => { + await getUploadedBackups(req, res); +}); + +router.get("/uploaded/:filename", async (req: Request, res: Response) => { + await downloadUploadedBackupFile(req, res); +}); + router.post( "/", PermissionHelper.passCheckMiddleware("create", "user", "backup"), @@ -48,13 +61,21 @@ router.post( ); router.post( - "/restore", + "/generated/restore", PermissionHelper.passCheckMiddleware("admin", "user", "backup"), async (req: Request, res: Response) => { await restoreBackupByLocalFile(req, res); } ); +router.post( + "/uploaded/restore", + PermissionHelper.passCheckMiddleware("admin", "user", "backup"), + async (req: Request, res: Response) => { + await restoreBackupByUploadedFile(req, res); + } +); + router.post( "/upload", PermissionHelper.passCheckMiddleware("create", "user", "backup"),