@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.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.
Files changed (42) hide show
  1. package/bin/copilot-session-viewer +2 -2
  2. package/dist/server.min.js +99 -0
  3. package/package.json +5 -17
  4. package/public/vendor/marked.umd.min.js +8 -0
  5. package/public/vendor/purify.min.js +3 -0
  6. package/public/vendor/vue-virtual-scroller.css +1 -0
  7. package/public/vendor/vue-virtual-scroller.min.js +2 -0
  8. package/public/vendor/vue.global.prod.min.js +19 -0
  9. package/views/session-vue.ejs +5 -5
  10. package/views/time-analyze.ejs +2 -2
  11. package/lib/parsers/README.md +0 -239
  12. package/lib/parsers/base-parser.js +0 -53
  13. package/lib/parsers/claude-parser.js +0 -181
  14. package/lib/parsers/copilot-parser.js +0 -143
  15. package/lib/parsers/index.js +0 -15
  16. package/lib/parsers/parser-factory.js +0 -77
  17. package/lib/parsers/pi-mono-parser.js +0 -119
  18. package/lib/parsers/vscode-parser.js +0 -591
  19. package/server.js +0 -29
  20. package/src/app.js +0 -129
  21. package/src/config/index.js +0 -27
  22. package/src/controllers/insightController.js +0 -136
  23. package/src/controllers/sessionController.js +0 -449
  24. package/src/controllers/tagController.js +0 -113
  25. package/src/controllers/uploadController.js +0 -648
  26. package/src/middleware/common.js +0 -67
  27. package/src/middleware/rateLimiting.js +0 -62
  28. package/src/models/Session.js +0 -146
  29. package/src/routes/api.js +0 -11
  30. package/src/routes/insights.js +0 -12
  31. package/src/routes/pages.js +0 -12
  32. package/src/routes/uploads.js +0 -14
  33. package/src/schemas/event.schema.js +0 -73
  34. package/src/services/eventNormalizer.js +0 -291
  35. package/src/services/insightService.js +0 -535
  36. package/src/services/sessionRepository.js +0 -1092
  37. package/src/services/sessionService.js +0 -1919
  38. package/src/services/tagService.js +0 -205
  39. package/src/telemetry.js +0 -152
  40. package/src/utils/fileUtils.js +0 -305
  41. package/src/utils/helpers.js +0 -45
  42. package/src/utils/processManager.js +0 -85
