backdot 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.js +4 -2
- package/dist/commands/backup.js +66 -4
- package/dist/commands/history.js +9 -6
- package/dist/commands/init.js +3 -2
- package/dist/commands/restore.js +52 -31
- package/dist/commands/schedule.js +14 -0
- package/dist/commands/status.js +46 -11
- package/dist/commitUrl.js +5 -4
- package/dist/config.d.ts +2 -1
- package/dist/config.js +14 -12
- package/dist/crypto/encryption.d.ts +8 -0
- package/dist/crypto/encryption.js +54 -0
- package/dist/crypto/password.d.ts +11 -0
- package/dist/crypto/password.js +74 -0
- package/dist/crypto.d.ts +13 -0
- package/dist/crypto.js +129 -0
- package/dist/encryption.d.ts +2 -0
- package/dist/encryption.js +39 -0
- package/dist/git.d.ts +1 -1
- package/dist/git.js +38 -35
- package/dist/launchd.js +11 -8
- package/dist/password.d.ts +11 -0
- package/dist/password.js +74 -0
- package/dist/repoVisibility.d.ts +15 -0
- package/dist/repoVisibility.js +58 -0
- package/dist/resolveFiles.js +2 -2
- package/dist/staging.d.ts +9 -3
- package/dist/staging.js +84 -36
- package/package.json +1 -9
package/dist/staging.js
CHANGED
|
@@ -7,42 +7,55 @@ import { logger } from "./log.js";
|
|
|
7
7
|
import { errorMessage, pluralize } from "./utils.js";
|
|
8
8
|
import { ensureRemoteUrl, getCurrentBranch, gitError } from "./git.js";
|
|
9
9
|
import { STAGING_DIR, STAGING_GIT_DIR, machineDir } from "./paths.js";
|
|
10
|
+
import { encrypt, decrypt } from "./crypto/encryption.js";
|
|
11
|
+
import { KEY_FILE_PATH, ENC_SUFFIX } from "./crypto/password.js";
|
|
10
12
|
export { STAGING_DIR, STAGING_GIT_DIR, machineDir };
|
|
11
13
|
const HOME = os.homedir();
|
|
12
14
|
export function getStagedPath(filePath, machine) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const relativePath = path.relative(HOME, filePath);
|
|
16
|
+
// Files outside HOME (e.g. /etc/foo) produce a relative path starting with "..",
|
|
17
|
+
// which would escape the machine dir. Use the absolute path minus the leading "/" instead.
|
|
18
|
+
const pathWithinMachineDir = relativePath.startsWith("..") ? filePath.slice(1) : relativePath;
|
|
19
|
+
return path.join(machineDir(machine), pathWithinMachineDir);
|
|
16
20
|
}
|
|
17
21
|
export function cleanStaging(machine) {
|
|
18
|
-
const
|
|
19
|
-
if (!fs.existsSync(
|
|
22
|
+
const machineStagingDir = machineDir(machine);
|
|
23
|
+
if (!fs.existsSync(machineStagingDir)) {
|
|
20
24
|
return;
|
|
21
25
|
}
|
|
22
|
-
fs.rmSync(
|
|
26
|
+
fs.rmSync(machineStagingDir, { recursive: true, force: true });
|
|
23
27
|
logger.info(`Cleaned staging directory for machine "${machine}"`);
|
|
24
28
|
}
|
|
25
|
-
export function copyToStaging(files, machine) {
|
|
26
|
-
const
|
|
27
|
-
fs.mkdirSync(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
export function copyToStaging(files, machine, derivedKey) {
|
|
30
|
+
const machineStagingDir = machineDir(machine);
|
|
31
|
+
fs.mkdirSync(machineStagingDir, { recursive: true });
|
|
32
|
+
const filesExcludingKeyFile = files.filter((f) => path.resolve(f) !== path.resolve(KEY_FILE_PATH));
|
|
33
|
+
let copiedCount = 0;
|
|
34
|
+
for (const filePath of filesExcludingKeyFile) {
|
|
35
|
+
const stagedPath = getStagedPath(filePath, machine);
|
|
36
|
+
const destinationPath = derivedKey ? stagedPath + ENC_SUFFIX : stagedPath;
|
|
31
37
|
try {
|
|
32
|
-
fs.mkdirSync(path.dirname(
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
39
|
+
if (derivedKey) {
|
|
40
|
+
const plaintext = fs.readFileSync(filePath);
|
|
41
|
+
fs.writeFileSync(destinationPath, encrypt(plaintext, derivedKey));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
fs.copyFileSync(filePath, destinationPath);
|
|
45
|
+
}
|
|
46
|
+
copiedCount++;
|
|
35
47
|
}
|
|
36
48
|
catch {
|
|
37
|
-
logger.warn(`Failed to copy: ${filePath} -> ${
|
|
49
|
+
logger.warn(`Failed to copy: ${filePath} -> ${destinationPath}`);
|
|
38
50
|
}
|
|
39
51
|
}
|
|
40
|
-
logger.info(`Copied ${pluralize(
|
|
52
|
+
logger.info(`Copied ${pluralize(copiedCount, "file")} to staging`);
|
|
41
53
|
}
|
|
42
54
|
function failedComparisonResult(err) {
|
|
43
55
|
return { backedUp: [], modified: [], notBackedUp: [], error: errorMessage(err) };
|
|
44
56
|
}
|
|
45
|
-
export async function compareFiles(
|
|
57
|
+
export async function compareFiles(opts) {
|
|
58
|
+
const { files, machine, repository, derivedKey } = opts;
|
|
46
59
|
if (files.length === 0) {
|
|
47
60
|
return { backedUp: [], modified: [], notBackedUp: [] };
|
|
48
61
|
}
|
|
@@ -64,52 +77,87 @@ export async function compareFiles(files, machine, repository) {
|
|
|
64
77
|
catch (err) {
|
|
65
78
|
return failedComparisonResult(err);
|
|
66
79
|
}
|
|
67
|
-
let
|
|
80
|
+
let remoteBlobHashes;
|
|
68
81
|
try {
|
|
69
82
|
const treeOutput = execFileSync("git", ["ls-tree", "-r", `origin/${branch}`, `${machine}/`], {
|
|
70
83
|
encoding: "utf-8",
|
|
71
84
|
cwd: STAGING_DIR,
|
|
72
85
|
});
|
|
73
|
-
|
|
86
|
+
remoteBlobHashes = new Map(treeOutput
|
|
74
87
|
.split("\n")
|
|
75
88
|
.map((line) => line.match(/^\d+ blob ([0-9a-f]+)\t(.+)$/))
|
|
76
|
-
.filter((
|
|
77
|
-
.map((
|
|
89
|
+
.filter((match) => match !== null)
|
|
90
|
+
.map((match) => [match[2], match[1]]));
|
|
78
91
|
}
|
|
79
92
|
catch (err) {
|
|
80
93
|
return failedComparisonResult(err);
|
|
81
94
|
}
|
|
82
|
-
|
|
95
|
+
if (derivedKey) {
|
|
96
|
+
return compareFilesEncrypted(files, machine, remoteBlobHashes, derivedKey);
|
|
97
|
+
}
|
|
98
|
+
let localFileHashes;
|
|
83
99
|
try {
|
|
84
100
|
const hashOutput = execFileSync("git", ["hash-object", "--stdin-paths"], {
|
|
85
101
|
encoding: "utf-8",
|
|
86
102
|
input: files.join("\n") + "\n",
|
|
87
103
|
});
|
|
88
|
-
|
|
104
|
+
localFileHashes = hashOutput.trim().split("\n");
|
|
89
105
|
}
|
|
90
106
|
catch (err) {
|
|
91
107
|
return failedComparisonResult(err);
|
|
92
108
|
}
|
|
93
|
-
return files.reduce((
|
|
109
|
+
return files.reduce((result, file, i) => {
|
|
94
110
|
const repoRelPath = path.relative(STAGING_DIR, getStagedPath(file, machine));
|
|
95
|
-
const
|
|
96
|
-
if (!
|
|
97
|
-
|
|
111
|
+
const remoteBlobHash = remoteBlobHashes.get(repoRelPath);
|
|
112
|
+
if (!remoteBlobHash) {
|
|
113
|
+
result.notBackedUp.push(file);
|
|
98
114
|
}
|
|
99
|
-
else if (
|
|
100
|
-
|
|
115
|
+
else if (remoteBlobHash === localFileHashes[i]) {
|
|
116
|
+
result.backedUp.push(file);
|
|
101
117
|
}
|
|
102
118
|
else {
|
|
103
|
-
|
|
119
|
+
result.modified.push(file);
|
|
104
120
|
}
|
|
105
|
-
return
|
|
121
|
+
return result;
|
|
106
122
|
}, { backedUp: [], modified: [], notBackedUp: [] });
|
|
107
123
|
}
|
|
108
|
-
function
|
|
124
|
+
function compareFilesEncrypted(files, machine, remoteBlobHashes, derivedKey) {
|
|
125
|
+
const result = { backedUp: [], modified: [], notBackedUp: [] };
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const repoRelPath = path.relative(STAGING_DIR, getStagedPath(file, machine)) + ENC_SUFFIX;
|
|
128
|
+
const remoteBlobHash = remoteBlobHashes.get(repoRelPath);
|
|
129
|
+
if (!remoteBlobHash) {
|
|
130
|
+
result.notBackedUp.push(file);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const blobContent = execFileSync("git", ["cat-file", "blob", remoteBlobHash], {
|
|
135
|
+
cwd: STAGING_DIR,
|
|
136
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
137
|
+
});
|
|
138
|
+
const decrypted = decrypt(blobContent, derivedKey);
|
|
139
|
+
const localContent = fs.readFileSync(file);
|
|
140
|
+
if (decrypted.equals(localContent)) {
|
|
141
|
+
result.backedUp.push(file);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
result.modified.push(file);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
result.modified.push(file);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function generateReadmeContent(repository, encrypted) {
|
|
154
|
+
const encryptionNote = encrypted
|
|
155
|
+
? "\n> **Note:** Files in this repository are encrypted. You will need the backup password to restore.\n"
|
|
156
|
+
: "";
|
|
109
157
|
return `# Backdot Backup
|
|
110
158
|
|
|
111
159
|
This repository contains files backed up automatically using [backdot](https://github.com/sorenlouv/backdot).
|
|
112
|
-
|
|
160
|
+
${encryptionNote}
|
|
113
161
|
## Restore
|
|
114
162
|
|
|
115
163
|
\`\`\`bash
|
|
@@ -119,7 +167,7 @@ npx backdot restore ${repository}
|
|
|
119
167
|
For full documentation, configuration options, and scheduling, see the [official README](https://github.com/sorenlouv/backdot).
|
|
120
168
|
`;
|
|
121
169
|
}
|
|
122
|
-
export function writeRepoReadme(repository) {
|
|
123
|
-
fs.writeFileSync(path.join(STAGING_DIR, "README.md"),
|
|
170
|
+
export function writeRepoReadme(repository, encrypted = false) {
|
|
171
|
+
fs.writeFileSync(path.join(STAGING_DIR, "README.md"), generateReadmeContent(repository, encrypted));
|
|
124
172
|
logger.info("Wrote README.md to staging directory");
|
|
125
173
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backdot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Lightweight CLI to backup dotfiles and gitignored files to a Git repo on a daily schedule",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,20 +53,12 @@
|
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|
|
55
55
|
},
|
|
56
|
-
"lint-staged": {
|
|
57
|
-
"*.{ts,js}": [
|
|
58
|
-
"prettier --write",
|
|
59
|
-
"eslint --fix"
|
|
60
|
-
],
|
|
61
|
-
"*.{json,md,yml}": "prettier --write"
|
|
62
|
-
},
|
|
63
56
|
"devDependencies": {
|
|
64
57
|
"@eslint/js": "^10.0.1",
|
|
65
58
|
"@types/node": "^22.13.5",
|
|
66
59
|
"eslint": "^10.0.2",
|
|
67
60
|
"eslint-config-prettier": "^10.1.8",
|
|
68
61
|
"husky": "^9.1.7",
|
|
69
|
-
"lint-staged": "^16.2.7",
|
|
70
62
|
"prettier": "^3.8.1",
|
|
71
63
|
"typescript": "^5.7.3",
|
|
72
64
|
"typescript-eslint": "^8.56.1",
|