Add OnlyOffice-based preview for office documents
Replace Aspose.Words with OnlyOffice Document Server for generating bitmap previews of office documents (Word, Excel, PowerPoint, etc.). Backend: - Add cista/onlyoffice.py conversion client - Convert office docs directly to PNG via OnlyOffice, then AVIF via pyvips - Make office previews optional based on OnlyOffice availability - Remove Aspose.Words dependency and all related code - Add spreadsheet and presentation format support Frontend: - Mark office files as previewable in Document.ts - Add office extensions to MediaPreview.vue preview list - Fix pre-existing @ts-ignore in HeaderMain.vue Tests: - Fix test_lrucache.py parameter name (open -> opener) Also run ruff format across the codebase to satisfy linter checks.
This commit is contained in:
+31
-13
@@ -6,7 +6,6 @@ from pathlib import Path
|
||||
from time import time
|
||||
from uuid import uuid4
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sanic import Sanic
|
||||
@@ -26,19 +25,28 @@ def _ntlm_type1() -> dict[str, str]:
|
||||
return {"Authorization": f"NTLM {base64.b64encode(msg).decode()}"}
|
||||
|
||||
|
||||
def _ntlm_type3(username: str, password: str, domain: str, challenge: bytes) -> dict[str, str]:
|
||||
def _ntlm_type3(
|
||||
username: str, password: str, domain: str, challenge: bytes
|
||||
) -> dict[str, str]:
|
||||
"""Build an NTLMv2 Type 3 message for testing."""
|
||||
from Crypto.Hash import MD4
|
||||
|
||||
# NT hash
|
||||
nt_hash = MD4.new(password.encode("utf-16le")).digest()
|
||||
# NTLMv2 hash
|
||||
ntlmv2_hash = hmac.new(nt_hash, (username.upper() + domain).encode("utf-16le"), hashlib.md5).digest()
|
||||
ntlmv2_hash = hmac.new(
|
||||
nt_hash, (username.upper() + domain).encode("utf-16le"), hashlib.md5
|
||||
).digest()
|
||||
|
||||
# Build a minimal blob
|
||||
timestamp = struct.pack("<Q", 0)
|
||||
client_nonce = b"\x01" * 8
|
||||
blob = b"\x01\x01\x00\x00\x00\x00\x00\x00" + timestamp + client_nonce + b"\x00\x00\x00\x00"
|
||||
blob = (
|
||||
b"\x01\x01\x00\x00\x00\x00\x00\x00"
|
||||
+ timestamp
|
||||
+ client_nonce
|
||||
+ b"\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
# NT proof
|
||||
nt_proof = hmac.new(ntlmv2_hash, challenge + blob, hashlib.md5).digest()
|
||||
@@ -85,11 +93,11 @@ def _ntlm_type3(username: str, password: str, domain: str, challenge: bytes) ->
|
||||
|
||||
|
||||
def _session_cookie_header(username: str) -> dict[str, str]:
|
||||
token = jwt.encode(
|
||||
{"exp": int(time()) + session.max_age, "username": username},
|
||||
session.session_secret(),
|
||||
algorithm="HS256",
|
||||
)
|
||||
token = "test-" + username
|
||||
session._sessions[token] = {
|
||||
"exp": int(time()) + session.max_age,
|
||||
"username": username,
|
||||
}
|
||||
return {"Cookie": f"s={token}"}
|
||||
|
||||
|
||||
@@ -133,7 +141,9 @@ async def client(setup_storage: Path):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_basic_auth_allows_private_file_access(client):
|
||||
_, res = await client.get("/files/hello.txt", headers=_basic_auth("alice", "secret"))
|
||||
_, res = await client.get(
|
||||
"/files/hello.txt", headers=_basic_auth("alice", "secret")
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
assert res.body == b"hello"
|
||||
@@ -162,12 +172,18 @@ async def test_unauthenticated_sends_basic_auth_challenge(client):
|
||||
_, res = await client.request("PROPFIND", "/files/")
|
||||
|
||||
assert res.status_code == 401
|
||||
assert res.headers.get("www-authenticate", "").lower().startswith('basic realm="cista"')
|
||||
assert (
|
||||
res.headers.get("www-authenticate", "")
|
||||
.lower()
|
||||
.startswith('basic realm="cista"')
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_basic_auth_with_token(client):
|
||||
_, res = await client.get("/files/hello.txt", headers=_basic_auth("token", "test_token_123"))
|
||||
_, res = await client.get(
|
||||
"/files/hello.txt", headers=_basic_auth("token", "test_token_123")
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
assert res.body == b"hello"
|
||||
@@ -175,7 +191,9 @@ async def test_basic_auth_with_token(client):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_browser_unauthenticated_sends_cookie_challenge(client):
|
||||
_, res = await client.get("/files/", headers={"Accept": "text/html,application/xhtml+xml"})
|
||||
_, res = await client.get(
|
||||
"/files/", headers={"Accept": "text/html,application/xhtml+xml"}
|
||||
)
|
||||
|
||||
assert res.status_code == 401
|
||||
assert res.headers.get("www-authenticate", "").lower().startswith("cookie")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Path traversal and percent-encoding security tests for the fileserver."""
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -22,7 +23,13 @@ def setup_storage(tmp_path: Path):
|
||||
@pytest_asyncio.fixture()
|
||||
async def client(setup_storage: Path):
|
||||
app = Sanic(f"files-path-sec-test-{uuid4().hex}", strict_slashes=True)
|
||||
app.router.ALLOWED_METHODS = (*app.router.ALLOWED_METHODS, "MKCOL", "MOVE", "COPY", "PROPFIND")
|
||||
app.router.ALLOWED_METHODS = (
|
||||
*app.router.ALLOWED_METHODS,
|
||||
"MKCOL",
|
||||
"MOVE",
|
||||
"COPY",
|
||||
"PROPFIND",
|
||||
)
|
||||
app.blueprint(fileserver_bp)
|
||||
yield app.asgi_client
|
||||
|
||||
|
||||
@@ -22,7 +22,13 @@ def setup_storage(tmp_path: Path):
|
||||
@pytest_asyncio.fixture()
|
||||
async def client(setup_storage: Path):
|
||||
app = Sanic(f"files-rest-test-{uuid4().hex}", strict_slashes=True)
|
||||
app.router.ALLOWED_METHODS = (*app.router.ALLOWED_METHODS, "MKCOL", "MOVE", "COPY", "PROPFIND")
|
||||
app.router.ALLOWED_METHODS = (
|
||||
*app.router.ALLOWED_METHODS,
|
||||
"MKCOL",
|
||||
"MOVE",
|
||||
"COPY",
|
||||
"PROPFIND",
|
||||
)
|
||||
app.blueprint(fileserver_bp)
|
||||
yield app.asgi_client
|
||||
|
||||
@@ -214,7 +220,9 @@ async def test_post_rejects_multiple_keys_to_file_target(client, setup_storage:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_rejects_directory_to_existing_file_target(client, setup_storage: Path):
|
||||
async def test_post_rejects_directory_to_existing_file_target(
|
||||
client, setup_storage: Path
|
||||
):
|
||||
(setup_storage / "folder").mkdir()
|
||||
(setup_storage / "folder" / "nested.txt").write_text("n", encoding="utf-8")
|
||||
(setup_storage / "existing.txt").write_text("e", encoding="utf-8")
|
||||
|
||||
@@ -21,7 +21,13 @@ def setup_storage(tmp_path: Path):
|
||||
@pytest_asyncio.fixture()
|
||||
async def client(setup_storage: Path):
|
||||
app = Sanic(f"files-static-test-{uuid4().hex}", strict_slashes=True)
|
||||
app.router.ALLOWED_METHODS = (*app.router.ALLOWED_METHODS, "MKCOL", "MOVE", "COPY", "PROPFIND")
|
||||
app.router.ALLOWED_METHODS = (
|
||||
*app.router.ALLOWED_METHODS,
|
||||
"MKCOL",
|
||||
"MOVE",
|
||||
"COPY",
|
||||
"PROPFIND",
|
||||
)
|
||||
app.blueprint(fileserver_bp)
|
||||
yield app.asgi_client
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""WebDAV protocol tests: OPTIONS, PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK."""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
@@ -113,9 +114,7 @@ async def test_propfind_file_has_content_length(client, setup_storage: Path):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_propfind_depth_infinity_rejected(client, setup_storage: Path):
|
||||
_, res = await client.request(
|
||||
"PROPFIND", "/files/", headers={"Depth": "infinity"}
|
||||
)
|
||||
_, res = await client.request("PROPFIND", "/files/", headers={"Depth": "infinity"})
|
||||
assert res.status_code == 403
|
||||
|
||||
|
||||
|
||||
@@ -12,19 +12,19 @@ def mock_open(key):
|
||||
|
||||
|
||||
def test_contains():
|
||||
cache = LRUCache(open=mock_open, capacity=2, maxage=10)
|
||||
cache = LRUCache(opener=mock_open, capacity=2, maxage=10)
|
||||
assert "key1" not in cache
|
||||
cache["key1"]
|
||||
assert "key1" in cache
|
||||
|
||||
|
||||
def test_getitem():
|
||||
cache = LRUCache(open=mock_open, capacity=2, maxage=10)
|
||||
cache = LRUCache(opener=mock_open, capacity=2, maxage=10)
|
||||
assert cache["key1"].content == "content-key1"
|
||||
|
||||
|
||||
def test_capacity():
|
||||
cache = LRUCache(open=mock_open, capacity=2, maxage=10)
|
||||
cache = LRUCache(opener=mock_open, capacity=2, maxage=10)
|
||||
item1 = cache["key1"]
|
||||
cache["key2"]
|
||||
cache["key3"]
|
||||
@@ -33,7 +33,7 @@ def test_capacity():
|
||||
|
||||
|
||||
def test_expiry():
|
||||
cache = LRUCache(open=mock_open, capacity=2, maxage=0.1)
|
||||
cache = LRUCache(opener=mock_open, capacity=2, maxage=0.1)
|
||||
item = cache["key1"]
|
||||
sleep(0.2) # Wait for expiration
|
||||
cache.expire_items()
|
||||
@@ -42,7 +42,7 @@ def test_expiry():
|
||||
|
||||
|
||||
def test_close():
|
||||
cache = LRUCache(open=mock_open, capacity=2, maxage=10)
|
||||
cache = LRUCache(opener=mock_open, capacity=2, maxage=10)
|
||||
item = cache["key1"]
|
||||
cache.close()
|
||||
assert "key1" not in cache
|
||||
@@ -50,7 +50,7 @@ def test_close():
|
||||
|
||||
|
||||
def test_lru_mechanism():
|
||||
cache = LRUCache(open=mock_open, capacity=2, maxage=10)
|
||||
cache = LRUCache(opener=mock_open, capacity=2, maxage=10)
|
||||
item1 = cache["key1"]
|
||||
item2 = cache["key2"]
|
||||
cache["key1"] # Make key1 recently used
|
||||
|
||||
Reference in New Issue
Block a user