-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgithub-backup.mjs
274 lines (239 loc) · 8.57 KB
/
github-backup.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import fs from "fs-extra";
import path from "path";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
// Define log levels
const LogLevel = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3,
};
// Convert string log level to numeric value
function getLogLevelValue(level) {
switch (level.toLowerCase()) {
case "debug":
return LogLevel.DEBUG;
case "info":
return LogLevel.INFO;
case "warn":
return LogLevel.WARN;
case "error":
default:
return LogLevel.ERROR;
}
}
// Set default log level
let currentLogLevel = LogLevel.INFO;
// Function to log messages with timestamps and log levels
function log(level, message) {
if (getLogLevelValue(level) <= currentLogLevel) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
}
}
// Function to handle errors and exit the process
function handleError(message, error, exitCode = 1) {
log("ERROR", message);
if (error) console.error(error);
process.exit(exitCode);
}
// Function to validate the configuration object
function validateConfig(config) {
const requiredFields = ["logseqPath", "backupPath", "githubRepo"];
for (const field of requiredFields) {
if (!config[field]) {
throw new Error(`Missing required config field: ${field}`);
}
}
log("INFO", "Configuration validated successfully");
}
// Read and validate the configuration file
let config;
try {
config = JSON.parse(await fs.readFile("config.json", "utf8"));
validateConfig(config);
// Set log level from config
if (config.logLevel) {
currentLogLevel = getLogLevelValue(config.logLevel);
log("INFO", `Log level set to ${config.logLevel.toUpperCase()}`);
}
log("INFO", "Configuration file read and validated");
} catch (error) {
handleError("Error reading or validating config file:", error);
}
// Function to execute shell commands with timeout
async function runCommand(command, timeout = 30000) {
try {
log("DEBUG", `Executing command: ${command}`);
const { stdout, stderr } = await execAsync(command, { timeout });
if (stdout) log("DEBUG", `Command output: ${stdout.trim()}`);
if (stderr) log("WARN", `Command stderr: ${stderr.trim()}`);
return stdout.trim();
} catch (error) {
if (error.code === "ETIMEDOUT") {
handleError(`Command timed out: ${command}`, error);
} else {
handleError(`Error executing command: ${command}`, error);
}
}
}
// Function to execute Git operations with retries
async function gitOperation(operation, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
log("DEBUG", `Attempting Git operation: ${operation} (attempt ${i + 1}/${maxRetries})`);
await runCommand(operation);
log("INFO", `Git operation successful: ${operation}`);
return;
} catch (error) {
log("WARN", `Git operation failed (attempt ${i + 1}/${maxRetries}): ${operation}`);
if (i === maxRetries - 1) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retrying
}
}
}
// Function to clear the directory while preserving specific files
async function clearDirectory(dir) {
const preserveFiles = [".git", "README.md", ".gitignore"];
const entries = await fs.readdir(dir);
log("INFO", `Clearing directory: ${dir}`);
for (const entry of entries) {
if (preserveFiles.includes(entry)) {
log("DEBUG", `Preserving file: ${entry}`);
continue;
}
const fullPath = path.join(dir, entry);
await fs.remove(fullPath);
log("DEBUG", `Removed: ${fullPath}`);
}
log("INFO", `Cleared directory: ${dir} (preserved ${preserveFiles.join(", ")})`);
}
// Function to ensure .gitignore file exists with correct content
async function ensureGitignore(dir) {
const gitignorePath = path.join(dir, ".gitignore");
const gitignoreContent = ".trash\n.trash 2\n.Trash\n";
try {
if (await fs.pathExists(gitignorePath)) {
log("DEBUG", "Updating existing .gitignore file");
const currentContent = await fs.readFile(gitignorePath, "utf8");
const updatedContent =
Array.from(new Set([...currentContent.split("\n"), ...gitignoreContent.split("\n")]))
.filter((line) => line.trim() !== "")
.join("\n") + "\n";
await fs.writeFile(gitignorePath, updatedContent);
log("INFO", ".gitignore file updated");
} else {
log("DEBUG", "Creating new .gitignore file");
await fs.writeFile(gitignorePath, gitignoreContent);
log("INFO", ".gitignore file created");
}
} catch (error) {
handleError("Error managing .gitignore file:", error);
}
}
// Main function to perform backup and push to GitHub
async function backupAndPush() {
const sourceDir = config.logseqPath;
const backupDir = config.backupPath;
const githubRepo = config.githubRepo;
log("INFO", "Starting backup process");
log("DEBUG", `Source directory: ${sourceDir}`);
log("DEBUG", `Backup directory: ${backupDir}`);
log("DEBUG", `GitHub repository: ${githubRepo}`);
// Check if source directory exists
if (!(await fs.pathExists(sourceDir))) {
handleError(`Source directory does not exist: ${sourceDir}`);
}
// Ensure backup directory exists
try {
await fs.ensureDir(backupDir);
log("INFO", `Backup directory ensured: ${backupDir}`);
} catch (error) {
handleError(`Error creating backup directory: ${backupDir}`, error);
}
// Clear backup directory (preserving specified files)
try {
await clearDirectory(backupDir);
} catch (error) {
handleError(`Error clearing backup directory: ${backupDir}`, error);
}
// Define copy options
const copyOptions = {
overwrite: true,
filter: (src) => {
const basename = path.basename(src);
return basename !== ".git" && basename !== ".trash" && basename !== ".trash 2" && basename !== ".Trash";
},
dereference: true,
concurrency: 100, // Limit concurrent operations
};
// Copy files from source to backup directory
try {
log("INFO", "Copying files from source to backup directory");
await fs.copy(sourceDir, backupDir, copyOptions);
log("INFO", "Files copied successfully");
} catch (error) {
handleError("Error copying files:", error);
}
// Ensure .gitignore file exists and contains correct content
await ensureGitignore(backupDir);
// Change to backup directory
process.chdir(backupDir);
log("DEBUG", `Changed working directory to: ${backupDir}`);
// Check if it's already a Git repository
const isGitRepo = await fs.pathExists(path.join(backupDir, ".git"));
if (!isGitRepo) {
log("INFO", "Initializing new Git repository");
await gitOperation("git init");
await gitOperation(`git remote add origin ${githubRepo}`);
} else {
log("INFO", "Updating existing Git repository");
await gitOperation(`git remote set-url origin ${githubRepo}`);
await gitOperation("git fetch origin");
await gitOperation("git checkout main || git checkout -b main");
}
// Check if remote repository is accessible
try {
log("INFO", "Checking remote repository accessibility");
await runCommand("git ls-remote --exit-code --heads origin main", 10000);
log("INFO", "Remote repository is accessible");
} catch (error) {
handleError("Error accessing remote repository. Please check your GitHub credentials and repository URL.", error);
}
// Check for changes
log("INFO", "Checking for changes");
const status = await runCommand("git status --porcelain");
if (status) {
log("INFO", "Changes detected. Proceeding with commit and push.");
await gitOperation("git add .");
const date = new Date().toISOString();
await gitOperation(`git commit -m "Backup: ${date}"`);
try {
log("INFO", "Pushing changes to remote repository");
await gitOperation("git push -u origin main");
log("INFO", "Backup and push completed successfully");
} catch (error) {
log("ERROR", "Error pushing to remote repository:");
console.error(error);
log("WARN", "Changes are committed locally. Please push manually when possible.");
}
} else {
log("INFO", "No changes detected. Skipping commit and push.");
}
}
// Cleanup function to be called on script exit
async function cleanup() {
// Add any cleanup operations here
log("INFO", "Cleanup completed");
}
// Set up event listeners for script exit and interruption
process.on("exit", cleanup);
process.on("SIGINT", () => {
log("WARN", "Script interrupted");
cleanup();
process.exit(2);
});
// Run the main function
backupAndPush().catch((error) => handleError("Unhandled error in backupAndPush:", error));