File size: 19,783 Bytes
4ad5efa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
import os
import hashlib
import uuid
import json
import logging
import shutil
from pathlib import Path
from datetime import datetime, timedelta
import pandas as pd

logger = logging.getLogger(__name__)

class SessionManager:
    """
    Менеджер сесій користувачів для управління даними в багатокористувацькому середовищі.
    Забезпечує ізоляцію даних між користувачами та уникнення конфліктів.
    """
    def __init__(self, base_dir="temp/sessions"):
        """
        Ініціалізація менеджера сесій.
        
        Args:
            base_dir (str): Базова директорія для зберігання сесій
        """
        self.base_dir = Path(base_dir)
        self.base_dir.mkdir(exist_ok=True, parents=True)
        
        # Очищення застарілих сесій при ініціалізації
        self.cleanup_old_sessions()
    
    def create_session(self, user_id=None):
        """
        Створення нової сесії користувача.
        
        Args:
            user_id (str, optional): Ідентифікатор користувача. Якщо None, генерується випадковий.
            
        Returns:
            str: Ідентифікатор сесії
        """
        # Якщо user_id не вказано, генеруємо випадковий
        if not user_id:
            user_id = str(uuid.uuid4())
        
        # Генеруємо унікальний ідентифікатор сесії
        session_id = f"{user_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
        
        # Створюємо директорію для сесії
        session_dir = self.base_dir / session_id
        session_dir.mkdir(exist_ok=True)
        
        # Створюємо підпапки для різних типів даних
        (session_dir / "data").mkdir(exist_ok=True)  # Для CSV та DataFrame
        (session_dir / "indices").mkdir(exist_ok=True)  # Для індексів FAISS та BM25
        (session_dir / "reports").mkdir(exist_ok=True)  # Для звітів
        (session_dir / "viz").mkdir(exist_ok=True)  # Для візуалізацій
        
        # Зберігаємо метадані сесії
        metadata = {
            "user_id": user_id,
            "created_at": datetime.now().isoformat(),
            "last_accessed": datetime.now().isoformat(),
            "status": "active",
            "data_files": []
        }
        
        self._save_session_metadata(session_id, metadata)
        
        logger.info(f"Створено нову сесію: {session_id}")
        return session_id
    
    def get_session_dir(self, session_id):
        """
        Отримання шляху до директорії сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            
        Returns:
            Path: Шлях до директорії сесії або None, якщо сесія не існує
        """
        session_dir = self.base_dir / session_id
        
        if not session_dir.exists():
            logger.warning(f"Сесія не знайдена: {session_id}")
            return None
        
        # Оновлюємо час останнього доступу
        self._update_session_access_time(session_id)
        
        return session_dir
    
    def get_session_data_dir(self, session_id):
        """
        Отримання шляху до директорії даних сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            
        Returns:
            Path: Шлях до директорії даних або None, якщо сесія не існує
        """
        session_dir = self.get_session_dir(session_id)
        if not session_dir:
            return None
        
        return session_dir / "data"
    
    def get_session_indices_dir(self, session_id):
        """
        Отримання шляху до директорії індексів сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            
        Returns:
            Path: Шлях до директорії індексів або None, якщо сесія не існує
        """
        session_dir = self.get_session_dir(session_id)
        if not session_dir:
            return None
        
        return session_dir / "indices"
    
    def add_data_file(self, session_id, file_path, file_type="uploaded", description=None):
        """
        Додавання інформації про файл даних до сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            file_path (str): Шлях до файлу
            file_type (str): Тип файлу ("uploaded", "local", "merged")
            description (str, optional): Опис файлу
            
        Returns:
            bool: True, якщо дані успішно додані, False у випадку помилки
        """
        session_dir = self.get_session_dir(session_id)
        if not session_dir:
            return False
        
        # Отримуємо поточні метадані сесії
        metadata = self._get_session_metadata(session_id)
        if not metadata:
            return False
        
        # Генеруємо хеш файлу для відстеження дублікатів
        file_hash = self._generate_file_hash(file_path)
        
        # Додаємо інформацію про файл
        file_info = {
            "path": str(file_path),
            "filename": os.path.basename(file_path),
            "type": file_type,
            "hash": file_hash,
            "size": os.path.getsize(file_path) if os.path.exists(file_path) else 0,
            "added_at": datetime.now().isoformat(),
            "description": description or ""
        }
        
        # Перевіряємо на дублікати
        for existing_file in metadata.get("data_files", []):
            if existing_file.get("hash") == file_hash:
                logger.warning(f"Файл вже існує в сесії: {file_path}")
                return True
        
        # Додаємо файл до списку
        metadata.setdefault("data_files", []).append(file_info)
        
        # Оновлюємо метадані
        self._save_session_metadata(session_id, metadata)
        
        logger.info(f"Додано файл даних до сесії {session_id}: {file_path}")
        return True
    
    def remove_data_file(self, session_id, file_path_or_hash):
        """
        Видалення інформації про файл даних із сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            file_path_or_hash (str): Шлях до файлу або його хеш
            
        Returns:
            bool: True, якщо дані успішно видалені, False у випадку помилки
        """
        session_dir = self.get_session_dir(session_id)
        if not session_dir:
            return False
        
        # Отримуємо поточні метадані сесії
        metadata = self._get_session_metadata(session_id)
        if not metadata:
            return False
        
        # Шукаємо файл за шляхом або хешем
        file_found = False
        updated_files = []
        
        for file_info in metadata.get("data_files", []):
            if file_info.get("path") == file_path_or_hash or file_info.get("hash") == file_path_or_hash:
                file_found = True
                # Файл може бути фізично видалений, якщо він знаходиться в директорії сесії
                if file_info.get("path").startswith(str(session_dir)):
                    try:
                        os.remove(file_info.get("path"))
                        logger.info(f"Фізично видалено файл: {file_info.get('path')}")
                    except Exception as e:
                        logger.warning(f"Не вдалося видалити файл {file_info.get('path')}: {e}")
            else:
                updated_files.append(file_info)
        
        if not file_found:
            logger.warning(f"Файл не знайдено в сесії: {file_path_or_hash}")
            return False
        
        # Оновлюємо список файлів
        metadata["data_files"] = updated_files
        
        # Оновлюємо метадані
        self._save_session_metadata(session_id, metadata)
        
        logger.info(f"Видалено файл з сесії {session_id}: {file_path_or_hash}")
        return True
    
    def get_session_files(self, session_id):
        """
        Отримання списку файлів даних сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            
        Returns:
            list: Список інформації про файли або порожній список у випадку помилки
        """
        # Отримуємо поточні метадані сесії
        metadata = self._get_session_metadata(session_id)
        if not metadata:
            return []
        
        return metadata.get("data_files", [])
    
    def save_merged_data(self, session_id, merged_df, output_filename=None):
        """
        Збереження об'єднаних даних у сесію.
        
        Args:
            session_id (str): Ідентифікатор сесії
            merged_df (pandas.DataFrame): DataFrame з об'єднаними даними
            output_filename (str, optional): Ім'я файлу для збереження. Якщо None, генерується автоматично.
            
        Returns:
            str: Шлях до збереженого файлу або None у випадку помилки
        """
        session_data_dir = self.get_session_data_dir(session_id)
        if not session_data_dir:
            return None
        
        try:
            # Генеруємо ім'я файлу, якщо не вказано
            if not output_filename:
                timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
                output_filename = f"merged_data_{timestamp}.csv"
            
            # Переконуємося, що файл має розширення .csv
            if not output_filename.lower().endswith(".csv"):
                output_filename += ".csv"
            
            # Шлях для збереження
            output_path = session_data_dir / output_filename
            
            # Зберігаємо DataFrame у CSV
            merged_df.to_csv(output_path, index=False)
            
            # Додаємо інформацію про файл до сесії
            self.add_data_file(
                session_id, 
                str(output_path), 
                file_type="merged", 
                description="Об'єднані дані"
            )
            
            logger.info(f"Збережено об'єднані дані у сесії {session_id}: {output_path}")
            return str(output_path)
            
        except Exception as e:
            logger.error(f"Помилка при збереженні об'єднаних даних: {e}")
            return None
    
    def cleanup_session(self, session_id):
        """
        Очищення сесії (видалення всіх файлів і директорій).
        
        Args:
            session_id (str): Ідентифікатор сесії
            
        Returns:
            bool: True, якщо сесія успішно очищена, False у випадку помилки
        """
        session_dir = self.base_dir / session_id
        
        if not session_dir.exists():
            logger.warning(f"Сесія не знайдена: {session_id}")
            return False
        
        try:
            # Видаляємо всю директорію сесії
            shutil.rmtree(session_dir)
            logger.info(f"Сесію {session_id} успішно очищено")
            return True
        except Exception as e:
            logger.error(f"Помилка при очищенні сесії {session_id}: {e}")
            return False
    
    def cleanup_old_sessions(self, max_age_hours=24):
        """
        Очищення застарілих сесій.
        
        Args:
            max_age_hours (int): Максимальний вік сесії в годинах для збереження
            
        Returns:
            int: Кількість видалених сесій
        """
        cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
        deleted_count = 0
        
        # Перебираємо всі підпапки в базовій директорії
        for session_dir in self.base_dir.iterdir():
            if not session_dir.is_dir():
                continue
            
            # Перевіряємо час останнього доступу до сесії
            metadata_file = session_dir / "metadata.json"
            if not metadata_file.exists():
                # Якщо немає метаданих, видаляємо директорію
                try:
                    shutil.rmtree(session_dir)
                    deleted_count += 1
                    logger.info(f"Видалено сесію без метаданих: {session_dir.name}")
                except Exception as e:
                    logger.error(f"Помилка при видаленні сесії {session_dir.name}: {e}")
                continue
            
            try:
                with open(metadata_file, "r", encoding="utf-8") as f:
                    metadata = json.load(f)
                
                last_accessed = datetime.fromisoformat(metadata.get("last_accessed", metadata.get("created_at")))
                
                if last_accessed < cutoff_time:
                    # Сесія застаріла, видаляємо її
                    shutil.rmtree(session_dir)
                    deleted_count += 1
                    logger.info(f"Видалено застарілу сесію: {session_dir.name}, " 
                                f"останній доступ: {last_accessed.isoformat()}")
            except Exception as e:
                logger.error(f"Помилка при перевірці сесії {session_dir.name}: {e}")
        
        logger.info(f"Очищено {deleted_count} застарілих сесій")
        return deleted_count
    
    def _save_session_metadata(self, session_id, metadata):
        """
        Збереження метаданих сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            metadata (dict): Метадані для збереження
            
        Returns:
            bool: True, якщо метадані успішно збережені, False у випадку помилки
        """
        session_dir = self.base_dir / session_id
        
        if not session_dir.exists():
            logger.warning(f"Сесія не знайдена: {session_id}")
            return False
        
        metadata_file = session_dir / "metadata.json"
        
        try:
            with open(metadata_file, "w", encoding="utf-8") as f:
                json.dump(metadata, f, ensure_ascii=False, indent=2)
            return True
        except Exception as e:
            logger.error(f"Помилка при збереженні метаданих сесії {session_id}: {e}")
            return False
    
    def _get_session_metadata(self, session_id):
        """
        Отримання метаданих сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            
        Returns:
            dict: Метадані сесії або None у випадку помилки
        """
        session_dir = self.base_dir / session_id
        metadata_file = session_dir / "metadata.json"
        
        if not metadata_file.exists():
            logger.warning(f"Метадані сесії не знайдені: {session_id}")
            return None
        
        try:
            with open(metadata_file, "r", encoding="utf-8") as f:
                metadata = json.load(f)
            return metadata
        except Exception as e:
            logger.error(f"Помилка при читанні метаданих сесії {session_id}: {e}")
            return None
    
    def _update_session_access_time(self, session_id):
        """
        Оновлення часу останнього доступу до сесії.
        
        Args:
            session_id (str): Ідентифікатор сесії
            
        Returns:
            bool: True, якщо час доступу успішно оновлено, False у випадку помилки
        """
        metadata = self._get_session_metadata(session_id)
        if not metadata:
            return False
        
        metadata["last_accessed"] = datetime.now().isoformat()
        return self._save_session_metadata(session_id, metadata)
    
    @staticmethod
    def _generate_file_hash(file_path):
        """
        Генерує хеш для файлу на основі його вмісту або шляху.
        
        Args:
            file_path (str): Шлях до файлу
            
        Returns:
            str: Хеш файлу
        """
        try:
            if os.path.exists(file_path):
                # Для невеликих файлів використовуємо вміст файлу
                if os.path.getsize(file_path) < 10 * 1024 * 1024:  # < 10 MB
                    sha256 = hashlib.sha256()
                    with open(file_path, "rb") as f:
                        for byte_block in iter(lambda: f.read(4096), b""):
                            sha256.update(byte_block)
                    return sha256.hexdigest()
                else:
                    # Для великих файлів використовуємо шлях, розмір і час модифікації
                    file_stat = os.stat(file_path)
                    hash_input = f"{file_path}_{file_stat.st_size}_{file_stat.st_mtime}"
                    return hashlib.md5(hash_input.encode()).hexdigest()
            else:
                # Якщо файл не існує, повертаємо хеш шляху
                return hashlib.md5(file_path.encode()).hexdigest()
        except Exception as e:
            logger.warning(f"Помилка при генерації хешу файлу {file_path}: {e}")
            # У випадку помилки, повертаємо хеш шляху
            return hashlib.md5(str(file_path).encode()).hexdigest()