vmsnap
Version:
A Node based backup and backup rotation tool for KVM domains.
2 lines (1 loc) • 3.13 kB
JavaScript
import{sep as f}from"path";import{spawn as b}from"child_process";import{rm as R}from"fs/promises";import a from"dayjs";import h from"dayjs/plugin/advancedFormat.js";import F from"dayjs/plugin/quarterOfYear.js";import A from"dayjs/plugin/dayOfYear.js";import{logger as s}from"../vmsnap.js";import{cleanupCheckpoints as N,domainExists as O,fetchAllDomains as U}from"./virsh.js";import{fileExists as Y,parseArrayParam as x}from"./general.js";import{cleanupBitmaps as _}from"./qemu-img.js";a.extend(h),a.extend(F),a.extend(A);export const BACKUP="virtnbdbackup";const $="YYYY-MM",k="YYYY-[Q]Q",L="YYYY-",w="YYYY";export const FREQUENCY_MONTHLY="month";const p="quarter",d="bi-annual",l="year",E=[FREQUENCY_MONTHLY,p,d,l],u=(r=FREQUENCY_MONTHLY,t=!0)=>{let n;switch(r){case p:n=t?a().format(k):a().subtract(3,"months").format(k);break;case d:let e=a().dayOfYear()>=180?2:1;t||(e=e===1?2:1);const i=`${L}p${e}`;n=t?a().format(i):a().subtract(6,"months").format(i);break;case l:n=t?a().format(w):a().subtract(1,"year").format(w);break;case FREQUENCY_MONTHLY:n=t?a().format($):a().subtract(1,"month").format($);break;default:return}return`vmsnap-backup-${r}ly-${n}`},v=async({domains:r,output:t,raw:n,groupBy:e,prune:i})=>{if(!r)throw new Error("No domains specified",{code:ERR_DOMAINS});if(!t)throw new Error("No output directory specified",{code:ERR_OUTPUT_DIR});for(const o of await x(r,U))await M(o,e,t)&&(s.info("Creating a new backup directory, running bitmap cleanup"),await N(o),await _(o)),await Q(o,t,n,e),await C(o,e,i,t)&&(s.info("Middle of the current backup window, running a cleanup on old backups"),await T(o,e,t))},M=async(r,t,n)=>await Y(`${n}${f}${r}${f}${u(t)}`)?!1:(s.info("Backup folder does not exist, cleanup required"),!0),C=async(r,t,n,e)=>{if(n===!1)return!1;if(!E.includes(t))return s.warn(`Invalid groupBy: ${t}. Pruning disabled`),!1;const i=u(t,!1);if(console.log(i),i===void 0)return s.info("Unable to determine previous backup folder, skipping pruning"),!1;if(!await Y(`${e}${f}${r}${f}${i}`))return!1;const c=a().diff(P(t),"days");switch(s.info(`Days since the start of the ${t}: ${c}`),t.toLowerCase()){case FREQUENCY_MONTHLY:return c>=15;case p:return c>=45;case d:return c>=90;case l:return c>=180;default:return!1}},T=async(r,t,n)=>{const e=u(t,!1);if(e===void 0){s.info("Unable to determine previous backup folder, skipping pruning");return}s.info(`Pruning ${t}ly backup (${e}) for ${r}`),await R(`${n}${f}${r}${f}${e}`,{recursive:!0,force:!0})},P=r=>r===d?a().startOf(FREQUENCY_MONTHLY).subtract(6,"months"):E.includes(r)?a().startOf(r):a().startOf(FREQUENCY_MONTHLY),Q=async(r,t,n,e)=>{if(!await O(r)){s.warn(`${r} does not exist`);return}const i=["-S","--noprogress","-d",r,"-l","auto","-o",`${t}${f}${r}${f}${u(e)}`];n&&i.push("--raw");const o=b(BACKUP,i,{uid:0,gid:0,stdio:"inherit"});o.stdout&&(o.stdout.setEncoding("utf8"),o.stdout.on("data",c=>{s.info(c)})),o.stderr&&(o.stderr.setEncoding("utf8"),o.stderr.on("data",c=>{s.error(c)})),await new Promise(c=>{o.on("close",m=>{m!==0&&s.error(`Backup for ${r} failed with code ${m}`),c()})})};export{u as getBackupFolder,v as performBackup};