BibexPy 2.0.0__py3-none-any.whl

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 (171) hide show
  1. bibex_core/MergeDB.py +1189 -0
  2. bibex_core/__init__.py +13 -0
  3. bibex_core/modules/__init__.py +35 -0
  4. bibex_core/modules/api_utils.py +1172 -0
  5. bibex_core/modules/c1_utils.py +423 -0
  6. bibex_core/modules/file_utils.py +71 -0
  7. bibex_core/modules/ml_utils.py +332 -0
  8. bibex_core/modules/post_process.py +50 -0
  9. bibex_core/modules/stats_utils.py +216 -0
  10. bibex_core/scp2xlsx.py +235 -0
  11. bibex_core/wos2xlsx.py +159 -0
  12. bibex_core/xlsx2vos.py +185 -0
  13. bibexpy/__init__.py +9 -0
  14. bibexpy/__main__.py +6 -0
  15. bibexpy/_server/__init__.py +1 -0
  16. bibexpy/_server/config.py +92 -0
  17. bibexpy/_server/jobs/__init__.py +3 -0
  18. bibexpy/_server/jobs/runner.py +301 -0
  19. bibexpy/_server/main.py +211 -0
  20. bibexpy/_server/models/__init__.py +0 -0
  21. bibexpy/_server/models/project.py +25 -0
  22. bibexpy/_server/routers/__init__.py +0 -0
  23. bibexpy/_server/routers/audit.py +65 -0
  24. bibexpy/_server/routers/convert.py +101 -0
  25. bibexpy/_server/routers/disambiguate.py +177 -0
  26. bibexpy/_server/routers/downloads.py +26 -0
  27. bibexpy/_server/routers/enrich.py +42 -0
  28. bibexpy/_server/routers/export.py +116 -0
  29. bibexpy/_server/routers/export_folder.py +97 -0
  30. bibexpy/_server/routers/filter.py +131 -0
  31. bibexpy/_server/routers/jobs.py +72 -0
  32. bibexpy/_server/routers/merge.py +437 -0
  33. bibexpy/_server/routers/prepare.py +241 -0
  34. bibexpy/_server/routers/projects.py +49 -0
  35. bibexpy/_server/routers/quality.py +288 -0
  36. bibexpy/_server/routers/records.py +204 -0
  37. bibexpy/_server/routers/report.py +111 -0
  38. bibexpy/_server/routers/settings.py +383 -0
  39. bibexpy/_server/routers/system.py +209 -0
  40. bibexpy/_server/routers/tools.py +257 -0
  41. bibexpy/_server/routers/upload.py +110 -0
  42. bibexpy/_server/services/__init__.py +0 -0
  43. bibexpy/_server/services/analyses.py +364 -0
  44. bibexpy/_server/services/audit.py +340 -0
  45. bibexpy/_server/services/bibex_adapter.py +47 -0
  46. bibexpy/_server/services/bibtex_writer.py +104 -0
  47. bibexpy/_server/services/converter.py +205 -0
  48. bibexpy/_server/services/disambiguation/__init__.py +19 -0
  49. bibexpy/_server/services/disambiguation/blocking.py +581 -0
  50. bibexpy/_server/services/disambiguation/cache.py +47 -0
  51. bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
  52. bibexpy/_server/services/disambiguation/orcid.py +261 -0
  53. bibexpy/_server/services/disambiguation/pipeline.py +985 -0
  54. bibexpy/_server/services/disambiguation/similarity.py +91 -0
  55. bibexpy/_server/services/enricher.py +338 -0
  56. bibexpy/_server/services/exporter.py +112 -0
  57. bibexpy/_server/services/filter_engine.py +300 -0
  58. bibexpy/_server/services/merger.py +202 -0
  59. bibexpy/_server/services/methodology.py +102 -0
  60. bibexpy/_server/services/report_export.py +300 -0
  61. bibexpy/_server/services/ris_writer.py +62 -0
  62. bibexpy/_server/services/smart_merger.py +967 -0
  63. bibexpy/_server/services/storage.py +165 -0
  64. bibexpy/_web/404/index.html +1 -0
  65. bibexpy/_web/404.html +1 -0
  66. bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
  67. bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
  68. bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
  69. bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
  70. bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
  71. bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
  72. bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
  73. bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
  74. bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
  75. bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
  76. bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
  77. bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
  78. bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
  79. bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
  80. bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
  81. bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
  82. bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
  83. bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
  84. bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
  85. bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
  86. bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
  87. bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
  88. bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
  89. bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
  90. bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
  91. bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
  92. bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
  93. bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
  94. bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  95. bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
  96. bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
  97. bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  98. bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  99. bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  100. bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
  101. bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
  102. bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
  103. bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
  104. bibexpy/_web/images/PROMPTS.md +83 -0
  105. bibexpy/_web/images/about-illustration.png +0 -0
  106. bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
  107. bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
  108. bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
  109. bibexpy/_web/images/bibexpy-logo-full.png +0 -0
  110. bibexpy/_web/images/bibexpy-logo-header.png +0 -0
  111. bibexpy/_web/images/bibexpy-logo-in.png +0 -0
  112. bibexpy/_web/images/bibexpy-logo.webp +0 -0
  113. bibexpy/_web/images/hero-illustration.png +0 -0
  114. bibexpy/_web/images/loader.gif +0 -0
  115. bibexpy/_web/images/workflow-diagram.png +0 -0
  116. bibexpy/_web/index.html +1 -0
  117. bibexpy/_web/index.txt +11 -0
  118. bibexpy/_web/projects/_/convert/index.html +1 -0
  119. bibexpy/_web/projects/_/convert/index.txt +12 -0
  120. bibexpy/_web/projects/_/disambiguate/index.html +1 -0
  121. bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
  122. bibexpy/_web/projects/_/enrich/index.html +1 -0
  123. bibexpy/_web/projects/_/enrich/index.txt +12 -0
  124. bibexpy/_web/projects/_/export/index.html +1 -0
  125. bibexpy/_web/projects/_/export/index.txt +12 -0
  126. bibexpy/_web/projects/_/merge/index.html +1 -0
  127. bibexpy/_web/projects/_/merge/index.txt +12 -0
  128. bibexpy/_web/projects/_/records/index.html +1 -0
  129. bibexpy/_web/projects/_/records/index.txt +12 -0
  130. bibexpy/_web/projects/_/report/index.html +1 -0
  131. bibexpy/_web/projects/_/report/index.txt +12 -0
  132. bibexpy/_web/projects/_/upload/index.html +1 -0
  133. bibexpy/_web/projects/_/upload/index.txt +12 -0
  134. bibexpy/_web/projects/index.html +1 -0
  135. bibexpy/_web/projects/index.txt +11 -0
  136. bibexpy/_web/settings/index.html +1 -0
  137. bibexpy/_web/settings/index.txt +11 -0
  138. bibexpy/_web/tools/README.md +65 -0
  139. bibexpy/_web/tools/bibexcel.svg +6 -0
  140. bibexpy/_web/tools/bibliometrix.png +0 -0
  141. bibexpy/_web/tools/biblioshiny.svg +12 -0
  142. bibexpy/_web/tools/citavi.svg +6 -0
  143. bibexpy/_web/tools/citespace.svg +7 -0
  144. bibexpy/_web/tools/citnetexplorer.svg +8 -0
  145. bibexpy/_web/tools/endnote.svg +6 -0
  146. bibexpy/_web/tools/excel.svg +1 -0
  147. bibexpy/_web/tools/gephi.svg +66 -0
  148. bibexpy/_web/tools/histcite.svg +7 -0
  149. bibexpy/_web/tools/index.html +1 -0
  150. bibexpy/_web/tools/index.txt +11 -0
  151. bibexpy/_web/tools/jabref.svg +122 -0
  152. bibexpy/_web/tools/latex.svg +1 -0
  153. bibexpy/_web/tools/mendeley.svg +1 -0
  154. bibexpy/_web/tools/openrefine.svg +15 -0
  155. bibexpy/_web/tools/overleaf.svg +1 -0
  156. bibexpy/_web/tools/papers.svg +7 -0
  157. bibexpy/_web/tools/powerbi.svg +1 -0
  158. bibexpy/_web/tools/python.svg +1 -0
  159. bibexpy/_web/tools/r.svg +1 -0
  160. bibexpy/_web/tools/refworks.svg +5 -0
  161. bibexpy/_web/tools/scite.svg +5 -0
  162. bibexpy/_web/tools/tableau.svg +1 -0
  163. bibexpy/_web/tools/vosviewer.png +0 -0
  164. bibexpy/_web/tools/zotero.svg +1 -0
  165. bibexpy/cli.py +242 -0
  166. bibexpy-2.0.0.dist-info/METADATA +105 -0
  167. bibexpy-2.0.0.dist-info/RECORD +171 -0
  168. bibexpy-2.0.0.dist-info/WHEEL +5 -0
  169. bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
  170. bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
  171. bibexpy-2.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,257 @@
