Вики: вложение без файла скачивается заново, признак картинки у вложений страницы (#135, T8.2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
39821b8ee6
commit
52db644034
@ -73,8 +73,9 @@ def h_page(hub, conn, m, q, body):
|
|||||||
end = text.find("\n---\n", 4)
|
end = text.find("\n---\n", 4)
|
||||||
text = text[end + 5:].lstrip("\n") if end != -1 else text
|
text = text[end + 5:].lstrip("\n") if end != -1 else text
|
||||||
atts = [{"id": a["id"], "name": a["name"], "state": a["state"], "size": a["size"],
|
atts = [{"id": a["id"], "name": a["name"], "state": a["state"], "size": a["size"],
|
||||||
"error": a["error"]} for a in conn.execute(
|
"error": a["error"], "image": attachments.is_image(a["url"])} for a in conn.execute(
|
||||||
"SELECT id, name, state, size, error FROM wiki_attachment WHERE label = ? AND page_code = ? ORDER BY id", (label, code))]
|
"SELECT id, name, state, size, error, url FROM wiki_attachment WHERE label = ? AND page_code = ? ORDER BY id",
|
||||||
|
(label, code))]
|
||||||
return 200, {"code": code, "title": r["title"], "path": r["path"], "url": r["url"], "synced_at": r["synced_at"],
|
return 200, {"code": code, "title": r["title"], "path": r["path"], "url": r["url"], "synced_at": r["synced_at"],
|
||||||
"markdown": text, "attachments": atts}
|
"markdown": text, "attachments": atts}
|
||||||
|
|
||||||
@ -122,7 +123,7 @@ def h_attachment_fetch(hub, conn, m, q, body):
|
|||||||
if not isinstance(author, str):
|
if not isinstance(author, str):
|
||||||
raise ApiError(422, "bad_request", "author — строка")
|
raise ApiError(422, "bad_request", "author — строка")
|
||||||
try:
|
try:
|
||||||
info = attachments.request_fetch(conn, int_group(m), author)
|
info = attachments.request_fetch(conn, int_group(m), author, hub.files_dir)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ApiError(404, "not_found", f"вложение {m.group(1)}") from None
|
raise ApiError(404, "not_found", f"вложение {m.group(1)}") from None
|
||||||
queued = info.pop("queued")
|
queued = info.pop("queued")
|
||||||
|
|||||||
@ -1,10 +1,18 @@
|
|||||||
"""Вложения вики по запросу: состояние, постановка wiki_fetch и хуки его результата (spec wiki «Вложения вики по запросу»).
|
"""Вложения вики по запросу: состояние, постановка wiki_fetch и хуки его результата (spec wiki «Вложения вики по запросу»).
|
||||||
Адрес вложения хаб берёт только из своей таблицы wiki_attachment — из запросов клиента он не приходит."""
|
Адрес вложения хаб берёт только из своей таблицы wiki_attachment — из запросов клиента он не приходит."""
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote, unquote, urlsplit
|
||||||
|
|
||||||
from ...core import db, files, queue
|
from ...core import db, files, queue
|
||||||
|
|
||||||
FIELDS = "id, label, page_code, url, name, state, file, size, error"
|
FIELDS = "id, label, page_code, url, name, state, file, size, error"
|
||||||
|
IMAGE_EXT = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp") # SVG — нет: активный формат из недоверенного источника
|
||||||
|
|
||||||
|
|
||||||
|
def is_image(url: str | None) -> bool:
|
||||||
|
"""Картинка ли вложение — по имени файла в адресе Eva: имя вложения (alt/title/текст ссылки) бывает без расширения."""
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
return unquote(urlsplit(url).path.rsplit("/", 1)[-1]).lower().endswith(IMAGE_EXT)
|
||||||
|
|
||||||
|
|
||||||
def _public(row) -> dict:
|
def _public(row) -> dict:
|
||||||
@ -33,15 +41,17 @@ def _active(conn, attachment_id: int) -> bool:
|
|||||||
"AND json_extract(params, '$.attachment_id') = ?", (attachment_id,)).fetchone() is not None
|
"AND json_extract(params, '$.attachment_id') = ?", (attachment_id,)).fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
def request_fetch(conn, attachment_id: int, author: str | None) -> dict:
|
def request_fetch(conn, attachment_id: int, author: str | None, files_dir) -> dict:
|
||||||
"""Поставить скачивание. Одна транзакция: `ready` и `fetching` отвечают как есть; иначе state = fetching и пакет
|
"""Поставить скачивание. Одна транзакция: `ready` и `fetching` отвечают как есть; иначе state = fetching и пакет
|
||||||
wiki_fetch. `fetching` без живой команды (пакет отменён) — зависшее состояние: ставится новый пакет.
|
wiki_fetch. `fetching` без живой команды (пакет отменён) — зависшее состояние: ставится новый пакет. `ready`, чьего
|
||||||
|
файла нет в files/, — тоже нескачанное (T8.2); files_dir None — файл не сверяется.
|
||||||
Ответ — {id, name, page_code, url, state, size, error, queued}. KeyError — вложения нет."""
|
Ответ — {id, name, page_code, url, state, size, error, queued}. KeyError — вложения нет."""
|
||||||
with db.transaction(conn):
|
with db.transaction(conn):
|
||||||
row = conn.execute(f"SELECT {FIELDS} FROM wiki_attachment WHERE id = ?", (attachment_id,)).fetchone()
|
row = conn.execute(f"SELECT {FIELDS} FROM wiki_attachment WHERE id = ?", (attachment_id,)).fetchone()
|
||||||
if row is None:
|
if row is None:
|
||||||
raise KeyError(attachment_id)
|
raise KeyError(attachment_id)
|
||||||
if row["state"] == "ready" or (row["state"] == "fetching" and _active(conn, attachment_id)):
|
on_disk = files_dir is None or files.size(files_dir, row["file"]) is not None
|
||||||
|
if (row["state"] == "ready" and on_disk) or (row["state"] == "fetching" and _active(conn, attachment_id)):
|
||||||
return dict(_public(row), queued=False)
|
return dict(_public(row), queued=False)
|
||||||
title = f"Вложение вики {file_name(_public(row))}"
|
title = f"Вложение вики {file_name(_public(row))}"
|
||||||
queue.insert_batch(conn, title, author, [{"type": "wiki_fetch", "params": {"attachment_id": row["id"], "url": row["url"]}}],
|
queue.insert_batch(conn, title, author, [{"type": "wiki_fetch", "params": {"attachment_id": row["id"], "url": row["url"]}}],
|
||||||
|
|||||||
@ -10,7 +10,8 @@ import pytest
|
|||||||
|
|
||||||
from sdlc_hub.core import files
|
from sdlc_hub.core import files
|
||||||
from sdlc_hub.core.client import HubError
|
from sdlc_hub.core.client import HubError
|
||||||
from test_wiki_convert import FILE_REF, FILE_URL, THUMB_REF, _html
|
from sdlc_hub.modules.wiki import attachments
|
||||||
|
from test_wiki_convert import FILE_REF, FILE_URL, IMG_REF, THUMB_REF, _html
|
||||||
from test_wiki_sync import CODES, TREE, api, first_sync, page # noqa: F401 — api — фикстура
|
from test_wiki_sync import CODES, TREE, api, first_sync, page # noqa: F401 — api — фикстура
|
||||||
|
|
||||||
ATTS = [{"ref": THUMB_REF, "url": FILE_URL, "name": "model.xlsx"}, {"ref": FILE_REF, "url": FILE_URL, "name": "model.xlsx"}]
|
ATTS = [{"ref": THUMB_REF, "url": FILE_URL, "name": "model.xlsx"}, {"ref": FILE_REF, "url": FILE_URL, "name": "model.xlsx"}]
|
||||||
@ -62,7 +63,7 @@ def test_status_tree_page(api):
|
|||||||
p = api.client.get("/wiki/pages/DOC-3", project="AWPCA")
|
p = api.client.get("/wiki/pages/DOC-3", project="AWPCA")
|
||||||
assert p["title"] == "Статусы" and p["url"].endswith("DOC-3") and p["synced_at"]
|
assert p["title"] == "Статусы" and p["url"].endswith("DOC-3") and p["synced_at"]
|
||||||
assert f"attachment:{aid}" in p["markdown"] and not p["markdown"].startswith("---")
|
assert f"attachment:{aid}" in p["markdown"] and not p["markdown"].startswith("---")
|
||||||
assert p["attachments"] == [{"id": aid, "name": "model.xlsx", "state": "none", "size": None, "error": None}]
|
assert p["attachments"] == [{"id": aid, "name": "model.xlsx", "state": "none", "size": None, "error": None, "image": False}]
|
||||||
assert err(api.client.get, "/wiki/pages/DOC-99", project="AWPCA").status == 404
|
assert err(api.client.get, "/wiki/pages/DOC-99", project="AWPCA").status == 404
|
||||||
|
|
||||||
|
|
||||||
@ -237,3 +238,43 @@ def test_sync_project_not_string_422(api, project):
|
|||||||
e = err(api.client.post, "/wiki/sync", {"project": project})
|
e = err(api.client.post, "/wiki/sync", {"project": project})
|
||||||
assert e.status == 422 and e.error == "bad_request" and e.detail == "project — строка"
|
assert e.status == 422 and e.error == "bad_request" and e.detail == "project — строка"
|
||||||
assert api.rows("SELECT id FROM batch") == before # пакета не создано
|
assert api.rows("SELECT id FROM batch") == before # пакета не создано
|
||||||
|
|
||||||
|
|
||||||
|
# пакет D (#135): T8.2 — вложение без файла, признак картинки
|
||||||
|
def _ready(api, aid, data=b"hello"):
|
||||||
|
api.client.post(f"/wiki/attachments/{aid}/fetch", {})
|
||||||
|
f = upload(api, data)
|
||||||
|
api.report(fetch_cmd(api), "done", {"file": f["file"], "size": f["size"]})
|
||||||
|
return f["file"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_ready_file_missing_requeues(api):
|
||||||
|
aid = synced(api)
|
||||||
|
sha = _ready(api, aid)
|
||||||
|
(api.wiki_dir.parent / "files" / sha).unlink()
|
||||||
|
r = api.client.post(f"/wiki/attachments/{aid}/fetch", {})
|
||||||
|
assert r["state"] == "fetching"
|
||||||
|
assert fetch_cmd(api)["params"]["attachment_id"] == aid
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_ready_file_present_no_batch(api):
|
||||||
|
aid = synced(api)
|
||||||
|
_ready(api, aid)
|
||||||
|
n = len(api.rows("SELECT id FROM batch"))
|
||||||
|
assert api.client.post(f"/wiki/attachments/{aid}/fetch", {})["state"] == "ready"
|
||||||
|
assert len(api.rows("SELECT id FROM batch")) == n
|
||||||
|
|
||||||
|
|
||||||
|
def test_page_attachment_image_flag(api):
|
||||||
|
html = f'<p><a href="{FILE_REF}">файл</a> <img src="{IMG_REF}"></p>'
|
||||||
|
synced(api, html, [{"ref": FILE_REF, "url": FILE_URL, "name": "attachment"},
|
||||||
|
{"ref": IMG_REF, "url": "https://eva.example.test" + IMG_REF, "name": "Схема без расширения"}])
|
||||||
|
atts = api.client.get("/wiki/pages/DOC-3", project="AWPCA")["attachments"]
|
||||||
|
assert [(a["name"], a["image"]) for a in atts] == [("attachment", False), ("Схема без расширения", True)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url,expected", [("https://e/files/a/x.PNG?version=1", True), ("https://e/files/a/x.svg", False),
|
||||||
|
("https://e/files/a/%D1%81.jpeg", True), (None, False),
|
||||||
|
("https://e/files/a/x.png.meta/", False), ("https://e/files/a/x.xlsx", False)])
|
||||||
|
def test_is_image(url, expected):
|
||||||
|
assert attachments.is_image(url) is expected
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user