stegdoc 4.0.0 → 5.0.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.
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Hebrew Incident Report Templates — Procedural Generation
3
+ *
4
+ * Uses component pools mixed via hash-seeded selection to produce
5
+ * thousands of unique report variants deterministically.
6
+ */
7
+
8
+ // ─── Component Pools ────────────────────────────────────────────────────────
9
+
10
+ const SERVICES = [
11
+ 'שירות אימות',
12
+ 'שירות API',
13
+ 'שירות הזמנות',
14
+ 'מערכת תשלומים',
15
+ 'שירות התראות',
16
+ 'שירות דוא"ל',
17
+ 'שירות ניהול משתמשים',
18
+ 'מערכת ניטור',
19
+ 'שירות חיפוש',
20
+ 'שירות סנכרון נתונים',
21
+ 'שירות דוחות',
22
+ 'מערכת לוגים',
23
+ 'שירות קבצים',
24
+ 'שירות תורים',
25
+ 'מערכת ניהול תוכן',
26
+ ];
27
+
28
+ const INCIDENT_TYPES = [
29
+ { type: 'זמני תגובה חריגים', category: 'perf' },
30
+ { type: 'כשל בחיבור לשרת מטמון', category: 'conn' },
31
+ { type: 'דליפת זיכרון בשרת ייצור', category: 'mem' },
32
+ { type: 'כשל בעדכון תעודת SSL', category: 'ssl' },
33
+ { type: 'עומס חריג בעקבות קמפיין שיווקי', category: 'load' },
34
+ { type: 'כשל בהתחברות לבסיס נתונים', category: 'conn' },
35
+ { type: 'שגיאות 500 חוזרות', category: 'err' },
36
+ { type: 'קריסת שירות בעקבות עדכון גרסה', category: 'deploy' },
37
+ { type: 'חסימת IP בעקבות חריגה ב-rate limit', category: 'load' },
38
+ { type: 'כשל בתהליך גיבוי אוטומטי', category: 'backup' },
39
+ { type: 'השהיה חריגה ב-DNS', category: 'perf' },
40
+ { type: 'שגיאת סנכרון בין שרתים', category: 'conn' },
41
+ ];
42
+
43
+ const SUMMARIES = {
44
+ perf: [
45
+ 'בשעות הבוקר התקבלה התרעה על עלייה חדה בזמני התגובה. צוות התשתיות פתח בחקירה ואיתר עומס חריג על שרת הנתונים הראשי. לאחר ניתוח הלוגים המצורפים, בוצעה הרחבת משאבים והשירות חזר לפעילות תקינה.',
46
+ 'דווח על האטה משמעותית בזמני התגובה. בדיקה מעמיקה חשפה שאילתות כבדות שרצו ברקע ויצרו נעילות. הבעיה טופלה על ידי אופטימיזציה של השאילתות.',
47
+ 'ניטור אוטומטי זיהה חריגה בזמני תגובה. החקירה העלתה בעיה בתצורת load balancer שהפנתה עומס לא אחיד בין השרתים.',
48
+ ],
49
+ conn: [
50
+ 'דווחה בעיה בחיבוריות לרכיב חיצוני. בדיקה העלתה שחיבור נותק בעקבות עדכון תצורה שבוצע ללא תיאום מול צוות הפיתוח. השירות חזר לפעילות לאחר שיחזור התצורה הקודמת.',
51
+ 'התקבלו דיווחים על כשלי חיבור מתמשכים. הבעיה אותרה בשינוי תצורת firewall שחסם פורט תקשורת פנימי. לאחר עדכון החוקים, השירות התייצב.',
52
+ 'זוהתה בעיית קישוריות בין השירותים. הסיבה נמצאה בתעודת אימות שפגה תוקפה בין שני רכיבים פנימיים.',
53
+ ],
54
+ mem: [
55
+ 'במהלך שעות הלילה אותרה דליפת זיכרון בשרת ייצור. הבעיה גרמה להאטה הדרגתית עד לקריסת השירות. צוות הכוננות ביצע הפעלה מחדש ופתח בחקירה.',
56
+ 'צריכת זיכרון עלתה באופן מתמשך לאורך מספר שעות. הבעיה נגרמה מחיבורים שלא שוחררו כראוי במאגר החיבורים.',
57
+ 'התרעת OOM (Out of Memory) התקבלה ממערכת הניטור. ניתוח ה-heap dump חשף אובייקטים שלא נאספו על ידי ה-GC עקב הפניות מעגליות.',
58
+ ],
59
+ ssl: [
60
+ 'לקוחות דיווחו על שגיאות SSL בעת גישה לשירות. החקירה העלתה שתעודה פגה תוקף ולא חודשה אוטומטית. לאחר חידוש ידני, השירות חזר לפעילות.',
61
+ 'התקבלו דיווחים על שגיאות אבטחה בחיבורים מאובטחים. הבעיה נבעה מתצורת TLS לא תואמת בין שירותים פנימיים.',
62
+ ],
63
+ load: [
64
+ 'חלה עלייה חדה וחריגה בכמות הבקשות. השירות לא היה מוגדר לעמוד בעומס זה וחלק מהבקשות נדחו. בוצעה הרחבת משאבים אוטומטית.',
65
+ 'בעקבות אירוע חיצוני, כמות הבקשות לשירות עלתה פי חמישה מהממוצע. חלק מההודעות הוכנסו לתור וטופלו באיחור.',
66
+ 'זוהה עומס חריג שנגרם מתהליך batch שרץ בזמן שיא. הגבלת ה-rate limit הופעלה ומנעה עומס נוסף.',
67
+ ],
68
+ err: [
69
+ 'התקבלו דיווחים על שגיאות 500 חוזרות מצד לקוחות. הבעיה אותרה בבאג בטיפול בבקשות לא תקינות שהוכנס בגרסה האחרונה.',
70
+ 'מספר רב של שגיאות שרת נרשמו בלוגים. הסיבה נמצאה בכשל של שירות צד שלישי שלא טופל כראוי.',
71
+ ],
72
+ deploy: [
73
+ 'לאחר פריסת גרסה חדשה, השירות החל להחזיר שגיאות. בוצע rollback מיידי לגרסה הקודמת והשירות התייצב.',
74
+ 'עדכון גרסה גרם לאי-תאימות עם תצורת בסיס הנתונים. הבעיה אותרה ותוקנה על ידי migration שלא הופעל.',
75
+ ],
76
+ backup: [
77
+ 'תהליך הגיבוי היומי נכשל ולא בוצע במשך 3 ימים. הבעיה נגרמה משינוי הרשאות על תיקיית היעד.',
78
+ 'זוהה כשל בגיבוי אוטומטי. החקירה חשפה שנפח האחסון המיועד מלא ולא הוגדרה מדיניות מחיקה.',
79
+ ],
80
+ };
81
+
82
+ const TIMELINE_EVENTS = {
83
+ perf: [
84
+ [
85
+ { offset: 0, desc: 'התרעה ראשונה ממערכת ניטור — זמן תגובה חריג' },
86
+ { offset: 7, desc: 'צוות תשתיות מתחיל חקירה' },
87
+ { offset: 15, desc: 'זיהוי צוואר בקבוק — בדיקת לוגים' },
88
+ { offset: 25, desc: 'ביצוע פעולה מתקנת' },
89
+ { offset: 35, desc: 'זמני תגובה חוזרים לנורמה' },
90
+ ],
91
+ [
92
+ { offset: 0, desc: 'חריגה בזמני תגובה — ניטור מזהה' },
93
+ { offset: 5, desc: 'פתיחת חקירה' },
94
+ { offset: 12, desc: 'איתור שאילתות כבדות ברקע' },
95
+ { offset: 18, desc: 'עצירת תהליך רקע בעייתי' },
96
+ { offset: 22, desc: 'שירות חוזר לתקינות' },
97
+ ],
98
+ ],
99
+ conn: [
100
+ [
101
+ { offset: 0, desc: 'דיווח ראשון על שגיאות חיבור' },
102
+ { offset: 5, desc: 'זיהוי כשל — connection refused' },
103
+ { offset: 8, desc: 'בדיקת שינויים אחרונים בתצורה' },
104
+ { offset: 12, desc: 'שיחזור תצורה קודמת (rollback)' },
105
+ { offset: 15, desc: 'שירות חוזר לפעילות תקינה' },
106
+ ],
107
+ [
108
+ { offset: 0, desc: 'התרעה על כשל חיבור' },
109
+ { offset: 3, desc: 'בדיקת רשת ו-firewall' },
110
+ { offset: 10, desc: 'איתור חוק firewall חדש שחוסם תעבורה' },
111
+ { offset: 14, desc: 'עדכון חוקי firewall' },
112
+ { offset: 18, desc: 'חיבורים חוזרים, אימות תקינות' },
113
+ ],
114
+ ],
115
+ mem: [
116
+ [
117
+ { offset: 0, desc: 'צריכת זיכרון חוצה סף 70% — התרעה צהובה' },
118
+ { offset: 75, desc: 'צריכת זיכרון מגיעה ל-90% — התרעה אדומה' },
119
+ { offset: 134, desc: 'קריסת שירות — OOM Kill' },
120
+ { offset: 138, desc: 'כוננות מבצע הפעלה מחדש' },
121
+ { offset: 145, desc: 'שירות חוזר, ניטור מוגבר' },
122
+ ],
123
+ ],
124
+ ssl: [
125
+ [
126
+ { offset: 0, desc: 'דיווחים ראשונים — שגיאת SSL/TLS' },
127
+ { offset: 10, desc: 'זיהוי תעודה פגת תוקף' },
128
+ { offset: 15, desc: 'התחלת תהליך חידוש' },
129
+ { offset: 25, desc: 'תעודה חדשה הותקנה' },
130
+ { offset: 30, desc: 'אימות תקינות כל הנתיבים' },
131
+ ],
132
+ ],
133
+ load: [
134
+ [
135
+ { offset: 0, desc: 'עלייה חדה בכמות הבקשות' },
136
+ { offset: 5, desc: 'תור הודעות מתחיל להצטבר' },
137
+ { offset: 15, desc: 'שירות מחזיר שגיאות 429 (rate limit)' },
138
+ { offset: 30, desc: 'הגדלת משאבים (auto-scaling)' },
139
+ { offset: 60, desc: 'עומס מתייצב, כל הבקשות טופלו' },
140
+ ],
141
+ ],
142
+ err: [
143
+ [
144
+ { offset: 0, desc: 'דיווחים ראשונים על שגיאות שרת' },
145
+ { offset: 5, desc: 'בדיקת לוגים — זיהוי stack trace' },
146
+ { offset: 12, desc: 'זיהוי באג בגרסה האחרונה' },
147
+ { offset: 15, desc: 'ביצוע rollback לגרסה קודמת' },
148
+ { offset: 20, desc: 'שירות חוזר, שגיאות נעצרו' },
149
+ ],
150
+ ],
151
+ deploy: [
152
+ [
153
+ { offset: 0, desc: 'פריסת גרסה חדשה לייצור' },
154
+ { offset: 3, desc: 'זיהוי שגיאות לאחר הפריסה' },
155
+ { offset: 5, desc: 'החלטה על rollback' },
156
+ { offset: 8, desc: 'ביצוע rollback לגרסה קודמת' },
157
+ { offset: 12, desc: 'שירות יציב, פתיחת חקירה' },
158
+ ],
159
+ ],
160
+ backup: [
161
+ [
162
+ { offset: 0, desc: 'התרעה על כשל בגיבוי אוטומטי' },
163
+ { offset: 10, desc: 'בדיקת תיקיית יעד והרשאות' },
164
+ { offset: 20, desc: 'תיקון הרשאות / פינוי שטח' },
165
+ { offset: 25, desc: 'הפעלת גיבוי ידני — הצלחה' },
166
+ { offset: 30, desc: 'בדיקת תזמון גיבוי עתידי' },
167
+ ],
168
+ ],
169
+ };
170
+
171
+ const ROOT_CAUSES = {
172
+ perf: [
173
+ 'שורש הבעיה אותר בשאילתת SQL לא מותאמת שרצה כחלק מתהליך סנכרון לילי ולא הסתיימה בזמן. השאילתה יצרה נעילה על טבלה מרכזית, מה שגרם להאטה משמעותית בכל הבקשות.',
174
+ 'הבעיה נבעה מתצורת load balancer שלא עודכנה לאחר הוספת שרתים חדשים, מה שגרם לשרת יחיד לקבל את רוב העומס.',
175
+ 'שאילתת חיפוש ללא אינדקס מתאים גרמה לסריקת טבלה מלאה, מה שהעמיס את בסיס הנתונים.',
176
+ ],
177
+ conn: [
178
+ 'שינוי תצורה בוצע כחלק מתחזוקה שוטפת אך לא תואם מול צוות הפיתוח. השינוי גרם לכך שהשירותים לא הצליחו להתחבר לפורט החדש.',
179
+ 'חוק firewall חדש שנוסף חסם תעבורה פנימית בין שני סגמנטים ברשת. החוק הופעל ללא בדיקה מוקדמת.',
180
+ 'תעודת TLS mutual auth פגה תוקפה בין שני שירותים פנימיים ללא התרעה מוקדמת.',
181
+ ],
182
+ mem: [
183
+ 'גרסה חדשה הכילה באג בניהול מאגר חיבורים — חיבורים לא שוחררו כראוי לאחר timeout. הבאג הוכנס בגרסה שפורסמה ימים קודם.',
184
+ 'הפניות מעגליות בין אובייקטים מנעו את ה-GC מלשחרר זיכרון. הבעיה התגלתה רק תחת עומס גבוה.',
185
+ 'מנגנון caching מקומי צבר נתונים ללא הגבלה ולא מימש מדיניות eviction.',
186
+ ],
187
+ ssl: [
188
+ 'תהליך החידוש האוטומטי של certbot נכשל בעקבות שינוי הרשאות בתיקייה. השגיאה לא טופלה כי התרעת certbot לא הייתה מוגדרת.',
189
+ 'עדכון תצורת TLS הוביל לאי-תאימות בין גרסת הפרוטוקול בצד השרת לבין הלקוח.',
190
+ ],
191
+ load: [
192
+ 'השירות היה מוגדר למספר מצומצם של workers עם rate limit נמוך. אירוע חיצוני הכפיל את כמות הבקשות פי חמישה תוך דקות.',
193
+ 'תהליך batch שתוזמן לרוץ בזמן שיא יצר עומס נוסף על שירות שכבר היה בעומס גבוה.',
194
+ ],
195
+ err: [
196
+ 'באג בטיפול ב-edge case בבקשות API הוכנס בגרסה האחרונה. הבדיקות האוטומטיות לא כיסו את התרחיש הספציפי.',
197
+ 'שירות צד שלישי שינה את פורמט התגובה ללא הודעה מראש, מה שגרם לשגיאות parsing.',
198
+ ],
199
+ deploy: [
200
+ 'הפריסה כללה שינוי סכמה בבסיס הנתונים, אך ה-migration לא הופעל אוטומטית. השירות החדש ניסה לגשת לעמודות שלא קיימות.',
201
+ 'תלות חדשה שנוספה דרשה משתנה סביבה שלא הוגדר בסביבת הייצור.',
202
+ ],
203
+ backup: [
204
+ 'שינוי הרשאות על תיקיית הגיבוי מנע מתהליך האוטומטי לכתוב אליה. השינוי בוצע כחלק מהקשחת הרשאות.',
205
+ 'נפח האחסון המיועד לגיבויים התמלא ולא הוגדרה מדיניות מחיקת גיבויים ישנים.',
206
+ ],
207
+ };
208
+
209
+ const RECOMMENDATIONS_POOL = [
210
+ // Performance
211
+ 'הוספת אינדקסים לשאילתות כבדות שזוהו',
212
+ 'הגדרת timeout מקסימלי לשאילתות ארוכות',
213
+ 'הוספת התרעה על CPU מעל 80% בשרתי DB',
214
+ 'תכנון מעבר שאילתות כבדות לרפליקת קריאה',
215
+ 'עדכון תצורת load balancer לחלוקת עומס אחידה',
216
+ // Connectivity
217
+ 'הטמעת תהליך Change Management לכל שינוי תשתיתי',
218
+ 'הוספת health check אוטומטי לכל חיבורי שירות',
219
+ 'הגדרת rollback אוטומטי בעת כשל health check',
220
+ 'עדכון מערכת ניטור לזיהוי כשלי חיבור בזמן אמת',
221
+ 'הגדרת circuit breaker לשירותים חיצוניים',
222
+ // Memory
223
+ 'הגדרת restart אוטומטי בחריגת זיכרון מעל 85%',
224
+ 'ביצוע code review מעמיק לשינויים בשכבת החיבורים',
225
+ 'הוספת בדיקת עומס לתהליך CI/CD',
226
+ 'הגדרת מדיניות eviction למנגנוני caching',
227
+ // SSL
228
+ 'הגדרת ניטור לתוקף תעודות SSL — התרעה 30 יום לפני פקיעה',
229
+ 'הוספת בדיקה יומית לתקינות certbot',
230
+ 'הגדרת תעודת גיבוי (backup certificate)',
231
+ 'תיעוד תהליך חידוש ידני ב-runbook',
232
+ // Load
233
+ 'תיאום חובה בין שיווק/עסקים לפיתוח לפני כל אירוע',
234
+ 'הגדרת auto-scaling לשירותים קריטיים',
235
+ 'הגדלת rate limit בהתאם לקיבולת',
236
+ 'הוספת תור עדיפויות — בקשות קריטיות מקבלות עדיפות',
237
+ // Errors / Deploy
238
+ 'הרחבת כיסוי בדיקות אוטומטיות ל-edge cases',
239
+ 'הוספת canary deployment לפני פריסה מלאה',
240
+ 'הגדרת rollback אוטומטי בעת חריגה ב-error rate',
241
+ 'בדיקת תאימות לאחור בתהליך הבדיקות',
242
+ // Backup
243
+ 'הגדרת ניטור לתקינות גיבויים — התרעה על כשל',
244
+ 'הגדרת מדיניות retention לגיבויים ישנים',
245
+ 'ביצוע שחזור מדומה חודשי לבדיקת תקינות גיבויים',
246
+ // General
247
+ 'עדכון runbook עם תהליך התמודדות מפורט',
248
+ 'ביצוע תרגיל Incident Response רבעוני',
249
+ 'הוספת דשבורד ניטור ייעודי לרכיב',
250
+ ];
251
+
252
+ // ─── Seeded Random ──────────────────────────────────────────────────────────
253
+
254
+ /**
255
+ * Simple deterministic PRNG from a hash string.
256
+ * Returns a function that yields numbers in [0, 1).
257
+ */
258
+ function createRng(hash) {
259
+ let state = 0;
260
+ for (let i = 0; i < hash.length; i++) {
261
+ state = ((state << 5) - state + hash.charCodeAt(i)) | 0;
262
+ }
263
+ return function next() {
264
+ state = (state * 1664525 + 1013904223) | 0;
265
+ return ((state >>> 0) / 0x100000000);
266
+ };
267
+ }
268
+
269
+ function pick(rng, arr) {
270
+ return arr[Math.floor(rng() * arr.length)];
271
+ }
272
+
273
+ function pickN(rng, arr, n) {
274
+ const shuffled = [...arr];
275
+ for (let i = shuffled.length - 1; i > 0; i--) {
276
+ const j = Math.floor(rng() * (i + 1));
277
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
278
+ }
279
+ return shuffled.slice(0, n);
280
+ }
281
+
282
+ // ─── Public API ─────────────────────────────────────────────────────────────
283
+
284
+ /**
285
+ * Generate a unique incident report from a hash + part number.
286
+ * Different parts get different reports.
287
+ *
288
+ * @param {string} hash - File hash for deterministic selection
289
+ * @param {number} [partNumber=1] - Part number (each part gets a different variant)
290
+ * @returns {object} { title, summary, timeline, rootCause, recommendations }
291
+ */
292
+ function generateIncident(hash, partNumber = 1) {
293
+ // Mix part number into seed so each part gets a different report
294
+ const seed = `${hash}_part${partNumber}`;
295
+ const rng = createRng(seed);
296
+
297
+ const service = pick(rng, SERVICES);
298
+ const incidentType = pick(rng, INCIDENT_TYPES);
299
+ const category = incidentType.category;
300
+
301
+ const title = `${service} — ${incidentType.type}`;
302
+
303
+ const summaryPool = SUMMARIES[category] || SUMMARIES.err;
304
+ const summary = pick(rng, summaryPool);
305
+
306
+ // Timeline
307
+ const timelinePool = TIMELINE_EVENTS[category] || TIMELINE_EVENTS.err;
308
+ const baseTimeline = pick(rng, timelinePool);
309
+
310
+ // Generate a start hour
311
+ const startHour = Math.floor(rng() * 20) + 1; // 01:00 - 20:00
312
+ const startMin = Math.floor(rng() * 4) * 15; // 00, 15, 30, 45
313
+
314
+ const timeline = baseTimeline.map(evt => {
315
+ const totalMin = startHour * 60 + startMin + evt.offset;
316
+ const h = Math.floor(totalMin / 60) % 24;
317
+ const m = totalMin % 60;
318
+ return {
319
+ time: `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`,
320
+ desc: evt.desc,
321
+ };
322
+ });
323
+
324
+ const rootCausePool = ROOT_CAUSES[category] || ROOT_CAUSES.err;
325
+ const rootCause = pick(rng, rootCausePool);
326
+
327
+ // Pick 3-5 recommendations, mixing category-relevant and general
328
+ const numRecs = 3 + Math.floor(rng() * 3); // 3-5
329
+ const recommendations = pickN(rng, RECOMMENDATIONS_POOL, numRecs);
330
+
331
+ return { title, summary, timeline, rootCause, recommendations };
332
+ }
333
+
334
+ /**
335
+ * Generate a date string in Hebrew format.
336
+ * @param {string} hash - Hash for deterministic date
337
+ * @returns {string} Hebrew date string
338
+ */
339
+ function generateHebrewDate(hash) {
340
+ const hashNum = parseInt(hash.slice(0, 8), 16) >>> 0;
341
+ const daysAgo = hashNum % 7;
342
+ const date = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
343
+
344
+ const months = [
345
+ 'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני',
346
+ 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר',
347
+ ];
348
+
349
+ return `${date.getDate()} ב${months[date.getMonth()]} ${date.getFullYear()}`;
350
+ }
351
+
352
+ module.exports = {
353
+ generateIncident,
354
+ generateHebrewDate,
355
+ };
@@ -1,113 +1,113 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- /**
5
- * Read a file and encode it to base64
6
- * @param {string} filePath - Path to the file
7
- * @returns {object} Object containing base64 string, filename, extension, and size
8
- */
9
- function encodeFileToBase64(filePath) {
10
- if (!fs.existsSync(filePath)) {
11
- throw new Error(`File not found: ${filePath}`);
12
- }
13
-
14
- const stats = fs.statSync(filePath);
15
-
16
- if (!stats.isFile()) {
17
- throw new Error(`Path is not a file: ${filePath}`);
18
- }
19
-
20
- const fileBuffer = fs.readFileSync(filePath);
21
- const base64 = fileBuffer.toString('base64');
22
- const filename = path.basename(filePath);
23
- const extension = path.extname(filePath);
24
-
25
- return {
26
- base64,
27
- filename,
28
- extension,
29
- size: stats.size,
30
- };
31
- }
32
-
33
- /**
34
- * Decode base64 string and write to file
35
- * @param {string} base64 - Base64 encoded string
36
- * @param {string} outputPath - Output file path
37
- */
38
- function decodeBase64ToFile(base64, outputPath) {
39
- const buffer = Buffer.from(base64, 'base64');
40
-
41
- // Ensure output directory exists
42
- const outputDir = path.dirname(outputPath);
43
- if (!fs.existsSync(outputDir)) {
44
- fs.mkdirSync(outputDir, { recursive: true });
45
- }
46
-
47
- fs.writeFileSync(outputPath, buffer);
48
- }
49
-
50
- /**
51
- * Split base64 string into chunks based on size limit
52
- * @param {string} base64 - Base64 string to split
53
- * @param {number} chunkSizeBytes - Maximum size per chunk in bytes
54
- * @returns {Array<string>} Array of base64 chunks
55
- */
56
- function splitBase64(base64, chunkSizeBytes) {
57
- const chunks = [];
58
- let offset = 0;
59
-
60
- while (offset < base64.length) {
61
- chunks.push(base64.slice(offset, offset + chunkSizeBytes));
62
- offset += chunkSizeBytes;
63
- }
64
-
65
- return chunks;
66
- }
67
-
68
- /**
69
- * Merge base64 chunks back into a single string
70
- * @param {Array<string>} chunks - Array of base64 chunks
71
- * @returns {string} Merged base64 string
72
- */
73
- function mergeBase64Chunks(chunks) {
74
- return chunks.join('');
75
- }
76
-
77
- /**
78
- * Calculate how many chunks will be needed for a file
79
- * @param {number} fileSize - File size in bytes
80
- * @param {number} chunkSizeBytes - Chunk size in bytes
81
- * @returns {number} Number of chunks needed
82
- */
83
- function calculateChunkCount(fileSize, chunkSizeBytes) {
84
- // Base64 encoding increases size by ~33%
85
- const base64Size = Math.ceil(fileSize * 4 / 3);
86
- return Math.ceil(base64Size / chunkSizeBytes);
87
- }
88
-
89
- /**
90
- * Validate if a path is writable
91
- * @param {string} dirPath - Directory path to check
92
- * @returns {boolean} True if writable
93
- */
94
- function isDirectoryWritable(dirPath) {
95
- try {
96
- if (!fs.existsSync(dirPath)) {
97
- fs.mkdirSync(dirPath, { recursive: true });
98
- }
99
- fs.accessSync(dirPath, fs.constants.W_OK);
100
- return true;
101
- } catch (error) {
102
- return false;
103
- }
104
- }
105
-
106
- module.exports = {
107
- encodeFileToBase64,
108
- decodeBase64ToFile,
109
- splitBase64,
110
- mergeBase64Chunks,
111
- calculateChunkCount,
112
- isDirectoryWritable,
113
- };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Read a file and encode it to base64
6
+ * @param {string} filePath - Path to the file
7
+ * @returns {object} Object containing base64 string, filename, extension, and size
8
+ */
9
+ function encodeFileToBase64(filePath) {
10
+ if (!fs.existsSync(filePath)) {
11
+ throw new Error(`File not found: ${filePath}`);
12
+ }
13
+
14
+ const stats = fs.statSync(filePath);
15
+
16
+ if (!stats.isFile()) {
17
+ throw new Error(`Path is not a file: ${filePath}`);
18
+ }
19
+
20
+ const fileBuffer = fs.readFileSync(filePath);
21
+ const base64 = fileBuffer.toString('base64');
22
+ const filename = path.basename(filePath);
23
+ const extension = path.extname(filePath);
24
+
25
+ return {
26
+ base64,
27
+ filename,
28
+ extension,
29
+ size: stats.size,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Decode base64 string and write to file
35
+ * @param {string} base64 - Base64 encoded string
36
+ * @param {string} outputPath - Output file path
37
+ */
38
+ function decodeBase64ToFile(base64, outputPath) {
39
+ const buffer = Buffer.from(base64, 'base64');
40
+
41
+ // Ensure output directory exists
42
+ const outputDir = path.dirname(outputPath);
43
+ if (!fs.existsSync(outputDir)) {
44
+ fs.mkdirSync(outputDir, { recursive: true });
45
+ }
46
+
47
+ fs.writeFileSync(outputPath, buffer);
48
+ }
49
+
50
+ /**
51
+ * Split base64 string into chunks based on size limit
52
+ * @param {string} base64 - Base64 string to split
53
+ * @param {number} chunkSizeBytes - Maximum size per chunk in bytes
54
+ * @returns {Array<string>} Array of base64 chunks
55
+ */
56
+ function splitBase64(base64, chunkSizeBytes) {
57
+ const chunks = [];
58
+ let offset = 0;
59
+
60
+ while (offset < base64.length) {
61
+ chunks.push(base64.slice(offset, offset + chunkSizeBytes));
62
+ offset += chunkSizeBytes;
63
+ }
64
+
65
+ return chunks;
66
+ }
67
+
68
+ /**
69
+ * Merge base64 chunks back into a single string
70
+ * @param {Array<string>} chunks - Array of base64 chunks
71
+ * @returns {string} Merged base64 string
72
+ */
73
+ function mergeBase64Chunks(chunks) {
74
+ return chunks.join('');
75
+ }
76
+
77
+ /**
78
+ * Calculate how many chunks will be needed for a file
79
+ * @param {number} fileSize - File size in bytes
80
+ * @param {number} chunkSizeBytes - Chunk size in bytes
81
+ * @returns {number} Number of chunks needed
82
+ */
83
+ function calculateChunkCount(fileSize, chunkSizeBytes) {
84
+ // Base64 encoding increases size by ~33%
85
+ const base64Size = Math.ceil(fileSize * 4 / 3);
86
+ return Math.ceil(base64Size / chunkSizeBytes);
87
+ }
88
+
89
+ /**
90
+ * Validate if a path is writable
91
+ * @param {string} dirPath - Directory path to check
92
+ * @returns {boolean} True if writable
93
+ */
94
+ function isDirectoryWritable(dirPath) {
95
+ try {
96
+ if (!fs.existsSync(dirPath)) {
97
+ fs.mkdirSync(dirPath, { recursive: true });
98
+ }
99
+ fs.accessSync(dirPath, fs.constants.W_OK);
100
+ return true;
101
+ } catch (error) {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ module.exports = {
107
+ encodeFileToBase64,
108
+ decodeBase64ToFile,
109
+ splitBase64,
110
+ mergeBase64Chunks,
111
+ calculateChunkCount,
112
+ isDirectoryWritable,
113
+ };