Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 49 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<div align="center">

[![Status](https://img.shields.io/badge/status-active-success.svg)](https://sykesdev.ca/projects/)
[![Version](https://img.shields.io/badge/version-1.0.2-blue.svg)](https://sykesdev.ca/projects/)
[![CI](https://github.com/SystemFiles/backuply/actions/workflows/ci.yml/badge.svg)](https://github.com/SystemFiles/backuply/actions/workflows/ci.yml)
[![CD](https://github.com/SystemFiles/backuply/actions/workflows/cd.yml/badge.svg)](https://github.com/SystemFiles/backuply/actions/workflows/cd.yml)
[![GitHub Issues](https://img.shields.io/github/issues/systemfiles/backuply.svg)](https://github.com/SystemFiles/backuply/issues)
Expand All @@ -24,102 +25,128 @@

## 🧐 About <a name = "about"></a>

Simple backup client written in NodeJS with an emphasis on ease-of-use. Has the ability to create both full backups and then differential backups to save space and time.
Simple backup client written in NodeJS with an emphasis on ease-of-use and speed. Has the ability to create both full backups and then differential backups to save space and time.

## 💾 Installation

Install Backuply using NPM

```bash
# Install the latest version
npm i -g backuply

# Install a specific version
npm i -g backuply@<tag>
```

## 👷‍♂️ Usage

Using backuply is simple by design. Simply start with the operation (backup, restore, or config) and specify any options to apply.
Using Backuply is simple by design. Simply start with the operation (backup, restore, or config) and optionally specify any additional options to apply. Backuply will work without any initial configuration making it easy to pick up and use right away...

```
Usage: backuply <command> [options...]

Commands:
backuply config configure backuply
backuply backup performs a custom backup of a select directory(s)
backuply restore perform a restore from a target backup
app.js config configure backuply
app.js list Displays a list of all backups that are currently known by the
system. Use --name to filter backups by name
app.js backup Performs a custom backup of a select directory(s)
app.js restore Perform a restore from a target backup

Options:
--help Show help [boolean]
--version Show version number [boolean]
```

Most functionality such as backup type is automatically determined based on how you are creating the backup. Backup options shown below
Most functionality such as backup type is automatically determined based on how you are creating the backup. Backup options shown below.

```
backuply backup
Usage: backuply backup [options...]

Descrtiption: Performs a custom backup of a select directory(s)

Positionals:
name the name for this backup [string]
source the source directory to use for the backup. This is the directory that
will be at the root of your backup [string]
dest the destination path which will contain the backup. [string]
name The name for this backup [string]
source The source directory to use for the backup. This is the directory that
will be at the root of your backup [string]
dest The destination path which will contain the backup. [string]

Options:
--help Show help [boolean]
--version Show version number [boolean]
--ref a reference id or name for the full backup used in generating a dif
--ref A reference id or name for the full backup used in generating a dif
ferential backup based on the reference. [string]

Examples:
# Will create a full backup of the source directory
- backuply backup <name> <source_path> <destination_path>
# Will create a differential backup when recognizing referenced full backup
- backuply backup <name> <source_path> <destination_path> --ref <name/uuid>
```

> By using a backup name for the reference parameter will provide **only** the latest full backup that matches the specified name ... If you intend on creating a differential backup with a full backup base other than the most recent full backup for backups matching a certain name, you will likely have to list and select the backup using a reference ID for the target backup. [Tracking Issue](https://github.com/SystemFiles/backuply/issues/38)

Restoring from an existing backup could not be easier.

```
backuply restore
Usage: backuply restore [options...]

Description: Perform a restore from a target backup

Positionals:
ref the full uuid or name for the backup to restore [string]
dest path to destination restore directory [string]
ref The full uuid or name for the backup to restore [string]
dest Path to destination restore directory [string]

Options:
--help Show help [boolean]
--version Show version number [boolean]
--full If using a name reference, tells backuply whether to restore only t
he latest full backup (if exists with ref) [boolean]
```

Listing backups can be a useful way to discover important information related to the backups you have created in the past. It can also be a good way to get reference ID's for old backups that you may want to restore or use as a base for a new backup.

```
Usage: backuply list [search term]

Description: Displays a list of all backups that are currently known by the system.
Use --name to filter backups by name

Options:
--help Show help [boolean]
--version Show version number [boolean]
--name An optional variable used to filter backups by their
user-given names [string]
```

### ⚙️ App Configuration

Making changes to any app configuration can be done in a single one-line command which is capable of modifying multiple attributes at a time. See below for usage details and some examples

```
backuply config
Usage: backuply config [keys...] [values...]

Description: Configure backuply
Description: Configures backuply

Options:
--help Show help [boolean]
--version Show version number [boolean]
--db.path Configure the path to the local database used to store backup
metadata [string]
--db.path Configure the path to the local database used to store
backup metadata [string]
--log.level Configure the logging level [string]

Examples:
# Enable debug logging
# Enable debug logging (default is INFO)
- backuply config --log.level DEBUG
# Change local db path
# Change local db path (default depends on platform ... for example on linux: /home/<you>/.config/backuply/db.json)
- backuply config --db.path ~/Documents/backuply/db.json
```

Future iterations with more config options will follow the same format and will also be documented in the `config --help` subcommand.

## 🧩 Contributing

If you would like to contribute an idea, feature request, or bugfix please start by creating an [issue](https://github.com/SystemFiles/backuply/issues)
If you would like to contribute an idea, feature request, or bugfix please start by creating an [issue](https://github.com/SystemFiles/backuply/issues). I would greatly appreciate any constructive criticism and help from the community!

## 👷‍♂️ Authors <a name = "authors" >

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"start": "NODE_ENV=dev tsc && node dist/app.js restore --ref 7203e58c-2e31-40f1-b6db-fd48cf98ca05 --dest ./dev/restore2",
"start": "NODE_ENV=dev tsc && node dist/app.js backup --name backup-test --source ~/Documents/Personal --dest ./dev --ref backup-test",
"build": "NODE_ENV=production tsc",
"lint": "NODE_ENV=test eslint \"{src,libs,test}/**/*.ts\" --fix",
"test": "echo \"WARN: no test specified\" && exit 0"
Expand Down
8 changes: 4 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { AppConfig } from './lib/configuration.js'
import { DatabaseManager } from './lib/database.js'
import { log } from './lib/logger.js'

// Define commandline options
const cmdArgs = parseArgs()

const run = async () => {
sayHello()

// Parse commandline options
const cmdArgs = parseArgs()

// Handle: Perform all app configurations first
if (cmdArgs['_'].toString() === 'config') {
const conf = AppConfig.getInstance()
Expand Down Expand Up @@ -52,7 +52,7 @@ const run = async () => {
break
}
case 'restore': {
const [ res, err ] = await restoreBackup(cmdArgs['ref'], cmdArgs['dest'])
const [ res, err ] = await restoreBackup(cmdArgs['ref'], cmdArgs['dest'], cmdArgs['full'])

if (err) {
log(`Something went wrong when attempting to restore a backup. Reason: ${err.message}`)
Expand Down
16 changes: 12 additions & 4 deletions src/common/commands/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { cwd } from 'process'
import { BackupManager } from '../../lib/backup.js'
import { DatabaseManager } from '../../lib/database.js'
import { log } from '../../lib/logger.js'
import { BackupRecord } from '../types.js'
import { BackupException } from '../exceptions.js'
import { compareRecordsByCreationTime, getLatestBackupByName } from '../functions.js'
import { BackupRecord, BackupType } from '../types.js'

export async function makeBackup(
name: string,
Expand All @@ -30,7 +32,7 @@ export async function makeBackup(
}
export async function listBackups(name?: string): Promise<Error> {
const db: DatabaseManager = DatabaseManager.getInstance()
const [ records, err ] = await db.findAllRecords()
const [ records, err ] = db.findAllRecords()
if (err) return err
let fRecords = records

Expand Down Expand Up @@ -71,9 +73,15 @@ export async function differentialBackup(
// Check if refID passed or refName
let refId = ref
if (!/\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-\b[0-9a-fA-F]{12}\b/.test(ref)) {
// Translate ref name to an ID to use
log(`Reference backup was not presented as UUID ... attempting to translate to UUID from presumed name ...`)
refId = ref

// Translate ref name to an ID to use
const [ latestFull, err ] = getLatestBackupByName(ref, BackupType.FULL)
if (err)
throw new BackupException(`Failed to translate backup for backup reference, ${ref} ... Reason: ${err.message}`)

refId = latestFull.id
log(`Translation complete. NAME (${ref}) > UUID (${refId})`)
}

// Perform the backup and return
Expand Down
27 changes: 16 additions & 11 deletions src/common/commands/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function parseArgs():
const cmd = yargs(hideBin(process.argv))

// Set options
cmd.command('config', 'configure backuply', (yargs) => {
cmd.command('config', 'Configure Backuply', (yargs) => {
return yargs
.option('db.path', {
type: 'string',
Expand All @@ -30,49 +30,54 @@ export function parseArgs():

cmd.command(
'list',
'displays a list of all backups that are currently known by the system. Use --name to filter backups by name',
'Displays a list of all backups that are currently known by the system. Use --name to filter backups by name',
(yargs) => {
return yargs.option('name', {
describe: 'An optional variable used for some info commands to refine info operations',
describe: 'An optional variable used to filter backups by their user-given names',
type: 'string'
})
}
)

// Configure custom backups
cmd.command('backup', 'performs a custom backup of a select directory(s)', (yargs) => {
cmd.command('backup', 'Performs a custom backup of a select directory(s)', (yargs) => {
return yargs
.positional('name', {
describe: 'the name for this backup',
describe: 'The name for this backup',
type: 'string'
})
.positional('source', {
describe:
'the source directory to use for the backup. This is the directory that will be at the root of your backup',
'The source directory to use for the backup. This is the directory that will be at the root of your backup',
type: 'string'
})
.positional('dest', {
describe: 'the destination path which will contain the backup.',
describe: 'The destination path which will contain the backup.',
type: 'string'
})
.option('ref', {
description:
'a reference id or name for the full backup used in generating a differential backup based on the reference.',
'A reference id or name for the full backup used in generating a differential backup based on the reference.',
type: 'string'
})
})

// Restore from backup
cmd.command('restore', 'perform a restore from a target backup', (yargs) => {
cmd.command('restore', 'Perform a restore from a target backup', (yargs) => {
yargs
.positional('ref', {
describe: 'the full uuid or name for the backup to restore',
describe: 'The full uuid or name for the backup to restore',
type: 'string'
})
.positional('dest', {
describe: 'path to destination restore directory',
describe: 'Path to destination restore directory',
type: 'string'
})
.option('full', {
describe:
'If using a name reference, tells backuply whether to restore only the latest full backup (if exists with ref)',
type: 'boolean'
})
})

// Set general parse config
Expand Down
13 changes: 11 additions & 2 deletions src/common/commands/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { resolve } from 'path/posix'
import { cwd } from 'process'
import { log } from '../../lib/logger.js'
import { RestoreManager } from '../../lib/restore.js'
import { BackupException } from '../exceptions.js'
import { getLatestBackupByName } from '../functions.js'
import { BackupType } from '../types.js'

export async function restoreBackup(ref: string, dest: string): Promise<[string, Error]> {
export async function restoreBackup(ref: string, dest: string, full = false): Promise<[string, Error]> {
if (!ref || ref.length === 0 || !dest || dest.length === 0) {
return [ null, new Error('Invalid reference ID or destination specified ...') ]
}
Expand All @@ -16,7 +19,13 @@ export async function restoreBackup(ref: string, dest: string): Promise<[string,
if (!/\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-\b[0-9a-fA-F]{12}\b/.test(ref)) {
// Translate ref name to an ID to use
log(`Reference backup was not presented as UUID ... attempting to translate to UUID from presumed name ...`)
refId = ref

// Translate ref name to an ID to use
const [ latest, err ] = getLatestBackupByName(ref, full ? BackupType.FULL : BackupType.DIFF)
if (err)
throw new BackupException(`Failed to translate backup for backup reference, ${ref} ... Reason: ${err.message}`)

refId = latest.id
log(`Translation complete. NAME (${ref}) > UUID (${refId})`)
}

Expand Down
27 changes: 26 additions & 1 deletion src/common/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { pathExists } from 'fs-extra'
import { chown, readdir, mkdir } from 'fs/promises'
import { userInfo } from 'os'
import { join } from 'path/posix'
import { DatabaseManager } from '../lib/database.js'
import { log } from '../lib/logger.js'
import { PACKAGE_NAME } from './constants.js'
import { BackupRecord, Directory } from './types.js'
import { BackupRecord, BackupType, Directory } from './types.js'

// Pure getAppDataPath
export function getAppDataPath(): string {
Expand Down Expand Up @@ -51,6 +52,30 @@ export function compareByDepth(dirA: Directory, dirB: Directory): number {
return 0
}

// Used to sort backup records by name (alphabetical, ascending)
export function compareRecordsByCreationTime(recordA: BackupRecord, recordB: BackupRecord): number {
const dateA = new Date(recordA.created)
const dateB = new Date(recordB.created)

if (dateA < dateB) return 1
if (dateA > dateB) return -1
return 0
}

// Translate name to uuid
export function getLatestBackupByName(name: string, type?: BackupType): [BackupRecord, Error] {
try {
const db = DatabaseManager.getInstance()
const [ res, err ] = db.findRecordsByName(name, type)
if (err || res.length === 0) {
throw new Error(`Failed to get any records for the given name, ${name}`)
}
return [ res.sort(compareRecordsByCreationTime).shift(), null ]
} catch (err) {
return [ null, err ]
}
}

// Create a directory with optional permissions modes
export async function createDirectory(
rootPath: string,
Expand Down
Loading