Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec47877277 | ||
|
|
9a33749eca | ||
|
|
c104931341 | ||
|
|
6a245d70d9 | ||
|
|
4ca3b180a8 | ||
|
|
a520e25cbb | ||
|
|
271a4c1c40 | ||
|
|
c935a2b801 | ||
|
|
d1c692f044 | ||
|
|
f6f7be2ddc | ||
|
|
f38bb4c2e4 | ||
|
|
a577ea147f | ||
|
|
89007da097 | ||
|
|
ea243e1bcb | ||
|
|
f85e8d5dc0 | ||
|
|
400de11916 | ||
|
|
fce31ac965 | ||
|
|
014e193e6b | ||
|
|
505f53c733 | ||
|
|
fe9b59eb63 | ||
|
|
b097af5417 | ||
|
|
27ac1cc66f | ||
|
|
b65151dbbe | ||
|
|
5c0a065242 | ||
|
|
a908323133 | ||
|
|
f5d4645f41 | ||
|
|
422fc5dfef | ||
|
|
d8e6a56738 | ||
|
|
50a462e786 | ||
|
|
d7f67ba5bc | ||
|
|
563ca151f5 | ||
|
|
9f6535006a | ||
|
|
0497d4ff0c | ||
|
|
42ed15b733 | ||
|
|
15f6f7260e | ||
|
|
cd159cf7cf |
No files matched your search
@@ -1,79 +1,50 @@
|
||||
name: Deploy Publicsite to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- publicsite/**
|
||||
- archivebox/**
|
||||
- bin/collect_ui_screenshots.*
|
||||
- bin/generate_ui_screenshot_gallery.py
|
||||
- bin/take_screenshot.js
|
||||
- docs/Screenshots.md
|
||||
- pyproject.toml
|
||||
- uv.lock
|
||||
- .github/workflows/deploy-publicsite.yml
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
workflow_run:
|
||||
workflows: ['Capture publicsite screenshots']
|
||||
types: [completed]
|
||||
branches: [dev]
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
actions: read
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'workflow_dispatch' && format('github-pages-publicsite-manual-{0}', github.run_id) || 'github-pages-publicsite' }}
|
||||
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
|
||||
# Failed captures must not displace a pending push deployment.
|
||||
group: fast-pages-${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion != 'success' && github.run_id || github.ref }}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.ref || 'dev' }}
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- name: Restore latest complete screenshots
|
||||
run: uv run --no-project python bin/restore_site_screenshots.py archivebox publicsite/screenshots
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
- name: Render current gallery layout without recapturing images
|
||||
run: uv run --no-project python bin/generate_ui_screenshot_gallery.py refresh publicsite/screenshots/index.html
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: publicsite
|
||||
deploy:
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Python and uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: false
|
||||
|
||||
- name: Install ArchiveBox and screenshot dependencies
|
||||
run: uv sync --no-cache --frozen
|
||||
|
||||
- name: Build UI screenshot gallery
|
||||
run: bin/collect_ui_screenshots.sh
|
||||
env:
|
||||
BASE_URL: http://archivebox.localhost:8000
|
||||
|
||||
- name: Verify gallery was generated from this exact revision
|
||||
run: |
|
||||
uv run --no-cache --no-sync python - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
provenance = json.loads(Path("publicsite/screenshots/build.json").read_text())
|
||||
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
||||
assert provenance["revision"] == os.environ["GITHUB_SHA"], provenance
|
||||
assert provenance["version"] == version, provenance
|
||||
assert provenance["capture_count"] >= 3 * 35, provenance
|
||||
assert len(provenance["files"]) == provenance["capture_count"], provenance
|
||||
PY
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
with:
|
||||
path: ./publicsite
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
- uses: actions/configure-pages@v5
|
||||
- uses: actions/deploy-pages@v4
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Capture publicsite screenshots
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- publicsite/**
|
||||
- archivebox/**
|
||||
- bin/collect_ui_screenshots.*
|
||||
- bin/generate_ui_screenshot_gallery.py
|
||||
- bin/take_screenshot.js
|
||||
- docs/Screenshots.md
|
||||
- pyproject.toml
|
||||
- uv.lock
|
||||
- .github/workflows/screenshots.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'workflow_dispatch' && format('publicsite-captures-manual-{0}', github.run_id) || 'publicsite-captures' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Python and uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: false
|
||||
|
||||
- name: Install ArchiveBox and screenshot dependencies
|
||||
run: uv sync --no-cache --frozen
|
||||
|
||||
- name: Build UI screenshot gallery
|
||||
run: bin/collect_ui_screenshots.sh
|
||||
env:
|
||||
BASE_URL: http://archivebox.localhost:8000
|
||||
|
||||
- name: Verify gallery was generated from this exact revision
|
||||
run: |
|
||||
uv run --no-cache --no-sync python - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
provenance = json.loads(Path("publicsite/screenshots/build.json").read_text())
|
||||
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
||||
assert provenance["revision"] == os.environ["GITHUB_SHA"], provenance
|
||||
assert provenance["version"] == version, provenance
|
||||
assert provenance["capture_count"] >= 3 * 35, provenance
|
||||
assert len(provenance["files"]) == provenance["capture_count"], provenance
|
||||
PY
|
||||
|
||||
- name: Save validated screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: site-screenshots
|
||||
path: publicsite/screenshots
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
+2
-2
@@ -10,10 +10,10 @@
|
||||
# --build-context abx-plugins=../abx-plugins \
|
||||
# -t archivebox/abx-dl:dev
|
||||
# docker buildx build . -f Dockerfile \
|
||||
# --build-arg ABX_DL_IMAGE=archivebox/abx-dl:1.12.282 \
|
||||
# --build-arg ABX_DL_IMAGE=archivebox/abx-dl:1.12.289 \
|
||||
# -t archivebox:multistage
|
||||
|
||||
ARG ABX_DL_IMAGE=archivebox/abx-dl:1.12.282
|
||||
ARG ABX_DL_IMAGE=archivebox/abx-dl:1.12.289
|
||||
|
||||
FROM archivebox/sonic:1.4.9 AS sonic
|
||||
FROM ${ABX_DL_IMAGE} AS archivebox-runtime-base
|
||||
|
||||
@@ -151,6 +151,19 @@ curl -fsSL 'https://get.archivebox.io' | bash
|
||||
|
||||
#### ✳️ Easy Setup
|
||||
|
||||
<details>
|
||||
<summary><b><img src="https://archivebox.github.io/ios-archivebox/docs/icons/apple.svg" alt="Apple" height="28px" align="top"/> <code>ArchiveBox Server.app</code></b> (macOS)</summary>
|
||||
<br/>
|
||||
<ol>
|
||||
<li><a href="https://github.com/ArchiveBox/ios-archivebox/releases">Download <code>ArchiveBox Server.app</code></a>, move it to Applications, and open it.
|
||||
<pre lang="bash"><code style="white-space: pre-line">open -a 'ArchiveBox Server'
|
||||
</code></pre></li>
|
||||
<li>Set up an admin account and generate an API Key.</li>
|
||||
<li>Use the Web UI or <a href="https://archivebox.github.io/ios-archivebox/"><code>ArchiveBox.app</code></a>, and optionally connect REST API clients, the <a href="https://archivebox.github.io/ios-archivebox/">mobile app</a>, the <a href="https://archivebox.github.io/archivebox-browser-extension/">browser extension</a>, and other integrations to your server using your API Key.</li>
|
||||
</ol>
|
||||
<br/>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b><img src="https://user-images.githubusercontent.com/511499/117447182-29758200-af0b-11eb-97bd-58723fee62ab.png" alt="Docker" height="28px" align="top"/> <code>docker-compose</code></b> (macOS/Linux) <b>👈 recommended</b> <i>(click to expand)</i></summary>
|
||||
<br/>
|
||||
@@ -561,6 +574,20 @@ docker compose run --rm archivebox config --set PERMISSIONS=public
|
||||
</code></pre>
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><b><img src="https://archivebox.github.io/ios-archivebox/docs/icons/apple.svg" alt="Apple" height="22px" align="top"/> <img src="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.17.0/icons/windows11/windows11-original.svg" alt="Windows" height="22px" align="top"/> 🐧 Desktop / Mobile / Extension Usage</b></summary>
|
||||
<br/>
|
||||
|
||||
- 📱 [<code>ArchiveBox</code> iOS Mobile App](https://archivebox.github.io/ios-archivebox/)
|
||||
- 🖥️ [<code>ArchiveBox.app</code> macOS Desktop App](https://archivebox.github.io/ios-archivebox/)
|
||||
- 🧩 [ArchiveBox Browser Extension](https://archivebox.github.io/archivebox-browser-extension/)
|
||||
- 💻 [Linux/Windows Desktop App (Requires Docker Desktop)](https://github.com/ArchiveBox/electron-archivebox)
|
||||
|
||||
<br/>
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
@@ -614,7 +641,10 @@ docker run -it -v $PWD:/data archivebox/archivebox:dev add --depth=1 'https://ex
|
||||
|
||||
|
||||
- <img src="https://github.com/ArchiveBox/ArchiveBox/assets/511499/ff20d251-5347-4b85-ae9b-83037d0ac01e" height="28px"/> <b>From the official <a href="https://github.com/ArchiveBox/archivebox-extension">ArchiveBox Browser Extension</a></b>
|
||||
<i>Provides realtime archiving of browsing history or selected pages from Chrome/Chromium/Firefox browsers.</i>
|
||||
<i>Provides realtime archiving of browsing history or selected pages from Chrome/Chromium/Firefox/Safari browsers.</i>
|
||||
|
||||
- <img src="https://archivebox.github.io/ios-archivebox/App/AppIcon.icon/Assets/ArchiveBox.png" alt="ArchiveBox app icon" height="28px"/> <b>From the official <a href="https://archivebox.github.io/ios-archivebox/">ArchiveBox macOS Desktop App or iOS Mobile App</a></b><br/>
|
||||
<i>Save directly from the Share menu or Siri/Shortcuts to your server.</i>
|
||||
|
||||
- <img src="https://github.com/ArchiveBox/ArchiveBox/assets/511499/64078483-21d7-4eb1-aa6e-9ad55afe45b8" height="22px"/> From manual imports of URLs from RSS, JSON, CSV, TXT, SQL, HTML, Markdown, etc. files
|
||||
<i>ArchiveBox supports injesting URLs in [any text-based format](https://github.com/ArchiveBox/ArchiveBox/wiki/Usage#Import-a-list-of-URLs-from-a-text-file).</i>
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
# Bootable hashing helpers (file content + mtime/size cache, mime detection).
|
||||
# MUST NOT import archivebox.config, archivebox.core, or Django — stdlib only.
|
||||
|
||||
import hashlib
|
||||
import mimetypes
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def _cached_file_hash(filepath: str, size: int, mtime: float) -> str:
|
||||
"""Internal function to calculate file hash with cache key based on path, size and mtime."""
|
||||
sha256_hash = hashlib.sha256()
|
||||
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
sha256_hash.update(chunk)
|
||||
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
|
||||
@lru_cache(maxsize=10)
|
||||
def hash_file(file_path: Path, pwd: Path | None = None) -> str:
|
||||
"""Calculate SHA256 hash of a file with caching based on path, size and mtime."""
|
||||
pwd = Path(pwd) if pwd else None
|
||||
file_path = Path(file_path)
|
||||
if not file_path.is_absolute():
|
||||
file_path = pwd / file_path if pwd else file_path.absolute()
|
||||
|
||||
abs_path = file_path.resolve()
|
||||
stat_info = abs_path.stat()
|
||||
|
||||
return _cached_file_hash(
|
||||
str(abs_path),
|
||||
stat_info.st_size,
|
||||
stat_info.st_mtime,
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=10)
|
||||
def get_dir_hashes(dir_path: Path, pwd: Path | None = None, filter_func: Callable | None = None, max_depth: int = -1) -> dict[str, str]:
|
||||
"""Calculate SHA256 hashes for all files and directories recursively."""
|
||||
pwd = Path(pwd) if pwd else None
|
||||
dir_path = Path(dir_path)
|
||||
if not dir_path.is_absolute():
|
||||
dir_path = pwd / dir_path if pwd else dir_path.absolute()
|
||||
|
||||
if not dir_path.is_dir():
|
||||
raise ValueError(f"Not a directory: {dir_path}")
|
||||
if max_depth < -1:
|
||||
raise ValueError(f"max_depth must be >= -1, got {max_depth}")
|
||||
|
||||
# Get all files recursively
|
||||
all_files = get_dir_entries(
|
||||
dir_path,
|
||||
pwd=pwd,
|
||||
recursive=True,
|
||||
include_files=True,
|
||||
include_dirs=False,
|
||||
filter_func=filter_func,
|
||||
)
|
||||
|
||||
hashes: dict[str, str] = {}
|
||||
hashable_summary = []
|
||||
|
||||
# Calculate hashes for all files
|
||||
for subfile in all_files:
|
||||
subfile_path = dir_path / subfile
|
||||
sha256_hash = hash_file(subfile_path)
|
||||
hashes[subfile] = sha256_hash
|
||||
hashable_summary.append(f"{sha256_hash} ./{subfile}")
|
||||
|
||||
# Calculate hashes for all directories
|
||||
subdirs = get_dir_entries(
|
||||
dir_path,
|
||||
pwd=pwd,
|
||||
recursive=True,
|
||||
include_files=False,
|
||||
include_dirs=True,
|
||||
include_hidden=False,
|
||||
filter_func=filter_func,
|
||||
max_depth=max_depth,
|
||||
)
|
||||
|
||||
for subdir in subdirs:
|
||||
subdir_path = dir_path / subdir
|
||||
subdir_hashes = get_dir_hashes(
|
||||
subdir_path,
|
||||
filter_func=filter_func,
|
||||
max_depth=0,
|
||||
)
|
||||
hashes[subdir] = subdir_hashes["."]
|
||||
|
||||
# Filter results by max_depth
|
||||
if max_depth >= 0:
|
||||
hashes = {path: value for path, value in hashes.items() if len(Path(path).parts) <= max_depth + 1}
|
||||
|
||||
# Calculate root directory hash
|
||||
hashable_summary.sort()
|
||||
root_sha256 = hashlib.sha256("\n".join(hashable_summary).encode()).hexdigest()
|
||||
hashes["."] = root_sha256
|
||||
|
||||
return hashes
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def get_dir_entries(
|
||||
dir_path: Path,
|
||||
pwd: Path | None = None,
|
||||
recursive: bool = True,
|
||||
include_files: bool = True,
|
||||
include_dirs: bool = True,
|
||||
include_hidden: bool = False,
|
||||
filter_func: Callable | None = None,
|
||||
max_depth: int = -1,
|
||||
) -> tuple[str, ...]:
|
||||
"""Get filtered list of directory entries."""
|
||||
pwd = Path(pwd) if pwd else None
|
||||
dir_path = Path(dir_path)
|
||||
if not dir_path.is_absolute():
|
||||
dir_path = pwd / dir_path if pwd else dir_path.absolute()
|
||||
|
||||
results = []
|
||||
|
||||
def process_path(path: Path, depth: int):
|
||||
if not include_hidden and path.name.startswith("."):
|
||||
return False
|
||||
if max_depth >= 0 and depth > max_depth:
|
||||
return False
|
||||
if filter_func:
|
||||
info = {
|
||||
"abspath": str(path.absolute()),
|
||||
"relpath": str(path.relative_to(dir_path)),
|
||||
}
|
||||
if not filter_func(info):
|
||||
return False
|
||||
return True
|
||||
|
||||
for path in dir_path.rglob("*") if recursive else dir_path.glob("*"):
|
||||
current_depth = len(path.relative_to(dir_path).parts)
|
||||
|
||||
if path.is_file() and include_files and process_path(path, current_depth):
|
||||
results.append(str(path.relative_to(dir_path)))
|
||||
elif path.is_dir() and include_dirs and process_path(path, current_depth):
|
||||
results.append(str(path.relative_to(dir_path)))
|
||||
|
||||
if not recursive:
|
||||
break
|
||||
|
||||
return tuple(sorted(results)) # Make immutable for caching
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def get_dir_sizes(dir_path: Path, pwd: Path | None = None, **kwargs) -> dict[str, int]:
|
||||
"""Calculate sizes for all files and directories recursively."""
|
||||
sizes: dict[str, int] = {}
|
||||
hashes = get_dir_hashes(dir_path, pwd=pwd, **kwargs)
|
||||
dir_path = Path(dir_path)
|
||||
|
||||
for path_key in hashes:
|
||||
full_path = dir_path / path_key
|
||||
if full_path.is_file():
|
||||
sizes[path_key] = full_path.stat().st_size
|
||||
else:
|
||||
total = 0
|
||||
for file_path in full_path.rglob("*"):
|
||||
if file_path.is_file() and not file_path.name.startswith("."):
|
||||
total += file_path.stat().st_size
|
||||
sizes[path_key + "/"] = total
|
||||
|
||||
return sizes
|
||||
|
||||
|
||||
@lru_cache(maxsize=10)
|
||||
def get_dir_info(dir_path: Path, pwd: Path | None = None, filter_func: Callable | None = None, max_depth: int = -1) -> dict:
|
||||
"""Get detailed information about directory contents including hashes and sizes."""
|
||||
pwd = Path(pwd) if pwd else None
|
||||
dir_path = Path(dir_path)
|
||||
if not dir_path.is_absolute():
|
||||
dir_path = pwd / dir_path if pwd else dir_path.absolute()
|
||||
|
||||
hashes = get_dir_hashes(dir_path, pwd=pwd, filter_func=filter_func, max_depth=max_depth)
|
||||
sizes = get_dir_sizes(str(dir_path), pwd=pwd, filter_func=filter_func, max_depth=max_depth)
|
||||
|
||||
num_total_subpaths = sum(1 for name in hashes if name != ".")
|
||||
details = {}
|
||||
|
||||
for filename, sha256_hash in sorted(hashes.items()):
|
||||
abs_path = (dir_path / filename).resolve()
|
||||
stat_info = abs_path.stat()
|
||||
num_subpaths = sum(1 for p in hashes if p.startswith(filename + "/"))
|
||||
is_dir = abs_path.is_dir()
|
||||
if is_dir:
|
||||
mime_type = "inode/directory"
|
||||
basename = abs_path.name
|
||||
extension = ""
|
||||
num_bytes = sizes[filename + "/"]
|
||||
if filename == ".":
|
||||
num_subpaths = num_total_subpaths
|
||||
else:
|
||||
filename += "/"
|
||||
num_subpaths = num_subpaths
|
||||
else: # is_file
|
||||
num_subpaths = None
|
||||
mime_type = mimetypes.guess_type(str(abs_path))[0]
|
||||
extension = abs_path.suffix
|
||||
basename = abs_path.name.rsplit(extension, 1)[0]
|
||||
num_bytes = sizes[filename]
|
||||
|
||||
details[filename] = {
|
||||
"basename": basename,
|
||||
"mime_type": mime_type,
|
||||
"extension": extension,
|
||||
"num_subpaths": num_subpaths,
|
||||
"num_bytes": num_bytes,
|
||||
"hash_sha256": sha256_hash,
|
||||
"created_at": datetime.fromtimestamp(stat_info.st_ctime).isoformat(),
|
||||
"modified_at": datetime.fromtimestamp(stat_info.st_mtime).isoformat(),
|
||||
}
|
||||
|
||||
if filter_func and not filter_func(details[filename]):
|
||||
del details[filename]
|
||||
|
||||
return details
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
dir_info = get_dir_info(Path("."), max_depth=6)
|
||||
with open(".hashes.json", "w") as f:
|
||||
json.dump(dir_info, f, indent=4)
|
||||
print("Wrote .hashes.json")
|
||||
|
||||
# Example output:
|
||||
# {
|
||||
# ".": {
|
||||
# "basename": "misc",
|
||||
# "mime_type": "inode/directory",
|
||||
# "extension": "",
|
||||
# "num_subpaths": 25,
|
||||
# "num_bytes": 214677,
|
||||
# "hash_sha256": "addfacf88b2ff6b564846415fb7b21dcb7e63ee4e911bc0aec255ee354958530",
|
||||
# "created_at": "2024-12-04T00:08:38.537449",
|
||||
# "modified_at": "2024-12-04T00:08:38.537449"
|
||||
# },
|
||||
# "__init__.py": {
|
||||
# "basename": "__init__",
|
||||
# "mime_type": "text/x-python",
|
||||
# "extension": ".py",
|
||||
# "num_subpaths": null,
|
||||
# "num_bytes": 32,
|
||||
# "hash_sha256": "b0e5e7ff17db3b60535cf664282787767c336e3e203a43e21b6326c6fe457551",
|
||||
# "created_at": "2024-10-08T00:51:41.001359",
|
||||
# "modified_at": "2024-10-08T00:51:41.001359"
|
||||
# },
|
||||
# ...
|
||||
# }
|
||||
@@ -32,7 +32,6 @@ import sys
|
||||
import json
|
||||
from typing import Any, TextIO
|
||||
from collections.abc import Iterable, Iterator
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Type constants for JSONL records
|
||||
@@ -120,19 +119,6 @@ def read_stdin(stream: TextIO | None = None) -> Iterator[dict[str, Any]]:
|
||||
yield record
|
||||
|
||||
|
||||
def read_file(path: Path) -> Iterator[dict[str, Any]]:
|
||||
"""
|
||||
Read JSONL or plain URLs from a file.
|
||||
|
||||
Yields parsed records as dicts.
|
||||
"""
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
record = parse_line(line)
|
||||
if record:
|
||||
yield record
|
||||
|
||||
|
||||
def read_args_or_stdin(args: Iterable[str], stream: TextIO | None = None) -> Iterator[dict[str, Any]]:
|
||||
"""
|
||||
Read from CLI arguments if provided, otherwise from stdin.
|
||||
|
||||
@@ -2,94 +2,11 @@ from typing import Any, cast
|
||||
from collections.abc import Callable
|
||||
|
||||
import json
|
||||
import ast
|
||||
import inspect
|
||||
import toml
|
||||
import re
|
||||
import configparser
|
||||
|
||||
from pathlib import Path, PosixPath
|
||||
|
||||
from pydantic.json_schema import GenerateJsonSchema
|
||||
from pydantic_core import to_jsonable_python
|
||||
|
||||
JSONValue = str | bool | int | None | list["JSONValue"]
|
||||
|
||||
TOML_HEADER = "# Converted from INI to TOML format: https://toml.io/en/\n\n"
|
||||
|
||||
|
||||
def load_ini_value(val: str) -> JSONValue:
|
||||
"""Convert lax INI values into strict TOML-compliant (JSON) values"""
|
||||
if val.lower() in ("true", "yes", "1"):
|
||||
return True
|
||||
if val.lower() in ("false", "no", "0"):
|
||||
return False
|
||||
if val.isdigit():
|
||||
return int(val)
|
||||
|
||||
try:
|
||||
return ast.literal_eval(val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return json.loads(val)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return val
|
||||
|
||||
|
||||
def convert(ini_str: str) -> str:
|
||||
"""Convert a string of INI config into its TOML equivalent (warning: strips comments)"""
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
setattr(config, "optionxform", str) # capitalize key names
|
||||
config.read_string(ini_str)
|
||||
|
||||
# Initialize an empty dictionary to store the TOML representation
|
||||
toml_dict = {}
|
||||
|
||||
# Iterate over each section in the INI configuration
|
||||
for section in config.sections():
|
||||
toml_dict[section] = {}
|
||||
|
||||
# Iterate over each key-value pair in the section
|
||||
for key, value in config.items(section):
|
||||
parsed_value = load_ini_value(value)
|
||||
|
||||
# Convert the parsed value to its TOML-compatible JSON representation
|
||||
toml_dict[section.upper()][key.upper()] = json.dumps(parsed_value)
|
||||
|
||||
# Build the TOML string
|
||||
toml_str = TOML_HEADER
|
||||
for section, items in toml_dict.items():
|
||||
toml_str += f"[{section}]\n"
|
||||
for key, value in items.items():
|
||||
toml_str += f"{key} = {value}\n"
|
||||
toml_str += "\n"
|
||||
|
||||
return toml_str.strip()
|
||||
|
||||
|
||||
class JSONSchemaWithLambdas(GenerateJsonSchema):
|
||||
"""
|
||||
Encode lambda functions in default values properly.
|
||||
Usage:
|
||||
>>> json.dumps(value, encoder=JSONSchemaWithLambdas())
|
||||
"""
|
||||
|
||||
def encode_default(self, dft: Any) -> Any:
|
||||
config = self._config
|
||||
if isinstance(dft, Callable):
|
||||
return "{{lambda " + inspect.getsource(dft).split("=lambda ")[-1].strip()[:-1] + "}}"
|
||||
return to_jsonable_python(
|
||||
dft,
|
||||
timedelta_mode=config.ser_json_timedelta,
|
||||
bytes_mode=config.ser_json_bytes,
|
||||
serialize_unknown=True,
|
||||
)
|
||||
|
||||
|
||||
def better_toml_dump_str(val: Any) -> str:
|
||||
try:
|
||||
|
||||
@@ -55,10 +55,6 @@ def get_plugins() -> list[str]:
|
||||
return sorted(get_plugin_catalog())
|
||||
|
||||
|
||||
def get_plugin_models():
|
||||
return get_plugin_catalog().plugins
|
||||
|
||||
|
||||
def get_plugin_name(plugin: str) -> str:
|
||||
"""
|
||||
Get the base plugin name without numeric prefix.
|
||||
|
||||
@@ -1339,20 +1339,36 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.action-rearchive-select,
|
||||
.action-permissions-select {
|
||||
/* The native select receives the actual click, including in WKWebView. */
|
||||
body.change-list #changelist .actions .action-rearchive-select,
|
||||
body.change-list #changelist .actions .action-permissions-select {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body.change-list #changelist .action-rearchive-wrapper > .button,
|
||||
body.change-list #changelist .action-permissions-wrapper > .button {
|
||||
padding: 8px 16px !important;
|
||||
width: max-content;
|
||||
min-width: max-content;
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-rearchive-wrapper:focus-within,
|
||||
.action-permissions-wrapper:focus-within {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Object count */
|
||||
.actions .action-counter {
|
||||
color: #64748b;
|
||||
@@ -1960,22 +1976,10 @@
|
||||
.addClass('action-rearchive-select')
|
||||
.attr('aria-label', 'Re-Archive')
|
||||
.appendTo(wrapper)
|
||||
$('<button>')
|
||||
.attr('type', 'button')
|
||||
$('<span>')
|
||||
.attr('aria-hidden', 'true')
|
||||
.addClass('button')
|
||||
.html('Re-Archive <span aria-hidden="true">▾</span>')
|
||||
.click(function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const el = select[0]
|
||||
if (typeof el.showPicker === 'function') {
|
||||
try { el.showPicker() } catch (err) { el.focus(); el.click() }
|
||||
} else {
|
||||
el.focus()
|
||||
el.click()
|
||||
}
|
||||
return false
|
||||
})
|
||||
.appendTo(wrapper)
|
||||
$('<option></option>').attr('value', '').attr('hidden', true).appendTo(select)
|
||||
rearchiveActions.forEach(function (action_value) {
|
||||
@@ -2021,23 +2025,11 @@
|
||||
.addClass('action-permissions-select')
|
||||
.attr('aria-label', 'Permissions')
|
||||
.appendTo(wrapper)
|
||||
$('<button>')
|
||||
.attr('type', 'button')
|
||||
$('<span>')
|
||||
.attr('aria-hidden', 'true')
|
||||
.attr('name', action_type)
|
||||
.addClass('button')
|
||||
.text(this.text)
|
||||
.click(function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const el = select[0]
|
||||
if (typeof el.showPicker === 'function') {
|
||||
try { el.showPicker() } catch (err) { el.focus(); el.click() }
|
||||
} else {
|
||||
el.focus()
|
||||
el.click()
|
||||
}
|
||||
return false
|
||||
})
|
||||
.appendTo(wrapper)
|
||||
$('<option></option>').attr('value', '').attr('hidden', true).appendTo(select)
|
||||
;[
|
||||
|
||||
@@ -153,7 +153,7 @@ class TestArchiveBoxWithLDAP:
|
||||
|
||||
def test_archivebox_init_without_ldap(self, tmp_path):
|
||||
"""Test that archivebox init works without LDAP enabled."""
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["init"],
|
||||
cwd=tmp_path,
|
||||
timeout=45,
|
||||
@@ -161,14 +161,13 @@ class TestArchiveBoxWithLDAP:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
# Should succeed
|
||||
assert code == 0, f"archivebox init failed: {stderr}"
|
||||
assert result.returncode == 0, f"archivebox init failed: {result.stderr}"
|
||||
|
||||
def test_archivebox_version_with_ldap_config(self, tmp_path):
|
||||
"""Test that archivebox version works with LDAP config set."""
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["version"],
|
||||
cwd=tmp_path,
|
||||
timeout=10,
|
||||
@@ -179,10 +178,9 @@ class TestArchiveBoxWithLDAP:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
# Should succeed
|
||||
assert code == 0, f"archivebox version failed: {stderr}"
|
||||
assert result.returncode == 0, f"archivebox version failed: {result.stderr}"
|
||||
|
||||
|
||||
class TestLDAPConfigValidationInArchiveBox:
|
||||
@@ -190,7 +188,7 @@ class TestLDAPConfigValidationInArchiveBox:
|
||||
|
||||
def test_archivebox_init_with_incomplete_ldap_config(self, tmp_path):
|
||||
"""Test that archivebox init fails with helpful error when LDAP config is incomplete."""
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["init"],
|
||||
cwd=tmp_path,
|
||||
timeout=45,
|
||||
@@ -201,10 +199,9 @@ class TestLDAPConfigValidationInArchiveBox:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
# Should fail with validation error
|
||||
assert code != 0, "Should fail with incomplete LDAP config"
|
||||
assert result.returncode != 0, "Should fail with incomplete LDAP config"
|
||||
|
||||
# Check error message
|
||||
assert "LDAP_* config options must all be set" in stderr, f"Expected validation error message in: {stderr}"
|
||||
assert "LDAP_* config options must all be set" in result.stderr, f"Expected validation error message in: {result.stderr}"
|
||||
@@ -1204,7 +1204,7 @@ def test_cli_recursive_crawl_processes_discovered_html_urls(initialized_archive,
|
||||
root_url = recursive_test_site["root_url"]
|
||||
child_url = recursive_test_site["child_urls"][0]
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
[
|
||||
"add",
|
||||
"--depth=2",
|
||||
@@ -1219,8 +1219,7 @@ def test_cli_recursive_crawl_processes_discovered_html_urls(initialized_archive,
|
||||
env=env,
|
||||
timeout=180,
|
||||
)
|
||||
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert returncode == 0, stderr or stdout
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
|
||||
with use_archivebox_db(initialized_archive):
|
||||
crawl = Crawl.objects.order_by("-created_at").values_list("max_depth", "tags_str", "config").first()
|
||||
|
||||
@@ -142,18 +142,17 @@ class TestArchiveResultCreate:
|
||||
assert crawl_result.returncode == 0, crawl_result.stderr
|
||||
crawl_record = parse_jsonl_output(crawl_result.stdout)[0]
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["archiveresult", "create"],
|
||||
stdin=crawl_result.stdout,
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
assert "Passed through" in stderr
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
assert "Passed through" in result.stderr
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 1
|
||||
assert records[0]["id"] == crawl_record["id"]
|
||||
|
||||
@@ -163,16 +162,15 @@ class TestArchiveResultList:
|
||||
|
||||
def test_list_empty(self, initialized_archive):
|
||||
"""List with no archive results returns empty."""
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["archiveresult", "list"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
assert "Listed 0 archive results" in stderr
|
||||
assert result.returncode == 0
|
||||
assert "Listed 0 archive results" in result.stderr
|
||||
|
||||
def test_list_filter_by_status(self, initialized_archive):
|
||||
"""Filter archive results by status."""
|
||||
|
||||
@@ -33,19 +33,18 @@ class TestCrawlCreate:
|
||||
"""Create crawl from URL arguments."""
|
||||
url = create_test_url()
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "create", url],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
assert "Created crawl" in stderr
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
assert "Created crawl" in result.stderr
|
||||
|
||||
# Check JSONL output
|
||||
records = parse_jsonl_output(stdout)
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 1
|
||||
assert records[0]["type"] == "Crawl"
|
||||
assert url in records[0]["urls"]
|
||||
@@ -55,18 +54,17 @@ class TestCrawlCreate:
|
||||
urls = [create_test_url() for _ in range(3)]
|
||||
stdin = "\n".join(urls)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "create"],
|
||||
stdin=stdin,
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
|
||||
records = parse_jsonl_output(stdout)
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 1
|
||||
crawl = records[0]
|
||||
assert crawl["type"] == "Crawl"
|
||||
@@ -78,32 +76,30 @@ class TestCrawlCreate:
|
||||
"""Create crawl with --depth flag."""
|
||||
url = create_test_url()
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "create", "--depth=2", url],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert records[0]["max_depth"] == 2
|
||||
|
||||
def test_create_with_tag(self, initialized_archive):
|
||||
"""Create crawl with --tag flag."""
|
||||
url = create_test_url()
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "create", "--tag=test-tag", url],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert "test-tag" in records[0].get("tags_str", "")
|
||||
|
||||
def test_create_passes_through_tag_emitted_by_cli(self, initialized_archive):
|
||||
@@ -119,17 +115,16 @@ class TestCrawlCreate:
|
||||
url = create_test_url()
|
||||
stdin = tag_result.stdout + json.dumps({"url": url}) + "\n"
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "create"],
|
||||
stdin=stdin,
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
|
||||
assert any(record.get("type") == "Tag" and record["id"] == tag_record["id"] for record in records)
|
||||
assert any(record.get("type") == "Crawl" and url in record["urls"] for record in records)
|
||||
@@ -163,32 +158,30 @@ class TestCrawlList:
|
||||
|
||||
def test_list_empty(self, initialized_archive):
|
||||
"""List with no crawls returns empty."""
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "list"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
assert "Listed 0 crawls" in stderr
|
||||
assert result.returncode == 0
|
||||
assert "Listed 0 crawls" in result.stderr
|
||||
|
||||
def test_list_returns_created(self, initialized_archive):
|
||||
"""List returns previously created crawls."""
|
||||
url = create_test_url()
|
||||
run_archivebox_cmd(["crawl", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "list"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) >= 1
|
||||
assert any(url in r.get("urls", "") for r in records)
|
||||
|
||||
@@ -197,16 +190,15 @@ class TestCrawlList:
|
||||
url = create_test_url()
|
||||
run_archivebox_cmd(["crawl", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "list", "--status=queued"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
for r in records:
|
||||
assert r["status"] == "queued"
|
||||
|
||||
@@ -221,16 +213,15 @@ class TestCrawlList:
|
||||
disable_extractors=True,
|
||||
)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["crawl", "list", "--limit=2"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 2
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ Tests for archivebox list command.
|
||||
Verify list emits snapshot JSONL and applies the documented filters.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
@@ -427,16 +429,15 @@ def test_snapshot_list_search_meta(initialized_archive):
|
||||
url = create_test_url(domain="meta-search-example.com")
|
||||
run_archivebox_cmd(["snapshot", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "list", "--search=meta", "meta-search-example.com"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 1
|
||||
assert "meta-search-example.com" in records[0]["url"]
|
||||
|
||||
@@ -446,16 +447,15 @@ def test_list_search_meta_matches_metadata(initialized_archive):
|
||||
url = create_test_url(domain="top-level-meta-search-example.com")
|
||||
run_archivebox_cmd(["snapshot", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["list", "--search=meta", "top-level-meta-search-example.com"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 1
|
||||
assert "top-level-meta-search-example.com" in records[0]["url"]
|
||||
|
||||
@@ -468,11 +468,10 @@ def test_search_command_finds_snapshots(initialized_archive):
|
||||
disable_extractors=True,
|
||||
)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(["search", "example"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
result = run_archivebox_cmd(["search", "example"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
assert code == 0, stderr
|
||||
assert "example" in stdout
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "example" in result.stdout
|
||||
|
||||
|
||||
def test_search_command_returns_no_results_for_missing_term(initialized_archive):
|
||||
@@ -483,22 +482,20 @@ def test_search_command_returns_no_results_for_missing_term(initialized_archive)
|
||||
disable_extractors=True,
|
||||
)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["search", "nonexistentterm12345"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code in [0, 1]
|
||||
assert result.returncode in [0, 1]
|
||||
|
||||
|
||||
def test_search_command_on_empty_archive(initialized_archive):
|
||||
_cmd_result = run_archivebox_cmd(["search", "anything"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
_stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
result = run_archivebox_cmd(["search", "anything"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
assert code in [0, 1]
|
||||
assert result.returncode in [0, 1]
|
||||
|
||||
|
||||
def test_search_command_outputs_matching_snapshots_as_jsonl(initialized_archive):
|
||||
@@ -509,11 +506,10 @@ def test_search_command_outputs_matching_snapshots_as_jsonl(initialized_archive)
|
||||
disable_extractors=True,
|
||||
)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(["search"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
result = run_archivebox_cmd(["search"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
assert code == 0, stderr
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0, result.stderr
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert any("example.com" in row.get("url", "") for row in records)
|
||||
|
||||
|
||||
@@ -576,52 +572,47 @@ def test_search_command_csv_outputs_requested_column(initialized_archive):
|
||||
disable_extractors=True,
|
||||
)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["search", "--csv", "url", "--with-headers"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, stderr
|
||||
assert "url" in stdout
|
||||
assert "example.com" in stdout
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert list(csv.reader(io.StringIO(result.stdout))) == [["url"], ["https://example.com"]]
|
||||
|
||||
|
||||
def test_search_command_with_headers_requires_structured_output_format(initialized_archive):
|
||||
_cmd_result = run_archivebox_cmd(["search", "--with-headers"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
_stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
result = run_archivebox_cmd(["search", "--with-headers"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
assert code != 0
|
||||
assert "requires" in stderr.lower()
|
||||
assert "json" in stderr.lower()
|
||||
assert result.returncode != 0
|
||||
assert "requires" in result.stderr.lower()
|
||||
assert "json" in result.stderr.lower()
|
||||
|
||||
|
||||
def test_search_command_sort_option_runs_successfully(initialized_archive):
|
||||
for url in ["https://iana.org", "https://example.com"]:
|
||||
run_archivebox_cmd(["snapshot", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["search", "--csv", "url", "--sort=url"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, stderr
|
||||
assert "example.com" in stdout or "iana.org" in stdout
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert list(csv.reader(io.StringIO(result.stdout))) == [["https://example.com"], ["https://iana.org"]]
|
||||
|
||||
|
||||
def test_search_command_help_lists_supported_filters(initialized_archive):
|
||||
_cmd_result = run_archivebox_cmd(["search", "--help"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
result = run_archivebox_cmd(["search", "--help"], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
assert code == 0
|
||||
assert "--url__icontains" in stdout
|
||||
assert "--crawl-id" in stdout
|
||||
assert "--status" in stdout
|
||||
assert "--sort" in stdout
|
||||
assert "--json" in stdout
|
||||
assert "--html" in stdout
|
||||
assert result.returncode == 0
|
||||
assert "--url__icontains" in result.stdout
|
||||
assert "--crawl-id" in result.stdout
|
||||
assert "--status" in result.stdout
|
||||
assert "--sort" in result.stdout
|
||||
assert "--json" in result.stdout
|
||||
assert "--html" in result.stdout
|
||||
@@ -53,15 +53,14 @@ def test_cli_run_signal_cleans_real_chrome_hook_process_group(initialized_archiv
|
||||
env = cli_env(live=True, PLUGINS="chrome", CHROME_ISOLATION="crawl", CHROME_HEADLESS="true", CHROME_SANDBOX="false")
|
||||
_install_real_chrome_for_test(initialized_archive, env, isolation="crawl")
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "create", recursive_test_site["root_url"]],
|
||||
cwd=initialized_archive,
|
||||
env=env,
|
||||
timeout=60,
|
||||
)
|
||||
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert returncode == 0, stderr or stdout
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
snapshot_id = next(record["id"] for record in records if record.get("type") == "Snapshot")
|
||||
with use_archivebox_db(initialized_archive):
|
||||
browser_state = Snapshot.objects.get(id=snapshot_id).output_dir / "chrome" / "browser.json"
|
||||
@@ -107,7 +106,7 @@ class TestRunWithCrawl:
|
||||
)
|
||||
assert create_result.returncode == 0, create_result.stderr
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin=create_result.stdout,
|
||||
cwd=initialized_archive,
|
||||
@@ -116,12 +115,11 @@ class TestRunWithCrawl:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
|
||||
# Should output the created Crawl
|
||||
records = parse_jsonl_output(stdout)
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
crawl_records = [r for r in records if r.get("type") == "Crawl"]
|
||||
assert len(crawl_records) >= 1
|
||||
assert crawl_records[0].get("id") # Should have an id now
|
||||
@@ -170,7 +168,7 @@ class TestRunWithSnapshot:
|
||||
)
|
||||
assert create_result.returncode == 0, create_result.stderr
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin=create_result.stdout,
|
||||
cwd=initialized_archive,
|
||||
@@ -179,11 +177,10 @@ class TestRunWithSnapshot:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
|
||||
records = parse_jsonl_output(stdout)
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
snapshot_records = [r for r in records if r.get("type") == "Snapshot"]
|
||||
assert len(snapshot_records) >= 1
|
||||
assert snapshot_records[0].get("id")
|
||||
@@ -220,7 +217,7 @@ class TestRunWithSnapshot:
|
||||
def test_run_with_plain_url(self, initialized_archive):
|
||||
"""Run accepts plain URL records (no type field)."""
|
||||
url = create_test_url()
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin=url + "\n",
|
||||
cwd=initialized_archive,
|
||||
@@ -229,10 +226,9 @@ class TestRunWithSnapshot:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) >= 1
|
||||
|
||||
|
||||
@@ -405,7 +401,7 @@ class TestRunRecovery:
|
||||
crawl_id = crawl.id
|
||||
snapshot_id = snapshot.id
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run", "--maintenance-only"],
|
||||
cwd=initialized_archive,
|
||||
timeout=90,
|
||||
@@ -413,13 +409,12 @@ class TestRunRecovery:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, stdout + stderr
|
||||
assert "Repairing" in stderr
|
||||
assert "Resuming 1 Crawl(s) with pending URLs ready to archive" in stderr
|
||||
assert "interrupted before" in stderr
|
||||
assert "remaining URLs" in stderr
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "Repairing" in result.stderr
|
||||
assert "Resuming 1 Crawl(s) with pending URLs ready to archive" in result.stderr
|
||||
assert "interrupted before" in result.stderr
|
||||
assert "remaining URLs" in result.stderr
|
||||
|
||||
with use_archivebox_db(initialized_archive):
|
||||
crawl = Crawl.objects.get(id=crawl_id)
|
||||
@@ -444,17 +439,16 @@ class TestRunPassThrough:
|
||||
assert tag_result.returncode == 0, tag_result.stderr
|
||||
tag_record = parse_jsonl_output(tag_result.stdout)[0]
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin=tag_result.stdout,
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
tag_records = [record for record in records if record.get("type") == "Tag"]
|
||||
assert len(tag_records) == 1
|
||||
assert tag_records[0]["id"] == tag_record["id"]
|
||||
@@ -471,7 +465,7 @@ class TestRunPassThrough:
|
||||
)
|
||||
assert create_result.returncode == 0, create_result.stderr
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin=create_result.stdout,
|
||||
cwd=initialized_archive,
|
||||
@@ -480,10 +474,9 @@ class TestRunPassThrough:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
# Should have at least the Crawl in output
|
||||
assert len(records) >= 1
|
||||
|
||||
@@ -516,7 +509,7 @@ class TestRunMixedInput:
|
||||
)
|
||||
assert snapshot_result.returncode == 0, snapshot_result.stderr
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin=tag_result.stdout + snapshot_result.stdout,
|
||||
cwd=initialized_archive,
|
||||
@@ -525,10 +518,9 @@ class TestRunMixedInput:
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
|
||||
types = {record.get("type") for record in records}
|
||||
assert {"Crawl", "Snapshot", "Tag"}.issubset(types)
|
||||
@@ -539,16 +531,15 @@ class TestRunEmpty:
|
||||
|
||||
def test_run_empty_stdin(self, initialized_archive):
|
||||
"""Run with empty stdin returns success."""
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin="",
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
assert result.returncode == 0
|
||||
|
||||
def test_run_no_runnable_records_to_process(self, initialized_archive):
|
||||
"""Run with only a real non-runnable Tag reports no work."""
|
||||
@@ -560,17 +551,16 @@ class TestRunEmpty:
|
||||
)
|
||||
assert tag_result.returncode == 0, tag_result.stderr
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["run"],
|
||||
stdin=tag_result.stdout,
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
assert "No records to process" in stderr
|
||||
assert result.returncode == 0
|
||||
assert "No records to process" in result.stderr
|
||||
|
||||
|
||||
class TestRunDaemonMode:
|
||||
|
||||
@@ -124,10 +124,17 @@ def test_server_auth_secret_and_cookie_settings_are_restart_stable(tmp_path):
|
||||
assert first_lines[3:] == ["None", "False", "False"]
|
||||
|
||||
|
||||
def test_https_base_url_enables_proxy_ssl_header_and_secure_cookies(tmp_path):
|
||||
@pytest.mark.parametrize(
|
||||
"base_url, secure",
|
||||
[
|
||||
("https://archive.example.com", True),
|
||||
("", False),
|
||||
],
|
||||
)
|
||||
def test_base_url_controls_secure_cookies(tmp_path, base_url, secure):
|
||||
(tmp_path / ".archivebox_id").write_text("testcoll")
|
||||
env = os.environ.copy()
|
||||
env["BASE_URL"] = "https://archive.example.com"
|
||||
env["BASE_URL"] = base_url
|
||||
env["DJANGO_SETTINGS_MODULE"] = "archivebox.core.settings"
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
env["PYTHONPATH"] = f"{repo_root}{os.pathsep}{env.get('PYTHONPATH', '')}"
|
||||
@@ -155,45 +162,8 @@ def test_https_base_url_enables_proxy_ssl_header_and_secure_cookies(tmp_path):
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"csrf_secure": True,
|
||||
"session_secure": True,
|
||||
"proxy_ssl_header": ["HTTP_X_FORWARDED_PROTO", "https"],
|
||||
}
|
||||
|
||||
|
||||
def test_unconfigured_base_url_enables_proxy_ssl_header_without_secure_cookies(tmp_path):
|
||||
(tmp_path / ".archivebox_id").write_text("testcoll")
|
||||
env = os.environ.copy()
|
||||
env["BASE_URL"] = ""
|
||||
env["DJANGO_SETTINGS_MODULE"] = "archivebox.core.settings"
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
env["PYTHONPATH"] = f"{repo_root}{os.pathsep}{env.get('PYTHONPATH', '')}"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
(
|
||||
"import django, json;"
|
||||
"django.setup();"
|
||||
"from django.conf import settings;"
|
||||
"print(json.dumps({"
|
||||
"'csrf_secure': settings.CSRF_COOKIE_SECURE,"
|
||||
"'session_secure': settings.SESSION_COOKIE_SECURE,"
|
||||
"'proxy_ssl_header': settings.SECURE_PROXY_SSL_HEADER,"
|
||||
"}))"
|
||||
),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
env=env,
|
||||
cwd=tmp_path,
|
||||
)
|
||||
|
||||
assert json.loads(result.stdout) == {
|
||||
"csrf_secure": False,
|
||||
"session_secure": False,
|
||||
"csrf_secure": secure,
|
||||
"session_secure": secure,
|
||||
"proxy_ssl_header": ["HTTP_X_FORWARDED_PROTO", "https"],
|
||||
}
|
||||
|
||||
@@ -759,9 +729,8 @@ def test_live_server_signal_exit_and_resume_uses_existing_supervisor_state(initi
|
||||
|
||||
resumed = start_archivebox_server(initialized_archive, port=port, log_name=f"server-{stop_signal.name}-resumed.log", env=env)
|
||||
resumed_log = resumed.log_path
|
||||
_cmd_result = run_archivebox_cmd(["status"], cwd=initialized_archive, env=env, timeout=60)
|
||||
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert returncode == 0, stderr or stdout
|
||||
result = run_archivebox_cmd(["status"], cwd=initialized_archive, env=env, timeout=60)
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
|
||||
os.kill(resumed.pid, signal.SIGTERM)
|
||||
resumed.wait(timeout=20)
|
||||
@@ -783,14 +752,13 @@ def test_live_daemonized_server_keeps_supervisord_owned_by_archivebox_parent(ini
|
||||
port = get_free_port()
|
||||
bind_url = f"http://127.0.0.1:{port}"
|
||||
try:
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["server", "--daemonize", f"127.0.0.1:{port}"],
|
||||
cwd=initialized_archive,
|
||||
env=env,
|
||||
timeout=90,
|
||||
)
|
||||
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert returncode == 0, stderr or stdout
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
_wait_for_archivebox_workers(initialized_archive, env, ("worker_daphne", "worker_runner"), timeout=30)
|
||||
assert_port_open("127.0.0.1", port, timeout=30)
|
||||
|
||||
|
||||
@@ -33,18 +33,17 @@ class TestSnapshotCreate:
|
||||
"""Create snapshot from URL arguments."""
|
||||
url = create_test_url()
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "create", url],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
assert "Created" in stderr
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
assert "Created" in result.stderr
|
||||
|
||||
records = parse_jsonl_output(stdout)
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 1
|
||||
assert records[0]["type"] == "Snapshot"
|
||||
assert records[0]["url"] == url
|
||||
@@ -83,16 +82,15 @@ class TestSnapshotCreate:
|
||||
"""Create snapshot with --tag flag."""
|
||||
url = create_test_url()
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "create", "--tag=test-tag", url],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert "test-tag" in records[0].get("tags", "")
|
||||
|
||||
def test_create_passes_through_tag_emitted_by_cli(self, initialized_archive):
|
||||
@@ -108,17 +106,16 @@ class TestSnapshotCreate:
|
||||
url = create_test_url()
|
||||
stdin = tag_result.stdout + url + "\n"
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "create"],
|
||||
stdin=stdin,
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
|
||||
assert any(record.get("type") == "Tag" and record["id"] == tag_record["id"] for record in records)
|
||||
assert any(record.get("type") == "Snapshot" and record["url"] == url for record in records)
|
||||
@@ -127,16 +124,15 @@ class TestSnapshotCreate:
|
||||
"""Create snapshots from multiple URLs."""
|
||||
urls = [create_test_url() for _ in range(3)]
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "create"] + urls,
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 3
|
||||
|
||||
created_urls = {r["url"] for r in records}
|
||||
@@ -149,32 +145,30 @@ class TestSnapshotList:
|
||||
|
||||
def test_list_empty(self, initialized_archive):
|
||||
"""List with no snapshots returns empty."""
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "list"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
_stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
assert "Listed 0 snapshots" in stderr
|
||||
assert result.returncode == 0
|
||||
assert "Listed 0 snapshots" in result.stderr
|
||||
|
||||
def test_list_returns_created(self, initialized_archive):
|
||||
"""List returns previously created snapshots."""
|
||||
url = create_test_url()
|
||||
run_archivebox_cmd(["snapshot", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "list"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) >= 1
|
||||
assert any(r.get("url") == url for r in records)
|
||||
|
||||
@@ -183,16 +177,15 @@ class TestSnapshotList:
|
||||
url = create_test_url()
|
||||
run_archivebox_cmd(["snapshot", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "list", "--status=queued"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
for r in records:
|
||||
assert r["status"] == "queued"
|
||||
|
||||
@@ -201,16 +194,15 @@ class TestSnapshotList:
|
||||
url = create_test_url(domain="unique-domain-12345.com")
|
||||
run_archivebox_cmd(["snapshot", "create", url], cwd=initialized_archive, default_cli_env=True, disable_extractors=True)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "list", "--url__icontains=unique-domain-12345"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 1
|
||||
assert "unique-domain-12345" in records[0]["url"]
|
||||
|
||||
@@ -224,16 +216,15 @@ class TestSnapshotList:
|
||||
disable_extractors=True,
|
||||
)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "list", "--limit=2"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, _stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 2
|
||||
|
||||
def test_list_with_sort_and_limit(self, initialized_archive):
|
||||
@@ -246,16 +237,15 @@ class TestSnapshotList:
|
||||
disable_extractors=True,
|
||||
)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["snapshot", "list", "--limit=2", "--sort=-created_at"],
|
||||
cwd=initialized_archive,
|
||||
default_cli_env=True,
|
||||
disable_extractors=True,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert code == 0, f"Command failed: {stderr}"
|
||||
records = parse_jsonl_output(stdout)
|
||||
assert result.returncode == 0, f"Command failed: {result.stderr}"
|
||||
records = parse_jsonl_output(result.stdout)
|
||||
assert len(records) == 2
|
||||
|
||||
|
||||
|
||||
@@ -635,7 +635,22 @@ def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(
|
||||
for _snapshot_id, url, depth, plugin, hook_name, status, _files, _size, output_str in archive_results
|
||||
if not (status in allowed_statuses or (plugin == "archivedotorg" and status == ArchiveResult.StatusChoices.FAILED))
|
||||
]
|
||||
assert not unexpected_results
|
||||
# This fixture serves HTTP. TLSNotary must reject every plaintext document;
|
||||
# a setup, timeout, or verifier error must not count as that expected refusal.
|
||||
tlsnotary_results = [row for row in archive_results if row[3] == "tlsnotary"]
|
||||
assert {row[1] for row in tlsnotary_results} == expected_urls
|
||||
assert len(tlsnotary_results) == len(expected_urls)
|
||||
with use_archivebox_db(initialized_archive):
|
||||
for snapshot_id, _url, _depth, _plugin, hook_name, status, files, size, _output in tlsnotary_results:
|
||||
assert status == ArchiveResult.StatusChoices.FAILED
|
||||
assert not files
|
||||
assert size == 0
|
||||
process = ArchiveResult.objects.get(snapshot_id=snapshot_id, plugin="tlsnotary", hook_name=hook_name).process
|
||||
assert process is not None
|
||||
assert process.status == Process.StatusChoices.EXITED
|
||||
assert process.exit_code == 1
|
||||
assert process.stderr.strip() == "[tlsnotary] TLSNotary requires an HTTPS document"
|
||||
assert not [result for result in unexpected_results if result["plugin"] != "tlsnotary"]
|
||||
ytdlp_results = [
|
||||
(url, status, output_str)
|
||||
for _snapshot_id, url, _depth, plugin, _hook_name, status, _files, _size, output_str in archive_results
|
||||
@@ -682,19 +697,6 @@ def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(
|
||||
assert set(recursive_test_site["deep_urls"]).issubset(parsed_urls)
|
||||
|
||||
assert processes
|
||||
failed_hook_results = [
|
||||
{
|
||||
"url": url,
|
||||
"depth": depth,
|
||||
"plugin": plugin,
|
||||
"hook_name": hook_name,
|
||||
"status": status,
|
||||
"output_str": output_str,
|
||||
}
|
||||
for _snapshot_id, url, depth, plugin, hook_name, status, _files, _size, output_str in archive_results
|
||||
if status == ArchiveResult.StatusChoices.FAILED and plugin != "archivedotorg"
|
||||
]
|
||||
assert not failed_hook_results
|
||||
assert all(status == Process.StatusChoices.EXITED for _id, _pwd, _cmd, status, _exit_code, _started_at, _ended_at in processes)
|
||||
|
||||
intervals = []
|
||||
|
||||
@@ -963,14 +963,13 @@ def test_archivewebpage_wacz_preview_serves_real_capture_frame(initialized_archi
|
||||
log_name="archivewebpage_server.log",
|
||||
)
|
||||
get_http_response(port, host=f"archivebox.localhost:{port}", path="/")
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
command_result = run_archivebox_cmd(
|
||||
["add", "--depth=0", "--max-urls=1", "--plugins=archivewebpage", url],
|
||||
cwd=initialized_archive,
|
||||
env=env,
|
||||
timeout=120,
|
||||
)
|
||||
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert returncode == 0, f"archivebox add failed:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
|
||||
assert command_result.returncode == 0, f"archivebox add failed:\nSTDOUT:\n{command_result.stdout}\nSTDERR:\n{command_result.stderr}"
|
||||
|
||||
capture = _get_archivewebpage_capture(initialized_archive, url)
|
||||
snapshot_host = f"{get_snapshot_subdomain(capture['snapshot_id'])}.archivebox.localhost:{port}"
|
||||
|
||||
@@ -163,14 +163,15 @@ def test_snapshot_service_cli_add_seals_snapshot_and_writes_indexes(tmp_path, re
|
||||
|
||||
port = get_free_port()
|
||||
env = cli_env(port=port, server=True, PLUGINS="wget", SAVE_WGET="True")
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
command_result = run_archivebox_cmd(
|
||||
["add", "--depth=0", "--plugins=wget", recursive_test_site["root_url"]],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=180,
|
||||
)
|
||||
stdout, stderr, code = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert code == 0, f"archivebox add failed with code {code}\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
|
||||
assert command_result.returncode == 0, (
|
||||
f"archivebox add failed with code {command_result.returncode}\nSTDOUT:\n{command_result.stdout}\nSTDERR:\n{command_result.stderr}"
|
||||
)
|
||||
|
||||
state = _snapshot_state(tmp_path, recursive_test_site["root_url"])
|
||||
snapshot_dir = state["snapshot_dir"]
|
||||
|
||||
@@ -387,14 +387,13 @@ def test_live_second_server_takes_over_existing_server_process(tmp_path, initial
|
||||
assert "A newer archivebox process took over the orchestrator, server" in first_text
|
||||
assert "Starting orchestrator, server" in second_text
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["status"],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=60,
|
||||
)
|
||||
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert returncode == 0, stderr or stdout
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
|
||||
first_resumes = first_log.read_text(encoding="utf-8", errors="replace").count("Other newer archivebox process")
|
||||
stop_archivebox_process(second, signal.SIGTERM)
|
||||
@@ -422,15 +421,14 @@ def test_live_update_index_only_does_not_take_over_server_runtime(tmp_path, init
|
||||
supervisor_pid_before = supervisor_pid_from_log(server_log)
|
||||
daphne_pid_before = worker_pid_from_log(server_log, "worker_daphne")
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["update", "--index-only", "--before=0"],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=90,
|
||||
)
|
||||
update_stdout, update_stderr, update_returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert update_returncode == 0, update_stderr or update_stdout
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
assert pid_is_alive(server.pid)
|
||||
assert pid_is_alive(supervisor_pid_before)
|
||||
assert pid_is_alive(daphne_pid_before)
|
||||
@@ -471,15 +469,14 @@ def test_live_server_keeps_http_runtime_while_update_runs_real_sqlite_indexer(tm
|
||||
runner_spawn_text = "spawned: 'worker_runner' with pid"
|
||||
runner_spawn_count = supervisord_log.read_text(encoding="utf-8", errors="replace").count(runner_spawn_text)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["update", "--index-only", "--batch-size=1"],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=180,
|
||||
)
|
||||
update_stdout, update_stderr, update_returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
|
||||
assert update_returncode == 0, update_stderr or update_stdout
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
assert pid_is_alive(server.pid)
|
||||
assert supervisor_pid_from_log(server_log) == supervisor_pid_before
|
||||
assert pid_is_alive(daphne_pid_before)
|
||||
@@ -628,14 +625,13 @@ def test_live_repeated_server_startups_take_over_cleanly(tmp_path, initialized_a
|
||||
wait_for_pid_to_disappear(daphne_pids[index - 1], timeout=15)
|
||||
wait_for_pid_to_disappear(runner_pids[index - 1], timeout=15)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["status"],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=60,
|
||||
)
|
||||
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert returncode == 0, stderr or stdout
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
|
||||
assert pid_is_alive(servers[-1].pid)
|
||||
assert all(pid_is_alive(server.pid) for server in servers)
|
||||
@@ -673,14 +669,13 @@ def test_live_background_add_survives_server_exit_and_foreground_run_reclaims(tm
|
||||
server_log = server.log_path
|
||||
supervisor_pid_before = supervisor_pid_from_log(server_log)
|
||||
|
||||
_cmd_result = run_archivebox_cmd(
|
||||
result = run_archivebox_cmd(
|
||||
["update", "--index-only", "--batch-size=10"],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
timeout=90,
|
||||
)
|
||||
update_stdout, update_stderr, update_returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
|
||||
assert update_returncode == 0, update_stderr or update_stdout
|
||||
assert result.returncode == 0, result.stderr or result.stdout
|
||||
assert pid_is_alive(server.pid)
|
||||
assert pid_is_alive(supervisor_pid_before)
|
||||
assert supervisor_pid_from_log(server_log) == supervisor_pid_before
|
||||
|
||||
@@ -20,6 +20,23 @@ WEB_HOST = "web.archivebox.localhost:8000"
|
||||
ADMIN_HOST = "admin.archivebox.localhost:8000"
|
||||
|
||||
|
||||
ADD_FORM_DEFAULTS = {
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user(db):
|
||||
return User.objects.create_superuser(
|
||||
@@ -140,22 +157,13 @@ def test_add_view_staff_user_cannot_override_raw_or_plugin_config(client):
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com/staff-config",
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"crawl_timeout": "0",
|
||||
"timeout": "60",
|
||||
"snapshot_max_size": "0",
|
||||
"delete_after": "0",
|
||||
"crawl_max_concurrent_snapshots": "1",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"notes": "",
|
||||
"schedule": "daily",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "on",
|
||||
"main_plugins": ["wget"],
|
||||
"plugin_config__wget__WGET_TIMEOUT": "77",
|
||||
@@ -246,6 +254,7 @@ def test_add_view_creates_crawl_with_tag_and_url_filter_overrides(client, admin_
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com\nhttps://cdn.example.com/asset.js",
|
||||
"tag": "alpha,beta",
|
||||
"depth": "1",
|
||||
@@ -260,11 +269,6 @@ def test_add_view_creates_crawl_with_tag_and_url_filter_overrides(client, admin_
|
||||
"url_filters_denylist": "cdn.example.com",
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "Created from /add/",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
@@ -290,24 +294,12 @@ def test_add_view_creates_crawl_with_tag_and_url_filter_overrides(client, admin_
|
||||
|
||||
def _csrf_test_add_data(url: str) -> dict[str, str]:
|
||||
return {
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": url,
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"crawl_timeout": "0",
|
||||
"timeout": "",
|
||||
"snapshot_max_size": "0",
|
||||
"delete_after": "0",
|
||||
"crawl_max_concurrent_snapshots": "1",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
}
|
||||
|
||||
|
||||
@@ -337,24 +329,14 @@ def test_add_view_sanitizes_crawl_notes_before_safe_update(client, admin_user):
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com/notes-xss",
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "1",
|
||||
"crawl_max_size": "0",
|
||||
"crawl_timeout": "0",
|
||||
"timeout": "",
|
||||
"snapshot_max_size": "0",
|
||||
"delete_after": "0",
|
||||
"crawl_max_concurrent_snapshots": "1",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"notes": malicious_notes,
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
@@ -373,20 +355,8 @@ def test_add_view_unchecked_only_new_sets_crawl_override(client, admin_user):
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com/rearchive",
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
@@ -407,20 +377,10 @@ def test_add_view_selected_persona_wins_over_stale_config_override(client, admin
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com/private",
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Private",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": '{"DEFAULT_PERSONA": "Default"}',
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
@@ -447,20 +407,9 @@ def test_add_view_applies_plugin_config_overrides(client, admin_user):
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com/plugin-config",
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"main_plugins": ["wget"],
|
||||
"plugin_config__wget__WGET_ENABLED": "false",
|
||||
"plugin_config__wget__WGET_TIMEOUT": "77",
|
||||
@@ -488,9 +437,8 @@ def test_add_view_public_submission_ignores_plugin_and_custom_config(client, adm
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com/public-safe",
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "10",
|
||||
"crawl_max_size": "45mb",
|
||||
"crawl_timeout": "120",
|
||||
@@ -503,8 +451,6 @@ def test_add_view_public_submission_ignores_plugin_and_custom_config(client, adm
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "public add",
|
||||
"schedule": "daily",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "on",
|
||||
"main_plugins": ["wget"],
|
||||
"plugin_config__twocaptcha__TWOCAPTCHA_API_KEY": "posted-token",
|
||||
@@ -541,21 +487,9 @@ def test_add_view_queues_crawl_for_background_runner(client, admin_user):
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com",
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
@@ -575,21 +509,11 @@ def test_add_view_start_paused_creates_paused_crawl_without_snapshots(client, ad
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": "https://example.com/paused",
|
||||
"tag": "",
|
||||
"depth": "1",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "on",
|
||||
"config": "{}",
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
@@ -610,6 +534,7 @@ def test_add_view_extracts_urls_from_mixed_text_input(client, admin_user):
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": (
|
||||
"https://sweeting.me,https://google.com\n"
|
||||
"Notes: [ArchiveBox](https://github.com/ArchiveBox/ArchiveBox), https://news.ycombinator.com\n"
|
||||
@@ -617,20 +542,7 @@ def test_add_view_extracts_urls_from_mixed_text_input(client, admin_user):
|
||||
'{"items":["https://example.com/three"]}\n'
|
||||
"csv,https://example.com/four"
|
||||
),
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
@@ -656,21 +568,9 @@ def test_add_view_trims_trailing_punctuation_from_markdown_urls(client, admin_us
|
||||
response = client.post(
|
||||
reverse("add"),
|
||||
data={
|
||||
**ADD_FORM_DEFAULTS,
|
||||
"url": ("Docs: https://github.com/ArchiveBox/ArchiveBox.\nIssue: https://github.com/abc?abc#234234?."),
|
||||
"tag": "",
|
||||
"depth": "0",
|
||||
"max_urls": "0",
|
||||
"crawl_max_size": "0",
|
||||
"snapshot_max_size": "0",
|
||||
"url_filters_allowlist": "",
|
||||
"url_filters_denylist": "",
|
||||
"url_filters_only_new": "1",
|
||||
"notes": "",
|
||||
"schedule": "",
|
||||
"persona": "Default",
|
||||
"permissions": "public",
|
||||
"start_paused": "",
|
||||
"config": "{}",
|
||||
},
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
|
||||
@@ -431,6 +431,10 @@ def test_public_add_view_import_text_formats_preserve_metadata_and_resume_withou
|
||||
URL_ALLOWLIST=r"127\.0\.0\.1[:/].*",
|
||||
)
|
||||
create_admin_and_token(tmp_path)
|
||||
# These tests deliberately stop the server while crawls are queued. Finish
|
||||
# dependency installation first so shutdown cannot leave a half-built venv.
|
||||
install_result = run_archivebox_cmd(["install", *env["PLUGINS"].split(",")], cwd=tmp_path, env=env, timeout=180)
|
||||
assert install_result.returncode == 0, install_result.stderr or install_result.stdout
|
||||
|
||||
try:
|
||||
start_archivebox_server(tmp_path, env=env, port=port)
|
||||
@@ -536,6 +540,10 @@ def test_public_add_view_rejects_file_path_and_shell_injection_payloads(tmp_path
|
||||
URL_ALLOWLIST=r"example\.com|example\.org|iana\.org|www\.iana\.org",
|
||||
)
|
||||
create_admin_and_token(tmp_path)
|
||||
# These tests deliberately stop the server while crawls are queued. Finish
|
||||
# dependency installation first so shutdown cannot leave a half-built venv.
|
||||
install_result = run_archivebox_cmd(["install", *env["PLUGINS"].split(",")], cwd=tmp_path, env=env, timeout=180)
|
||||
assert install_result.returncode == 0, install_result.stderr or install_result.stdout
|
||||
|
||||
try:
|
||||
start_archivebox_server(tmp_path, env=env, port=port)
|
||||
|
||||
@@ -607,7 +607,14 @@ class TestSnapshotProgressStats:
|
||||
|
||||
assert result.embed_path_db() is None
|
||||
|
||||
def test_embed_path_db_prefers_valid_output_str_over_first_output_file(self, snapshot, real_hash_projection):
|
||||
@pytest.mark.parametrize(
|
||||
"output_str",
|
||||
[
|
||||
("wget/example.com/index.html"),
|
||||
(""),
|
||||
],
|
||||
)
|
||||
def test_embed_path_db_selects_html_output(self, snapshot, real_hash_projection, output_str):
|
||||
from archivebox.core.models import ArchiveResult
|
||||
|
||||
output_dir = Path(snapshot.output_dir) / "wget" / "example.com" / "assets" / "css"
|
||||
@@ -619,32 +626,7 @@ class TestSnapshotProgressStats:
|
||||
_process, result = real_hash_projection
|
||||
ArchiveResult.objects.filter(pk=result.pk).update(
|
||||
plugin="wget",
|
||||
output_str="wget/example.com/index.html",
|
||||
output_files={
|
||||
"example.com/assets/css/mobile.css": {"size": (output_dir / "mobile.css").stat().st_size, "mimetype": "text/css"},
|
||||
"example.com/index.html": {
|
||||
"size": (Path(snapshot.output_dir) / "wget" / "example.com" / "index.html").stat().st_size,
|
||||
"mimetype": "text/html",
|
||||
},
|
||||
},
|
||||
)
|
||||
result.refresh_from_db()
|
||||
|
||||
assert result.embed_path_db() == "wget/example.com/index.html"
|
||||
|
||||
def test_embed_path_db_scores_output_files_instead_of_using_first_entry(self, snapshot, real_hash_projection):
|
||||
from archivebox.core.models import ArchiveResult
|
||||
|
||||
output_dir = Path(snapshot.output_dir) / "wget" / "example.com" / "assets" / "css"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
(Path(snapshot.output_dir) / "wget" / "example.com" / "index.html").parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(REPO_ROOT / "README.md", Path(snapshot.output_dir) / "wget" / "example.com" / "index.html")
|
||||
shutil.copyfile(REPO_ROOT / "archivebox" / "templates" / "static" / "bootstrap.min.css", output_dir / "mobile.css")
|
||||
|
||||
_process, result = real_hash_projection
|
||||
ArchiveResult.objects.filter(pk=result.pk).update(
|
||||
plugin="wget",
|
||||
output_str="",
|
||||
output_str=output_str,
|
||||
output_files={
|
||||
"example.com/assets/css/mobile.css": {"size": (output_dir / "mobile.css").stat().st_size, "mimetype": "text/css"},
|
||||
"example.com/index.html": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import hashlib
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import struct
|
||||
import sys
|
||||
@@ -123,6 +124,53 @@ def append_manifest(manifest_path: Path, screenshot_path: Path) -> None:
|
||||
manifest.write(json.dumps(item) + "\n")
|
||||
|
||||
|
||||
def render_gallery(gallery_header: str, gallery_sections: str) -> str:
|
||||
return (
|
||||
'<!doctype html><html lang="en"><head><meta charset="utf-8">'
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
"<title>ArchiveBox UI Screenshots</title><style>"
|
||||
":root{--ink:#28252a;--muted:#716c74;--accent:#9b2854;--paper:#fcfaf7;--line:#e9e2e3}"
|
||||
"*{box-sizing:border-box}body{margin:0;font:16px/1.75 system-ui,sans-serif;"
|
||||
"background:var(--paper);color:var(--ink)}.screenshot-content{max-width:1100px;margin:0 auto;padding:28px}"
|
||||
"a{color:var(--accent)}h1,h2{line-height:1.15;letter-spacing:-.045em}"
|
||||
"article{margin:40px 0 65px}article h2{font-size:29px;margin:0 0 10px}article p{font-size:13px;color:var(--muted);margin:0 0 20px}"
|
||||
".profile-switch{display:flex;flex-wrap:wrap;gap:4px;background:#f0e8eb;border:1px solid var(--line);padding:5px;"
|
||||
"border-radius:28px;width:max-content;max-width:100%;margin:30px 0}.profile-switch button{font:600 13px system-ui;"
|
||||
"cursor:pointer;border:0;border-radius:22px;background:transparent;color:var(--muted);padding:10px 20px}"
|
||||
".profile-switch button[aria-pressed=true]{background:var(--accent);color:white;box-shadow:0 3px 10px #9b285425}"
|
||||
"a:focus-visible,button:focus-visible{outline:3px solid var(--accent);outline-offset:4px}"
|
||||
"figure{min-width:0;margin:0 0 20px;padding:18px;border:1px solid var(--line);border-radius:20px;"
|
||||
"background:linear-gradient(140deg,#fff,#f8edf2)}figure[hidden]{display:none}"
|
||||
".shot-mobile{max-width:440px;margin-inline:auto}.shot-tablet{max-width:800px;margin-inline:auto}"
|
||||
"figcaption{font-size:12px;color:var(--muted);text-align:center;margin:0 0 12px}"
|
||||
"img{display:block;width:100%;height:auto;border-radius:8px}code{overflow-wrap:anywhere}"
|
||||
"@media(max-width:550px){.screenshot-content{padding:20px}figure{padding:8px;border-radius:12px}.profile-switch button{padding:9px 16px}}"
|
||||
'</style><link rel="stylesheet" href="../site-chrome.css"></head><body>'
|
||||
+ (REPO_DIR / "bin/templates/screenshots-header.html").read_text(encoding="utf-8")
|
||||
+ '<div class="screenshot-content">'
|
||||
+ gallery_header
|
||||
+ '<div class="profile-switch" role="group" aria-label="Screenshot viewport">'
|
||||
+ "".join(
|
||||
f'<button type="button" data-viewport="{profile}" aria-pressed="{str(profile == "desktop").lower()}">{profile.title()}</button>'
|
||||
for profile in CAPTURE_PROFILES
|
||||
)
|
||||
+ "</div><main>"
|
||||
+ gallery_sections
|
||||
+ """</main><script>
|
||||
const figures = document.querySelectorAll('.shot');
|
||||
const buttons = document.querySelectorAll('[data-viewport]');
|
||||
function selectProfile(profile) {
|
||||
for (const figure of figures) figure.hidden = figure.dataset.profile !== profile;
|
||||
for (const button of buttons) button.setAttribute('aria-pressed', String(button.dataset.viewport === profile));
|
||||
}
|
||||
for (const button of buttons) button.addEventListener('click', () => selectProfile(button.dataset.viewport));
|
||||
selectProfile('desktop');
|
||||
</script></div>"""
|
||||
+ (REPO_DIR / "bin/templates/screenshots-footer.html").read_text(encoding="utf-8")
|
||||
+ "</body></html>\n"
|
||||
)
|
||||
|
||||
|
||||
def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -> None:
|
||||
provenance = build_provenance()
|
||||
source_base_url = f"https://github.com/ArchiveBox/ArchiveBox/blob/{provenance['revision']}/"
|
||||
@@ -205,7 +253,7 @@ def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -
|
||||
f'alt="{html.escape(str(capture["name"]))} — {profile}" width="{width}"></td>',
|
||||
)
|
||||
html_figures.append(
|
||||
f'<figure class="shot shot-{profile}"><figcaption>{label}</figcaption>'
|
||||
f'<figure class="shot shot-{profile}" data-profile="{profile}"><figcaption>{label}</figcaption>'
|
||||
f'<a href="./{versioned_filename}">'
|
||||
f'<img src="./{versioned_filename}" width="{width}" height="{height}" loading="lazy" '
|
||||
f'alt="{html.escape(str(capture["name"]))} — {profile}"></a></figure>',
|
||||
@@ -252,25 +300,15 @@ def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
html_path.write_text(
|
||||
'<!doctype html><html lang="en"><head><meta charset="utf-8">'
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
"<title>ArchiveBox UI Screenshots</title><style>"
|
||||
"body{max-width:1900px;margin:0 auto;padding:20px;font:16px system-ui,sans-serif;background:#f6f7f9;color:#18202a}"
|
||||
"a{color:#2563eb}article{margin:32px 0 64px}.shots{display:grid;grid-template-columns:2fr 1.35fr .8fr;gap:14px;align-items:start}"
|
||||
"figure{min-width:0;margin:0}figcaption{font-weight:650;margin:0 0 8px}img{display:block;width:100%;height:auto;"
|
||||
"border:1px solid #cbd5e1;border-radius:8px;background:white;box-shadow:0 8px 24px #0f172a18}code{overflow-wrap:anywhere}"
|
||||
"@media(max-width:900px){.shots{grid-template-columns:1fr}body{padding:12px}}"
|
||||
'</style></head><body><header><p><a href="../">← ArchiveBox</a></p><h1>ArchiveBox UI Screenshots</h1>'
|
||||
f"<p>Generated from ArchiveBox <code>{html.escape(provenance['version'])}</code> at "
|
||||
gallery_header = (
|
||||
'<header><p><a href="../">← ArchiveBox</a></p><h1>ArchiveBox UI Screenshots</h1>'
|
||||
+ f"<p>Generated from ArchiveBox <code>{html.escape(provenance['version'])}</code> at "
|
||||
f'<a href="https://github.com/ArchiveBox/ArchiveBox/commit/{html.escape(provenance["revision"])}">'
|
||||
f"<code>{html.escape(provenance['revision'][:12])}</code></a> on "
|
||||
f'<time datetime="{html.escape(provenance["generated_at"])}">{html.escape(provenance["generated_at"])}</time>. '
|
||||
"Desktop, tablet, and mobile viewports are captured from the same build.</p></header><main>"
|
||||
+ "".join(html_sections)
|
||||
+ "</main></body></html>\n",
|
||||
encoding="utf-8",
|
||||
"Desktop, tablet, and mobile viewports are captured from the same build.</p></header>"
|
||||
)
|
||||
html_path.write_text(render_gallery(gallery_header, "".join(html_sections)), encoding="utf-8")
|
||||
|
||||
file_hashes = {
|
||||
capture["filename"]: hashlib.sha256((html_path.parent / capture["filename"]).read_bytes()).hexdigest() for capture in captures
|
||||
@@ -297,7 +335,27 @@ def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -
|
||||
screenshot_path.unlink()
|
||||
|
||||
|
||||
def refresh_gallery(html_path: Path) -> None:
|
||||
"""Apply current presentation to published capture content without changing provenance."""
|
||||
document = html_path.read_text(encoding="utf-8")
|
||||
header = re.search(r"<header>.*?</header>", document, re.DOTALL)
|
||||
sections = re.search(r"<main>(.*?)</main>", document, re.DOTALL)
|
||||
if not header or not sections:
|
||||
raise SystemExit("Published screenshot gallery is missing its capture header or content")
|
||||
content = sections[1]
|
||||
for profile in CAPTURE_PROFILES:
|
||||
# Galleries published before the switcher used classes alone.
|
||||
content = content.replace(
|
||||
f'<figure class="shot shot-{profile}">',
|
||||
f'<figure class="shot shot-{profile}" data-profile="{profile}">',
|
||||
)
|
||||
html_path.write_text(render_gallery(header[0], content), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) == 3 and sys.argv[1] == "refresh":
|
||||
refresh_gallery(Path(sys.argv[2]))
|
||||
return
|
||||
if len(sys.argv) == 4 and sys.argv[1] == "validate":
|
||||
validate_navigation(Path(sys.argv[2]), sys.argv[3])
|
||||
return
|
||||
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Restore validated capture output without running application CI."""
|
||||
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import subprocess
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import urlopen
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("kind", choices=["archivebox", "ios", "extension"])
|
||||
parser.add_argument("destination", type=Path)
|
||||
args = parser.parse_args()
|
||||
args.destination.mkdir(parents=True, exist_ok=True)
|
||||
# Only successful default-branch capture runs are eligible, never PR artifacts.
|
||||
if os.environ.get("GH_TOKEN"):
|
||||
repo = os.environ["GITHUB_REPOSITORY"]
|
||||
branch = "dev" if args.kind == "archivebox" else "main"
|
||||
runs = json.loads(
|
||||
subprocess.check_output(
|
||||
["gh", "api", f"repos/{repo}/actions/workflows/screenshots.yml/runs?branch={branch}&status=success&per_page=100"],
|
||||
),
|
||||
)
|
||||
for run in runs["workflow_runs"]:
|
||||
if run["event"] not in ("push", "workflow_dispatch"):
|
||||
continue
|
||||
artifacts = json.loads(subprocess.check_output(["gh", "api", f"repos/{repo}/actions/runs/{run['id']}/artifacts"]))
|
||||
if any(a["name"] == "site-screenshots" and not a["expired"] for a in artifacts["artifacts"]):
|
||||
subprocess.run(
|
||||
["gh", "run", "download", str(run["id"]), "--repo", repo, "--name", "site-screenshots", "--dir", str(args.destination)],
|
||||
check=True,
|
||||
)
|
||||
print(f"Restored validated captures from run {run['id']} ({run['head_sha']})")
|
||||
raise SystemExit(0)
|
||||
# Published captures remain available even after Actions artifacts expire.
|
||||
base, manifest_name = {
|
||||
"archivebox": ("https://archivebox.io/screenshots/", "build.json"),
|
||||
"ios": ("https://archivebox.github.io/ios-archivebox/screenshots/", "manifest.json"),
|
||||
"extension": ("https://archivebox.github.io/archivebox-browser-extension/screenshots/", "manifest.json"),
|
||||
}[args.kind]
|
||||
|
||||
|
||||
def fetch(name):
|
||||
path = PurePosixPath(name)
|
||||
if path.is_absolute() or ".." in path.parts or ":" in name or "\\" in name:
|
||||
raise ValueError(f"Unsafe capture path: {name}")
|
||||
with urlopen(base + name, timeout=60) as response:
|
||||
data = response.read()
|
||||
expected = hashes.get(name)
|
||||
if expected and hashlib.sha256(data).hexdigest() != expected:
|
||||
raise ValueError(f"Capture checksum mismatch: {name}")
|
||||
target = args.destination / name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(data)
|
||||
|
||||
|
||||
try:
|
||||
with urlopen(base + manifest_name, timeout=60) as response:
|
||||
raw = response.read()
|
||||
except HTTPError as error:
|
||||
if error.code == 404 and args.kind == "ios":
|
||||
print("No complete native gallery published yet; use the curated README screenshots.")
|
||||
raise SystemExit(0)
|
||||
raise
|
||||
manifest = json.loads(raw)
|
||||
hashes = {}
|
||||
if args.kind == "archivebox":
|
||||
hashes = manifest["files"]
|
||||
names = [*hashes, "index.html"]
|
||||
elif args.kind == "extension":
|
||||
names = [image["file"] for capture in manifest["screenshots"] for image in capture["images"]]
|
||||
else:
|
||||
if manifest.get("complete") is not True:
|
||||
raise ValueError("Published native gallery must be complete")
|
||||
names = [capture["path"] for capture in manifest["captures"]]
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
list(pool.map(fetch, names))
|
||||
(args.destination / manifest_name).write_bytes(raw)
|
||||
print(f"Restored {len(names)} published capture files, retaining revision {manifest['revision']}")
|
||||
@@ -0,0 +1,26 @@
|
||||
<footer class="abx-footer" aria-label="ArchiveBox ecosystem">
|
||||
<div class="abx-footer-intro"><div><a class="abx-brand" href="https://archivebox.io/"><svg class="abx-logo" viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="#9b2854"/><path d="M10 14h20v17H10z" fill="#fff4ed"/><path d="M8 9h24v6H8z" fill="#fff4ed"/><path d="M16 19h8v3h-8z" fill="#9b2854"/><path d="M12 31V17" stroke="#9b2854" stroke-width="2"/></svg><span>ArchiveBox</span></a><p class="abx-tagline">Open-source tools for keeping a lasting copy of the web.<br>Your data. Your devices. Your archive.</p></div><p class="abx-footer-motto">A home for the web<br>you want to keep.</p></div>
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Apps & tools</h2><ul><li><a href="https://archivebox.io/">Self-hosted Web Archive</a><a class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" aria-label="Self-hosted Web Archive source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/ios-archivebox/#get-started">iOS App for iPhone & iPad</a><a class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" aria-label="iOS App for iPhone & iPad source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/ios-archivebox/#your-server-your-choice">macOS App + Server App</a><a class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" aria-label="macOS App + Server App source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/archivebox-browser-extension/">Browser Extensions</a><a class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" aria-label="Browser Extensions source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/abx-dl/">Command-line Downloader</a><a class="abx-source" href="https://github.com/ArchiveBox/abx-dl" aria-label="Command-line Downloader source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/abx-plugins/">Plugins & Extractors</a><a class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" aria-label="Plugins & Extractors source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 Line truncated
|
||||
|
||||
<div class="abx-footer-bottom"><p>Built in the open. Supported by our community.</p><p><a href="https://github.com/ArchiveBox">GitHub organization</a> · <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview">Security & privacy</a> · <a href="https://archivebox.io">ArchiveBox.io ↗</a></p></div>
|
||||
</footer>
|
||||
<script>
|
||||
(() => {
|
||||
const menu = document.querySelector('.abx-apps');
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape' && menu?.open) { menu.open = false; menu.querySelector('summary').focus(); }
|
||||
});
|
||||
document.addEventListener('click', event => { if (menu?.open && !menu.contains(event.target)) menu.open = false; });
|
||||
function revealAnchor() {
|
||||
let target;
|
||||
try { target = document.getElementById(decodeURIComponent(location.hash.slice(1))); } catch { return; }
|
||||
if (!target) return;
|
||||
let details = target.closest('details');
|
||||
let opened = false;
|
||||
while (details) { if (!details.open) { details.open = true; opened = true; } details = details.parentElement.closest('details'); }
|
||||
if (opened) requestAnimationFrame(() => target.scrollIntoView());
|
||||
}
|
||||
addEventListener('hashchange', revealAnchor);
|
||||
revealAnchor();
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<header class="abx-header">
|
||||
<a class="abx-brand" href="https://archivebox.io/"><svg class="abx-logo" viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="#9b2854"/><path d="M10 14h20v17H10z" fill="#fff4ed"/><path d="M8 9h24v6H8z" fill="#fff4ed"/><path d="M16 19h8v3h-8z" fill="#9b2854"/><path d="M12 31V17" stroke="#9b2854" stroke-width="2"/></svg><span>ArchiveBox</span><span class="abx-subsite">UI Screenshots</span></a>
|
||||
<nav class="abx-nav" aria-label="Main navigation">
|
||||
<details class="abx-apps"><summary>Apps</summary><div class="abx-app-links"><a href="https://archivebox.io/">ArchiveBox Server</a><a href="https://archivebox.github.io/ios-archivebox/">ArchiveBox for macOS & iOS</a><a href="https://archivebox.github.io/archivebox-browser-extension/">ArchiveBox Browser Extension</a><a href="https://archivebox.github.io/abx-plugins/">ArchiveBox Plugin Library</a><a href="https://archivebox.github.io/abx-dl/"><span>One-shot <code>abx-dl</code> CLI</span></a><a href="https://github.com/ArchiveBox">More on Github...</a></div></details>
|
||||
<a href="https://archivebox.io/screenshots/">Screenshots</a><a href="https://github.com/ArchiveBox/ArchiveBox/wiki">Docs</a>
|
||||
<a href="https://github.com/ArchiveBox/ArchiveBox">GitHub ↗</a>
|
||||
<a class="abx-cta" href="https://archivebox.io/#install">Get started</a>
|
||||
</nav>
|
||||
</header>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "archivebox",
|
||||
"version": "0.9.35rc461",
|
||||
"version": "0.9.35rc475",
|
||||
"repository": "github:ArchiveBox/ArchiveBox",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+41
-25
@@ -42,25 +42,20 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="site-chrome.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<a class="brand" href="#top" aria-label="ArchiveBox home">
|
||||
<img src="./icon.png" alt="" width="42" height="42">
|
||||
<span>ArchiveBox</span>
|
||||
</a>
|
||||
<nav aria-label="Primary navigation">
|
||||
<a href="#install"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v11"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></svg><span>Install</span></a>
|
||||
<a href="#use-cases"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg><span>Use cases</span></a>
|
||||
<a href="#configuration"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 21v-7"/><path d="M4 10V3"/><path d="M12 21v-9"/><path d="M12 8V3"/><path d="M20 21v-5"/><path d="M20 12V3"/><path d="M2 14h4"/><path d="M10 8h4"/><path d="M18 16h4"/></svg><span>Configuration</span></a>
|
||||
<a href="#layout-security"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/><path d="m9 12 2 2 4-4"/></svg><span>Layout & security</span></a>
|
||||
<a href="#background"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8v4l3 3"/><circle cx="12" cy="12" r="9"/></svg><span>Backstory</span></a>
|
||||
<a href="#ecosystem"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/><path d="M10 6.5h4"/><path d="M17.5 10v4"/><path d="M10 17.5h4"/></svg><span>Ecosystem</span></a>
|
||||
<a href="https://github.com/ArchiveBox/ArchiveBox/wiki"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z"/></svg><span>Docs</span></a>
|
||||
<a href="https://github.com/ArchiveBox/ArchiveBox"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 22v-4a4.8 4.8 0 0 0-.09-.93"/><path d="M9 22v-4a4.8 4.8 0 0 1 .09-.93"/><path d="M17.8 20.2c1.4-.5 2.2-1.7 2.2-3.7V12.7c0-.7-.2-1.4-.7-2 .1-.3.3-1.7-.1-3.2 0 0-1.1-.4-3.4.9a11.5 11.5 0 0 0-6 0C7.5 7.1 6.4 7.5 6.4 7.5c-.4 1.5-.2 2.9-.1 3.2-.5.6-.7 1.3-.7 2v3.8c0 2 1 3.2 2.4 3.7"/><path d="M9 18c-3 1.3-5 0-6-2"/></svg><span>GitHub</span></a>
|
||||
<a href="https://pypi.org/project/archivebox/"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16.5 9.4 7.5 4.2"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="M3.3 7 12 12l8.7-5"/><path d="M12 22V12"/></svg><span>PyPI</span></a>
|
||||
</nav>
|
||||
</header>
|
||||
<!-- BEGIN ARCHIVEBOX HEADER — generated by monorepo/site-chrome/sync.py -->
|
||||
<header class="abx-header">
|
||||
<a class="abx-brand" href="https://archivebox.io/"><svg class="abx-logo" viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="#9b2854"/><path d="M10 14h20v17H10z" fill="#fff4ed"/><path d="M8 9h24v6H8z" fill="#fff4ed"/><path d="M16 19h8v3h-8z" fill="#9b2854"/><path d="M12 31V17" stroke="#9b2854" stroke-width="2"/></svg><span>ArchiveBox</span><span class="abx-subsite">Web Archive</span></a>
|
||||
<nav class="abx-nav" aria-label="Main navigation">
|
||||
<details class="abx-apps"><summary>Apps</summary><div class="abx-app-links"><a href="https://archivebox.io/">ArchiveBox Server</a><a href="https://archivebox.github.io/ios-archivebox/">ArchiveBox for macOS & iOS</a><a href="https://archivebox.github.io/archivebox-browser-extension/">ArchiveBox Browser Extension</a><a href="https://archivebox.github.io/abx-plugins/">ArchiveBox Plugin Library</a><a href="https://archivebox.github.io/abx-dl/"><span>One-shot <code>abx-dl</code> CLI</span></a><a href="https://github.com/ArchiveBox">More on Github...</a></div></details>
|
||||
<a href="#use-cases">Use cases</a><a href="#configuration">Configuration</a><a href="#layout-security">Layout & security</a><a href="https://github.com/ArchiveBox/ArchiveBox/wiki">Docs</a>
|
||||
<a href="https://github.com/ArchiveBox/ArchiveBox">GitHub ↗</a>
|
||||
<a class="abx-cta" href="#install">Get started</a>
|
||||
</nav>
|
||||
</header>
|
||||
<!-- END ARCHIVEBOX HEADER -->
|
||||
|
||||
<main id="top">
|
||||
<section class="hero section">
|
||||
@@ -463,18 +458,39 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="brand">
|
||||
<img src="./icon.png" alt="" width="34" height="34">
|
||||
<span>ArchiveBox</span>
|
||||
</div>
|
||||
<p>Open-source self-hosted web archiving. MIT licensed, nonprofit-supported, and built for durable collections.</p>
|
||||
<!-- BEGIN ARCHIVEBOX FOOTER — generated by monorepo/site-chrome/sync.py -->
|
||||
<footer class="abx-footer" aria-label="ArchiveBox ecosystem">
|
||||
<div class="abx-footer-intro"><div><a class="abx-brand" href="https://archivebox.io/"><svg class="abx-logo" viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="10" fill="#9b2854"/><path d="M10 14h20v17H10z" fill="#fff4ed"/><path d="M8 9h24v6H8z" fill="#fff4ed"/><path d="M16 19h8v3h-8z" fill="#9b2854"/><path d="M12 31V17" stroke="#9b2854" stroke-width="2"/></svg><span>ArchiveBox</span></a><p class="abx-tagline">Open-source tools for keeping a lasting copy of the web.<br>Your data. Your devices. Your archive.</p></div><p class="abx-footer-motto">A home for the web<br>you want to keep.</p></div>
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Apps & tools</h2><ul><li><a href="https://archivebox.io/">Self-hosted Web Archive</a><a class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" aria-label="Self-hosted Web Archive source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/ios-archivebox/#get-started">iOS App for iPhone & iPad</a><a class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" aria-label="iOS App for iPhone & iPad source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/ios-archivebox/#your-server-your-choice">macOS App + Server App</a><a class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" aria-label="macOS App + Server App source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/archivebox-browser-extension/">Browser Extensions</a><a class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" aria-label="Browser Extensions source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/abx-dl/">Command-line Downloader</a><a class="abx-source" href="https://github.com/ArchiveBox/abx-dl" aria-label="Command-line Downloader source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 .75Z"/></svg></a></li><li><a href="https://archivebox.github.io/abx-plugins/">Plugins & Extractors</a><a class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" aria-label="Plugins & Extractors source on GitHub" title="View source on GitHub"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-.54v-2.1c-3.14.68-3.8-1.33-3.8-1.33-.51-1.3-1.25-1.65-1.25-1.65-1.02-.7.08-.69.08-.69 1.13.08 1.73 1.16 1.73 1.16 1 1.72 2.63 1.22 3.27.93.1-.73.39-1.22.71-1.5-2.5-.28-5.13-1.25-5.13-5.56 0-1.23.44-2.23 1.16-3.02-.12-.28-.5-1.43.11-2.98 0 0 .95-.3 3.1 1.15a10.8 10.8 0 0 1 5.64 0c2.15-1.46 3.1-1.15 3.1-1.15.62 1.55.23 2.7.12 2.98.72.79 1.16 1.8 1.16 3.02 0 4.32-2.64 5.27-5.15 5.55.4.35.76 1.03.76 2.08v3.11c0 .3.2.65.77.54A11.25 11.25 0 0 0 12 Line truncated
|
||||
<div class="abx-footer-local"><p>Open-source self-hosted web archiving. MIT licensed, nonprofit-supported, and built for durable collections.</p>
|
||||
<div>
|
||||
<a href="https://github.com/ArchiveBox/ArchiveBox/blob/dev/LICENSE">License</a>
|
||||
<a href="https://github.com/ArchiveBox/ArchiveBox/releases">Changelog</a>
|
||||
<a href="https://zulip.archivebox.io">Community chat</a>
|
||||
<a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Donations">Donations</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div></div>
|
||||
<div class="abx-footer-bottom"><p>Built in the open. Supported by our community.</p><p><a href="https://github.com/ArchiveBox">GitHub organization</a> · <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Security-Overview">Security & privacy</a> · <a href="https://archivebox.io">ArchiveBox.io ↗</a></p></div>
|
||||
</footer>
|
||||
<script>
|
||||
(() => {
|
||||
const menu = document.querySelector('.abx-apps');
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape' && menu?.open) { menu.open = false; menu.querySelector('summary').focus(); }
|
||||
});
|
||||
document.addEventListener('click', event => { if (menu?.open && !menu.contains(event.target)) menu.open = false; });
|
||||
function revealAnchor() {
|
||||
let target;
|
||||
try { target = document.getElementById(decodeURIComponent(location.hash.slice(1))); } catch { return; }
|
||||
if (!target) return;
|
||||
let details = target.closest('details');
|
||||
let opened = false;
|
||||
while (details) { if (!details.open) { details.open = true; opened = true; } details = details.parentElement.closest('details'); }
|
||||
if (opened) requestAnimationFrame(() => target.scrollIntoView());
|
||||
}
|
||||
addEventListener('hashchange', revealAnchor);
|
||||
revealAnchor();
|
||||
})();
|
||||
</script>
|
||||
<!-- END ARCHIVEBOX FOOTER -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
/* Shared ArchiveBox site chrome. Source: ArchiveBox/monorepo/site-chrome. */
|
||||
.abx-header, .abx-footer { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.5; letter-spacing: normal; text-align: left; }
|
||||
.abx-header *, .abx-footer * { box-sizing: border-box; }
|
||||
.abx-header { position: sticky; top: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; gap: 24px; width: 100%; max-width: none; margin: 0; padding: 15px max(24px, calc((100% - 1280px) / 2)); border: 0; border-bottom: 1px solid #e9e2e3; background: #fcfaf7f5; color: #28252a; backdrop-filter: blur(16px); }
|
||||
.abx-header a, .abx-footer a { text-decoration: none; text-underline-offset: .24em; }
|
||||
.abx-header .abx-brand, .abx-footer .abx-brand { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 11px; margin: 0; padding: 0; color: inherit; font-size: 19px; font-weight: 750; line-height: 1.2; letter-spacing: -.5px; white-space: nowrap; }
|
||||
.abx-brand .abx-logo { display: block; flex: 0 0 auto; width: 32px; height: 32px; margin: 0; }
|
||||
.abx-header .abx-subsite { margin-left: 2px; color: #716c74; font-size: 12px; font-weight: 450; letter-spacing: 0; }
|
||||
.abx-header .abx-nav { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 0; width: auto; max-width: none; margin: 0; padding: 0; font-size: 13px; font-weight: 600; }
|
||||
.abx-nav > a, .abx-nav > details { margin: 0 0 0 15px; padding: 0 0 0 15px; border: 0; border-left: 1px solid #ded4d8; border-radius: 0; background: none; }
|
||||
.abx-nav > :first-child { margin-left: 0; padding-left: 0; border-left: 0; }
|
||||
.abx-header .abx-nav a { display: inline-flex; align-items: center; min-height: 36px; color: #4d424b; white-space: nowrap; }
|
||||
.abx-header .abx-nav a:hover { color: #9b2854; }
|
||||
.abx-header .abx-nav .abx-cta { margin-left: 18px; padding: 9px 17px; border: 1px solid #9b2854; border-radius: 24px; background: #9b2854; color: #fff; }
|
||||
.abx-header .abx-nav .abx-cta:hover { background: #782040; color: #fff; }
|
||||
.abx-header .abx-apps { position: relative; }
|
||||
.abx-apps > summary { display: flex; align-items: center; gap: 7px; min-height: 36px; list-style: none; cursor: pointer; color: #4d424b; }
|
||||
.abx-apps > summary::-webkit-details-marker { display: none; }
|
||||
.abx-apps > summary::before { content: '›'; display: inline-block; color: #9b2854; font-size: 22px; line-height: 1; transition: transform .15s; }
|
||||
.abx-apps[open] > summary::before { transform: rotate(90deg); }
|
||||
.abx-header .abx-app-links { position: absolute; right: 0; top: calc(100% + 12px); width: 305px; max-height: min(70vh, 580px); overflow-y: auto; padding: 10px; border: 1px solid #e9e2e3; border-radius: 14px; background: #fcfaf7; box-shadow: 0 16px 48px #2d152522; }
|
||||
.abx-header .abx-app-links a { display: flex; min-height: 42px; padding: 9px 12px; border-radius: 7px; white-space: normal; }
|
||||
.abx-header .abx-app-links code { display: inline; vertical-align: baseline; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 1em; font-weight: inherit; line-height: inherit; margin: 0 .3em; padding: 0; border: 0; background: transparent; color: inherit; }
|
||||
.abx-header .abx-app-links a:hover { background: #f2e8ed; }
|
||||
.abx-footer { position: relative; z-index: 1; display: block; width: 100%; max-width: none; margin: 72px 0 0; padding: 60px max(24px, calc((100% - 1280px) / 2)) 24px; border: 0; border-top: 3px solid #7e284a; background: radial-gradient(ellipse at 12% 0, #54243b55, transparent 48%), linear-gradient(120deg, #201923, #14151b 75%); color: #ddd2dd; color-scheme: dark; }
|
||||
.abx-footer .abx-footer-intro { display: flex; align-items: flex-start; justify-content: space-between; gap: 32px; margin: 0 0 42px; }
|
||||
.abx-footer .abx-brand { color: #fff4ed; font-size: 24px; }
|
||||
.abx-footer .abx-brand .abx-logo { width: 40px; height: 40px; }
|
||||
.abx-footer .abx-tagline { max-width: 430px; margin: 12px 0 0; color: #b8acbb; font-size: 14px; line-height: 1.7; }
|
||||
.abx-footer .abx-footer-motto { margin: 2px 0 0; color: #d8b39f; font: italic 25px/1.45 Georgia, serif; text-align: right; }
|
||||
.abx-footer .abx-footer-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 42px; margin: 0; }
|
||||
.abx-footer .abx-footer-column { margin: 0; padding: 20px 0 0; border: 0; border-top: 1px solid #67505e66; background: none; }
|
||||
.abx-footer .abx-footer-column h2 { margin: 0 0 19px; padding: 0; border: 0; color: #e4acbb; font: 650 12px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.abx-footer .abx-footer-column ul { display: block; list-style: none; margin: 0; padding: 0; }
|
||||
.abx-footer .abx-footer-column li { display: flex; align-items: center; gap: 10px; margin: 0; padding: 0; min-height: 36px; }
|
||||
.abx-footer .abx-footer-column li::before { content: none; }
|
||||
.abx-footer .abx-footer-column a { color: #ddd2dd; font-size: 13px; line-height: 1.55; }
|
||||
.abx-footer .abx-footer-column a:first-child { padding: 6px 0; }
|
||||
.abx-footer a:hover { color: #fff; text-decoration: underline; }
|
||||
.abx-footer .abx-source { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 28px; width: 28px; height: 28px; border: 1px solid transparent; border-radius: 50%; color: #a58d9f; }
|
||||
.abx-footer .abx-source:hover { color: #f3b5cb; border-color: #80516a; background: #ffffff08; }
|
||||
.abx-source svg { width: 15px; height: 15px; fill: currentColor; }
|
||||
.abx-footer .abx-footer-bottom { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 24px; margin-top: 32px; padding-top: 21px; border-top: 1px solid #67505e66; color: #a99bab; font-size: 12px; }
|
||||
.abx-footer .abx-footer-bottom p { margin: 0; font-size: inherit; color: inherit; }
|
||||
.abx-footer .abx-footer-bottom a { color: #cebbc9; }
|
||||
.abx-footer .abx-footer-local { margin: 24px 0 0; padding: 16px 0 0; border: 0; border-top: 1px solid #67505e66; border-radius: 0; background: none; color: #baacbc; font-size: 12px; }
|
||||
.abx-footer-local > summary { cursor: pointer; color: #d6bfce; }
|
||||
.abx-footer-local > div { padding-top: 18px; }
|
||||
.abx-footer-local a { color: #e5aabd; }
|
||||
.abx-footer-local .brand, .abx-footer-local .footer-intro, .abx-footer-local .footer-grid, .abx-footer-local .footer-meta { color: inherit; }
|
||||
.abx-footer-local .footer-card { background: transparent; border-color: #67505e66; }
|
||||
.abx-footer-local h2, .abx-footer-local h3, .abx-footer-local strong { color: #e4d8e3; }
|
||||
.abx-footer-local .footer-link-copy span, .abx-footer-local .footer-card-subtitle { color: #baacbc; }
|
||||
.abx-header a:focus-visible, .abx-header summary:focus-visible, .abx-footer a:focus-visible, .abx-footer summary:focus-visible { outline: 3px solid #d67c9f; outline-offset: 4px; }
|
||||
@media (max-width: 1100px) {
|
||||
.abx-header { flex-wrap: wrap; gap: 8px 24px; padding: 12px 20px; }
|
||||
.abx-header .abx-nav { justify-content: flex-start; }
|
||||
.abx-header .abx-app-links { position: fixed; top: auto; left: 20px; right: 20px; width: auto; margin-top: 10px; }
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
.abx-header { position: relative; padding: 14px 18px; }
|
||||
.abx-header .abx-brand { flex-wrap: wrap; row-gap: 5px; }
|
||||
.abx-header .abx-nav { width: 100%; gap: 4px 0; font-size: 12px; }
|
||||
.abx-nav > a, .abx-nav > details { margin-left: 10px; padding-left: 10px; }
|
||||
.abx-header .abx-nav .abx-cta { margin-left: 10px; padding: 7px 13px; }
|
||||
.abx-header .abx-app-links { left: 12px; right: 12px; max-height: 60vh; }
|
||||
.abx-footer { padding: 40px 22px 22px; margin-top: 48px; }
|
||||
.abx-footer .abx-footer-intro { display: block; margin-bottom: 30px; }
|
||||
.abx-footer .abx-footer-motto { margin-top: 20px; text-align: left; font-size: 22px; }
|
||||
.abx-footer .abx-footer-grid { grid-template-columns: 1fr; gap: 26px; }
|
||||
.abx-footer .abx-footer-column li { min-height: 40px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .abx-apps > summary::before { transition: none; } }
|
||||
|
||||
.abx-footer-local > div > a + a { margin-left: 12px; }
|
||||
+20
-4
@@ -125,7 +125,8 @@ code {
|
||||
gap: 8px 18px;
|
||||
}
|
||||
|
||||
.site-header nav a {
|
||||
.site-header nav a,
|
||||
.site-header nav .project-menu > summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -140,7 +141,8 @@ code {
|
||||
transition: border-color 160ms ease, color 160ms ease;
|
||||
}
|
||||
|
||||
.site-header nav a:hover {
|
||||
.site-header nav a:hover,
|
||||
.site-header nav .project-menu > summary:hover {
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
border-color: hsl(var(--primary) / 0.42);
|
||||
@@ -806,7 +808,7 @@ h3 {
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: static;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
@@ -816,7 +818,8 @@ h3 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.site-header nav a {
|
||||
.site-header nav a,
|
||||
.site-header nav .project-menu > summary {
|
||||
font-size: 0.74rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
@@ -881,3 +884,16 @@ h3 {
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shared project navigation stays usable without JavaScript. */
|
||||
.project-menu { position: relative; flex: 0 0 auto; }
|
||||
.project-menu > summary { cursor: pointer; list-style: none; }
|
||||
.project-menu > summary::-webkit-details-marker { display: none; }
|
||||
.project-menu > summary::after { content: "▾"; }
|
||||
.project-menu > summary:focus-visible, .project-menu__links a:focus-visible { outline: 3px solid #9b2854; outline-offset: 3px; }
|
||||
.project-menu__links { position: absolute; right: 0; top: 100%; z-index: 30; width: 220px; max-width: calc(100vw - 32px); padding: 8px; border: 1px solid #e9e2e3; border-radius: 12px; background: #fff; box-shadow: 0 12px 32px #28252a20; }
|
||||
header nav .project-menu__links a { display: flex; align-items: center; min-height: 44px; padding: 8px 12px; color: #28252a; font-size: 13px; letter-spacing: normal; text-transform: none; text-decoration: none; white-space: nowrap; }
|
||||
header nav .project-menu__links a:hover, header nav .project-menu__links a[aria-current="page"] { background: #f8edf2; color: #9b2854; border-radius: 6px; }
|
||||
@media (max-width: 700px) { header.site-header { flex-wrap: wrap; } header.site-header nav { flex-wrap: wrap; gap: 8px 16px; } }
|
||||
|
||||
@media (max-width: 1100px) { .project-menu__links { position: fixed; left: 16px; right: 16px; top: auto; width: auto; } }
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "archivebox"
|
||||
version = "0.9.35rc461"
|
||||
version = "0.9.35rc475"
|
||||
requires-python = ">=3.13"
|
||||
description = "Self-hosted internet archiving solution."
|
||||
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
|
||||
@@ -78,9 +78,9 @@ dependencies = [
|
||||
### Extractor dependencies (runtime binaries resolved through abxpkg)
|
||||
### Binary/Package Management
|
||||
"abxbus==2.5.56", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abxpkg==1.12.117", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abx-plugins[opencode]==1.12.244", # optional AI runtime clients are owned by the plugin
|
||||
"abx-dl==1.12.282", # shared ArchiveBox downloader package
|
||||
"abxpkg==1.12.118", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abx-plugins[opencode]==1.12.252", # optional AI runtime clients are owned by the plugin
|
||||
"abx-dl==1.12.289", # shared ArchiveBox downloader package
|
||||
### UUID7 backport for Python <3.14
|
||||
"uuid7>=0.1.0; python_version < '3.14'", # provides the uuid_extensions module on Python 3.13
|
||||
"browser-cookie3>=0.20.1",
|
||||
@@ -342,7 +342,7 @@ Donate = "https://github.com/ArchiveBox/ArchiveBox/wiki/Donations"
|
||||
|
||||
|
||||
[tool.bumpver]
|
||||
current_version = "v0.9.35rc461"
|
||||
current_version = "v0.9.35rc475"
|
||||
version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]"
|
||||
commit_message = "bump version {old_version} -> {new_version}"
|
||||
tag_message = "{new_version}"
|
||||
|
||||
@@ -13,18 +13,18 @@ supported-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-09-12T23:41:52.996600165Z"
|
||||
exclude-newer = "2026-09-13T06:06:59.555346705Z"
|
||||
exclude-newer-span = "P5D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
abxbus = { timestamp = "2026-09-17T23:41:51.996613044Z", span = "PT1S" }
|
||||
abxbus = { timestamp = "2026-09-18T06:06:58.555362062Z", span = "PT1S" }
|
||||
abx-plugins = "2100-01-01T00:00:00Z"
|
||||
abx-dl = "2100-01-01T00:00:00Z"
|
||||
abxpkg = "2100-01-01T00:00:00Z"
|
||||
|
||||
[[package]]
|
||||
name = "abx-dl"
|
||||
version = "1.12.282"
|
||||
version = "1.12.289"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "abx-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -38,14 +38,14 @@ dependencies = [
|
||||
{ name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/61/6cb403767815780042875c68137a924e1a20862a50c9e7eefe7c09671817/abx_dl-1.12.282.tar.gz", hash = "sha256:3c49774432a3777c4441d5306ad174019653f8a668f46b1ee57258c5f65d6361", size = 88227 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/18/99113fc3117ab0cb272582e2ed66b959fc7f0efcb763f5d1ccc04095b7cc/abx_dl-1.12.289.tar.gz", hash = "sha256:b91ed92fa4d431a01165880054e85e4da00e6ebbd6d24b2472eb4e7b341f2ef5", size = 88248 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/b6/a19d8a07aa4e375c880101d99d8a63749b5cc6cb1542ea4761a9d233f089/abx_dl-1.12.282-py3-none-any.whl", hash = "sha256:7a922ee8b35b711c26c489549845b49897c712664f47bc08b8a85f2bd8577a8d", size = 93263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/56/daf1fc30cce421356d6cea1497fa7e3fa31d19b84f4f554c161b50ccf479/abx_dl-1.12.289-py3-none-any.whl", hash = "sha256:bd94d50010470393b5ad6438487d63e9bfb84eb6b26217c1585be81e862cf803", size = 93264 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "abx-plugins"
|
||||
version = "1.12.244"
|
||||
version = "1.12.252"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -54,9 +54,9 @@ dependencies = [
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/bf/dfa79260a5d019452a912371c2fedee9cf7cc85aeadf0f14c6110b1c2dca/abx_plugins-1.12.244.tar.gz", hash = "sha256:c9f0170fcd0e1a83aada3f425c0999dcb9355ae75dfd6410a1cab32f15c1789b", size = 293567, upload-time = "2026-09-17T23:27:22.652Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/8c/66d64ce850fbb41b8e941f2b95d79b7ddc9ebd4f7238f447a7bf98fd91d0/abx_plugins-1.12.252.tar.gz", hash = "sha256:dac5a4529fc439e37c2442d5f5a06e17dbafa33fb4a4662123e02147b2e18e41", size = 463016, upload-time = "2026-09-18T05:34:20.338Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1b/5056288814dc2c07dde0f220ca9d1871ae8c508d5602a5aa6bb7cc1bcf88/abx_plugins-1.12.244-py3-none-any.whl", hash = "sha256:92f3cd8fbab5ee248e6460deda715225a165386693acd5c783571a56bc4d7acd", size = 457740, upload-time = "2026-09-17T23:27:20.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/2d/151511349ff8f2ca6e11c756c98c6797030511fbad95d66461feb07a895a/abx_plugins-1.12.252-py3-none-any.whl", hash = "sha256:1d2a42d055a26d448b3a27664ff6ca80aa01498636377052eda6664e16206afa", size = 629564, upload-time = "2026-09-18T05:34:18.845Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -82,7 +82,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "abxpkg"
|
||||
version = "1.12.117"
|
||||
version = "1.12.118"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -90,9 +90,9 @@ dependencies = [
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/35/bbfaccae109328c716cea4e0cdbf2d2dc5975f448203bb3f4daa605591a6/abxpkg-1.12.117.tar.gz", hash = "sha256:4d88cc293f343eee2be7ecaab1203dfbc20571a4a39b2afa2f866c9ef8032dec", size = 239387, upload-time = "2026-09-02T19:57:26.142Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c1/c6/d6d0c7c2481a0525e451230e34ab832c4abc71dcbadd734fdcacac18465f/abxpkg-1.12.118.tar.gz", hash = "sha256:a68b0d72b495142e87a014cd90cfe949524eb7187d981bf0775579454f7c29ef", size = 239381, upload-time = "2026-09-18T05:27:25.739Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/46/372be1f8fa915eb254ad865b97fb41073acdb5573dbce3e23a6083417d87/abxpkg-1.12.117-py3-none-any.whl", hash = "sha256:76ec8afaac7b2d7aece7bdda2bbf606e6373db51eb4dcb906008052d09e59577", size = 253749, upload-time = "2026-09-02T19:57:27.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/67/860d7e3d81e7f15f836d1157f77955b047808237fa8ef8949b21be38adc9/abxpkg-1.12.118-py3-none-any.whl", hash = "sha256:b632be829ae692d35b88da605341bf4d4fd78af6fb7cca2ea1feddf4a39290e0", size = 253748, upload-time = "2026-09-18T05:27:24.028Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -127,7 +127,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "archivebox"
|
||||
version = "0.9.35rc461"
|
||||
version = "0.9.35rc475"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -226,10 +226,10 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "abx-dl", specifier = "==1.12.282" },
|
||||
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.12.244" },
|
||||
{ name = "abx-dl", specifier = "==1.12.289" },
|
||||
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.12.252" },
|
||||
{ name = "abxbus", specifier = "==2.5.56" },
|
||||
{ name = "abxpkg", specifier = "==1.12.117" },
|
||||
{ name = "abxpkg", specifier = "==1.12.118" },
|
||||
{ name = "archivebox", extras = ["sonic", "ldap", "debug"], marker = "extra == 'all'" },
|
||||
{ name = "atomicwrites", specifier = "==1.4.1" },
|
||||
{ name = "base32-crockford", specifier = ">=0.3.0" },
|
||||
|
||||
Reference in new issue
Block a user