From 082bb50eda33f6e3a4d1e843b66075717fb4f1d4 Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Sun, 24 May 2026 23:51:46 -0700 Subject: [PATCH] release: archivebox 0.9.31rc43 --- archivebox/core/views.py | 35 ++++++-- archivebox/misc/serve_static.py | 103 +++++++++++++++++++----- archivebox/templates/core/snapshot.html | 81 +++++++++++++++---- etc/package.json | 2 +- pyproject.toml | 8 +- 5 files changed, 180 insertions(+), 49 deletions(-) diff --git a/archivebox/core/views.py b/archivebox/core/views.py index 68a799a0..5e20f62b 100644 --- a/archivebox/core/views.py +++ b/archivebox/core/views.py @@ -764,9 +764,14 @@ def _serve_responses_path(request, responses_root: Path, rel_path: str, show_ind def _serve_snapshot_replay(request: HttpRequest, snapshot: Snapshot, path: str = ""): + request_config = getattr(request, "archivebox_config", None) + if request_config is None: + request_config = get_config(resolve_plugins=False) + request.archivebox_config = request_config + snapshot._runtime_config = request_config rel_path = path or "" is_directory_request = bool(path) and path.endswith("/") - show_indexes = bool(request.GET.get("files")) or (get_config().USES_SUBDOMAIN_ROUTING and is_directory_request) + show_indexes = bool(request.GET.get("files")) or (request_config.USES_SUBDOMAIN_ROUTING and is_directory_request) if not show_indexes and (not rel_path or rel_path == "index.html"): return SnapshotView.render_live_index(request, snapshot) @@ -847,16 +852,20 @@ class SnapshotHostView(View): """Serve snapshot directory contents on ./.""" def get(self, request, snapshot_id: str, path: str = ""): - if not request.user.is_authenticated and not get_config().PUBLIC_SNAPSHOTS: + request_config = getattr(request, "archivebox_config", None) + if request_config is None: + request_config = get_config(resolve_plugins=False) + request.archivebox_config = request_config + if not request.user.is_authenticated and not request_config.PUBLIC_SNAPSHOTS: return _admin_login_redirect_or_forbidden(request) snapshot = _find_snapshot_by_ref(snapshot_id) if not snapshot: raise Http404 - canonical_host = get_snapshot_host(str(snapshot.id)) + canonical_host = get_snapshot_host(str(snapshot.id), config=request_config) if not host_matches(request.get_host(), canonical_host): - target = build_snapshot_url(str(snapshot.id), path, request=request) + target = build_snapshot_url(str(snapshot.id), path, request=request, config=request_config) if request.META.get("QUERY_STRING"): target = f"{target}?{request.META['QUERY_STRING']}" return redirect(target) @@ -868,7 +877,11 @@ class SnapshotReplayView(View): """Serve snapshot directory contents on a one-domain replay path.""" def get(self, request, snapshot_id: str, path: str = ""): - if not request.user.is_authenticated and not get_config().PUBLIC_SNAPSHOTS: + request_config = getattr(request, "archivebox_config", None) + if request_config is None: + request_config = get_config(resolve_plugins=False) + request.archivebox_config = request_config + if not request.user.is_authenticated and not request_config.PUBLIC_SNAPSHOTS: return _admin_login_redirect_or_forbidden(request) snapshot = _find_snapshot_by_ref(snapshot_id) @@ -882,7 +895,11 @@ class OriginalDomainHostView(View): """Serve responses from the most recent snapshot when using ./.""" def get(self, request, domain: str, path: str = ""): - if not request.user.is_authenticated and not get_config().PUBLIC_SNAPSHOTS: + request_config = getattr(request, "archivebox_config", None) + if request_config is None: + request_config = get_config(resolve_plugins=False) + request.archivebox_config = request_config + if not request.user.is_authenticated and not request_config.PUBLIC_SNAPSHOTS: return _admin_login_redirect_or_forbidden(request) return _serve_original_domain_replay(request, domain, path) @@ -891,7 +908,11 @@ class OriginalDomainReplayView(View): """Serve original-domain replay content on a one-domain replay path.""" def get(self, request, domain: str, path: str = ""): - if not request.user.is_authenticated and not get_config().PUBLIC_SNAPSHOTS: + request_config = getattr(request, "archivebox_config", None) + if request_config is None: + request_config = get_config(resolve_plugins=False) + request.archivebox_config = request_config + if not request.user.is_authenticated and not request_config.PUBLIC_SNAPSHOTS: return _admin_login_redirect_or_forbidden(request) return _serve_original_domain_replay(request, domain, path) diff --git a/archivebox/misc/serve_static.py b/archivebox/misc/serve_static.py index a41346be..76cad123 100644 --- a/archivebox/misc/serve_static.py +++ b/archivebox/misc/serve_static.py @@ -61,8 +61,38 @@ def _hash_for_path(document_root: Path, rel_path: str) -> str | None: return file_map.get(rel_path) +def _resolve_archive_path(document_root: str | Path, rel_path: str) -> tuple[Path, str]: + rel_path = posixpath.normpath(rel_path).lstrip("/") if rel_path else "" + fullpath = Path(safe_join(document_root, rel_path)) + if os.access(fullpath, os.R_OK): + return fullpath, rel_path + + root = Path(document_root) + current = root + resolved_parts: list[str] = [] + for part in Path(rel_path).parts: + exact = current / part + if os.access(exact, os.R_OK): + current = exact + resolved_parts.append(part) + continue + + folded_part = part.casefold() + try: + match = next((child for child in current.iterdir() if child.name.casefold() == folded_part), None) + except OSError: + match = None + if match is None: + return fullpath, rel_path + + current = match + resolved_parts.append(match.name) + + return current, posixpath.join(*resolved_parts) if resolved_parts else "" + + def _cache_policy(config=None, **config_kwargs) -> str: - config = config or get_config(**config_kwargs) + config = config or get_config(resolve_plugins=False, **config_kwargs) return "public" if config.PUBLIC_SNAPSHOTS else "private" @@ -139,6 +169,7 @@ def _build_directory_zip_response( *, is_archive_replay: bool, use_async_stream: bool, + config=None, ) -> StreamingHttpResponse: root_name = _safe_zip_stem(fullpath.name or Path(path).name or "archivebox") sentinel = object() @@ -210,7 +241,7 @@ def _build_directory_zip_response( content_type="application/zip", ) response.headers["Content-Disposition"] = f'attachment; filename="{root_name}.zip"' - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=60, stale-while-revalidate=300" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" response.headers["Last-Modified"] = http_date(fullpath.stat().st_mtime) response.headers["X-Accel-Buffering"] = "no" return _apply_archive_replay_headers( @@ -218,6 +249,7 @@ def _build_directory_zip_response( fullpath=fullpath, content_type="application/zip", is_archive_replay=is_archive_replay, + config=config, ) @@ -640,7 +672,7 @@ def _apply_archive_replay_headers( return response response.headers.setdefault("X-Content-Type-Options", "nosniff") - config = config or get_config(**config_kwargs) + config = config or get_config(resolve_plugins=False, **config_kwargs) response.headers.setdefault("X-ArchiveBox-Security-Mode", config.SERVER_SECURITY_MODE) if config.SHOULD_NEUTER_RISKY_REPLAY and _is_risky_replay_document(fullpath, content_type): @@ -671,8 +703,11 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ https://github.com/satchamo/django/commit/2ce75c5c4bee2a858c0214d136bfcd351fcde11d """ assert document_root - path = posixpath.normpath(path).lstrip("/") - fullpath = Path(safe_join(document_root, path)) + config = getattr(request, "archivebox_config", None) + if config is None: + config = get_config(resolve_plugins=False) + request.archivebox_config = config + 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: return _build_directory_zip_response( @@ -680,10 +715,19 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ path, is_archive_replay=is_archive_replay, use_async_stream=hasattr(request, "scope"), + config=config, ) if show_indexes: response = _render_directory_index(request, path, fullpath) - return _apply_archive_replay_headers(response, fullpath=fullpath, content_type="text/html", is_archive_replay=is_archive_replay) + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" + response.headers["Last-Modified"] = http_date(fullpath.stat().st_mtime) + return _apply_archive_replay_headers( + response, + fullpath=fullpath, + content_type="text/html", + is_archive_replay=is_archive_replay, + config=config, + ) raise Http404(_("Directory indexes are not allowed here.")) if not os.access(fullpath, os.R_OK): raise Http404(_("ā€œ%(path)sā€ does not exist") % {"path": fullpath}) @@ -704,9 +748,15 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ if etag in inm_list or etag.strip('"') in [i.strip('"') for i in inm_list]: not_modified = HttpResponseNotModified() not_modified.headers["ETag"] = etag - not_modified.headers["Cache-Control"] = f"{_cache_policy()}, max-age=31536000, immutable" + not_modified.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=31536000, immutable" not_modified.headers["Last-Modified"] = http_date(statobj.st_mtime) - return _apply_archive_replay_headers(not_modified, fullpath=fullpath, content_type="", is_archive_replay=is_archive_replay) + return _apply_archive_replay_headers( + not_modified, + fullpath=fullpath, + content_type="", + is_archive_replay=is_archive_replay, + config=config, + ) content_type, encoding = mimetypes.guess_type(str(fullpath)) content_type = content_type or "application/octet-stream" @@ -739,6 +789,7 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ fullpath=fullpath, content_type=content_type, is_archive_replay=is_archive_replay, + config=config, ) # Wrap text-like outputs in HTML when explicitly requested for iframe previewing. @@ -752,9 +803,9 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ response.headers["Last-Modified"] = http_date(statobj.st_mtime) if etag: response.headers["ETag"] = etag - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=31536000, immutable" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=31536000, immutable" else: - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=60, stale-while-revalidate=300" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" response.headers["Content-Disposition"] = f'inline; filename="{fullpath.name}"' if encoding: response.headers["Content-Encoding"] = encoding @@ -763,6 +814,7 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ fullpath=fullpath, content_type="text/html; charset=utf-8", is_archive_replay=is_archive_replay, + config=config, ) except Exception: pass @@ -779,9 +831,9 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ response.headers["Last-Modified"] = http_date(statobj.st_mtime) if etag: response.headers["ETag"] = etag - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=31536000, immutable" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=31536000, immutable" else: - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=60, stale-while-revalidate=300" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" response.headers["Content-Disposition"] = f'inline; filename="{fullpath.name}"' if encoding: response.headers["Content-Encoding"] = encoding @@ -790,6 +842,7 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ fullpath=fullpath, content_type="text/html; charset=utf-8", is_archive_replay=is_archive_replay, + config=config, ) except Exception: pass @@ -806,9 +859,9 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ response.headers["Last-Modified"] = http_date(statobj.st_mtime) if etag: response.headers["ETag"] = etag - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=31536000, immutable" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=31536000, immutable" else: - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=60, stale-while-revalidate=300" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" response.headers["Content-Disposition"] = f'inline; filename="{fullpath.stem}.html"' response.headers["X-Content-Type-Options"] = "nosniff" response.headers["Content-Security-Policy"] = ( @@ -846,9 +899,9 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ response.headers["Last-Modified"] = http_date(statobj.st_mtime) if etag: response.headers["ETag"] = etag - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=31536000, immutable" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=31536000, immutable" else: - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=60, stale-while-revalidate=300" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" response.headers["Content-Disposition"] = f'inline; filename="{fullpath.name}"' if encoding: response.headers["Content-Encoding"] = encoding @@ -857,15 +910,16 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ fullpath=fullpath, content_type="text/html; charset=utf-8", is_archive_replay=is_archive_replay, + config=config, ) if escaped_count and escaped_count > tag_count * 2: response = HttpResponse(decoded, content_type=content_type) response.headers["Last-Modified"] = http_date(statobj.st_mtime) if etag: response.headers["ETag"] = etag - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=31536000, immutable" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=31536000, immutable" else: - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=60, stale-while-revalidate=300" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" response.headers["Content-Disposition"] = f'inline; filename="{fullpath.name}"' if encoding: response.headers["Content-Encoding"] = encoding @@ -874,6 +928,7 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ fullpath=fullpath, content_type=content_type, is_archive_replay=is_archive_replay, + config=config, ) except Exception: pass @@ -884,9 +939,9 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ response.headers["Last-Modified"] = http_date(statobj.st_mtime) if etag: response.headers["ETag"] = etag - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=31536000, immutable" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=31536000, immutable" else: - response.headers["Cache-Control"] = f"{_cache_policy()}, max-age=60, stale-while-revalidate=300" + response.headers["Cache-Control"] = f"{_cache_policy(config=config)}, max-age=60, stale-while-revalidate=300" if is_text_like: response.headers["Content-Disposition"] = f'inline; filename="{fullpath.name}"' if content_type.startswith("image/"): @@ -918,7 +973,13 @@ def serve_static_with_byterange_support(request, path, document_root=None, show_ response.status_code = 206 if encoding: response.headers["Content-Encoding"] = encoding - return _apply_archive_replay_headers(response, fullpath=fullpath, content_type=content_type, is_archive_replay=is_archive_replay) + return _apply_archive_replay_headers( + response, + fullpath=fullpath, + content_type=content_type, + is_archive_replay=is_archive_replay, + config=config, + ) def serve_static(request, path, **kwargs): diff --git a/archivebox/templates/core/snapshot.html b/archivebox/templates/core/snapshot.html index 93158170..bdb6bad9 100644 --- a/archivebox/templates/core/snapshot.html +++ b/archivebox/templates/core/snapshot.html @@ -15,7 +15,11 @@ margin: 0; padding: 0; } - body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } + body { + display: flex; + flex-direction: column; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + } a { color: inherit; text-decoration: none; } a:hover { text-decoration: underline; } .container-fluid { width: 100%; margin: 0 auto; padding: 0 12px; } @@ -33,6 +37,8 @@ .alert { padding: 6px 10px; border-radius: 6px; background: #f5f5f5; color: #333; } header { background-color: #aa1e55; + order: 1; + flex: 0 0 auto; } small { font-weight: 200; @@ -436,6 +442,8 @@ height: calc(100vh - 210px); border-top: 3px solid #aa1e55; overflow: hidden; + order: 2; + flex: 0 0 auto; } #main-frame-wrapper iframe { width: 100%; @@ -963,6 +971,48 @@ +
+ +
+
@@ -1174,19 +1224,6 @@
-
- -
- - -