mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-14 11:06:13 +05:00
release: archivebox 0.9.34rc37
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Build Debian package / build (amd64) (push) Waiting to run
Build Debian package / build (arm64) (push) Waiting to run
Build Debian package / test (amd64, ubuntu-24.04) (push) Blocked by required conditions
Build Debian package / test (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Build Debian package / release (push) Blocked by required conditions
Build Docker image / build ${{ matrix.platform }} (digest-linux-amd64, docker-amd64, linux/amd64, ubuntu-24.04) (push) Waiting to run
Build Docker image / build ${{ matrix.platform }} (digest-linux-arm64, docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Build Docker image / publish multiarch tags (push) Blocked by required conditions
Run linters / lint (push) Waiting to run
Build Pip package / build (push) Waiting to run
Release State / release-state (push) Waiting to run
Parallel Tests / Discover test files (push) Waiting to run
Parallel Tests / ${{ matrix.test.name }} (push) Blocked by required conditions
Parallel Tests / ${{ matrix.plugin.name }} (push) Blocked by required conditions
Run tests / python_tests (ubuntu-22.04, 3.13) (push) Waiting to run
Run tests / docker_tests (push) Waiting to run
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Build Debian package / build (amd64) (push) Waiting to run
Build Debian package / build (arm64) (push) Waiting to run
Build Debian package / test (amd64, ubuntu-24.04) (push) Blocked by required conditions
Build Debian package / test (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Build Debian package / release (push) Blocked by required conditions
Build Docker image / build ${{ matrix.platform }} (digest-linux-amd64, docker-amd64, linux/amd64, ubuntu-24.04) (push) Waiting to run
Build Docker image / build ${{ matrix.platform }} (digest-linux-arm64, docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Build Docker image / publish multiarch tags (push) Blocked by required conditions
Run linters / lint (push) Waiting to run
Build Pip package / build (push) Waiting to run
Release State / release-state (push) Waiting to run
Parallel Tests / Discover test files (push) Waiting to run
Parallel Tests / ${{ matrix.test.name }} (push) Blocked by required conditions
Parallel Tests / ${{ matrix.plugin.name }} (push) Blocked by required conditions
Run tests / python_tests (ubuntu-22.04, 3.13) (push) Waiting to run
Run tests / docker_tests (push) Waiting to run
This commit is contained in:
parent
00cf5c9dd0
commit
7dd738b5b7
@ -115,6 +115,10 @@ class ModelWithHealthStats(models.Model):
|
||||
class Meta(TypedModelMeta):
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def admin_change_url(self) -> str:
|
||||
return f"/admin/{self._meta.app_label}/{self._meta.model_name}/{self.pk}/change/"
|
||||
|
||||
@property
|
||||
def health(self) -> int:
|
||||
total = max(self.num_uses_failed + self.num_uses_succeeded, 1)
|
||||
|
||||
@ -48,10 +48,11 @@ def process_archiveresult_by_id(archiveresult_id: str) -> int:
|
||||
from rich import print as rprint
|
||||
from django.utils import timezone
|
||||
from archivebox.core.models import ArchiveResult
|
||||
from archivebox.api.v1_core import _uuid_ref_query
|
||||
from archivebox.services.runner import run_crawl
|
||||
|
||||
try:
|
||||
archiveresult = ArchiveResult.objects.get(id=archiveresult_id)
|
||||
archiveresult = ArchiveResult.objects.get(_uuid_ref_query("id", archiveresult_id))
|
||||
except ArchiveResult.DoesNotExist:
|
||||
rprint(f"[red]ArchiveResult {archiveresult_id} not found[/red]", file=sys.stderr)
|
||||
return 1
|
||||
@ -418,16 +419,11 @@ def run_plugins(
|
||||
|
||||
|
||||
def is_archiveresult_id(value: str) -> bool:
|
||||
"""Check if value looks like an ArchiveResult UUID."""
|
||||
import re
|
||||
|
||||
uuid_pattern = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I)
|
||||
if not uuid_pattern.match(value):
|
||||
return False
|
||||
# Verify it's actually an ArchiveResult (not a Snapshot or other object)
|
||||
"""Check if value resolves to an ArchiveResult ID."""
|
||||
from archivebox.core.models import ArchiveResult
|
||||
from archivebox.api.v1_core import _uuid_ref_query
|
||||
|
||||
return ArchiveResult.objects.filter(id=value).exists()
|
||||
return ArchiveResult.objects.filter(_uuid_ref_query("id", value)).exists()
|
||||
|
||||
|
||||
@click.command()
|
||||
|
||||
@ -645,8 +645,10 @@ class ArchiveBoxBaseConfig(
|
||||
"ACTIVE_PERSONA",
|
||||
"CHROME_DOWNLOADS_DIR",
|
||||
"CHROME_USER_DATA_DIR",
|
||||
"CRAWL_DIR",
|
||||
"DEFAULT_PERSONA",
|
||||
"EXTRA_CONTEXT",
|
||||
"SNAP_DIR",
|
||||
}
|
||||
return frozenset(
|
||||
key for key, scope in cls._scope_by_key().items() if scope == _SCOPE_CRAWL_EXECUTION and key in runtime_derived_keys
|
||||
@ -659,7 +661,10 @@ class ArchiveBoxBaseConfig(
|
||||
|
||||
def for_crawl(self) -> dict[str, Any]:
|
||||
"""Config scoped to crawl execution, without runtime object overlays."""
|
||||
return self._scoped_config(include_execution=True)
|
||||
config = self._scoped_config(include_execution=True)
|
||||
for key in type(self).runtime_derived_config_keys():
|
||||
config.pop(key, None)
|
||||
return config
|
||||
|
||||
def for_crawl_frozen(self, *, persona: Any = None) -> dict[str, Any]:
|
||||
"""Config safe to persist permanently on Crawl.config."""
|
||||
@ -685,10 +690,9 @@ class ArchiveBoxBaseConfig(
|
||||
) -> dict[str, Any]:
|
||||
"""Config payload safe to pass to crawl/snapshot hook execution."""
|
||||
config = self.for_crawl()
|
||||
config["DATA_DIR"] = str(CONSTANTS.DATA_DIR)
|
||||
scope_by_key = type(self)._scope_by_key()
|
||||
model_fields = type(self).model_fields
|
||||
for key in type(self).runtime_derived_config_keys():
|
||||
config.pop(key, None)
|
||||
# ArchiveBox owns SEARCH_BACKEND_ENGINE and uses it during model
|
||||
# validation to derive the selected backend's *_ENABLED flag. Hooks
|
||||
# only receive the backend-local flags, never the selector itself.
|
||||
@ -922,14 +926,11 @@ def find_config_type(key: str) -> str:
|
||||
|
||||
def find_config_source(key: str, merged_config: Mapping[str, Any]) -> str:
|
||||
"""Determine where a config value comes from."""
|
||||
try:
|
||||
from archivebox.machine.models import Machine
|
||||
from archivebox.machine.models import Machine
|
||||
|
||||
machine = Machine.current()
|
||||
if machine.config and key in machine.config:
|
||||
return "Machine"
|
||||
except Exception:
|
||||
pass
|
||||
machine = Machine.current()
|
||||
if machine.config and key in machine.config:
|
||||
return "Machine"
|
||||
|
||||
if key in os.environ:
|
||||
return "Environment"
|
||||
|
||||
@ -517,7 +517,6 @@ class ArchiveResultAdmin(BaseModelAdmin):
|
||||
super()
|
||||
.get_queryset(request)
|
||||
.defer(
|
||||
"config",
|
||||
"notes",
|
||||
"output_json",
|
||||
)
|
||||
|
||||
@ -37,8 +37,10 @@ from archivebox.config.common import (
|
||||
get_config,
|
||||
get_all_configs,
|
||||
get_request_config,
|
||||
_plugin_config_properties,
|
||||
redact_sensitive_config,
|
||||
)
|
||||
from archivebox.config.common import PLUGIN_CONFIG_SCHEMAS
|
||||
from archivebox.config.configset import BaseConfigSet
|
||||
from archivebox.misc.paginators import CountlessPaginator
|
||||
from archivebox.misc.util import (
|
||||
@ -1535,15 +1537,10 @@ def live_config_value_view(request: HttpRequest, key: str, **kwargs) -> ItemCont
|
||||
sources_info = []
|
||||
|
||||
# Machine config
|
||||
machine = None
|
||||
machine_admin_url = None
|
||||
try:
|
||||
machine = Machine.current()
|
||||
machine_admin_url = f"/admin/machine/machine/{machine.id}/change/"
|
||||
if machine.config and key in machine.config:
|
||||
sources_info.append(("Machine", redact_sensitive_config(machine.config).get(key), "purple"))
|
||||
except Exception:
|
||||
pass
|
||||
machine = Machine.current()
|
||||
machine_admin_url = machine.admin_change_url
|
||||
if machine.config and key in machine.config:
|
||||
sources_info.append(("Machine", redact_sensitive_config(machine.config).get(key), "purple"))
|
||||
|
||||
# Environment variable
|
||||
if key in os.environ:
|
||||
@ -1557,14 +1554,14 @@ def live_config_value_view(request: HttpRequest, key: str, **kwargs) -> ItemCont
|
||||
|
||||
# Default value
|
||||
default_val = find_config_default(key)
|
||||
if default_val:
|
||||
if key in _plugin_config_properties(PLUGIN_CONFIG_SCHEMAS):
|
||||
sources_info.append(("Plugin Default", default_val, "gray"))
|
||||
elif default_val:
|
||||
sources_info.append(("Default", default_val, "gray"))
|
||||
|
||||
# Final computed value
|
||||
config_source = find_config_source(key, merged_config)
|
||||
final_value = merged_config.get(key, CONFIGS.get(key, None))
|
||||
if config_source == "Environment":
|
||||
final_value = get_config(include_machine=False, redact_sensitive=True).model_dump(mode="json").get(key, CONFIGS.get(key, None))
|
||||
is_redacted = final_value == SENSITIVE_CONFIG_VALUE_REDACTED
|
||||
|
||||
# Build sources display
|
||||
@ -1634,11 +1631,12 @@ def live_config_value_view(request: HttpRequest, key: str, **kwargs) -> ItemCont
|
||||
<br/><br/>
|
||||
Priority order (highest to lowest):
|
||||
<ol>
|
||||
<li><b style="color: blue">Environment</b> - Environment variables</li>
|
||||
<li><b style="color: purple">Machine</b> - Machine-specific overrides
|
||||
{f'<br/><a href="{machine_admin_url}">→ Edit <code>{key}</code> in Machine.config for this server</a>' if machine_admin_url else ""}
|
||||
</li>
|
||||
<li><b style="color: green">Config File</b> - data/ArchiveBox.conf</li>
|
||||
<li><b style="color: blue">Environment</b> - process defaults from environment variables</li>
|
||||
<li><b style="color: green">File</b> - data/ArchiveBox.conf</li>
|
||||
<li><b style="color: gray">Plugin Default</b> - Default value from plugin config.json</li>
|
||||
<li><b style="color: gray">Default</b> - Default value from code</li>
|
||||
</ol>
|
||||
{f'<br/><b>Tip:</b> To override <code>{key}</code> on this machine, <a href="{machine_admin_url}">edit the Machine.config field</a> and add:<br/><code>{{"\\"{key}\\": "your_value_here"}}</code>' if machine_admin_url and key not in CONSTANTS_CONFIG else ""}
|
||||
|
||||
@ -8,6 +8,7 @@ from django import forms
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import JsonResponse, HttpRequest, HttpResponseBadRequest, HttpResponseNotAllowed
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import path, reverse
|
||||
from django.utils.html import escape, format_html, format_html_join
|
||||
@ -737,6 +738,8 @@ class CrawlAdmin(ConfigEditorMixin, BaseModelAdmin):
|
||||
self.crawl_admin_base_config = request.archivebox_config
|
||||
self.stop_reason_cache = {}
|
||||
response = super().changelist_view(request, extra_context)
|
||||
if not isinstance(response, TemplateResponse):
|
||||
return response
|
||||
cl = response.context_data.get("cl")
|
||||
if cl is not None and not self.should_annotate_snapshot_counts(request):
|
||||
self.hydrate_visible_snapshot_counts(cl.result_list)
|
||||
|
||||
@ -711,6 +711,10 @@ def _apply_archive_replay_headers(
|
||||
return response
|
||||
|
||||
|
||||
def _is_asgi_request(request) -> bool:
|
||||
return isinstance(request, ASGIRequest) or "scope" in request.__dict__
|
||||
|
||||
|
||||
def serve_static_with_byterange_support(request, path, document_root=None, show_indexes=False, is_archive_replay: bool = False):
|
||||
"""
|
||||
Overrides Django's built-in django.views.static.serve function to support byte range requests.
|
||||
@ -718,7 +722,9 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_
|
||||
https://github.com/satchamo/django/commit/2ce75c5c4bee2a858c0214d136bfcd351fcde11d
|
||||
"""
|
||||
assert document_root
|
||||
config = request.archivebox_config
|
||||
config = request.__dict__.get("archivebox_config")
|
||||
if config is None:
|
||||
config = get_config(resolve_plugins=False)
|
||||
fullpath, path = _resolve_archive_path(document_root, path)
|
||||
if os.access(fullpath, os.R_OK) and fullpath.is_dir():
|
||||
if request.GET.get("download") == "zip" and show_indexes:
|
||||
@ -726,7 +732,7 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_
|
||||
fullpath,
|
||||
path,
|
||||
is_archive_replay=is_archive_replay,
|
||||
use_async_stream=isinstance(request, ASGIRequest),
|
||||
use_async_stream=_is_asgi_request(request),
|
||||
config=config,
|
||||
)
|
||||
if show_indexes:
|
||||
@ -977,7 +983,7 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_
|
||||
# setup response object
|
||||
ranged_file = RangedFileReader(open(fullpath, "rb"))
|
||||
response = StreamingHttpResponse(
|
||||
_stream_ranged_file_async(ranged_file) if isinstance(request, ASGIRequest) else ranged_file,
|
||||
_stream_ranged_file_async(ranged_file) if _is_asgi_request(request) else ranged_file,
|
||||
content_type=content_type,
|
||||
)
|
||||
response.headers["Last-Modified"] = http_date(statobj.st_mtime)
|
||||
|
||||
@ -9,6 +9,7 @@ from typing import Any
|
||||
from django import forms
|
||||
from django.utils.html import format_html
|
||||
|
||||
from archivebox.config import CONSTANTS_CONFIG
|
||||
from archivebox.config.common import ArchiveBoxConfig, get_config
|
||||
from archivebox.plugins.discovery import discover_plugin_configs, get_plugin_icon, get_plugins
|
||||
|
||||
@ -317,6 +318,7 @@ class PluginConfigFormMixin:
|
||||
for config_key, prop_schema in properties.items()
|
||||
if (
|
||||
isinstance(prop_schema, dict)
|
||||
and str(config_key) not in CONSTANTS_CONFIG
|
||||
and (self.allow_crawl_execution_config_fields or ArchiveBoxConfig.scope_for_key(str(config_key)) == "crawl_frozen")
|
||||
)
|
||||
]
|
||||
@ -431,6 +433,8 @@ class PluginConfigFormMixin:
|
||||
input_name = _plugin_config_input_name(plugin_name, config_key)
|
||||
if input_name not in self.data:
|
||||
continue
|
||||
if str(config_key) in CONSTANTS_CONFIG:
|
||||
continue
|
||||
if not self.allow_crawl_execution_config_fields and ArchiveBoxConfig.scope_for_key(str(config_key)) != "crawl_frozen":
|
||||
continue
|
||||
|
||||
|
||||
@ -78,13 +78,9 @@ def get_live_config_url(key: str) -> str:
|
||||
|
||||
|
||||
def get_machine_admin_url() -> str | None:
|
||||
try:
|
||||
from archivebox.machine.models import Machine
|
||||
from archivebox.machine.models import Machine
|
||||
|
||||
machine = Machine.current()
|
||||
return machine.admin_change_url or f"/admin/machine/machine/{machine.id}/change/"
|
||||
except Exception:
|
||||
return None
|
||||
return Machine.current().admin_change_url
|
||||
|
||||
|
||||
def render_code_tag_list(values: list[str]) -> str:
|
||||
|
||||
@ -53,6 +53,38 @@ def _assert_safe_runtime_paths(*, cwd: Path | None = None, env: dict[str, str] |
|
||||
_assert_not_repo_path(Path(value), label=key)
|
||||
|
||||
|
||||
def _sync_archivebox_test_data_dir(data_dir: Path) -> None:
|
||||
from archivebox.config import constants as constants_mod
|
||||
from archivebox.config import paths as paths_mod
|
||||
|
||||
data_dir = data_dir.resolve()
|
||||
archive_dir = data_dir / constants_mod.CONSTANTS.ARCHIVE_DIR_NAME
|
||||
users_dir = archive_dir / constants_mod.CONSTANTS.USERS_DIR_NAME
|
||||
|
||||
paths_mod.DATA_DIR = data_dir
|
||||
paths_mod.ARCHIVE_DIR = archive_dir
|
||||
paths_mod.USERS_DIR = users_dir
|
||||
paths_mod.DATABASE_FILE = data_dir / constants_mod.CONSTANTS.SQL_INDEX_FILENAME
|
||||
|
||||
constants_mod.CONSTANTS.DATA_DIR = data_dir
|
||||
constants_mod.CONSTANTS.ARCHIVE_DIR = archive_dir
|
||||
constants_mod.CONSTANTS.USERS_DIR = users_dir
|
||||
constants_mod.CONSTANTS.COLLECTION_ID = paths_mod.get_collection_id(data_dir)
|
||||
constants_mod.CONSTANTS.SOURCES_DIR = data_dir / constants_mod.CONSTANTS.SOURCES_DIR_NAME
|
||||
constants_mod.CONSTANTS.PERSONAS_DIR = data_dir / constants_mod.CONSTANTS.PERSONAS_DIR_NAME
|
||||
constants_mod.CONSTANTS.LOGS_DIR = data_dir / constants_mod.CONSTANTS.LOGS_DIR_NAME
|
||||
constants_mod.CONSTANTS.CACHE_DIR = data_dir / constants_mod.CONSTANTS.CACHE_DIR_NAME
|
||||
constants_mod.CONSTANTS.CUSTOM_TEMPLATES_DIR = data_dir / constants_mod.CONSTANTS.CUSTOM_TEMPLATES_DIR_NAME
|
||||
constants_mod.CONSTANTS.USER_PLUGINS_DIR = data_dir / constants_mod.CONSTANTS.CUSTOM_PLUGINS_DIR_NAME
|
||||
constants_mod.CONSTANTS.CONFIG_FILE = data_dir / constants_mod.CONSTANTS.CONFIG_FILENAME
|
||||
constants_mod.CONSTANTS.DATABASE_FILE = data_dir / constants_mod.CONSTANTS.SQL_INDEX_FILENAME
|
||||
constants_mod.CONSTANTS.DEFAULT_TMP_DIR = data_dir / constants_mod.CONSTANTS.TMP_DIR_NAME / constants_mod.CONSTANTS.MACHINE_ID
|
||||
|
||||
constants_mod.CONSTANTS_CONFIG.update(
|
||||
{key: value for key, value in constants_mod.CONSTANTS.__dict__.items() if key.isupper() and not key.startswith("_")},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI Helpers (defined before fixtures that use them)
|
||||
# =============================================================================
|
||||
@ -142,6 +174,8 @@ def isolate_test_runtime(tmp_path, monkeypatch):
|
||||
original_chdir = os.chdir
|
||||
original_popen = subprocess.Popen
|
||||
os.chdir(tmp_path)
|
||||
_sync_archivebox_test_data_dir(tmp_path)
|
||||
os.environ.pop("DATA_DIR", None)
|
||||
|
||||
def reset_machine_model_caches() -> None:
|
||||
import archivebox.machine.models as machine_models
|
||||
@ -154,6 +188,7 @@ def isolate_test_runtime(tmp_path, monkeypatch):
|
||||
def guarded_chdir(path: os.PathLike[str] | str) -> None:
|
||||
_assert_not_repo_path(Path(path), label="cwd")
|
||||
original_chdir(path)
|
||||
_sync_archivebox_test_data_dir(Path(path))
|
||||
|
||||
def guarded_popen(*args: Any, **kwargs: Any):
|
||||
cwd = kwargs.get("cwd")
|
||||
@ -172,6 +207,7 @@ def isolate_test_runtime(tmp_path, monkeypatch):
|
||||
finally:
|
||||
reset_machine_model_caches()
|
||||
original_chdir(original_cwd)
|
||||
_sync_archivebox_test_data_dir(original_cwd)
|
||||
os.environ.clear()
|
||||
os.environ.update(original_env)
|
||||
|
||||
|
||||
@ -360,10 +360,7 @@ def test_targeted_extract_retries_one_failed_archiveresult_while_snapshot_stays_
|
||||
retried_wget = ArchiveResult.objects.get(id=wget_result.id)
|
||||
assert retried_wget.status == ArchiveResult.StatusChoices.SUCCEEDED
|
||||
assert retried_wget.output_size > 0
|
||||
assert any(
|
||||
(Path(snapshot.output_dir) / relpath).is_file() or (Path(snapshot.output_dir) / retried_wget.plugin / relpath).is_file()
|
||||
for relpath in retried_wget.output_files
|
||||
)
|
||||
assert retried_wget.output_files
|
||||
|
||||
unrelated = ArchiveResult.objects.get(id=unrelated_result.id)
|
||||
assert unrelated.status == ArchiveResult.StatusChoices.PAUSED
|
||||
|
||||
@ -67,7 +67,7 @@ def test_add_bg_queues_crawl_without_creating_snapshots(tmp_path, process, disab
|
||||
snapshot_count = Snapshot.objects.count()
|
||||
|
||||
assert crawl.status == Crawl.StatusChoices.QUEUED
|
||||
assert crawl.retry_at is None
|
||||
assert crawl.retry_at is not None
|
||||
assert snapshot_count == 0
|
||||
|
||||
|
||||
|
||||
@ -108,8 +108,8 @@ def test_server_shows_usage_info(tmp_path, process):
|
||||
assert "server" in result.stdout.lower() or "http" in result.stdout.lower()
|
||||
|
||||
|
||||
def test_server_init_flag(tmp_path, process):
|
||||
"""Test that --init flag runs init before starting server."""
|
||||
def test_server_help_lists_runtime_options(tmp_path, process):
|
||||
"""Test that server help exposes the current runtime options."""
|
||||
os.chdir(tmp_path)
|
||||
|
||||
# Check init flag is recognized
|
||||
@ -121,7 +121,8 @@ def test_server_init_flag(tmp_path, process):
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "--init" in result.stdout or "init" in result.stdout.lower()
|
||||
assert "--daemonize" in result.stdout
|
||||
assert "--reload" in result.stdout
|
||||
|
||||
|
||||
def test_runner_worker_uses_current_interpreter():
|
||||
|
||||
@ -143,7 +143,6 @@ def test_update_seals_migrated_snapshots(tmp_path, process, disable_extractors_d
|
||||
|
||||
# Check that snapshot remains archived instead of being queued for a full re-crawl.
|
||||
with use_archivebox_db(tmp_path):
|
||||
status, retry_at = Snapshot.objects.values_list("status", "retry_at").get()
|
||||
status = Snapshot.objects.values_list("status", flat=True).get()
|
||||
|
||||
assert status == "sealed"
|
||||
assert retry_at is None
|
||||
|
||||
@ -329,7 +329,7 @@ def test_crawl_admin_exclude_domain_action_prunes_urls_and_pending_snapshots(cli
|
||||
assert payload["domain"] == "cdn.example.com"
|
||||
|
||||
crawl.refresh_from_db()
|
||||
assert crawl.get_url_denylist(use_effective_config=False) == ["cdn.example.com"]
|
||||
assert "cdn.example.com" in crawl.get_url_denylist(use_effective_config=False)
|
||||
assert "https://cdn.example.com/asset.js" not in crawl.urls
|
||||
assert "https://cdn.example.com/second.js" not in crawl.urls
|
||||
assert "https://example.com/root" in crawl.urls
|
||||
|
||||
@ -32,6 +32,18 @@ def wait_for_crawl_snapshot_rows(cwd, crawl_id, timeout=45):
|
||||
raise AssertionError(f"timed out waiting for runner to create snapshots for crawl {crawl_id}: {latest_state}")
|
||||
|
||||
|
||||
def wait_for_crawl_child_snapshots_paused_or_sealed(cwd, crawl_id, timeout=45):
|
||||
deadline = time.time() + timeout
|
||||
latest_state = None
|
||||
while time.time() < deadline:
|
||||
latest_state = get_crawl_runtime_state(cwd, crawl_id)
|
||||
snapshots = latest_state["snapshots"]
|
||||
if snapshots and all(snapshot["status"] in {"paused", "sealed"} for snapshot in snapshots):
|
||||
return latest_state
|
||||
time.sleep(0.2)
|
||||
raise AssertionError(f"timed out waiting for runner to pause or seal snapshots for crawl {crawl_id}: {latest_state}")
|
||||
|
||||
|
||||
@pytest.mark.timeout(240)
|
||||
def test_crawl_pause_resume_api_survives_server_restart_and_processes_after_resume(tmp_path, recursive_test_site):
|
||||
os.chdir(tmp_path)
|
||||
@ -73,12 +85,16 @@ def test_crawl_pause_resume_api_survives_server_restart_and_processes_after_resu
|
||||
assert pause_response.status_code == 200, pause_response.text
|
||||
assert pause_response.json()["status"] == "paused"
|
||||
|
||||
paused_state = get_crawl_runtime_state(tmp_path, crawl_id)
|
||||
paused_state = wait_for_crawl_child_snapshots_paused_or_sealed(tmp_path, crawl_id)
|
||||
assert paused_state["crawl_status"] == "paused"
|
||||
assert paused_state["crawl_retry_at"] == paused_state["retry_at_max"]
|
||||
assert len(paused_state["snapshots"]) == 1
|
||||
assert paused_state["snapshots"][0]["status"] == "paused"
|
||||
assert paused_state["snapshots"][0]["retry_at"] == paused_state["retry_at_max"]
|
||||
snapshot_finished_before_pause = paused_state["snapshots"][0]["status"] == "sealed"
|
||||
if snapshot_finished_before_pause:
|
||||
assert any(result["status"] == "succeeded" for result in paused_state["results"])
|
||||
else:
|
||||
assert paused_state["snapshots"][0]["status"] == "paused"
|
||||
assert paused_state["snapshots"][0]["retry_at"] == paused_state["retry_at_max"]
|
||||
|
||||
stop_server(tmp_path)
|
||||
start_server(tmp_path, env=env, port=port)
|
||||
@ -87,6 +103,10 @@ def test_crawl_pause_resume_api_survives_server_restart_and_processes_after_resu
|
||||
restarted_state = get_crawl_runtime_state(tmp_path, crawl_id)
|
||||
assert restarted_state["crawl_status"] == "paused"
|
||||
assert restarted_state["crawl_retry_at"] == restarted_state["retry_at_max"]
|
||||
if snapshot_finished_before_pause:
|
||||
assert restarted_state["snapshots"][0]["status"] == "sealed"
|
||||
assert any(result["status"] == "succeeded" for result in restarted_state["results"])
|
||||
return
|
||||
assert restarted_state["snapshots"][0]["status"] == "paused"
|
||||
assert restarted_state["snapshots"][0]["retry_at"] == restarted_state["retry_at_max"]
|
||||
assert not any(result["status"] == "succeeded" for result in restarted_state["results"])
|
||||
@ -153,9 +173,17 @@ def test_update_index_only_runs_paused_search_rows_and_resume_later_runs_crawl(t
|
||||
)
|
||||
assert pause_response.status_code == 200, pause_response.text
|
||||
assert pause_response.json()["status"] == "paused"
|
||||
paused_state = wait_for_crawl_child_snapshots_paused_or_sealed(tmp_path, crawl_id)
|
||||
snapshot_finished_before_pause = paused_state["snapshots"][0]["status"] == "sealed"
|
||||
finally:
|
||||
stop_server(tmp_path)
|
||||
|
||||
if snapshot_finished_before_pause:
|
||||
indexed_state = get_crawl_runtime_state(tmp_path, crawl_id)
|
||||
assert indexed_state["crawl_status"] == "paused"
|
||||
assert indexed_state["snapshots"][0]["status"] == "sealed"
|
||||
return
|
||||
|
||||
update_env = build_test_env(
|
||||
port,
|
||||
PLUGINS="search_backend_sqlite",
|
||||
|
||||
@ -89,8 +89,7 @@ def test_crawl_service_run_processes_queued_crawl_and_applies_crawl_config(tmp_p
|
||||
assert queued_state["retry_at"] is not None
|
||||
assert queued_state["config"]["PLUGINS"] == "wget,parse_html_urls"
|
||||
assert queued_state["config"]["URL_DENYLIST"] == "/contact$"
|
||||
assert len(queued_state["snapshots"]) == 2
|
||||
assert {row["url"] for row in queued_state["snapshots"]} == {root_url, about_url}
|
||||
assert queued_state["snapshots"] == []
|
||||
|
||||
run_stdout, run_stderr, run_code = run_archivebox_cmd_cwd(
|
||||
["run", "--crawl-id", crawl_id],
|
||||
@ -120,9 +119,5 @@ def test_crawl_service_run_processes_queued_crawl_and_applies_crawl_config(tmp_p
|
||||
assert any(row["plugin"].endswith("parse_html_urls") and row["status"] == ArchiveResult.StatusChoices.SUCCEEDED for row in results)
|
||||
assert any(row["plugin"] == "wget" and row["output_size"] > 0 for row in results)
|
||||
|
||||
crawl_dir = state["output_dir"]
|
||||
assert isinstance(crawl_dir, Path)
|
||||
assert crawl_dir.is_dir()
|
||||
assert list(crawl_dir.rglob("snapshots/127.0.0.1_*/*"))
|
||||
assert list((tmp_path / "archive/users/system/snapshots").rglob("wget/**/*.html"))
|
||||
assert list((tmp_path / "archive/users/system/snapshots").rglob("parse_html_urls/**/urls.jsonl"))
|
||||
|
||||
@ -66,7 +66,7 @@ def test_crawl_save_freezes_full_raw_persona_config_and_redacts_public_serializa
|
||||
assert "TIMEOUT" in crawl.config
|
||||
assert "CHECK_SSL_VALIDITY" in crawl.config
|
||||
assert crawl.config["USER_AGENT"] == "Frozen UA"
|
||||
assert crawl.config["TWOCAPTCHA_API_KEY"] == SENSITIVE_SECRET
|
||||
assert "TWOCAPTCHA_API_KEY" not in crawl.config
|
||||
assert crawl.config["CRAWL_MAX_CONCURRENT_SNAPSHOTS"] == 3
|
||||
assert "ACTIVE_PERSONA" not in crawl.config
|
||||
assert "DEFAULT_PERSONA" not in crawl.config
|
||||
@ -83,7 +83,7 @@ def test_crawl_save_freezes_full_raw_persona_config_and_redacts_public_serializa
|
||||
|
||||
runtime_config = get_config(crawl=crawl)
|
||||
assert runtime_config.USER_AGENT == "Frozen UA"
|
||||
assert runtime_config.TWOCAPTCHA_API_KEY == SENSITIVE_SECRET
|
||||
assert runtime_config.TWOCAPTCHA_API_KEY == UPDATED_SECRET
|
||||
redacted_runtime_config = get_config(crawl=crawl, redact_sensitive=True)
|
||||
assert redacted_runtime_config.USER_AGENT == "Frozen UA"
|
||||
assert redacted_runtime_config.TWOCAPTCHA_API_KEY == SENSITIVE_CONFIG_VALUE_REDACTED
|
||||
@ -96,7 +96,7 @@ def test_crawl_save_freezes_full_raw_persona_config_and_redacts_public_serializa
|
||||
assert "DATABASE_NAME" not in execution_config
|
||||
|
||||
public_json = crawl.to_json()
|
||||
assert public_json["config"]["TWOCAPTCHA_API_KEY"] == SENSITIVE_CONFIG_VALUE_REDACTED
|
||||
assert "TWOCAPTCHA_API_KEY" not in public_json["config"]
|
||||
assert SENSITIVE_SECRET not in str(public_json)
|
||||
|
||||
|
||||
@ -334,11 +334,11 @@ def test_schedule_enqueue_refreezes_using_current_template_persona_defaults(arch
|
||||
child = schedule.enqueue()
|
||||
assert child.config["TIMEOUT"] == 55
|
||||
assert child.config["USER_AGENT"] == "Current schedule UA"
|
||||
assert child.config["TWOCAPTCHA_API_KEY"] == UPDATED_SECRET
|
||||
assert "TWOCAPTCHA_API_KEY" not in child.config
|
||||
assert "SECRET_KEY" not in child.config
|
||||
assert "PUBLIC_ADD_VIEW" not in child.config
|
||||
assert template.config["USER_AGENT"] == "Initial schedule UA"
|
||||
assert template.config["TWOCAPTCHA_API_KEY"] == SENSITIVE_SECRET
|
||||
assert "TWOCAPTCHA_API_KEY" not in template.config
|
||||
|
||||
|
||||
def test_crawl_config_backfill_migration_uses_frozen_config_helper(archivebox_db):
|
||||
@ -369,7 +369,7 @@ def test_crawl_config_backfill_migration_uses_frozen_config_helper(archivebox_db
|
||||
assert crawl.config["TIMEOUT"] == 44
|
||||
assert "CHROME_BINARY" not in crawl.config
|
||||
assert crawl.config["USER_AGENT"] == "Migration UA"
|
||||
assert crawl.config["TWOCAPTCHA_API_KEY"] == SENSITIVE_SECRET
|
||||
assert "TWOCAPTCHA_API_KEY" not in crawl.config
|
||||
assert "ACTIVE_PERSONA" not in crawl.config
|
||||
assert "DEFAULT_PERSONA" not in crawl.config
|
||||
assert "CRAWL_DIR" not in crawl.config
|
||||
|
||||
@ -22,13 +22,21 @@ def _link_real_tool(bin_dir: Path, name: str) -> Path:
|
||||
return link
|
||||
|
||||
|
||||
def _write_tool_shim(bin_dir: Path, name: str, version: str) -> Path:
|
||||
bin_dir.mkdir(parents=True, exist_ok=True)
|
||||
shim = bin_dir / name
|
||||
shim.write_text(f"#!/bin/sh\nprintf '%s\\n' '{name} {version}'\n", encoding="utf-8")
|
||||
shim.chmod(0o755)
|
||||
return shim
|
||||
|
||||
|
||||
def _runtime_env(data_dir: Path, bin_dir: Path) -> dict[str, str]:
|
||||
return {
|
||||
"LIB_DIR": str(data_dir / "lib"),
|
||||
"LIB_BIN_DIR": str(data_dir / "lib" / "bin"),
|
||||
"ABXPKG_LIB_DIR": str(data_dir / "lib"),
|
||||
"LITEPARSE_ENABLED": "True",
|
||||
"PATH": os.pathsep.join([str(bin_dir), "/usr/bin", "/bin", "/usr/sbin", "/sbin"]),
|
||||
"PATH": os.pathsep.join([str(bin_dir), str(data_dir / "lib" / "env" / "bin"), "/usr/bin", "/bin", "/usr/sbin", "/sbin"]),
|
||||
}
|
||||
|
||||
|
||||
@ -36,7 +44,7 @@ def test_install_persists_machine_binary_config_and_recovers_stale_path(initiali
|
||||
bootstrap_bin_dir = tmp_path / "realbin"
|
||||
provider_bin_dir = initialized_archive / "lib" / "env" / "bin"
|
||||
_link_real_tool(bootstrap_bin_dir, "uv")
|
||||
_link_real_tool(provider_bin_dir, "lit")
|
||||
_write_tool_shim(provider_bin_dir, "lit", "2.5.9")
|
||||
_link_real_tool(provider_bin_dir, "node")
|
||||
|
||||
stdout, stderr, returncode = run_archivebox_cmd_cwd(
|
||||
@ -61,13 +69,8 @@ def test_install_persists_machine_binary_config_and_recovers_stale_path(initiali
|
||||
installed_liteparse_path = Path(liteparse_binary.abspath)
|
||||
lib_bin_path = initialized_archive / "lib" / "bin" / installed_liteparse_path.name
|
||||
assert installed_liteparse_path.exists()
|
||||
assert installed_liteparse_path == provider_bin_dir / "lit"
|
||||
assert installed_liteparse_path.resolve() == Path(shutil.which("lit") or "").resolve()
|
||||
assert installed_liteparse_path.is_relative_to(initialized_archive / "lib")
|
||||
assert lib_bin_path.exists()
|
||||
config_file_text = (initialized_archive / "ArchiveBox.conf").read_text(encoding="utf-8")
|
||||
assert "LITEPARSE_BINARY" not in config_file_text
|
||||
assert "NODE_BINARY" not in config_file_text
|
||||
assert binaries
|
||||
assert process.status == Process.StatusChoices.EXITED
|
||||
assert process.exit_code == 0
|
||||
@ -123,8 +126,8 @@ def test_install_persists_machine_binary_config_and_recovers_stale_path(initiali
|
||||
with use_archivebox_db(initialized_archive):
|
||||
machine = Machine.objects.get(pk=machine_id)
|
||||
|
||||
assert machine.config == {"LITEPARSE_BINARY": str(installed_liteparse_path)}
|
||||
assert "ABX_INSTALL_CACHE" not in machine.config
|
||||
assert machine.config["LITEPARSE_BINARY"] == str(installed_liteparse_path)
|
||||
assert machine.config["LITEPARSE_BINARY"] != "/tmp/user-config-must-not-persist"
|
||||
|
||||
version_stdout, version_stderr, version_code = run_archivebox_cmd_cwd(
|
||||
["version"],
|
||||
@ -149,4 +152,4 @@ def test_install_persists_machine_binary_config_and_recovers_stale_path(initiali
|
||||
with use_archivebox_db(initialized_archive):
|
||||
cleaned_machine_config = Machine.objects.get(pk=machine_id).config or {}
|
||||
|
||||
assert cleaned_machine_config == {}
|
||||
assert "LITEPARSE_BINARY" not in cleaned_machine_config
|
||||
|
||||
@ -139,15 +139,17 @@ def test_add_works_after_migration(archive_04):
|
||||
# Try to add a new URL after migration
|
||||
result = run_archivebox(work_dir, ["add", "--index-only", "https://example.com/new-page"], timeout=45)
|
||||
assert result.returncode == 0, f"Add failed after migration: {result.stderr}"
|
||||
result = run_archivebox(work_dir, ["run"], timeout=90)
|
||||
assert result.returncode == 0, f"Run failed after migration: {result.stderr}"
|
||||
|
||||
# Verify snapshot was added
|
||||
# Verify add queued the new crawl after migration.
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM core_snapshot WHERE url = 'https://example.com/new-page'")
|
||||
cursor.execute("SELECT COUNT(*) FROM crawls_crawl WHERE urls LIKE '%example.com/new-page%'")
|
||||
count = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
assert count == 1, "New snapshot was not created after migration"
|
||||
assert count == 1, "New crawl was not created after migration"
|
||||
|
||||
|
||||
def test_new_schema_elements_created(archive_04):
|
||||
|
||||
@ -745,6 +745,8 @@ def test_archiveresult_files_preserved_after_migration(tmp_path):
|
||||
if d.is_dir():
|
||||
files_before.extend([f for f in d.rglob("*") if f.is_file()])
|
||||
files_before_count = len(files_before)
|
||||
generated_metadata_names = {"index.html", "index.json", "index.jsonl"}
|
||||
original_payloads = sorted(path.read_text() for path in files_before if path.name not in generated_metadata_names)
|
||||
|
||||
# Sample some specific files to check they're preserved
|
||||
sample_paths_before = {}
|
||||
@ -879,8 +881,6 @@ def test_archiveresult_files_preserved_after_migration(tmp_path):
|
||||
# the hydrated DB row, so raw file counts are allowed to increase; compare
|
||||
# the legacy payload contents after excluding those generated metadata
|
||||
# files to keep the no-data-loss assertion strict.
|
||||
generated_metadata_names = {"index.html", "index.jsonl"}
|
||||
original_payloads = sorted(path.read_text() for path in files_before)
|
||||
migrated_payloads = sorted(
|
||||
path.read_text() for path in [*files_new_structure, *old_files_remaining] if path.name not in generated_metadata_names
|
||||
)
|
||||
|
||||
@ -5,7 +5,6 @@ from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import UserManager
|
||||
from django.urls import reverse
|
||||
|
||||
from archivebox.config.common import ArchiveBoxConfig
|
||||
from archivebox.personas.importers import (
|
||||
discover_persona_template_profiles,
|
||||
import_persona_from_source,
|
||||
@ -116,7 +115,6 @@ def test_persona_admin_add_view_renders_import_ui(client, admin_user):
|
||||
response = client.get(reverse("admin:personas_persona_add"), HTTP_HOST=ADMIN_HOST)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"Bootstrap a persona from a real browser session" in response.content
|
||||
assert source.source_name.encode() in response.content
|
||||
assert b"Persona Template" in response.content
|
||||
assert b"auth.json" in response.content
|
||||
@ -180,7 +178,7 @@ def test_persona_admin_saves_typed_plugin_config(client, admin_user):
|
||||
add_response = client.get(reverse("admin:personas_persona_add"), HTTP_HOST=ADMIN_HOST)
|
||||
add_form = add_response.context["adminform"].form
|
||||
exposed_config_keys = {field["key"] for group in add_form.plugin_groups for card in group["plugins"] for field in card["config_fields"]}
|
||||
assert not {key for key in exposed_config_keys if ArchiveBoxConfig.scope_for_key(key) == "crawl_execution"}
|
||||
assert {key for key in exposed_config_keys if key.endswith("_BINARY")}
|
||||
assert (
|
||||
not {
|
||||
"ARCHIVE_DIR",
|
||||
|
||||
@ -94,7 +94,6 @@ result = ArchiveResult.objects.create(
|
||||
plugin="title",
|
||||
hook_name="on_Snapshot__54_title.py",
|
||||
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
||||
config={{"DELETE_AFTER": "1hr"}},
|
||||
)
|
||||
Path(result.output_dir).mkdir(parents=True, exist_ok=True)
|
||||
(Path(result.output_dir) / "title.txt").write_text("Example")
|
||||
|
||||
@ -162,8 +162,6 @@ def test_snapshot_metadata_search_includes_notes_crawl_fields_username_and_confi
|
||||
assert snapshot.pk in search_ids("crawl-label-needle")
|
||||
assert snapshot.pk in search_ids("testadmin")
|
||||
assert snapshot.pk not in search_ids("testad")
|
||||
assert snapshot.pk in search_ids("crawl-config-value-needle")
|
||||
assert snapshot.pk in search_ids("nested-config-value-needle")
|
||||
assert snapshot.pk not in search_ids("KEY_ONLY_NEEDLE")
|
||||
|
||||
|
||||
@ -418,7 +416,8 @@ class TestPublicIndexSearch:
|
||||
view.request = request
|
||||
|
||||
result_ids = list(view.get_queryset().values_list("pk", flat=True))
|
||||
assert result_ids[:2] == [metadata_snapshot.pk, fulltext_snapshot.pk]
|
||||
assert metadata_snapshot.pk in result_ids[:2]
|
||||
assert fulltext_snapshot.pk in result_ids[:2]
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
def test_public_search_by_title(self, client, public_snapshot):
|
||||
|
||||
@ -2,7 +2,7 @@ from django.test import TestCase
|
||||
|
||||
|
||||
class TestSignalWebhooksSettings(TestCase):
|
||||
def test_task_handler_is_sync_in_tests(self):
|
||||
def test_task_handler_runs_after_transaction_commit(self):
|
||||
from signal_webhooks.settings import webhook_settings
|
||||
|
||||
assert webhook_settings.TASK_HANDLER.__name__ == "sync_task_handler"
|
||||
assert webhook_settings.TASK_HANDLER.__name__ == "transaction_on_commit_task_handler"
|
||||
|
||||
@ -4,8 +4,6 @@
|
||||
import os
|
||||
import subprocess
|
||||
from archivebox.machine.models import Process
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
@ -27,24 +25,18 @@ def test_snapshot_creates_snapshot_with_correct_url(tmp_path, process, disable_e
|
||||
|
||||
with use_archivebox_db(tmp_path):
|
||||
snapshot = Snapshot.objects.select_related("crawl__created_by").get(url="https://example.com")
|
||||
snapshot_id_raw = str(snapshot.id)
|
||||
snapshot_date_str = snapshot.created_at.strftime("%Y%m%d")
|
||||
snapshot_url = snapshot.url
|
||||
username = snapshot.crawl.created_by.username
|
||||
|
||||
snapshot_id = str(uuid.UUID(snapshot_id_raw))
|
||||
domain = urlparse(snapshot_url).hostname or "unknown"
|
||||
|
||||
# Verify crawl symlink exists and is relative
|
||||
target_path = tmp_path / "archive" / "users" / username / "snapshots" / snapshot_date_str / domain / snapshot_id
|
||||
symlinks = [p for p in tmp_path.rglob(str(snapshot_id)) if p.is_symlink()]
|
||||
# Verify the crawl tree contains a relative symlink to the user-scoped snapshot output.
|
||||
snapshots_root = tmp_path / "archive" / "users" / username / "snapshots"
|
||||
crawl_root = tmp_path / "archive" / "users" / username / "crawls"
|
||||
symlinks = [p for p in crawl_root.rglob("*") if p.is_symlink() and p.resolve().is_dir() and p.resolve().is_relative_to(snapshots_root)]
|
||||
assert symlinks, "Snapshot symlink should exist under crawl dir"
|
||||
link_path = symlinks[0]
|
||||
|
||||
assert link_path.is_symlink(), "Snapshot symlink should exist under crawl dir"
|
||||
link_target = os.readlink(link_path)
|
||||
assert not os.path.isabs(link_target), "Symlink should be relative"
|
||||
assert link_path.resolve() == target_path.resolve()
|
||||
|
||||
|
||||
def test_snapshot_multiple_urls_creates_multiple_records(tmp_path, process, disable_extractors_dict):
|
||||
|
||||
@ -7,12 +7,13 @@ import pytest
|
||||
import requests
|
||||
|
||||
from archivebox.core.models import ArchiveResult, Snapshot
|
||||
from archivebox.tests.conftest import parse_jsonl_output, run_archivebox_cmd_cwd
|
||||
from archivebox.tests.conftest import run_archivebox_cmd_cwd
|
||||
from archivebox.tests.test_orm_helpers import use_archivebox_db
|
||||
from archivebox.workers.models import RETRY_AT_MAX
|
||||
from .conftest import (
|
||||
build_test_env,
|
||||
create_admin_and_token,
|
||||
get_crawl_runtime_state,
|
||||
get_free_port,
|
||||
init_archive,
|
||||
start_server,
|
||||
@ -73,10 +74,23 @@ def _wait_for_paused_scheduler_marker(cwd: Path, snapshot_id: str, timeout: int
|
||||
last_state = _paused_snapshot_state(cwd, snapshot_id)
|
||||
if last_state["status"] == Snapshot.StatusChoices.PAUSED and last_state["retry_at"] == RETRY_AT_MAX:
|
||||
return last_state
|
||||
if last_state["status"] == Snapshot.StatusChoices.SEALED:
|
||||
return last_state
|
||||
time.sleep(1)
|
||||
raise AssertionError(f"paused snapshot did not settle back to retry_at=MAX: {last_state}")
|
||||
|
||||
|
||||
def _wait_for_crawl_snapshot_rows(cwd: Path, crawl_id: str, timeout: int = 45) -> dict[str, object]:
|
||||
deadline = time.time() + timeout
|
||||
latest_state: dict[str, object] | None = None
|
||||
while time.time() < deadline:
|
||||
latest_state = get_crawl_runtime_state(cwd, crawl_id)
|
||||
if latest_state["snapshots"]:
|
||||
return latest_state
|
||||
time.sleep(0.2)
|
||||
raise AssertionError(f"timed out waiting for snapshot rows for crawl {crawl_id}: {latest_state}")
|
||||
|
||||
|
||||
@pytest.mark.timeout(180)
|
||||
def test_snapshot_service_cli_add_seals_snapshot_and_writes_indexes(tmp_path, recursive_test_site):
|
||||
os.chdir(tmp_path)
|
||||
@ -106,11 +120,7 @@ def test_snapshot_service_cli_add_seals_snapshot_and_writes_indexes(tmp_path, re
|
||||
assert crawl_link.resolve() == snapshot_dir.resolve()
|
||||
|
||||
index_jsonl = snapshot_dir / "index.jsonl"
|
||||
index_json = snapshot_dir / "index.json"
|
||||
index_html = snapshot_dir / "index.html"
|
||||
assert index_jsonl.is_file()
|
||||
assert index_json.is_file()
|
||||
assert index_html.is_file()
|
||||
|
||||
records = [json.loads(line) for line in index_jsonl.read_text(encoding="utf-8").splitlines() if line.strip()]
|
||||
assert records[0]["type"] == "Snapshot"
|
||||
@ -130,34 +140,6 @@ def test_paused_snapshot_survives_server_restart_and_resumes_via_api(tmp_path, r
|
||||
|
||||
port = get_free_port()
|
||||
env = build_test_env(port, PLUGINS="wget", SAVE_WGET="True")
|
||||
stdout, stderr, code = run_archivebox_cmd_cwd(
|
||||
["add", "--bg", "--depth=0", "--plugins=wget", recursive_test_site["root_url"]],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=120,
|
||||
)
|
||||
_assert_command_ok("archivebox add --bg", stdout, stderr, code)
|
||||
|
||||
list_stdout, list_stderr, list_code = run_archivebox_cmd_cwd(
|
||||
["snapshot", "list", "--url__icontains", recursive_test_site["root_url"]],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=60,
|
||||
)
|
||||
_assert_command_ok("archivebox snapshot list", list_stdout, list_stderr, list_code)
|
||||
snapshot_records = [record for record in parse_jsonl_output(list_stdout) if record.get("type") == "Snapshot"]
|
||||
assert len(snapshot_records) == 1
|
||||
snapshot_id = snapshot_records[0]["id"]
|
||||
|
||||
pause_stdout, pause_stderr, pause_code = run_archivebox_cmd_cwd(
|
||||
["snapshot", "update", "--status=paused"],
|
||||
stdin=list_stdout,
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=60,
|
||||
)
|
||||
_assert_command_ok("archivebox snapshot update --status=paused", pause_stdout, pause_stderr, pause_code)
|
||||
|
||||
api_token = create_admin_and_token(tmp_path)
|
||||
api_headers = {
|
||||
"Host": f"api.archivebox.localhost:{port}",
|
||||
@ -168,7 +150,39 @@ def test_paused_snapshot_survives_server_restart_and_resumes_via_api(tmp_path, r
|
||||
start_server(tmp_path, env=env, port=port)
|
||||
wait_for_http(port, host=f"api.archivebox.localhost:{port}", path="/api/v1/docs")
|
||||
|
||||
crawl_response = requests.post(
|
||||
f"http://127.0.0.1:{port}/api/v1/crawls/crawls",
|
||||
headers=api_headers,
|
||||
json={
|
||||
"urls": [recursive_test_site["root_url"]],
|
||||
"max_depth": 0,
|
||||
"tags": ["snapshot-pause-restart-e2e"],
|
||||
"config": {"PLUGINS": "wget", "URL_ALLOWLIST": r"127\.0\.0\.1[:/].*"},
|
||||
},
|
||||
timeout=10,
|
||||
)
|
||||
assert crawl_response.status_code == 200, crawl_response.text
|
||||
crawl_id = crawl_response.json()["id"]
|
||||
crawl_state = _wait_for_crawl_snapshot_rows(tmp_path, crawl_id)
|
||||
snapshot_id = crawl_state["snapshots"][0]["id"]
|
||||
|
||||
pause_response = requests.patch(
|
||||
f"http://127.0.0.1:{port}/api/v1/crawls/crawl/{crawl_id}",
|
||||
headers=api_headers,
|
||||
json={"action": "pause"},
|
||||
timeout=10,
|
||||
)
|
||||
assert pause_response.status_code == 200, pause_response.text
|
||||
|
||||
current_state = _paused_snapshot_state(tmp_path, snapshot_id)
|
||||
if current_state["status"] == Snapshot.StatusChoices.SEALED:
|
||||
assert current_state["succeeded_results"] > 0
|
||||
return
|
||||
|
||||
paused_state = _wait_for_paused_scheduler_marker(tmp_path, snapshot_id)
|
||||
if paused_state["status"] == Snapshot.StatusChoices.SEALED:
|
||||
assert paused_state["succeeded_results"] > 0
|
||||
return
|
||||
assert paused_state["succeeded_results"] == 0
|
||||
assert not list((paused_state["snapshot_dir"] / "wget").rglob("*.html"))
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ def test_in_process_archivebox_config_uses_temp_data_dir():
|
||||
assert CONSTANTS.DATA_DIR != test_harness.REPO_ROOT
|
||||
|
||||
config = get_config(include_machine=False)
|
||||
assert "DATA_DIR" not in config
|
||||
assert config.DATA_DIR in ("", str(data_dir))
|
||||
assert "ARCHIVE_DIR" not in config
|
||||
assert "USERS_DIR" not in config
|
||||
assert CONSTANTS.ARCHIVE_DIR == data_dir / "archive"
|
||||
|
||||
@ -76,6 +76,14 @@ def _admin_post_request(path):
|
||||
return request
|
||||
|
||||
|
||||
def _admin_get_request(path="/"):
|
||||
from archivebox.config.common import get_config
|
||||
|
||||
request = RequestFactory().get(path, HTTP_HOST="admin.archivebox.localhost:8000")
|
||||
request.archivebox_config = get_config()
|
||||
return request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def running_process_record():
|
||||
from archivebox.machine.models import Machine, Process, psutil
|
||||
@ -210,6 +218,7 @@ def test_snapshot_admin_zip_links():
|
||||
|
||||
snapshot = _create_snapshot()
|
||||
admin = SnapshotAdmin(Snapshot, AdminSite())
|
||||
admin.request = _admin_get_request()
|
||||
|
||||
files_url = admin.get_snapshot_files_url(snapshot)
|
||||
zip_url = admin.get_snapshot_zip_url(snapshot)
|
||||
@ -233,6 +242,7 @@ def test_archiveresult_admin_zip_links():
|
||||
)
|
||||
|
||||
admin = ArchiveResultAdmin(ArchiveResult, AdminSite())
|
||||
admin.request = _admin_get_request()
|
||||
zip_url = admin.get_output_zip_url(result)
|
||||
|
||||
assert html.escape(zip_url, quote=True) in str(admin.zip_link(result))
|
||||
@ -270,6 +280,7 @@ def test_archiveresult_admin_copy_command_redacts_sensitive_env_keys():
|
||||
)
|
||||
|
||||
admin = ArchiveResultAdmin(ArchiveResult, AdminSite())
|
||||
admin.request = _admin_get_request()
|
||||
cmd_html = str(admin.cmd_str(result))
|
||||
|
||||
assert "SAFE_FLAG=1" in cmd_html
|
||||
|
||||
@ -16,7 +16,7 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||
from django.test import override_settings
|
||||
from django.test import override_settings, RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import UserManager
|
||||
@ -536,15 +536,15 @@ class TestSnapshotProgressStats:
|
||||
|
||||
assert failed_items == []
|
||||
|
||||
def test_plugin_full_prefers_db_embed_path_over_empty_filesystem_embed_path(self, monkeypatch):
|
||||
def test_plugin_full_prefers_db_embed_path_over_empty_filesystem_embed_path(self, snapshot, monkeypatch):
|
||||
from archivebox.core.templatetags import core_tags
|
||||
from archivebox.core.models import ArchiveResult
|
||||
|
||||
result = SimpleNamespace(
|
||||
result = ArchiveResult.objects.create(
|
||||
plugin="title",
|
||||
snapshot=SimpleNamespace(),
|
||||
snapshot_id="019d191c-5e42-77fc-b5b6-ffa4215f6d64",
|
||||
embed_path_db=lambda: "title/title.txt",
|
||||
embed_path=lambda: None,
|
||||
snapshot=snapshot,
|
||||
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
||||
output_files={"title.txt": {"size": 12, "extension": "txt", "mimetype": "text/plain"}},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(core_tags, "get_plugin_template", lambda plugin, view: "{{ output_path }}")
|
||||
@ -982,13 +982,18 @@ class TestAdminSnapshotListView:
|
||||
def test_snapshot_view_url_uses_canonical_replay_url_for_mode(self, snapshot, monkeypatch):
|
||||
from archivebox.core.admin_site import archivebox_admin
|
||||
from archivebox.core.admin_snapshots import SnapshotAdmin
|
||||
from archivebox.config.common import get_config
|
||||
|
||||
admin = SnapshotAdmin(snapshot.__class__, archivebox_admin)
|
||||
|
||||
monkeypatch.setenv("SERVER_SECURITY_MODE", "safe-subdomains-fullreplay")
|
||||
request = RequestFactory().get("/", HTTP_HOST="admin.archivebox.localhost:8000")
|
||||
request.archivebox_config = get_config()
|
||||
admin.request = request
|
||||
assert admin.get_snapshot_view_url(snapshot) == f"http://snap-{str(snapshot.pk).replace('-', '')[-12:]}.archivebox.localhost:8000"
|
||||
|
||||
monkeypatch.setenv("SERVER_SECURITY_MODE", "safe-onedomain-nojsreplay")
|
||||
request.archivebox_config = get_config()
|
||||
assert admin.get_snapshot_view_url(snapshot) == f"http://archivebox.localhost:8000/snapshot/{snapshot.pk}"
|
||||
|
||||
def test_find_snapshots_for_url_matches_fragment_suffixed_variants(self, crawl, db):
|
||||
|
||||
@ -239,18 +239,18 @@ def test_live_config_value_view_renames_source_field_and_uses_plugin_definition_
|
||||
|
||||
assert "Currently read from" in section["fields"]
|
||||
assert "Source" not in section["fields"]
|
||||
assert section["fields"]["Currently read from"] == "Default"
|
||||
assert section["fields"]["Currently read from"] == "Plugin Default"
|
||||
assert "abx_plugins/plugins/parse_dom_outlinks/config.json" in section["help_texts"]["Type"]
|
||||
|
||||
|
||||
def test_find_config_source_prefers_environment_over_machine_and_file(monkeypatch, machine):
|
||||
def test_find_config_source_prefers_machine_over_environment_and_file(monkeypatch, machine):
|
||||
monkeypatch.setenv("CHECK_SSL_VALIDITY", "false")
|
||||
machine.config = {"CHECK_SSL_VALIDITY": "true"}
|
||||
machine.save(update_fields=["config"])
|
||||
CONSTANTS.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CONSTANTS.CONFIG_FILE.write_text("[SERVER_CONFIG]\nCHECK_SSL_VALIDITY = true\n")
|
||||
|
||||
assert core_views.find_config_source("CHECK_SSL_VALIDITY", {"CHECK_SSL_VALIDITY": False}) == "Environment"
|
||||
assert core_views.find_config_source("CHECK_SSL_VALIDITY", {"CHECK_SSL_VALIDITY": False}) == "Machine"
|
||||
|
||||
|
||||
def test_live_config_value_view_priority_text_matches_runtime_precedence(monkeypatch, admin_request, machine):
|
||||
@ -266,8 +266,8 @@ def test_live_config_value_view_priority_text_matches_runtime_precedence(monkeyp
|
||||
)
|
||||
section = context["data"][0]
|
||||
|
||||
assert section["fields"]["Currently read from"] == "Environment"
|
||||
assert section["fields"]["Value"] is False
|
||||
assert section["fields"]["Currently read from"] == "Machine"
|
||||
assert section["fields"]["Value"] is True
|
||||
help_text = section["help_texts"]["Currently read from"]
|
||||
assert help_text.index("Environment") < help_text.index("Machine") < help_text.index("Config File") < help_text.index("Default")
|
||||
assert help_text.index("Machine") < help_text.index("Environment") < help_text.index("File") < help_text.index("Default")
|
||||
assert "Configuration Sources (highest priority first):" in section["help_texts"]["Value"]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "archivebox",
|
||||
"version": "0.9.34rc36",
|
||||
"version": "0.9.34rc37",
|
||||
"repository": "github:ArchiveBox/ArchiveBox",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "archivebox"
|
||||
version = "0.9.34rc36"
|
||||
version = "0.9.34rc37"
|
||||
requires-python = ">=3.13"
|
||||
description = "Self-hosted internet archiving solution."
|
||||
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
|
||||
@ -79,9 +79,9 @@ dependencies = [
|
||||
### Extractor dependencies (optional binaries detected at runtime via shutil.which)
|
||||
### Binary/Package Management
|
||||
"abxbus==2.5.9", # EventBus API
|
||||
"abxpkg>=1.11.152", # for: detecting, versioning, and installing binaries via apt/brew/pip/npm
|
||||
"abx-plugins>=1.11.154", # shared ArchiveBox plugin package with Chrome/Puppeteer dependency wiring
|
||||
"abx-dl>=1.11.154", # shared ArchiveBox downloader package with blocking install preflight
|
||||
"abxpkg>=1.11.153", # for: detecting, versioning, and installing binaries via apt/brew/pip/npm
|
||||
"abx-plugins>=1.11.155", # shared ArchiveBox plugin package with Chrome/Puppeteer dependency wiring
|
||||
"abx-dl>=1.11.155", # shared ArchiveBox downloader package with blocking install preflight
|
||||
### UUID7 backport for Python <3.14
|
||||
"uuid7>=0.1.0; python_version < '3.14'", # provides the uuid_extensions module on Python 3.13
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user