@@ -1,648 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const multer = require('multer');
5
- const { spawn } = require('child_process');
6
- const { isValidSessionId } = require('../utils/helpers');
7
- const { trackEvent, trackException } = require('../telemetry');
8
- const processManager = require('../utils/processManager');
9
- const config = require('../config');
10
-
11
- class UploadController {
12
- constructor() {
13
- this.SESSION_DIR = process.env.SESSION_DIR || path.join(os.homedir(), '.copilot', 'session-state');
14
- this.uploadDir = process.env.UPLOAD_DIR || path.join(os.tmpdir(), 'copilot-session-uploads');
15
-
16
- // Multi-format session directories
17
- this.SESSION_DIRS = {
18
- copilot: this.SESSION_DIR,
19
- claude: path.join(os.homedir(), '.claude', 'projects'),
20
- 'pi-mono': path.join(os.homedir(), '.pi', 'agent', 'sessions')
21
- };
22
-
23
- // Don't create uploadDir here - multer's DiskStorage will handle it
24
- // This avoids EEXIST errors when multiple tests run in parallel
25
- this.upload = this.createMulterInstance();
26
- }
27
-
28
- createMulterInstance() {
29
- return multer({
30
- dest: this.uploadDir,
31
- limits: { fileSize: config.MAX_UPLOAD_SIZE },
32
- fileFilter: (req, file, cb) => {
33
- // Check both file extension and MIME type
34
- const isZipExtension = file.originalname.toLowerCase().endsWith('.zip');
35
- const isZipMime = file.mimetype === 'application/zip' ||
36
- file.mimetype === 'application/x-zip-compressed';
37
-
38
- if (!isZipExtension || !isZipMime) {
39
- return cb(new Error('Only .zip files are allowed'));
40
- }
41
- cb(null, true);
42
- }
43
- });
44
- }
45
-
46
- // Share session (export as zip)
47
- async shareSession(req, res) {
48
- try {
49
- const sessionId = req.params.id;
50
-
51
- if (!isValidSessionId(sessionId)) {
52
- return res.status(400).json({ error: 'Invalid session ID' });
53
- }
54
-
55
- const sessionPath = path.join(this.SESSION_DIR, sessionId);
56
-
57
- try {
58
- await fs.promises.access(sessionPath);
59
- } catch (_err) {
60
- return res.status(404).json({ error: 'Session not found' });
61
- }
62
-
63
- const zipFile = path.join(os.tmpdir(), `session-${sessionId}.zip`);
64
-
65
- const zipProcess = spawn('zip', ['-r', '-q', zipFile, sessionId], {
66
- cwd: this.SESSION_DIR
67
- });
68
-
69
- processManager.register(zipProcess, { name: `zip-${sessionId}` });
70
-
71
- zipProcess.on('close', (code) => {
72
- if (code !== 0) {
73
- return res.status(500).json({ error: 'Failed to create zip file' });
74
- }
75
-
76
- // Track SessionShared event
77
- trackEvent('SessionShared', { sessionId });
78
-
79
- res.download(zipFile, `session-${sessionId}.zip`, (err) => {
80
- fs.promises.unlink(zipFile).catch(() => {});
81
- if (err) {
82
- console.error('Error sending zip:', err);
83
- }
84
- });
85
- });
86
-
87
- zipProcess.on('error', (err) => {
88
- console.error('Error creating zip:', err);
89
- res.status(500).json({ error: 'Failed to create zip file' });
90
- });
91
- } catch (err) {
92
- console.error('Error sharing session:', err);
93
- res.status(500).json({ error: 'Error sharing session' });
94
- }
95
- }
96
-
97
- // Import session from zip (with validation)
98
- async importSession(req, res) {
99
- try {
100
- if (!req.file) {
101
- return res.status(400).json({ error: 'No file uploaded' });
102
- }
103
-
104
- const zipPath = req.file.path;
105
- const extractDir = path.join(this.uploadDir, `extract-${Date.now()}`);
106
-
107
- await fs.promises.mkdir(extractDir, { recursive: true });
108
-
109
- // ZIP bomb protection: Check compressed file size first
110
- const MAX_COMPRESSED_SIZE = 50 * 1024 * 1024; // 50MB (already enforced by multer)
111
- const MAX_UNCOMPRESSED_SIZE = 200 * 1024 * 1024; // 200MB
112
- const MAX_FILE_COUNT = 1000; // Maximum number of files
113
- const MAX_DEPTH = 5; // Maximum directory nesting depth
114
-
115
- const stats = await fs.promises.stat(zipPath);
116
- if (stats.size > MAX_COMPRESSED_SIZE) {
117
- await fs.promises.unlink(zipPath);
118
- return res.status(400).json({ error: 'Compressed file too large (max 50MB)' });
119
- }
120
-
121
- // First pass: List zip contents without extracting to check for bombs
122
- const listProcess = spawn('unzip', ['-l', zipPath]);
123
- let listOutput = '';
124
-
125
- listProcess.stdout.on('data', (data) => {
126
- listOutput += data.toString();
127
- });
128
-
129
- await new Promise((resolve, reject) => {
130
- listProcess.on('close', (code) => {
131
- if (code !== 0) {
132
- reject(new Error('Failed to list zip contents'));
133
- } else {
134
- resolve();
135
- }
136
- });
137
- listProcess.on('error', reject);
138
- });
139
-
140
- // Parse unzip output to check total size and file count
141
- const lines = listOutput.split('\n');
142
- let totalUncompressedSize = 0;
143
- let fileCount = 0;
144
- let maxDepth = 0;
145
-
146
- for (const line of lines) {
147
- const match = line.trim().match(/^\s*(\d+)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+(.+)$/);
148
- if (match) {
149
- const size = parseInt(match[1]);
150
- const filename = match[2];
151
- totalUncompressedSize += size;
152
- fileCount++;
153
-
154
- // Check directory depth
155
- const depth = (filename.match(/\//g) || []).length;
156
- maxDepth = Math.max(maxDepth, depth);
157
- }
158
- }
159
-
160
- // Validate against ZIP bomb thresholds
161
- if (totalUncompressedSize > MAX_UNCOMPRESSED_SIZE) {
162
- await fs.promises.unlink(zipPath);
163
- return res.status(400).json({
164
- error: `Uncompressed size too large (${Math.round(totalUncompressedSize / 1024 / 1024)}MB > ${MAX_UNCOMPRESSED_SIZE / 1024 / 1024}MB)`
165
- });
166
- }
167
-
168
- if (fileCount > MAX_FILE_COUNT) {
169
- await fs.promises.unlink(zipPath);
170
- return res.status(400).json({
171
- error: `Too many files in archive (${fileCount} > ${MAX_FILE_COUNT})`
172
- });
173
- }
174
-
175
- if (maxDepth > MAX_DEPTH) {
176
- await fs.promises.unlink(zipPath);
177
- return res.status(400).json({
178
- error: `Directory nesting too deep (${maxDepth} > ${MAX_DEPTH})`
179
- });
180
- }
181
-
182
- // If all checks pass, proceed with extraction
183
- const unzipProcess = spawn('unzip', ['-q', zipPath, '-d', extractDir]);
184
-
185
- processManager.register(unzipProcess, { name: 'unzip-import' });
186
-
187
- unzipProcess.on('close', async (code) => {
188
- let sessionDirName; // Declare here for access in catch block
189
- try {
190
- await fs.promises.unlink(zipPath);
191
-
192
- if (code !== 0) {
193
- await fs.promises.rm(extractDir, { recursive: true, force: true });
194
- return res.status(500).json({ error: 'Failed to extract zip file' });
195
- }
196
-
197
- const entries = await fs.promises.readdir(extractDir);
198
- if (entries.length === 0) {
199
- await fs.promises.rm(extractDir, { recursive: true, force: true });
200
- return res.status(400).json({ error: 'Empty zip file' });
201
- }
202
-
203
- sessionDirName = entries[0];
204
-
205
- // Validate session directory name to prevent Zip Slip path traversal
206
- if (!isValidSessionId(sessionDirName)) {
207
- await fs.promises.rm(extractDir, { recursive: true, force: true });
208
- return res.status(400).json({ error: 'Invalid session directory name in zip file' });
209
- }
210
-
211
- const sessionPath = path.join(extractDir, sessionDirName);
212
- const targetPath = path.join(this.SESSION_DIR, sessionDirName);
213
-
214
- const eventsFile = path.join(sessionPath, 'events.jsonl');
215
- try {
216
- await fs.promises.access(eventsFile);
217
- } catch (_err) {
218
- await fs.promises.rm(extractDir, { recursive: true, force: true });
219
- return res.status(400).json({ error: 'Invalid session structure (no events.jsonl)' });
220
- }
221
-
222
- if (fs.existsSync(targetPath)) {
223
- await fs.promises.rm(extractDir, { recursive: true, force: true });
224
- return res.status(409).json({ error: 'Session already exists' });
225
- }
226
-
227
- await fs.promises.rename(sessionPath, targetPath);
228
- await fs.promises.rm(extractDir, { recursive: true, force: true });
229
-
230
- // Track SessionImported event
231
- const stats = await fs.promises.stat(zipPath).catch(() => ({ size: 0 }));
232
- trackEvent('SessionImported', {
233
- format: 'copilot',
234
- fileSize: stats.size.toString()
235
- });
236
-
237
- res.json({ success: true, sessionId: sessionDirName });
238
- } catch (err) {
239
- console.error('Error importing session:', err);
240
-
241
- // Track import failure
242
- trackException(err, {
243
- operation: 'importSession',
244
- sessionId: sessionDirName || 'unknown'
245
- });
246
-
247
- await fs.promises.rm(extractDir, { recursive: true, force: true }).catch(() => {});
248
- res.status(500).json({ error: 'Error importing session' });
249
- }
250
- });
251
-
252
- unzipProcess.on('error', async (err) => {
253
- console.error('Error extracting zip:', err);
254
-
255
- // Track upload/extraction failure
256
- trackException(err, {
257
- operation: 'importSession_unzip'
258
- });
259
-
260
- await fs.promises.unlink(zipPath).catch(() => {});
261
- await fs.promises.rm(extractDir, { recursive: true, force: true }).catch(() => {});
262
- res.status(500).json({ error: 'Failed to extract zip file' });
263
- });
264
- } catch (err) {
265
- console.error('Error processing upload:', err);
266
-
267
- // Track upload processing failure
268
- trackException(err, {
269
- operation: 'importSession_upload'
270
- });
271
-
272
- if (req.file) {
273
- await fs.promises.unlink(req.file.path).catch(() => {});
274
- }
275
- res.status(500).json({ error: 'Error processing upload' });
276
- }
277
- }
278
-
279
- // Multer middleware accessor
280
- getUploadMiddleware() {
281
- return this.upload.single('zipFile');
282
- }
283
-
284
- /**
285
- * Detect the format of a session from extracted directory
286
- * @param {string} extractDir - Directory containing extracted session files
287
- * @returns {Promise<Object|null>} Format information or null if unknown
288
- */
289
- async _detectFormat(extractDir) {
290
- try {
291
- const entries = await fs.promises.readdir(extractDir);
292
-
293
- if (entries.length === 0) {
294
- return null;
295
- }
296
-
297
- // Check for Pi-Mono format: timestamped filename pattern YYYY-MM-DDTHH-MM-SS-SSSZ_sessionId.jsonl
298
- const piMonoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z_([a-zA-Z0-9_-]+)\.jsonl$/;
299
- for (const entry of entries) {
300
- const match = entry.match(piMonoPattern);
301
- if (match) {
302
- return {
303
- format: 'pi-mono',
304
- sessionId: match[1],
305
- fileName: entry,
306
- extractDir
307
- };
308
- }
309
- }
310
-
311
- // Check for Copilot format: directory with events.jsonl
312
- for (const entry of entries) {
313
- const entryPath = path.join(extractDir, entry);
314
- const stat = await fs.promises.stat(entryPath);
315
- if (stat.isDirectory()) {
316
- const eventsFile = path.join(entryPath, 'events.jsonl');
317
- if (fs.existsSync(eventsFile)) {
318
- return {
319
- format: 'copilot',
320
- sessionId: entry,
321
- directoryName: entry,
322
- extractDir
323
- };
324
- }
325
- }
326
- }
327
-
328
- // Check for Claude format: uuid.jsonl file
329
- const claudePattern = /^([a-zA-Z0-9_-]+)\.jsonl$/;
330
- for (const entry of entries) {
331
- const entryPath = path.join(extractDir, entry);
332
- const stat = await fs.promises.stat(entryPath);
333
-
334
- if (stat.isFile()) {
335
- const match = entry.match(claudePattern);
336
- if (match) {
337
- const sessionId = match[1];
338
- // Check if there's an optional directory with the same name
339
- const sessionDir = path.join(extractDir, sessionId);
340
- const hasDirectory = fs.existsSync(sessionDir);
341
-
342
- return {
343
- format: 'claude',
344
- sessionId,
345
- fileName: entry,
346
- hasDirectory,
347
- directoryName: hasDirectory ? sessionId : undefined,
348
- extractDir
349
- };
350
- }
351
- }
352
- }
353
-
354
- return null;
355
- } catch (err) {
356
- console.error('Error detecting format:', err);
357
- return null;
358
- }
359
- }
360
-
361
- /**
362
- * Import Copilot format session
363
- * @param {Object} formatInfo - Format detection result
364
- * @param {string} extractDir - Extraction directory
365
- * @returns {Promise<Object>} Import result
366
- */
367
- async _importCopilotSession(formatInfo, extractDir) {
368
- try {
369
- const { sessionId, directoryName } = formatInfo;
370
-
371
- // Validate session ID
372
- if (!isValidSessionId(sessionId)) {
373
- return {
374
- success: false,
375
- error: 'Invalid session ID',
376
- statusCode: 400
377
- };
378
- }
379
-
380
- const sessionPath = path.join(extractDir, directoryName);
381
- const targetPath = path.join(this.SESSION_DIRS.copilot, sessionId);
382
-
383
- // Check for events.jsonl
384
- const eventsFile = path.join(sessionPath, 'events.jsonl');
385
- if (!fs.existsSync(eventsFile)) {
386
- return {
387
- success: false,
388
- error: 'Invalid session structure (no events.jsonl)',
389
- statusCode: 400
390
- };
391
- }
392
-
393
- // Check if session already exists
394
- if (fs.existsSync(targetPath)) {
395
- return {
396
- success: false,
397
- error: 'Session already exists',
398
- statusCode: 409
399
- };
400
- }
401
-
402
- // Move session directory
403
- await fs.promises.rename(sessionPath, targetPath);
404
-
405
- // Mark as imported
406
- await fs.promises.writeFile(path.join(targetPath, '.imported'), '');
407
-
408
- return {
409
- success: true,
410
- sessionId,
411
- format: 'copilot'
412
- };
413
- } catch (err) {
414
- console.error('Error importing Copilot session:', err);
415
- return {
416
- success: false,
417
- error: `Error importing Copilot session: ${err.message}`,
418
- statusCode: 500
419
- };
420
- }
421
- }
422
-
423
- /**
424
- * Import Claude format session
425
- * @param {Object} formatInfo - Format detection result
426
- * @param {string} extractDir - Extraction directory
427
- * @param {Object} req - Express request object
428
- * @returns {Promise<Object>} Import result
429
- */
430
- async _importClaudeSession(formatInfo, extractDir, req) {
431
- try {
432
- const { sessionId, fileName, hasDirectory, directoryName } = formatInfo;
433
-
434
- // Validate session ID
435
- if (!isValidSessionId(sessionId)) {
436
- return {
437
- success: false,
438
- error: 'Invalid session ID',
439
- statusCode: 400
440
- };
441
- }
442
-
443
- // Get project from query or use default
444
- const project = req.query.project || 'imported-sessions';
445
-
446
- // Create project directory
447
- const projectPath = path.join(this.SESSION_DIRS.claude, project);
448
- await fs.promises.mkdir(projectPath, { recursive: true });
449
-
450
- // Move the .jsonl file
451
- const sourceFile = path.join(extractDir, fileName);
452
- const targetFile = path.join(projectPath, fileName);
453
- await fs.promises.rename(sourceFile, targetFile);
454
-
455
- // If there's a directory, move it too
456
- if (hasDirectory && directoryName) {
457
- const sourceDir = path.join(extractDir, directoryName);
458
- const targetDir = path.join(projectPath, directoryName);
459
- await fs.promises.rename(sourceDir, targetDir);
460
- }
461
-
462
- return {
463
- success: true,
464
- sessionId,
465
- format: 'claude',
466
- project
467
- };
468
- } catch (err) {
469
- console.error('Error importing Claude session:', err);
470
- return {
471
- success: false,
472
- error: `Error importing Claude session: ${err.message}`,
473
- statusCode: 500
474
- };
475
- }
476
- }
477
-
478
- /**
479
- * Import Pi-Mono format session
480
- * @param {Object} formatInfo - Format detection result
481
- * @param {string} extractDir - Extraction directory
482
- * @param {Object} req - Express request object
483
- * @returns {Promise<Object>} Import result
484
- */
485
- async _importPiMonoSession(formatInfo, extractDir, req) {
486
- try {
487
- const { sessionId, fileName } = formatInfo;
488
-
489
- // Validate session ID
490
- if (!isValidSessionId(sessionId)) {
491
- return {
492
- success: false,
493
- error: 'Invalid session ID',
494
- statusCode: 400
495
- };
496
- }
497
-
498
- // Get project from query or use default
499
- const project = req.query.project || 'imported-sessions';
500
-
501
- // Create project directory
502
- const projectPath = path.join(this.SESSION_DIRS['pi-mono'], project);
503
- await fs.promises.mkdir(projectPath, { recursive: true });
504
-
505
- // Move the .jsonl file
506
- const sourceFile = path.join(extractDir, fileName);
507
- const targetFile = path.join(projectPath, fileName);
508
- await fs.promises.rename(sourceFile, targetFile);
509
-
510
- return {
511
- success: true,
512
- sessionId,
513
- format: 'pi-mono',
514
- project
515
- };
516
- } catch (err) {
517
- console.error('Error importing Pi-Mono session:', err);
518
- return {
519
- success: false,
520
- error: `Error importing Pi-Mono session: ${err.message}`,
521
- statusCode: 500
522
- };
523
- }
524
- }
525
-
526
- /**
527
- * Import session by detected format
528
- * @param {Object} formatInfo - Format detection result
529
- * @param {string} extractDir - Extraction directory
530
- * @param {Object} req - Express request object
531
- * @returns {Promise<Object>} Import result
532
- */
533
- async _importByFormat(formatInfo, extractDir, req) {
534
- // Validate session ID
535
- if (!isValidSessionId(formatInfo.sessionId)) {
536
- return {
537
- success: false,
538
- error: 'Invalid session ID',
539
- statusCode: 400
540
- };
541
- }
542
-
543
- switch (formatInfo.format) {
544
- case 'copilot':
545
- return await this._importCopilotSession(formatInfo, extractDir);
546
- case 'claude':
547
- return await this._importClaudeSession(formatInfo, extractDir, req);
548
- case 'pi-mono':
549
- return await this._importPiMonoSession(formatInfo, extractDir, req);
550
- default:
551
- return {
552
- success: false,
553
- error: `Unsupported format: ${formatInfo.format}`,
554
- statusCode: 400
555
- };
556
- }
557
- }
558
-
559
- /**
560
- * Find session location across all session directories
561
- * @param {string} sessionId - Session identifier
562
- * @param {string} preferredSource - Preferred source to search first
563
- * @returns {Promise<Object|null>} Session location info or null
564
- */
565
- async _findSessionLocation(sessionId, preferredSource = null) {
566
- try {
567
- // Define search order based on preference
568
- const sources = preferredSource
569
- ? [preferredSource, ...Object.keys(this.SESSION_DIRS).filter(s => s !== preferredSource)]
570
- : Object.keys(this.SESSION_DIRS);
571
-
572
- for (const source of sources) {
573
- const baseDir = this.SESSION_DIRS[source];
574
-
575
- if (source === 'copilot') {
576
- // For Copilot, sessions are directly in SESSION_DIR
577
- const sessionPath = path.join(baseDir, sessionId);
578
- if (fs.existsSync(sessionPath)) {
579
- const eventsFile = path.join(sessionPath, 'events.jsonl');
580
- if (fs.existsSync(eventsFile)) {
581
- return {
582
- source: 'copilot',
583
- sessionId,
584
- sessionPath,
585
- baseDir
586
- };
587
- }
588
- }
589
- } else if (source === 'claude') {
590
- // For Claude, search in all project directories
591
- if (fs.existsSync(baseDir)) {
592
- const projects = await fs.promises.readdir(baseDir);
593
- for (const project of projects) {
594
- const projectPath = path.join(baseDir, project);
595
- const stat = await fs.promises.stat(projectPath);
596
- if (stat.isDirectory()) {
597
- const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
598
- if (fs.existsSync(sessionFile)) {
599
- return {
600
- source: 'claude',
601
- sessionId,
602
- sessionFile,
603
- projectPath,
604
- project,
605
- baseDir
606
- };
607
- }
608
- }
609
- }
610
- }
611
- } else if (source === 'pi-mono') {
612
- // For Pi-Mono, search in all project directories for timestamped files
613
- if (fs.existsSync(baseDir)) {
614
- const projects = await fs.promises.readdir(baseDir);
615
- for (const project of projects) {
616
- const projectPath = path.join(baseDir, project);
617
- const stat = await fs.promises.stat(projectPath);
618
- if (stat.isDirectory()) {
619
- const files = await fs.promises.readdir(projectPath);
620
- const piMonoPattern = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-\\d{3}Z_${sessionId}\\.jsonl$`);
621
- for (const file of files) {
622
- if (piMonoPattern.test(file)) {
623
- return {
624
- source: 'pi-mono',
625
- sessionId,
626
- fileName: file,
627
- sessionFile: path.join(projectPath, file),
628
- projectPath,
629
- project,
630
- baseDir
631
- };
632
- }
633
- }
634
- }
635
- }
636
- }
637
- }
638
- }
639
-
640
- return null;
641
- } catch (err) {
642
- console.error('Error finding session location:', err);
643
- return null;
644
- }
645
- }
646
- }
647
-
648
- module.exports = UploadController;