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.
- bibex_core/MergeDB.py +1189 -0
- bibex_core/__init__.py +13 -0
- bibex_core/modules/__init__.py +35 -0
- bibex_core/modules/api_utils.py +1172 -0
- bibex_core/modules/c1_utils.py +423 -0
- bibex_core/modules/file_utils.py +71 -0
- bibex_core/modules/ml_utils.py +332 -0
- bibex_core/modules/post_process.py +50 -0
- bibex_core/modules/stats_utils.py +216 -0
- bibex_core/scp2xlsx.py +235 -0
- bibex_core/wos2xlsx.py +159 -0
- bibex_core/xlsx2vos.py +185 -0
- bibexpy/__init__.py +9 -0
- bibexpy/__main__.py +6 -0
- bibexpy/_server/__init__.py +1 -0
- bibexpy/_server/config.py +92 -0
- bibexpy/_server/jobs/__init__.py +3 -0
- bibexpy/_server/jobs/runner.py +301 -0
- bibexpy/_server/main.py +211 -0
- bibexpy/_server/models/__init__.py +0 -0
- bibexpy/_server/models/project.py +25 -0
- bibexpy/_server/routers/__init__.py +0 -0
- bibexpy/_server/routers/audit.py +65 -0
- bibexpy/_server/routers/convert.py +101 -0
- bibexpy/_server/routers/disambiguate.py +177 -0
- bibexpy/_server/routers/downloads.py +26 -0
- bibexpy/_server/routers/enrich.py +42 -0
- bibexpy/_server/routers/export.py +116 -0
- bibexpy/_server/routers/export_folder.py +97 -0
- bibexpy/_server/routers/filter.py +131 -0
- bibexpy/_server/routers/jobs.py +72 -0
- bibexpy/_server/routers/merge.py +437 -0
- bibexpy/_server/routers/prepare.py +241 -0
- bibexpy/_server/routers/projects.py +49 -0
- bibexpy/_server/routers/quality.py +288 -0
- bibexpy/_server/routers/records.py +204 -0
- bibexpy/_server/routers/report.py +111 -0
- bibexpy/_server/routers/settings.py +383 -0
- bibexpy/_server/routers/system.py +209 -0
- bibexpy/_server/routers/tools.py +257 -0
- bibexpy/_server/routers/upload.py +110 -0
- bibexpy/_server/services/__init__.py +0 -0
- bibexpy/_server/services/analyses.py +364 -0
- bibexpy/_server/services/audit.py +340 -0
- bibexpy/_server/services/bibex_adapter.py +47 -0
- bibexpy/_server/services/bibtex_writer.py +104 -0
- bibexpy/_server/services/converter.py +205 -0
- bibexpy/_server/services/disambiguation/__init__.py +19 -0
- bibexpy/_server/services/disambiguation/blocking.py +581 -0
- bibexpy/_server/services/disambiguation/cache.py +47 -0
- bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
- bibexpy/_server/services/disambiguation/orcid.py +261 -0
- bibexpy/_server/services/disambiguation/pipeline.py +985 -0
- bibexpy/_server/services/disambiguation/similarity.py +91 -0
- bibexpy/_server/services/enricher.py +338 -0
- bibexpy/_server/services/exporter.py +112 -0
- bibexpy/_server/services/filter_engine.py +300 -0
- bibexpy/_server/services/merger.py +202 -0
- bibexpy/_server/services/methodology.py +102 -0
- bibexpy/_server/services/report_export.py +300 -0
- bibexpy/_server/services/ris_writer.py +62 -0
- bibexpy/_server/services/smart_merger.py +967 -0
- bibexpy/_server/services/storage.py +165 -0
- bibexpy/_web/404/index.html +1 -0
- bibexpy/_web/404.html +1 -0
- bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
- bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
- bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
- bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
- bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
- bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
- bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
- bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
- bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
- bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
- bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
- bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
- bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
- bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
- bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
- bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
- bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
- bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
- bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
- bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
- bibexpy/_web/images/PROMPTS.md +83 -0
- bibexpy/_web/images/about-illustration.png +0 -0
- bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
- bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
- bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
- bibexpy/_web/images/bibexpy-logo-full.png +0 -0
- bibexpy/_web/images/bibexpy-logo-header.png +0 -0
- bibexpy/_web/images/bibexpy-logo-in.png +0 -0
- bibexpy/_web/images/bibexpy-logo.webp +0 -0
- bibexpy/_web/images/hero-illustration.png +0 -0
- bibexpy/_web/images/loader.gif +0 -0
- bibexpy/_web/images/workflow-diagram.png +0 -0
- bibexpy/_web/index.html +1 -0
- bibexpy/_web/index.txt +11 -0
- bibexpy/_web/projects/_/convert/index.html +1 -0
- bibexpy/_web/projects/_/convert/index.txt +12 -0
- bibexpy/_web/projects/_/disambiguate/index.html +1 -0
- bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
- bibexpy/_web/projects/_/enrich/index.html +1 -0
- bibexpy/_web/projects/_/enrich/index.txt +12 -0
- bibexpy/_web/projects/_/export/index.html +1 -0
- bibexpy/_web/projects/_/export/index.txt +12 -0
- bibexpy/_web/projects/_/merge/index.html +1 -0
- bibexpy/_web/projects/_/merge/index.txt +12 -0
- bibexpy/_web/projects/_/records/index.html +1 -0
- bibexpy/_web/projects/_/records/index.txt +12 -0
- bibexpy/_web/projects/_/report/index.html +1 -0
- bibexpy/_web/projects/_/report/index.txt +12 -0
- bibexpy/_web/projects/_/upload/index.html +1 -0
- bibexpy/_web/projects/_/upload/index.txt +12 -0
- bibexpy/_web/projects/index.html +1 -0
- bibexpy/_web/projects/index.txt +11 -0
- bibexpy/_web/settings/index.html +1 -0
- bibexpy/_web/settings/index.txt +11 -0
- bibexpy/_web/tools/README.md +65 -0
- bibexpy/_web/tools/bibexcel.svg +6 -0
- bibexpy/_web/tools/bibliometrix.png +0 -0
- bibexpy/_web/tools/biblioshiny.svg +12 -0
- bibexpy/_web/tools/citavi.svg +6 -0
- bibexpy/_web/tools/citespace.svg +7 -0
- bibexpy/_web/tools/citnetexplorer.svg +8 -0
- bibexpy/_web/tools/endnote.svg +6 -0
- bibexpy/_web/tools/excel.svg +1 -0
- bibexpy/_web/tools/gephi.svg +66 -0
- bibexpy/_web/tools/histcite.svg +7 -0
- bibexpy/_web/tools/index.html +1 -0
- bibexpy/_web/tools/index.txt +11 -0
- bibexpy/_web/tools/jabref.svg +122 -0
- bibexpy/_web/tools/latex.svg +1 -0
- bibexpy/_web/tools/mendeley.svg +1 -0
- bibexpy/_web/tools/openrefine.svg +15 -0
- bibexpy/_web/tools/overleaf.svg +1 -0
- bibexpy/_web/tools/papers.svg +7 -0
- bibexpy/_web/tools/powerbi.svg +1 -0
- bibexpy/_web/tools/python.svg +1 -0
- bibexpy/_web/tools/r.svg +1 -0
- bibexpy/_web/tools/refworks.svg +5 -0
- bibexpy/_web/tools/scite.svg +5 -0
- bibexpy/_web/tools/tableau.svg +1 -0
- bibexpy/_web/tools/vosviewer.png +0 -0
- bibexpy/_web/tools/zotero.svg +1 -0
- bibexpy/cli.py +242 -0
- bibexpy-2.0.0.dist-info/METADATA +105 -0
- bibexpy-2.0.0.dist-info/RECORD +171 -0
- bibexpy-2.0.0.dist-info/WHEEL +5 -0
- bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
- bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
- 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
|