@abdess76/i18nkit 1.0.3 → 1.0.5
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/CHANGELOG.md +18 -0
- package/LICENSE +21 -21
- package/README.md +174 -15
- package/bin/commands/backup.js +226 -0
- package/bin/commands/extract.js +2 -2
- package/bin/core/applier-utils.js +8 -9
- package/bin/core/applier.js +90 -14
- package/bin/core/args.js +17 -0
- package/bin/core/backup/cleanup.js +176 -0
- package/bin/core/backup/constants.js +114 -0
- package/bin/core/backup/file-ops.js +188 -0
- package/bin/core/backup/gitignore.js +173 -0
- package/bin/core/backup/index.js +189 -0
- package/bin/core/backup/manifest.js +185 -0
- package/bin/core/backup/restore.js +216 -0
- package/bin/core/backup/session.js +258 -0
- package/bin/core/backup.js +91 -41
- package/bin/core/command-interface.js +1 -0
- package/bin/core/file-walker.js +26 -12
- package/bin/core/fs-adapter.js +10 -0
- package/bin/core/help-generator.js +1 -1
- package/bin/core/index.js +0 -4
- package/bin/core/json-utils.js +10 -6
- package/bin/core/key-generator.js +12 -12
- package/bin/core/orphan-finder.js +12 -15
- package/bin/core/paths.js +4 -4
- package/bin/core/plugin-resolver-utils.js +1 -1
- package/bin/core/translator.js +17 -8
- package/bin/plugins/adapter-transloco.js +3 -2
- package/bin/plugins/parser-primeng.js +1 -1
- package/bin/plugins/provider-deepl.js +22 -8
- package/package.json +1 -1
- package/bin/core/types.js +0 -297
package/bin/core/applier.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('./fs-adapter');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const {
|
|
11
|
+
const { withBackup } = require('./backup');
|
|
12
12
|
const {
|
|
13
13
|
groupFindingsByFile,
|
|
14
14
|
confirmFileModifications,
|
|
@@ -70,13 +70,12 @@ function buildFileResult(ctx) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function processFileReplacements(relativeFile, fileFindings, opts) {
|
|
73
|
-
const { srcDir,
|
|
73
|
+
const { srcDir, verbose = false, adapter } = opts;
|
|
74
74
|
const filePath = path.join(srcDir, relativeFile);
|
|
75
75
|
const originalContent = loadFileForReplacement(filePath, relativeFile, verbose);
|
|
76
76
|
if (!originalContent) {
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
|
-
createBackup(filePath, backupDir, { enabled: backup, dryRun });
|
|
80
79
|
const { content: replaced, count } = applyReplacementsToContent(
|
|
81
80
|
originalContent,
|
|
82
81
|
fileFindings,
|
|
@@ -125,6 +124,90 @@ function processAllFiles(findingsByFile, opts) {
|
|
|
125
124
|
return { totalFiles, totalReplacements, modifiedFiles };
|
|
126
125
|
}
|
|
127
126
|
|
|
127
|
+
function getFilesToBackup(findingsByFile, opts) {
|
|
128
|
+
const { srcDir } = opts;
|
|
129
|
+
const files = [];
|
|
130
|
+
for (const [relativeFile] of findingsByFile) {
|
|
131
|
+
const filePath = path.join(srcDir, relativeFile);
|
|
132
|
+
if (fs.existsSync(filePath)) {
|
|
133
|
+
files.push(filePath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return files;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function backupFilesForSession(filesToBackup, sessionHelpers) {
|
|
140
|
+
for (const filePath of filesToBackup) {
|
|
141
|
+
if (sessionHelpers?.backupFile) {
|
|
142
|
+
sessionHelpers.backupFile(filePath);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function saveReportIfNeeded(sessionHelpers, reportPath) {
|
|
148
|
+
if (reportPath && sessionHelpers?.saveReport) {
|
|
149
|
+
sessionHelpers.saveReport(reportPath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function transitionSessionState(sessionHelpers) {
|
|
154
|
+
if (sessionHelpers?.markReady) {
|
|
155
|
+
sessionHelpers.markReady();
|
|
156
|
+
}
|
|
157
|
+
if (sessionHelpers?.beginModifications) {
|
|
158
|
+
sessionHelpers.beginModifications();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function prepareSession(sessionHelpers, reportPath) {
|
|
163
|
+
saveReportIfNeeded(sessionHelpers, reportPath);
|
|
164
|
+
transitionSessionState(sessionHelpers);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createApplyExecutor(ctx) {
|
|
168
|
+
const { filesToBackup, reportPath, findingsByFile, opts } = ctx;
|
|
169
|
+
return sessionHelpers => {
|
|
170
|
+
backupFilesForSession(filesToBackup, sessionHelpers);
|
|
171
|
+
prepareSession(sessionHelpers, reportPath);
|
|
172
|
+
const results = processAllFiles(findingsByFile, opts);
|
|
173
|
+
logApplyResults(results, opts);
|
|
174
|
+
return { success: true, ...results };
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function logApplyHeader(log) {
|
|
179
|
+
log('Auto-Apply Translations');
|
|
180
|
+
log('-'.repeat(50));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildApplyContext(findingsByFile, opts) {
|
|
184
|
+
const cwd = process.cwd();
|
|
185
|
+
const { reportDir } = opts;
|
|
186
|
+
const filesToBackup = getFilesToBackup(findingsByFile, opts);
|
|
187
|
+
const reportPath = reportDir ? path.join(reportDir, 'report.json') : null;
|
|
188
|
+
return { cwd, filesToBackup, reportPath, findingsByFile, opts };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function confirmAndPrepare(findings, opts) {
|
|
192
|
+
const { log = console.log, interactive = false } = opts;
|
|
193
|
+
logApplyHeader(log);
|
|
194
|
+
const findingsByFile = groupFindingsByFile(findings);
|
|
195
|
+
const shouldProceed = await confirmFileModifications(findingsByFile, interactive, log);
|
|
196
|
+
return shouldProceed ? findingsByFile : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function executeApplyWithBackup(findingsByFile, opts) {
|
|
200
|
+
const { log = console.log, backup = true, dryRun = false } = opts;
|
|
201
|
+
const ctx = buildApplyContext(findingsByFile, opts);
|
|
202
|
+
const executeApply = createApplyExecutor(ctx);
|
|
203
|
+
return withBackup({
|
|
204
|
+
cwd: ctx.cwd,
|
|
205
|
+
command: 'apply',
|
|
206
|
+
operation: executeApply,
|
|
207
|
+
options: { log, backup, dryRun },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
128
211
|
/**
|
|
129
212
|
* Applies translation replacements from findings array
|
|
130
213
|
* @param {Finding[]} findings
|
|
@@ -132,17 +215,10 @@ function processAllFiles(findingsByFile, opts) {
|
|
|
132
215
|
* @returns {Promise<ApplyResult>}
|
|
133
216
|
*/
|
|
134
217
|
async function applyFindings(findings, opts = {}) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const shouldProceed = await confirmFileModifications(findingsByFile, interactive, log);
|
|
140
|
-
if (!shouldProceed) {
|
|
141
|
-
return { success: false, aborted: true };
|
|
142
|
-
}
|
|
143
|
-
const results = processAllFiles(findingsByFile, opts);
|
|
144
|
-
logApplyResults(results, opts);
|
|
145
|
-
return { success: true, ...results };
|
|
218
|
+
const findingsByFile = await confirmAndPrepare(findings, opts);
|
|
219
|
+
return findingsByFile ?
|
|
220
|
+
executeApplyWithBackup(findingsByFile, opts)
|
|
221
|
+
: { success: false, aborted: true };
|
|
146
222
|
}
|
|
147
223
|
|
|
148
224
|
/**
|
package/bin/core/args.js
CHANGED
|
@@ -98,6 +98,11 @@ const COMMAND_ALIASES = {
|
|
|
98
98
|
'--find-orphans': 'find-orphans',
|
|
99
99
|
'--check-sync': 'check-sync',
|
|
100
100
|
'--watch': 'watch',
|
|
101
|
+
'--list-backups': 'backup',
|
|
102
|
+
'--restore': 'backup',
|
|
103
|
+
'--cleanup-backups': 'backup',
|
|
104
|
+
'--backup-info': 'backup',
|
|
105
|
+
'--init-backups': 'backup',
|
|
101
106
|
};
|
|
102
107
|
|
|
103
108
|
const POSITIONAL_COMMANDS = [
|
|
@@ -107,6 +112,15 @@ const POSITIONAL_COMMANDS = [
|
|
|
107
112
|
'apply',
|
|
108
113
|
'watch',
|
|
109
114
|
'extract',
|
|
115
|
+
'backup',
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const BACKUP_FLAGS = [
|
|
119
|
+
'--list-backups',
|
|
120
|
+
'--restore',
|
|
121
|
+
'--cleanup-backups',
|
|
122
|
+
'--backup-info',
|
|
123
|
+
'--init-backups',
|
|
110
124
|
];
|
|
111
125
|
|
|
112
126
|
function detectFromFlags(args) {
|
|
@@ -116,6 +130,9 @@ function detectFromFlags(args) {
|
|
|
116
130
|
if (args.some(a => a.startsWith('--apply'))) {
|
|
117
131
|
return 'apply';
|
|
118
132
|
}
|
|
133
|
+
if (args.some(a => BACKUP_FLAGS.includes(a))) {
|
|
134
|
+
return 'backup';
|
|
135
|
+
}
|
|
119
136
|
const flagCmd = args.find(a => COMMAND_ALIASES[a]);
|
|
120
137
|
return flagCmd ? COMMAND_ALIASES[flagCmd] : null;
|
|
121
138
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Backup session cleanup and retention management.
|
|
5
|
+
* @module backup/cleanup
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Session, parseSessionId } = require('./session');
|
|
9
|
+
const { removeDirSync } = require('./file-ops');
|
|
10
|
+
const { DEFAULT_CONFIG, getSessionDir, SessionStatus } = require('./constants');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns session age in days
|
|
14
|
+
* @param {string} sessionId
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
function getSessionAge(sessionId) {
|
|
18
|
+
const parsed = parseSessionId(sessionId);
|
|
19
|
+
if (!parsed) {
|
|
20
|
+
return Infinity;
|
|
21
|
+
}
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const diff = now - parsed.timestamp;
|
|
24
|
+
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldKeepSession(session) {
|
|
28
|
+
return (
|
|
29
|
+
session.status === SessionStatus.IN_PROGRESS || session.status === SessionStatus.BACKING_UP
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isWithinLimits(index, age, config) {
|
|
34
|
+
return index < config.maxSessions && age <= config.maxAgeDays;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function categorizeSession(session, index, config) {
|
|
38
|
+
const age = getSessionAge(session.id);
|
|
39
|
+
return { session, keep: isWithinLimits(index, age, config) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {Object[]} sessions
|
|
44
|
+
* @param {Object} config
|
|
45
|
+
* @returns {{keep: Object[], delete: Object[]}}
|
|
46
|
+
*/
|
|
47
|
+
function selectSessionsForCleanup(sessions, config) {
|
|
48
|
+
const protectedSessions = sessions.filter(shouldKeepSession);
|
|
49
|
+
const cleanableSessions = sessions.filter(s => !shouldKeepSession(s));
|
|
50
|
+
const categorized = cleanableSessions.map((s, i) => categorizeSession(s, i, config));
|
|
51
|
+
const toKeep = categorized.filter(c => c.keep).map(c => c.session);
|
|
52
|
+
const toDelete = categorized.filter(c => !c.keep).map(c => c.session);
|
|
53
|
+
return { keep: [...protectedSessions, ...toKeep], delete: toDelete };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function logVerbose(verbose, log, message) {
|
|
57
|
+
if (verbose) {
|
|
58
|
+
log(message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function deleteSession(sessionDir, sessionId, ctx) {
|
|
63
|
+
const { dryRun, verbose, log } = ctx;
|
|
64
|
+
if (dryRun) {
|
|
65
|
+
log(`Would delete: ${sessionId}`);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
removeDirSync(sessionDir);
|
|
70
|
+
logVerbose(verbose, log, `Deleted: ${sessionId}`);
|
|
71
|
+
return true;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logVerbose(verbose, log, `Failed to delete ${sessionId}: ${err.message}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function processDeletions(cwd, toDelete, ctx) {
|
|
79
|
+
return toDelete.map(session => {
|
|
80
|
+
const sessionDir = getSessionDir(cwd, session.id);
|
|
81
|
+
return deleteSession(sessionDir, session.id, ctx);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleEmptySessions(verbose, log) {
|
|
86
|
+
logVerbose(verbose, log, 'No backup sessions found');
|
|
87
|
+
return { deleted: 0, kept: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleNoDeletions(keep, verbose, log) {
|
|
91
|
+
logVerbose(verbose, log, `Keeping all ${keep.length} sessions`);
|
|
92
|
+
return { deleted: 0, kept: keep.length };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function executeCleanup(ctx) {
|
|
96
|
+
const { cwd, sessions, config, dryRun, verbose, log } = ctx;
|
|
97
|
+
const { keep, delete: toDelete } = selectSessionsForCleanup(sessions, config);
|
|
98
|
+
if (toDelete.length === 0) {
|
|
99
|
+
return handleNoDeletions(keep, verbose, log);
|
|
100
|
+
}
|
|
101
|
+
processDeletions(cwd, toDelete, { dryRun, verbose, log });
|
|
102
|
+
return { deleted: dryRun ? 0 : toDelete.length, kept: keep.length };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function loadSessions(cwd) {
|
|
106
|
+
const sessions = Session.listAll(cwd);
|
|
107
|
+
return sessions.length === 0 ? null : sessions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildCleanupContext(cwd, options) {
|
|
111
|
+
const config = { ...DEFAULT_CONFIG, ...options };
|
|
112
|
+
const { dryRun = false, verbose = false, log = console.log } = options;
|
|
113
|
+
return { cwd, config, dryRun, verbose, log };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Removes sessions exceeding retention limits
|
|
118
|
+
* @param {string} cwd
|
|
119
|
+
* @param {Object} [options]
|
|
120
|
+
* @returns {{deleted: number, kept: number}}
|
|
121
|
+
*/
|
|
122
|
+
function cleanupOldSessions(cwd, options = {}) {
|
|
123
|
+
const ctx = buildCleanupContext(cwd, options);
|
|
124
|
+
const sessions = loadSessions(cwd);
|
|
125
|
+
if (!sessions) {
|
|
126
|
+
return handleEmptySessions(ctx.verbose, ctx.log);
|
|
127
|
+
}
|
|
128
|
+
return executeCleanup({ ...ctx, sessions });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function mapSessionForPreview(session) {
|
|
132
|
+
return {
|
|
133
|
+
id: session.id,
|
|
134
|
+
age: getSessionAge(session.id),
|
|
135
|
+
status: session.status,
|
|
136
|
+
fileCount: session.fileCount,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns cleanup preview without executing
|
|
142
|
+
* @param {string} cwd
|
|
143
|
+
* @param {Object} [options]
|
|
144
|
+
* @returns {{sessions: Object[], toDelete: Object[], toKeep: Object[]}}
|
|
145
|
+
*/
|
|
146
|
+
function getCleanupPreview(cwd, options = {}) {
|
|
147
|
+
const config = { ...DEFAULT_CONFIG, ...options };
|
|
148
|
+
const sessions = Session.listAll(cwd);
|
|
149
|
+
if (sessions.length === 0) {
|
|
150
|
+
return { sessions: [], toDelete: [], toKeep: [] };
|
|
151
|
+
}
|
|
152
|
+
const { keep, delete: toDelete } = selectSessionsForCleanup(sessions, config);
|
|
153
|
+
return {
|
|
154
|
+
sessions,
|
|
155
|
+
toDelete: toDelete.map(mapSessionForPreview),
|
|
156
|
+
toKeep: keep.map(mapSessionForPreview),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} cwd
|
|
162
|
+
* @param {Object} config
|
|
163
|
+
* @returns {{deleted: number, kept: number}|null}
|
|
164
|
+
*/
|
|
165
|
+
function autoCleanupIfEnabled(cwd, config) {
|
|
166
|
+
if (!config.autoCleanup) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return cleanupOldSessions(cwd, { ...config, verbose: false });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
cleanupOldSessions,
|
|
174
|
+
getCleanupPreview,
|
|
175
|
+
autoCleanupIfEnabled,
|
|
176
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Backup system constants and path utilities.
|
|
5
|
+
* @module backup/constants
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const BACKUP_ROOT = '.i18nkit';
|
|
11
|
+
const BACKUP_DIR = 'backups';
|
|
12
|
+
const LATEST_FILE = 'latest.txt';
|
|
13
|
+
const MANIFEST_FILE = 'manifest.json';
|
|
14
|
+
const STATUS_FILE = '.status';
|
|
15
|
+
const GITIGNORE_FILE = '.gitignore';
|
|
16
|
+
|
|
17
|
+
const SessionStatus = Object.freeze({
|
|
18
|
+
PENDING: 'pending',
|
|
19
|
+
BACKING_UP: 'backing-up',
|
|
20
|
+
READY: 'ready',
|
|
21
|
+
IN_PROGRESS: 'in-progress',
|
|
22
|
+
COMPLETED: 'completed',
|
|
23
|
+
ROLLED_BACK: 'rolled-back',
|
|
24
|
+
FAILED: 'failed',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const VALID_TRANSITIONS = Object.freeze({
|
|
28
|
+
[SessionStatus.PENDING]: [SessionStatus.BACKING_UP, SessionStatus.FAILED],
|
|
29
|
+
[SessionStatus.BACKING_UP]: [SessionStatus.READY, SessionStatus.FAILED],
|
|
30
|
+
[SessionStatus.READY]: [SessionStatus.IN_PROGRESS, SessionStatus.FAILED],
|
|
31
|
+
[SessionStatus.IN_PROGRESS]: [
|
|
32
|
+
SessionStatus.COMPLETED,
|
|
33
|
+
SessionStatus.ROLLED_BACK,
|
|
34
|
+
SessionStatus.FAILED,
|
|
35
|
+
],
|
|
36
|
+
[SessionStatus.COMPLETED]: [],
|
|
37
|
+
[SessionStatus.ROLLED_BACK]: [],
|
|
38
|
+
[SessionStatus.FAILED]: [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
42
|
+
maxSessions: 10,
|
|
43
|
+
maxAgeDays: 30,
|
|
44
|
+
autoCleanup: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} cwd
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function getBackupRoot(cwd) {
|
|
52
|
+
return path.join(cwd, BACKUP_ROOT);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} cwd
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function getBackupsDir(cwd) {
|
|
60
|
+
return path.join(cwd, BACKUP_ROOT, BACKUP_DIR);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} cwd
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
function getLatestFile(cwd) {
|
|
68
|
+
return path.join(cwd, BACKUP_ROOT, BACKUP_DIR, LATEST_FILE);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} cwd
|
|
73
|
+
* @param {string} sessionId
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
function getSessionDir(cwd, sessionId) {
|
|
77
|
+
return path.join(cwd, BACKUP_ROOT, BACKUP_DIR, sessionId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} cwd
|
|
82
|
+
* @param {string} sessionId
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function getManifestPath(cwd, sessionId) {
|
|
86
|
+
return path.join(getSessionDir(cwd, sessionId), MANIFEST_FILE);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} cwd
|
|
91
|
+
* @param {string} sessionId
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
function getStatusPath(cwd, sessionId) {
|
|
95
|
+
return path.join(getSessionDir(cwd, sessionId), STATUS_FILE);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
BACKUP_ROOT,
|
|
100
|
+
BACKUP_DIR,
|
|
101
|
+
LATEST_FILE,
|
|
102
|
+
MANIFEST_FILE,
|
|
103
|
+
STATUS_FILE,
|
|
104
|
+
GITIGNORE_FILE,
|
|
105
|
+
SessionStatus,
|
|
106
|
+
VALID_TRANSITIONS,
|
|
107
|
+
DEFAULT_CONFIG,
|
|
108
|
+
getBackupRoot,
|
|
109
|
+
getBackupsDir,
|
|
110
|
+
getLatestFile,
|
|
111
|
+
getSessionDir,
|
|
112
|
+
getManifestPath,
|
|
113
|
+
getStatusPath,
|
|
114
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Atomic file operations for backup system.
|
|
5
|
+
* @module backup/file-ops
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('../fs-adapter');
|
|
9
|
+
const nodeFsSync = require('fs');
|
|
10
|
+
const nodeFsPromises = require('fs').promises;
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
function generateTempPath(targetPath) {
|
|
15
|
+
const rand = crypto.randomBytes(4).toString('hex');
|
|
16
|
+
return `${targetPath}.${Date.now()}.${rand}.tmp`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Writes content atomically via temp file + rename
|
|
21
|
+
* @param {string} targetPath
|
|
22
|
+
* @param {string} content
|
|
23
|
+
* @returns {Promise<void>}
|
|
24
|
+
*/
|
|
25
|
+
async function atomicWrite(targetPath, content) {
|
|
26
|
+
const tempPath = generateTempPath(targetPath);
|
|
27
|
+
const dir = path.dirname(targetPath);
|
|
28
|
+
await fs.mkdir(dir, { recursive: true });
|
|
29
|
+
try {
|
|
30
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
31
|
+
await fs.rename(tempPath, targetPath);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
try {
|
|
34
|
+
await fs.unlink(tempPath);
|
|
35
|
+
} catch {
|
|
36
|
+
// Cleanup failure is non-critical
|
|
37
|
+
}
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} targetPath
|
|
44
|
+
* @param {string} content
|
|
45
|
+
*/
|
|
46
|
+
function atomicWriteSync(targetPath, content) {
|
|
47
|
+
const tempPath = generateTempPath(targetPath);
|
|
48
|
+
const dir = path.dirname(targetPath);
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(tempPath, content, 'utf-8');
|
|
52
|
+
nodeFsSync.renameSync(tempPath, targetPath);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
try {
|
|
55
|
+
nodeFsSync.unlinkSync(tempPath);
|
|
56
|
+
} catch {
|
|
57
|
+
// Cleanup failure is non-critical
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Copies file preserving directory structure relative to cwd
|
|
65
|
+
* @param {string} srcFile
|
|
66
|
+
* @param {string} destRoot
|
|
67
|
+
* @param {string} cwd
|
|
68
|
+
* @returns {Promise<{original: string, backup: string, size: number}>}
|
|
69
|
+
*/
|
|
70
|
+
async function copyFilePreservingStructure(srcFile, destRoot, cwd) {
|
|
71
|
+
const relativePath = path.relative(cwd, srcFile);
|
|
72
|
+
const destPath = path.join(destRoot, relativePath);
|
|
73
|
+
const content = await fs.readFile(srcFile, 'utf-8');
|
|
74
|
+
await atomicWrite(destPath, content);
|
|
75
|
+
return {
|
|
76
|
+
original: relativePath,
|
|
77
|
+
backup: relativePath,
|
|
78
|
+
size: Buffer.byteLength(content, 'utf-8'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} srcFile
|
|
84
|
+
* @param {string} destRoot
|
|
85
|
+
* @param {string} cwd
|
|
86
|
+
* @returns {{original: string, backup: string, size: number}}
|
|
87
|
+
*/
|
|
88
|
+
function copyFilePreservingStructureSync(srcFile, destRoot, cwd) {
|
|
89
|
+
const relativePath = path.relative(cwd, srcFile);
|
|
90
|
+
const destPath = path.join(destRoot, relativePath);
|
|
91
|
+
const content = fs.readFileSync(srcFile, 'utf-8');
|
|
92
|
+
atomicWriteSync(destPath, content);
|
|
93
|
+
return {
|
|
94
|
+
original: relativePath,
|
|
95
|
+
backup: relativePath,
|
|
96
|
+
size: Buffer.byteLength(content, 'utf-8'),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} backupPath
|
|
102
|
+
* @param {string} originalPath
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
*/
|
|
105
|
+
async function restoreFile(backupPath, originalPath, _cwd) {
|
|
106
|
+
const content = await fs.readFile(backupPath, 'utf-8');
|
|
107
|
+
await atomicWrite(originalPath, content);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} backupPath
|
|
112
|
+
* @param {string} originalPath
|
|
113
|
+
*/
|
|
114
|
+
function restoreFileSync(backupPath, originalPath) {
|
|
115
|
+
const content = fs.readFileSync(backupPath, 'utf-8');
|
|
116
|
+
atomicWriteSync(originalPath, content);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} dirPath
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
async function ensureDir(dirPath) {
|
|
124
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} dirPath
|
|
129
|
+
*/
|
|
130
|
+
function ensureDirSync(dirPath) {
|
|
131
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {string} dirPath
|
|
136
|
+
* @returns {Promise<void>}
|
|
137
|
+
*/
|
|
138
|
+
async function removeDir(dirPath) {
|
|
139
|
+
await nodeFsPromises.rm(dirPath, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} dirPath
|
|
144
|
+
*/
|
|
145
|
+
function removeDirSync(dirPath) {
|
|
146
|
+
nodeFsSync.rmSync(dirPath, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} parentDir
|
|
151
|
+
* @returns {Promise<string[]>}
|
|
152
|
+
*/
|
|
153
|
+
async function listDirs(parentDir) {
|
|
154
|
+
try {
|
|
155
|
+
const entries = await fs.readdir(parentDir, { withFileTypes: true });
|
|
156
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} parentDir
|
|
164
|
+
* @returns {string[]}
|
|
165
|
+
*/
|
|
166
|
+
function listDirsSync(parentDir) {
|
|
167
|
+
try {
|
|
168
|
+
const entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
169
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
170
|
+
} catch {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
atomicWrite,
|
|
177
|
+
atomicWriteSync,
|
|
178
|
+
copyFilePreservingStructure,
|
|
179
|
+
copyFilePreservingStructureSync,
|
|
180
|
+
restoreFile,
|
|
181
|
+
restoreFileSync,
|
|
182
|
+
ensureDir,
|
|
183
|
+
ensureDirSync,
|
|
184
|
+
removeDir,
|
|
185
|
+
removeDirSync,
|
|
186
|
+
listDirs,
|
|
187
|
+
listDirsSync,
|
|
188
|
+
};
|