Вики: вложение без файла скачивается заново, признак картинки у вложений страницы (#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)
|
||||
text = text[end + 5:].lstrip("\n") if end != -1 else text
|
||||
atts = [{"id": a["id"], "name": a["name"], "state": a["state"], "size": a["size"],
|
||||
"error": a["error"]} for a in conn.execute(
|
||||
"SELECT id, name, state, size, error FROM wiki_attachment WHERE label = ? AND page_code = ? ORDER BY id", (label, code))]
|
||||
"error": a["error"], "image": attachments.is_image(a["url"])} for a in conn.execute(
|
||||
"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"],
|
||||
"markdown": text, "attachments": atts}
|
||||
|
||||
@ -122,7 +123,7 @@ def h_attachment_fetch(hub, conn, m, q, body):
|
||||
if not isinstance(author, str):
|
||||
raise ApiError(422, "bad_request", "author — строка")
|
||||
try:
|
||||
info = attachments.request_fetch(conn, int_group(m), author)
|
||||
info = attachments.request_fetch(conn, int_group(m), author, hub.files_dir)
|
||||
except KeyError:
|
||||
raise ApiError(404, "not_found", f"вложение {m.group(1)}") from None
|
||||
queued = info.pop("queued")
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
"""Вложения вики по запросу: состояние, постановка wiki_fetch и хуки его результата (spec wiki «Вложения вики по запросу»).
|
||||
Адрес вложения хаб берёт только из своей таблицы wiki_attachment — из запросов клиента он не приходит."""
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import quote, unquote, urlsplit
|
||||
|
||||
from ...core import db, files, queue
|
||||
|
||||
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:
|
||||
@ -33,15 +41,17 @@ def _active(conn, attachment_id: int) -> bool:
|
||||
"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 и пакет
|
||||
wiki_fetch. `fetching` без живой команды (пакет отменён) — зависшее состояние: ставится новый пакет.
|
||||
wiki_fetch. `fetching` без живой команды (пакет отменён) — зависшее состояние: ставится новый пакет. `ready`, чьего
|
||||
файла нет в files/, — тоже нескачанное (T8.2); files_dir None — файл не сверяется.
|
||||
Ответ — {id, name, page_code, url, state, size, error, queued}. KeyError — вложения нет."""
|
||||
with db.transaction(conn):
|
||||
row = conn.execute(f"SELECT {FIELDS} FROM wiki_attachment WHERE id = ?", (attachment_id,)).fetchone()
|
||||
if row is None:
|
||||
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)
|
||||
title = f"Вложение вики {file_name(_public(row))}"
|
||||
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.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 — фикстура
|
||||
|
||||
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")
|
||||
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 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
|
||||
|
||||
|
||||
@ -237,3 +238,43 @@ def test_sync_project_not_string_422(api, project):
|
||||
e = err(api.client.post, "/wiki/sync", {"project": project})
|
||||
assert e.status == 422 and e.error == "bad_request" and e.detail == "project — строка"
|
||||
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