1
+ """Standalone format converter — proje gerektirmez.
2
+
3
+ Kullanıcı tek bir bibliometrik dosya yükler (WoS TXT, Scopus CSV, plain CSV,
4
+ TSV, Excel) ve istediği hedef formata (WoS / VOSviewer TSV / BibTeX / RIS /
5
+ CSV / XLSX / TSV) dönüştürür. Sonuç dosyası geçici klasörde üretilir, response
6
+ olarak stream edilir, sonra arka planda silinir.
7
+
8
+ Tasarım kararları:
9
+ - Project state'i yok — `/projects/...` URL ailesinden bağımsız.
10
+ - Geçici dosyalar `STORAGE_DIR/tools_tmp/<uuid>/` altına yazılır, response
11
+ döndükten sonra `BackgroundTasks` ile temizlenir.
12
+ - Source format kullanıcı tarafından seçilir (auto-detect kırılgan
13
+ olabileceği için manuel seçim daha güvenli).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import shutil
19
+ import time
20
+ import uuid
21
+ from pathlib import Path
22
+ from typing import Literal
23
+
24
+ import pandas as pd
25
+ from fastapi import APIRouter, BackgroundTasks, File, Form, HTTPException, UploadFile
26
+ from fastapi.responses import FileResponse
27
+
28
+ from config import settings
29
+ from services.bibex_adapter import _suppress_stdio
30
+
31
+ router = APIRouter(prefix="/tools", tags=["tools"])
32
+
33
+
34
+ SourceFormat = Literal["wos", "scopus_csv", "csv", "tsv", "xlsx"]
35
+ TargetFormat = Literal["wos", "vos", "bib", "ris", "csv", "tsv", "xlsx"]
36
+
37
+ VALID_SOURCE: set[str] = {"wos", "scopus_csv", "csv", "tsv", "xlsx"}
38
+ VALID_TARGET: set[str] = {"wos", "vos", "bib", "ris", "csv", "tsv", "xlsx"}
39
+
40
+ # Hedef format anahtarı ≠ dosya uzantısı olan durumlar: WoS plain text ve VOSviewer
41
+ # TSV çıktıları aslında .txt'tir (test bulgusu #9 — eskiden .wos/.vos olarak iniyordu).
42
+ # services/exporter.py:_EXT ile AYNI standart. Listelenmeyen formatlarda anahtar=uzantı.
43
+ _EXT: dict[str, str] = {"wos": "txt", "vos": "txt"}
44
+
45
+
46
+ def _tools_tmp_dir() -> Path:
47
+ base = settings.storage_path / "tools_tmp"
48
+ base.mkdir(parents=True, exist_ok=True)
49
+ return base
50
+
51
+
52
+ def _read_source(src_path: Path, source_format: str) -> pd.DataFrame:
53
+ """Yüklenmiş dosyayı DataFrame'e oku."""
54
+ try:
55
+ if source_format == "xlsx":
56
+ return pd.read_excel(src_path)
57
+
58
+ if source_format == "csv":
59
+ # Plain CSV — encoding auto fallback (UTF-8 → cp1254)
60
+ for enc in ("utf-8", "utf-8-sig", "cp1254", "latin-1"):
61
+ try:
62
+ return pd.read_csv(src_path, encoding=enc)
63
+ except UnicodeDecodeError:
64
+ continue
65
+ raise HTTPException(400, "csv_encoding_failed")
66
+
67
+ if source_format == "tsv":
68
+ for enc in ("utf-8", "utf-8-sig", "cp1254", "latin-1"):
69
+ try:
70
+ return pd.read_csv(src_path, sep="\t", encoding=enc)
71
+ except UnicodeDecodeError:
72
+ continue
73
+ raise HTTPException(400, "tsv_encoding_failed")
74
+
75
+ if source_format == "scopus_csv":
76
+ # bibex_core ile Scopus CSV → XLSX → read
77
+ from bibex_core.scp2xlsx import save_to_excel
78
+ tmp_xlsx = src_path.parent / "_scopus_tmp.xlsx"
79
+ with _suppress_stdio():
80
+ ok = save_to_excel([str(src_path)], str(tmp_xlsx))
81
+ if not ok or not tmp_xlsx.exists():
82
+ raise HTTPException(400, "scopus_convert_failed")
83
+ df = pd.read_excel(tmp_xlsx)
84
+ tmp_xlsx.unlink(missing_ok=True)
85
+ return df
86
+
87
+ if source_format == "wos":
88
+ from bibex_core.wos2xlsx import save_to_excel
89
+ tmp_xlsx = src_path.parent / "_wos_tmp.xlsx"
90
+ with _suppress_stdio():
91
+ ok = save_to_excel(str(src_path), str(tmp_xlsx))
92
+ if not ok or not tmp_xlsx.exists():
93
+ raise HTTPException(400, "wos_convert_failed")
94
+ df = pd.read_excel(tmp_xlsx)
95
+ tmp_xlsx.unlink(missing_ok=True)
96
+ return df
97
+
98
+ raise HTTPException(400, f"unknown_source_format: {source_format}")
99
+ except HTTPException:
100
+ raise
101
+ except Exception as e:
102
+ raise HTTPException(400, f"file_read_failed: {e}")
103
+
104
+
105
+ def _write_target(df: pd.DataFrame, target_format: str, out_path: Path) -> None:
106
+ """DataFrame'i hedef formatta yaz."""
107
+ if df.empty:
108
+ raise HTTPException(400, "file_empty")
109
+
110
+ if target_format == "xlsx":
111
+ df.to_excel(out_path, index=False)
112
+ elif target_format == "csv":
113
+ df.to_csv(out_path, index=False, encoding="utf-8")
114
+ elif target_format == "tsv":
115
+ df.to_csv(out_path, sep="\t", index=False, encoding="utf-8")
116
+ elif target_format == "wos":
117
+ from bibex_core.xlsx2vos import convert_excel_to_wos
118
+ tmp_xlsx = out_path.parent / f"_pre_wos_{out_path.stem}.xlsx"
119
+ df.to_excel(tmp_xlsx, index=False)
120
+ with _suppress_stdio():
121
+ convert_excel_to_wos(str(tmp_xlsx), str(out_path))
122
+ tmp_xlsx.unlink(missing_ok=True)
123
+ elif target_format == "vos":
124
+ cols = [c for c in ("AU", "TI", "SO", "PY", "VL", "IS", "PG", "DI",
125
+ "DE", "ID", "AB", "TC", "DT", "DB", "WC", "SC")
126
+ if c in df.columns]
127
+ if not cols:
128
+ # Hiçbir bibliometrik kolon yoksa tüm dataset'i TSV olarak yaz
129
+ df.to_csv(out_path, sep="\t", index=False, encoding="utf-8")
130
+ else:
131
+ df[cols].to_csv(out_path, sep="\t", index=False, encoding="utf-8")
132
+ elif target_format == "bib":
133
+ from services.bibtex_writer import write_bibtex
134
+ write_bibtex(df, out_path)
135
+ elif target_format == "ris":
136
+ from services.ris_writer import write_ris
137
+ write_ris(df, out_path)
138
+ else:
139
+ raise HTTPException(400, f"unsupported_target_format: {target_format}")
140
+
141
+
142
+ def _safe_cleanup(path: Path) -> None:
143
+ try:
144
+ if path.is_dir():
145
+ shutil.rmtree(path, ignore_errors=True)
146
+ except Exception:
147
+ pass
148
+
149
+
150
+ @router.post("/convert")
151
+ async def convert_file(
152
+ background_tasks: BackgroundTasks,
153
+ file: UploadFile = File(...),
154
+ source_format: str = Form(...),
155
+ target_format: str = Form(...),
156
+ output_name: str | None = Form(None),
157
+ ):
158
+ """Tek dosyayı yükle, hedef formata dönüştür ve indir.
159
+
160
+ Multipart form data:
161
+ - `file`: yüklenecek dosya
162
+ - `source_format`: 'wos' | 'scopus_csv' | 'csv' | 'tsv' | 'xlsx'
163
+ - `target_format`: 'wos' | 'vos' | 'bib' | 'ris' | 'csv' | 'tsv' | 'xlsx'
164
+ - `output_name`: (opsiyonel) çıktı dosya adı — uzantısız
165
+ """
166
+ if source_format not in VALID_SOURCE:
167
+ raise HTTPException(400, f"invalid_source_format: {source_format}")
168
+ if target_format not in VALID_TARGET:
169
+ raise HTTPException(400, f"invalid_target_format: {target_format}")
170
+
171
+ # Geçici klasör
172
+ job_id = uuid.uuid4().hex[:12]
173
+ workdir = _tools_tmp_dir() / job_id
174
+ workdir.mkdir(parents=True, exist_ok=True)
175
+
176
+ # Yüklenen dosyayı yaz
177
+ original_name = Path(file.filename or "input").name
178
+ src_path = workdir / original_name
179
+ try:
180
+ with src_path.open("wb") as out:
181
+ shutil.copyfileobj(file.file, out)
182
+ except Exception as e:
183
+ _safe_cleanup(workdir)
184
+ raise HTTPException(500, f"file_write_failed: {e}")
185
+ finally:
186
+ try:
187
+ file.file.close()
188
+ except Exception:
189
+ pass
190
+
191
+ # Oku
192
+ try:
193
+ df = _read_source(src_path, source_format)
194
+ except Exception:
195
+ _safe_cleanup(workdir)
196
+ raise
197
+
198
+ # Yaz
199
+ stamp = time.strftime("%Y%m%d_%H%M%S")
200
+ base = (output_name or f"converted_{stamp}").strip() or f"converted_{stamp}"
201
+ # Path-traversal'a karşı sadece dosya adı kısmı
202
+ base = Path(base).name
203
+ # Uzantıyı garanti et — wos/vos gibi anahtar≠uzantı durumunda GERÇEK uzantıyı
204
+ # kullan (.wos/.vos değil .txt). FileResponse filename=out_path.name → otomatik düzelir.
205
+ ext = _EXT.get(target_format, target_format)
206
+ if not base.lower().endswith(f".{ext}"):
207
+ base = f"{base}.{ext}"
208
+ out_path = workdir / base
209
+
210
+ try:
211
+ _write_target(df, target_format, out_path)
212
+ except Exception:
213
+ _safe_cleanup(workdir)
214
+ raise
215
+
216
+ if not out_path.exists():
217
+ _safe_cleanup(workdir)
218
+ raise HTTPException(500, "output_file_missing")
219
+
220
+ # Response stream sonrası temizlik
221
+ background_tasks.add_task(_safe_cleanup, workdir)
222
+
223
+ # Records sayısı header olarak da gönder — UI istatistik göstersin
224
+ return FileResponse(
225
+ path=str(out_path),
226
+ filename=out_path.name,
227
+ media_type="application/octet-stream",
228
+ headers={
229
+ "X-Records-Count": str(len(df)),
230
+ "X-Columns-Count": str(len(df.columns)),
231
+ "X-Source-Format": source_format,
232
+ "X-Target-Format": target_format,
233
+ },
234
+ )
235
+
236
+
237
+ @router.get("/formats")
238
+ def list_formats():
239
+ """UI için desteklenen formatlar listesi (kaynak ↔ hedef matrisi)."""
240
+ return {
241
+ "source_formats": [
242
+ {"id": "wos", "label": "WoS plain text", "extensions": ["txt", "isi"]},
243
+ {"id": "scopus_csv", "label": "Scopus CSV", "extensions": ["csv"]},
244
+ {"id": "xlsx", "label": "Excel", "extensions": ["xlsx"]},
245
+ {"id": "csv", "label": "Plain CSV", "extensions": ["csv"]},
246
+ {"id": "tsv", "label": "TSV", "extensions": ["tsv", "txt"]},
247
+ ],
248
+ "target_formats": [
249
+ {"id": "wos", "label": "WoS plain text", "extension": "txt"},
250
+ {"id": "vos", "label": "VOSviewer TSV", "extension": "txt"},
251
+ {"id": "bib", "label": "BibTeX", "extension": "bib"},
252
+ {"id": "ris", "label": "RIS", "extension": "ris"},
253
+ {"id": "xlsx", "label": "Excel", "extension": "xlsx"},
254
+ {"id": "csv", "label": "CSV", "extension": "csv"},
255
+ {"id": "tsv", "label": "TSV", "extension": "tsv"},
256
+ ],
257
+ }
@@ -0,0 +1,110 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi import APIRouter, File, HTTPException, UploadFile
4
+
5
+ from models.project import UploadedFile
6
+ from services import audit, storage
7
+
8
+ router = APIRouter(prefix="/projects/{project_id}/files", tags=["upload"])
9
+
10
+ # Güvenlik: dosya adındaki path-traversal'ı önle
11
+ def _safe_name(name: str) -> str:
12
+ base = Path(name).name # sadece son segment
13
+ if not base or base in {".", ".."}:
14
+ raise HTTPException(400, f"invalid_filename: {name!r}")
15
+ return base
16
+
17
+
18
+ @router.get("", response_model=list[UploadedFile])
19
+ def list_files(project_id: str):
20
+ meta = storage.get_project(project_id)
21
+ if meta is None:
22
+ raise HTTPException(404, "project_not_found")
23
+ raw = storage.project_dir(project_id) / "raw"
24
+ if not raw.exists():
25
+ return []
26
+ out: list[UploadedFile] = []
27
+ for f in sorted(raw.iterdir()):
28
+ if not f.is_file():
29
+ continue
30
+ out.append(UploadedFile(
31
+ name=f.name,
32
+ size=f.stat().st_size,
33
+ kind=storage.detect_file_kind(f.name),
34
+ saved_path=str(f.relative_to(storage.settings.storage_path)),
35
+ ))
36
+ return out
37
+
38
+
39
+ @router.post("", response_model=list[UploadedFile])
40
+ async def upload_files(project_id: str, files: list[UploadFile] = File(...)):
41
+ meta = storage.get_project(project_id)
42
+ if meta is None:
43
+ raise HTTPException(404, "project_not_found")
44
+ raw = storage.project_dir(project_id) / "raw"
45
+ raw.mkdir(parents=True, exist_ok=True)
46
+
47
+ saved: list[UploadedFile] = []
48
+ for upload in files:
49
+ name = _safe_name(upload.filename or "untitled")
50
+ dest = raw / name
51
+ # Aynı isim varsa numara ekle
52
+ i = 1
53
+ stem, suffix = dest.stem, dest.suffix
54
+ while dest.exists():
55
+ dest = raw / f"{stem} ({i}){suffix}"
56
+ i += 1
57
+ # Akışı diske yaz (yığın yığın — büyük dosyalar için)
58
+ with dest.open("wb") as out:
59
+ while chunk := await upload.read(1024 * 1024):
60
+ out.write(chunk)
61
+ saved.append(UploadedFile(
62
+ name=dest.name,
63
+ size=dest.stat().st_size,
64
+ kind=storage.detect_file_kind(dest.name),
65
+ saved_path=str(dest.relative_to(storage.settings.storage_path)),
66
+ ))
67
+
68
+ storage.touch_project(project_id)
69
+
70
+ # Audit
71
+ total_size = sum(f.size for f in saved)
72
+ audit.write(
73
+ project_id,
74
+ kind="upload",
75
+ title=f"{len(saved)} dosya yüklendi",
76
+ title_key="audit.titles.filesUploaded",
77
+ title_params={"n": len(saved)},
78
+ analysis_id=None, # proje-seviye: tüm analizlerde "kurulum" olarak görünür
79
+ details={
80
+ "files": [{"name": f.name, "size": f.size, "kind": f.kind} for f in saved],
81
+ "total_size": total_size,
82
+ },
83
+ user_action="upload",
84
+ )
85
+ return saved
86
+
87
+
88
+ @router.delete("/{filename}", status_code=204)
89
+ def delete_file(project_id: str, filename: str):
90
+ meta = storage.get_project(project_id)
91
+ if meta is None:
92
+ raise HTTPException(404, "project_not_found")
93
+ name = _safe_name(filename)
94
+ target = storage.project_dir(project_id) / "raw" / name
95
+ if not target.exists() or not target.is_file():
96
+ raise HTTPException(404, "file_not_found")
97
+ size = target.stat().st_size
98
+ target.unlink()
99
+ storage.touch_project(project_id)
100
+ audit.write(
101
+ project_id,
102
+ kind="upload",
103
+ title=f"Dosya silindi: {name}",
104
+ title_key="audit.titles.fileDeleted",
105
+ title_params={"name": name},
106
+ analysis_id=None,
107
+ details={"name": name, "size": size, "action": "delete"},
108
+ user_action="delete_file",
109
+ )
110
+ return None
File without changes