@ongtrieuhau861457/runner-tailscale-sync 1.260202.11920
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 +310 -0
- package/bin/runner-sync.js +85 -0
- package/package.json +41 -0
- package/src/adapters/fs.js +160 -0
- package/src/adapters/git.js +185 -0
- package/src/adapters/http.js +56 -0
- package/src/adapters/process.js +151 -0
- package/src/adapters/ssh.js +103 -0
- package/src/adapters/tailscale.js +406 -0
- package/src/cli/commands/init.js +13 -0
- package/src/cli/commands/push.js +13 -0
- package/src/cli/commands/status.js +13 -0
- package/src/cli/commands/sync.js +22 -0
- package/src/cli/parser.js +114 -0
- package/src/core/data-sync.js +177 -0
- package/src/core/init.js +141 -0
- package/src/core/push.js +113 -0
- package/src/core/runner-detector.js +167 -0
- package/src/core/service-controller.js +141 -0
- package/src/core/status.js +130 -0
- package/src/core/sync-orchestrator.js +260 -0
- package/src/index.js +140 -0
- package/src/utils/config.js +129 -0
- package/src/utils/constants.js +33 -0
- package/src/utils/errors.js +45 -0
- package/src/utils/logger.js +154 -0
- package/src/utils/time.js +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# runner-tailscale-sync
|
|
2
|
+
|
|
3
|
+
Đồng bộ runner-data giữa các runner trên GitHub Actions, Azure Pipeline qua Tailscale network.
|
|
4
|
+
|
|
5
|
+
## ✨ Tính năng
|
|
6
|
+
|
|
7
|
+
- 🔄 Tự động đồng bộ `.runner-data` giữa các runner
|
|
8
|
+
- 🌐 Sử dụng Tailscale để kết nối an toàn giữa runners
|
|
9
|
+
- 🛑 Tự động stop services trên runner cũ khi runner mới bắt đầu
|
|
10
|
+
- 📦 Push data lên git repository
|
|
11
|
+
- 🎯 Hỗ trợ cả CLI và Library
|
|
12
|
+
- 🪟 Cross-platform (Windows + Linux)
|
|
13
|
+
- 📊 Logging chi tiết với version tracking
|
|
14
|
+
|
|
15
|
+
## 📦 Cài đặt
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install runner-tailscale-sync
|
|
19
|
+
|
|
20
|
+
# Hoặc global
|
|
21
|
+
npm install -g runner-tailscale-sync
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 🚀 Sử dụng
|
|
25
|
+
|
|
26
|
+
### CLI
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Full sync workflow
|
|
30
|
+
TAILSCALE_ENABLE=1 runner-sync
|
|
31
|
+
|
|
32
|
+
# Chỉ khởi tạo Tailscale
|
|
33
|
+
runner-sync init
|
|
34
|
+
|
|
35
|
+
# Chỉ push git
|
|
36
|
+
runner-sync push
|
|
37
|
+
|
|
38
|
+
# Xem status
|
|
39
|
+
runner-sync status
|
|
40
|
+
|
|
41
|
+
# Custom working directory
|
|
42
|
+
runner-sync --cwd /path/to/project
|
|
43
|
+
|
|
44
|
+
# Verbose mode
|
|
45
|
+
runner-sync -v
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Library
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const runnerSync = require('runner-tailscale-sync');
|
|
52
|
+
|
|
53
|
+
// Full sync
|
|
54
|
+
await runnerSync.sync({
|
|
55
|
+
cwd: '/path/to/project',
|
|
56
|
+
verbose: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Chỉ init
|
|
60
|
+
await runnerSync.init();
|
|
61
|
+
|
|
62
|
+
// Chỉ push git
|
|
63
|
+
await runnerSync.push();
|
|
64
|
+
|
|
65
|
+
// Xem status
|
|
66
|
+
await runnerSync.status();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Advanced Usage - Sử dụng modules riêng lẻ
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const {
|
|
73
|
+
Config,
|
|
74
|
+
Logger,
|
|
75
|
+
syncOrchestrator,
|
|
76
|
+
runnerDetector,
|
|
77
|
+
dataSync,
|
|
78
|
+
tailscale
|
|
79
|
+
} = require('runner-tailscale-sync');
|
|
80
|
+
|
|
81
|
+
// Tạo config
|
|
82
|
+
const config = new Config({ cwd: process.cwd() });
|
|
83
|
+
const logger = new Logger({ packageName: 'my-tool', version: '1.0.0' });
|
|
84
|
+
|
|
85
|
+
// Detect previous runner
|
|
86
|
+
const detection = await runnerDetector.detectPreviousRunner(config, logger);
|
|
87
|
+
|
|
88
|
+
// Pull data
|
|
89
|
+
if (detection.previousRunner) {
|
|
90
|
+
await dataSync.pullData(config, detection.previousRunner, logger);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Hoặc orchestrate toàn bộ
|
|
94
|
+
await syncOrchestrator.orchestrate(config, logger);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## ⚙️ Cấu hình
|
|
98
|
+
|
|
99
|
+
### Environment Variables
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Tailscale (required nếu TAILSCALE_ENABLE=1)
|
|
103
|
+
TAILSCALE_CLIENT_ID=your_client_id
|
|
104
|
+
TAILSCALE_CLIENT_SECRET=your_client_secret
|
|
105
|
+
TAILSCALE_TAGS=tag:ci
|
|
106
|
+
TAILSCALE_ENABLE=1
|
|
107
|
+
|
|
108
|
+
# Services to stop on previous runner
|
|
109
|
+
SERVICES_TO_STOP=cloudflared,pocketbase,http-server
|
|
110
|
+
|
|
111
|
+
# Git
|
|
112
|
+
GIT_PUSH_ENABLED=1
|
|
113
|
+
GIT_BRANCH=main
|
|
114
|
+
|
|
115
|
+
# Working directory
|
|
116
|
+
TOOL_CWD=/path/to/project
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### .env File
|
|
120
|
+
|
|
121
|
+
```env
|
|
122
|
+
TAILSCALE_CLIENT_ID=tskey-client-xxxxx
|
|
123
|
+
TAILSCALE_CLIENT_SECRET=tskey-xxxxx
|
|
124
|
+
TAILSCALE_TAGS=tag:ci
|
|
125
|
+
TAILSCALE_ENABLE=1
|
|
126
|
+
SERVICES_TO_STOP=cloudflared,pocketbase
|
|
127
|
+
GIT_PUSH_ENABLED=1
|
|
128
|
+
GIT_BRANCH=main
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 📂 Cấu trúc dữ liệu
|
|
132
|
+
|
|
133
|
+
Tất cả dữ liệu được lưu trong `.runner-data/`:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
.runner-data/
|
|
137
|
+
├── logs/ # Log files
|
|
138
|
+
├── pid/ # PID files
|
|
139
|
+
├── data-services/ # Service data
|
|
140
|
+
└── tmp/ # Temporary files
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 🔄 Quy trình hoạt động
|
|
144
|
+
|
|
145
|
+
1. **Runner01** khởi động → Join Tailscale → Chạy 55 phút → Dữ liệu được lưu trong `.runner-data/`
|
|
146
|
+
2. **Runner02** bắt đầu:
|
|
147
|
+
- Join Tailscale network
|
|
148
|
+
- Detect Runner01 (cùng tag, đang active)
|
|
149
|
+
- Pull `.runner-data/` từ Runner01
|
|
150
|
+
- Stop services trên Runner01 (cloudflared, pocketbase, etc.)
|
|
151
|
+
- Chạy services trên Runner02
|
|
152
|
+
- Push `.runner-data/` lên git repository
|
|
153
|
+
3. **Runner01** → **Runner02** xoay vòng liên tục
|
|
154
|
+
|
|
155
|
+
## 🎯 Use Cases
|
|
156
|
+
|
|
157
|
+
### GitHub Actions
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
- name: Setup Tailscale Sync
|
|
161
|
+
env:
|
|
162
|
+
TAILSCALE_CLIENT_ID: ${{ secrets.TAILSCALE_CLIENT_ID }}
|
|
163
|
+
TAILSCALE_CLIENT_SECRET: ${{ secrets.TAILSCALE_CLIENT_SECRET }}
|
|
164
|
+
TAILSCALE_ENABLE: 1
|
|
165
|
+
run: |
|
|
166
|
+
npm install -g runner-tailscale-sync
|
|
167
|
+
runner-sync
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Azure DevOps
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
- script: |
|
|
174
|
+
npm install -g runner-tailscale-sync
|
|
175
|
+
runner-sync
|
|
176
|
+
env:
|
|
177
|
+
TAILSCALE_CLIENT_ID: $(TAILSCALE_CLIENT_ID)
|
|
178
|
+
TAILSCALE_CLIENT_SECRET: $(TAILSCALE_CLIENT_SECRET)
|
|
179
|
+
TAILSCALE_ENABLE: 1
|
|
180
|
+
displayName: 'Sync Runner Data'
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Self-hosted Runner
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Install
|
|
187
|
+
npm install -g runner-tailscale-sync
|
|
188
|
+
|
|
189
|
+
# Add to runner startup script
|
|
190
|
+
export TAILSCALE_ENABLE=1
|
|
191
|
+
export TAILSCALE_CLIENT_ID=your_client_id
|
|
192
|
+
export TAILSCALE_CLIENT_SECRET=your_secret
|
|
193
|
+
|
|
194
|
+
runner-sync
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 🛠️ Development
|
|
198
|
+
|
|
199
|
+
### Scripts
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Generate new version (VN timezone: 1.yyMMdd.1HHmm)
|
|
203
|
+
npm run version
|
|
204
|
+
|
|
205
|
+
# Build validation
|
|
206
|
+
npm run build
|
|
207
|
+
|
|
208
|
+
# Publish to npm
|
|
209
|
+
npm run publish
|
|
210
|
+
|
|
211
|
+
# Dry run publish
|
|
212
|
+
node scripts/publish.js --dry-run
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Testing Locally
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Link globally
|
|
219
|
+
npm link
|
|
220
|
+
|
|
221
|
+
# Test CLI
|
|
222
|
+
runner-sync --help
|
|
223
|
+
runner-sync status
|
|
224
|
+
|
|
225
|
+
# Test as library
|
|
226
|
+
node -e "require('./src/index.js').status().then(console.log)"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## 📝 Version Format
|
|
230
|
+
|
|
231
|
+
Version theo giờ Việt Nam (UTC+7): `1.yyMMdd.1HHmm`
|
|
232
|
+
|
|
233
|
+
Ví dụ:
|
|
234
|
+
- Build lúc 15:30 ngày 02/02/2025 → `1.250202.11530`
|
|
235
|
+
- Build lúc 09:45 ngày 15/03/2025 → `1.250315.10945`
|
|
236
|
+
|
|
237
|
+
Đảm bảo semver compliance và tự động tăng theo thời gian.
|
|
238
|
+
|
|
239
|
+
## 🔧 Yêu cầu hệ thống
|
|
240
|
+
|
|
241
|
+
- Node.js >= 20
|
|
242
|
+
- Git (cho tính năng push)
|
|
243
|
+
- Tailscale (sẽ tự động cài trên Linux)
|
|
244
|
+
- rsync hoặc scp (cho data sync)
|
|
245
|
+
|
|
246
|
+
### Windows
|
|
247
|
+
|
|
248
|
+
Trên Windows, cần cài thêm:
|
|
249
|
+
- [Tailscale for Windows](https://tailscale.com/download/windows)
|
|
250
|
+
- Git for Windows (có sẵn ssh/scp)
|
|
251
|
+
- Hoặc cài rsync qua: `choco install rsync` hoặc WSL
|
|
252
|
+
|
|
253
|
+
Cấu hình đường dẫn trong `.env`:
|
|
254
|
+
```env
|
|
255
|
+
SSH_PATH=C:\Program Files\Git\usr\bin\ssh.exe
|
|
256
|
+
RSYNC_PATH=C:\Program Files\rsync\rsync.exe
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## 🐛 Troubleshooting
|
|
260
|
+
|
|
261
|
+
### Tailscale không kết nối được
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Kiểm tra Tailscale status
|
|
265
|
+
tailscale status
|
|
266
|
+
|
|
267
|
+
# Login manually
|
|
268
|
+
tailscale login
|
|
269
|
+
|
|
270
|
+
# Check logs
|
|
271
|
+
runner-sync status -v
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Sync thất bại
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# Kiểm tra SSH connection
|
|
278
|
+
ssh runner01-ip echo "OK"
|
|
279
|
+
|
|
280
|
+
# Test rsync
|
|
281
|
+
rsync -avz runner01-ip:.runner-data/ .runner-data/
|
|
282
|
+
|
|
283
|
+
# Verbose mode
|
|
284
|
+
runner-sync -v
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Git push bị conflict
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# Pull latest trước
|
|
291
|
+
cd /path/to/repo
|
|
292
|
+
git pull origin main
|
|
293
|
+
|
|
294
|
+
# Hoặc disable git push
|
|
295
|
+
export GIT_PUSH_ENABLED=0
|
|
296
|
+
runner-sync
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## 📄 License
|
|
300
|
+
|
|
301
|
+
MIT
|
|
302
|
+
|
|
303
|
+
## 🤝 Contributing
|
|
304
|
+
|
|
305
|
+
Pull requests are welcome!
|
|
306
|
+
|
|
307
|
+
## 📧 Support
|
|
308
|
+
|
|
309
|
+
- Issues: [GitHub Issues](https://github.com/yourname/runner-tailscale-sync/issues)
|
|
310
|
+
- Email: your-email@example.com
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bin/runner-sync.js
|
|
4
|
+
* CLI entry point
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const Config = require("../src/utils/config");
|
|
9
|
+
const Logger = require("../src/utils/logger");
|
|
10
|
+
const { parseArgs, printHelp } = require("../src/cli/parser");
|
|
11
|
+
const pkg = require("../package.json");
|
|
12
|
+
|
|
13
|
+
// Parse arguments
|
|
14
|
+
const { command, options } = parseArgs(process.argv);
|
|
15
|
+
|
|
16
|
+
// Handle help
|
|
17
|
+
if (options.help) {
|
|
18
|
+
printHelp();
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Handle version
|
|
23
|
+
if (command === "version") {
|
|
24
|
+
console.log(`${pkg.name} v${pkg.version}`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Create config
|
|
29
|
+
const config = new Config(options);
|
|
30
|
+
|
|
31
|
+
// Create logger
|
|
32
|
+
const logger = new Logger({
|
|
33
|
+
packageName: pkg.name,
|
|
34
|
+
version: pkg.version,
|
|
35
|
+
command,
|
|
36
|
+
verbose: options.verbose,
|
|
37
|
+
quiet: options.quiet,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Print banner
|
|
41
|
+
logger.printBanner();
|
|
42
|
+
|
|
43
|
+
// Run command
|
|
44
|
+
(async () => {
|
|
45
|
+
try {
|
|
46
|
+
let commandModule;
|
|
47
|
+
|
|
48
|
+
switch (command) {
|
|
49
|
+
case "init":
|
|
50
|
+
commandModule = require("../src/cli/commands/init");
|
|
51
|
+
break;
|
|
52
|
+
case "sync":
|
|
53
|
+
commandModule = require("../src/cli/commands/sync");
|
|
54
|
+
break;
|
|
55
|
+
case "push":
|
|
56
|
+
commandModule = require("../src/cli/commands/push");
|
|
57
|
+
break;
|
|
58
|
+
case "status":
|
|
59
|
+
commandModule = require("../src/cli/commands/status");
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
logger.error(`Unknown command: ${command}`);
|
|
63
|
+
printHelp();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await commandModule.run(config, logger);
|
|
68
|
+
|
|
69
|
+
if (result.success) {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
} else {
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.error(err.message);
|
|
76
|
+
|
|
77
|
+
if (options.verbose && err.stack) {
|
|
78
|
+
logger.debug(err.stack);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Exit with appropriate code
|
|
82
|
+
const exitCode = err.exitCode || 1;
|
|
83
|
+
process.exit(exitCode);
|
|
84
|
+
}
|
|
85
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ongtrieuhau861457/runner-tailscale-sync",
|
|
3
|
+
"version": "1.260202.11920",
|
|
4
|
+
"description": "Đồng bộ runner-data giữa các runner trên GitHub Actions, Azure Pipeline qua Tailscale network",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"runner-sync": "./bin/runner-sync.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20.0.0"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"runner",
|
|
14
|
+
"sync",
|
|
15
|
+
"tailscale",
|
|
16
|
+
"github-actions",
|
|
17
|
+
"azure-devops",
|
|
18
|
+
"ci-cd"
|
|
19
|
+
],
|
|
20
|
+
"author": "Your Name",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"version": "node scripts/version.js",
|
|
24
|
+
"build": "node scripts/build.js",
|
|
25
|
+
"publish": "node scripts/publish.js"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"devDependencies": {},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin/",
|
|
31
|
+
"src/",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/ongtrieuhau861457-hue/runner-tailscale-sync.git"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/fs.js
|
|
3
|
+
* File system operations với atomic write, ensure dir
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure directory exists
|
|
11
|
+
*/
|
|
12
|
+
function ensureDir(dirPath) {
|
|
13
|
+
if (!fs.existsSync(dirPath)) {
|
|
14
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensure multiple directories exist
|
|
20
|
+
*/
|
|
21
|
+
function ensureDirs(dirPaths) {
|
|
22
|
+
dirPaths.forEach(ensureDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read JSON file
|
|
27
|
+
*/
|
|
28
|
+
function readJson(filePath) {
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
35
|
+
return JSON.parse(content);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error(`Failed to read JSON from ${filePath}: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write JSON file (atomic)
|
|
43
|
+
*/
|
|
44
|
+
function writeJson(filePath, data) {
|
|
45
|
+
const content = JSON.stringify(data, null, 2);
|
|
46
|
+
const tmpPath = `${filePath}.tmp`;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Write to temp file first
|
|
50
|
+
fs.writeFileSync(tmpPath, content, "utf8");
|
|
51
|
+
|
|
52
|
+
// Rename to actual file (atomic on most filesystems)
|
|
53
|
+
fs.renameSync(tmpPath, filePath);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Clean up temp file if exists
|
|
56
|
+
if (fs.existsSync(tmpPath)) {
|
|
57
|
+
fs.unlinkSync(tmpPath);
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Failed to write JSON to ${filePath}: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Write text file (atomic)
|
|
65
|
+
*/
|
|
66
|
+
function writeFile(filePath, content) {
|
|
67
|
+
const tmpPath = `${filePath}.tmp`;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(tmpPath, content, "utf8");
|
|
71
|
+
fs.renameSync(tmpPath, filePath);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (fs.existsSync(tmpPath)) {
|
|
74
|
+
fs.unlinkSync(tmpPath);
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Failed to write file ${filePath}: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read text file
|
|
82
|
+
*/
|
|
83
|
+
function readFile(filePath) {
|
|
84
|
+
if (!fs.existsSync(filePath)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return fs.readFileSync(filePath, "utf8");
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new Error(`Failed to read file ${filePath}: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if path exists
|
|
97
|
+
*/
|
|
98
|
+
function exists(filePath) {
|
|
99
|
+
return fs.existsSync(filePath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Delete file or directory recursively
|
|
104
|
+
*/
|
|
105
|
+
function remove(targetPath) {
|
|
106
|
+
if (!fs.existsSync(targetPath)) return;
|
|
107
|
+
|
|
108
|
+
if (fs.statSync(targetPath).isDirectory()) {
|
|
109
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
110
|
+
} else {
|
|
111
|
+
fs.unlinkSync(targetPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get directory size (recursive)
|
|
117
|
+
*/
|
|
118
|
+
function getDirSize(dirPath) {
|
|
119
|
+
let size = 0;
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(dirPath)) return 0;
|
|
122
|
+
|
|
123
|
+
const files = fs.readdirSync(dirPath);
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
const filePath = path.join(dirPath, file);
|
|
126
|
+
const stats = fs.statSync(filePath);
|
|
127
|
+
|
|
128
|
+
if (stats.isDirectory()) {
|
|
129
|
+
size += getDirSize(filePath);
|
|
130
|
+
} else {
|
|
131
|
+
size += stats.size;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return size;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format bytes to human readable
|
|
140
|
+
*/
|
|
141
|
+
function formatBytes(bytes) {
|
|
142
|
+
if (bytes === 0) return "0 B";
|
|
143
|
+
const k = 1024;
|
|
144
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
145
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
146
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
ensureDir,
|
|
151
|
+
ensureDirs,
|
|
152
|
+
readJson,
|
|
153
|
+
writeJson,
|
|
154
|
+
writeFile,
|
|
155
|
+
readFile,
|
|
156
|
+
exists,
|
|
157
|
+
remove,
|
|
158
|
+
getDirSize,
|
|
159
|
+
formatBytes,
|
|
160
|
+
};
|