Compare commits
88
Commits
v0.9.71rc25
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5519197f41 | ||
|
|
bad1cdd387 | ||
|
|
06b4e42fd5 | ||
|
|
56b57df407 | ||
|
|
4266370d94 | ||
|
|
d485d53ebd | ||
|
|
9756f61945 | ||
|
|
224e3ccbc3 | ||
|
|
c7de9c0ea5 | ||
|
|
615e8c584d | ||
|
|
6f41df3a58 | ||
|
|
06c9816c18 | ||
|
|
ec0c80a5eb | ||
|
|
03d4e382a2 | ||
|
|
395c7196c7 | ||
|
|
a1acc891ff | ||
|
|
0fb82fe6aa | ||
|
|
62baa955d0 | ||
|
|
6e113a2efd | ||
|
|
637ab2f2ee | ||
|
|
3b65ae8308 | ||
|
|
3495922862 | ||
|
|
e30cec0c46 | ||
|
|
76ef079b59 | ||
|
|
a1c74f9cd0 | ||
|
|
68466e5a66 | ||
|
|
d6fc07a5d3 | ||
|
|
528ae4640a | ||
|
|
58db6a7c11 | ||
|
|
4a6ebd2e7b | ||
|
|
f3da478d32 | ||
|
|
04f4bea333 | ||
|
|
193b53b10c | ||
|
|
a4f7f0fc07 | ||
|
|
61d13e7f9d | ||
|
|
ef7c6e0bcf | ||
|
|
3c51a21f20 | ||
|
|
6937fe1d68 | ||
|
|
25f04add82 | ||
|
|
bc2ba83c14 | ||
|
|
8990951faa | ||
|
|
8b771bf4bc | ||
|
|
bd2e1a8627 | ||
|
|
f32b735173 | ||
|
|
f6360c253f | ||
|
|
eaacfa697c | ||
|
|
3b99be7fcf | ||
|
|
07d0280307 | ||
|
|
4fe2b2b7ab | ||
|
|
e5412bfe5a | ||
|
|
916c57b42e | ||
|
|
2e6794dd9c | ||
|
|
6c67977ad6 | ||
|
|
03c6f6a311 | ||
|
|
1f710263c5 | ||
|
|
cab03ba544 | ||
|
|
2d128fdf4b | ||
|
|
dc1455ccf3 | ||
|
|
976b6e5b7d | ||
|
|
709cd07eab | ||
|
|
75dcce89dd | ||
|
|
ae82f09d1a | ||
|
|
8270556ccb | ||
|
|
3502cf4312 | ||
|
|
bd11f9541f | ||
|
|
7f326aa57d | ||
|
|
1ed926bcd8 | ||
|
|
f4c30f5b74 | ||
|
|
8362d35549 | ||
|
|
a786e79fcd | ||
|
|
29fbc9c4df | ||
|
|
d8e77d5097 | ||
|
|
4b301730cd | ||
|
|
97061fc849 | ||
|
|
1ea9315492 | ||
|
|
22c30a4f31 | ||
|
|
da0a6d2221 | ||
|
|
9315887f1d | ||
|
|
d4faa4a034 | ||
|
|
dc4d75ff2f | ||
|
|
00de9bbcbb | ||
|
|
5b25b39f7a | ||
|
|
5e069e6e1f | ||
|
|
acabe3fbd6 | ||
|
|
d0eb7c60e5 | ||
|
|
7f0298e1f4 | ||
|
|
4ee4613627 | ||
|
|
489f0a1b64 |
No files matched your search
@@ -33,3 +33,10 @@ screenshots are retained as Actions artifacts. Other failures remain
|
||||
failures; do not publish a success deployment record to bypass them. No code
|
||||
mounts are allowed. The Cabbage job must pass before its record authorizes a
|
||||
stable upload. Queued capture progress is polled; failed tests are not retried.
|
||||
|
||||
Browser acceptance follows the default WACZ viewer into the captured document,
|
||||
matching its replay base and original URL and checking that its body is visible
|
||||
and contains substantial text. An attached viewer iframe can still be
|
||||
`about:blank`, so its presence alone cannot prove replay works. Extracted display
|
||||
titles and the captured document's `<title>` may legitimately differ; neither
|
||||
title equality nor output thumbnails substitute for checking the actual page.
|
||||
@@ -10,4 +10,4 @@ Also review the inline navbar/footer in abx-dl/website/index.html and
|
||||
-->
|
||||
<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/">ArchiveBox Server</a><a class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" aria-label="ArchiveBox Server 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://app.archivebox.io/#get-started">ArchiveBox for macOS & iOS</a><a class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" aria-label="ArchiveBox for macOS & iOS 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://android.archivebox.io/">ArchiveBox for Android</a><a class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" aria-label="ArchiveBox for Android 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://electron.archivebox.io/">ArchiveBox Desktop</a><a class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" aria-label="ArchiveBox Desktop 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://extension.archivebox.io/">ArchiveBox Browser Extension</a><a class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" aria-label="ArchiveBox Browser Extension 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://plugins.archivebox.io/">ArchiveBox Plugin Library</a><a class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" aria-label="ArchiveBox Plugin Library 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://abx-dl.archivebox.io/">One-shot abx-Line truncated
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Apps & tools</h2><ul><li><a href="https://archivebox.io/">ArchiveBox Server</a><a class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" aria-label="ArchiveBox Server 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://app.archivebox.io/#get-started">ArchiveBox for macOS & iOS</a><a class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" aria-label="ArchiveBox for macOS & iOS 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://android.archivebox.io/">ArchiveBox for Android</a><a class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" aria-label="ArchiveBox for Android 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://electron.archivebox.io/">ArchiveBox Desktop</a><a class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" aria-label="ArchiveBox Desktop 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://extension.archivebox.io/">ArchiveBox Browser Extension</a><a class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" aria-label="ArchiveBox Browser Extension 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://plugins.archivebox.io/">ArchiveBox Plugin Library</a><a class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" aria-label="ArchiveBox Plugin Library 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://abx-dl.archivebox.io/">One-shot abx-Line truncated
|
||||
@@ -52,12 +52,15 @@ def main() -> None:
|
||||
# Ordinary Linux tests can use either pool. Only genuine hosted-only
|
||||
# requirements need a file header; filenames never enter runner policy.
|
||||
# Keep a bounded NAS share so hosted capacity continues working in parallel.
|
||||
capacity = int(os.environ.get("UGNAS_CI_MAX_JOBS", "3"))
|
||||
# Start long jobs first in both pools instead of leaving several minutes of
|
||||
# work behind every short job in an alphabetically ordered hosted queue.
|
||||
# This is queued work, not a worker count: several waves keep the capped
|
||||
# runners occupied while hosted jobs run in parallel.
|
||||
nas_job_limit = int(os.environ.get("UGNAS_CI_MAX_JOBS", "3"))
|
||||
# Keep long jobs early in the hosted queue. Giving the longest jobs to the
|
||||
# CPU-capped NAS instead created a tail after hosted work had finished; use
|
||||
# its extra slots for a bounded share of shorter work without filename rules.
|
||||
matrix.sort(key=lambda item: sum(durations.get(path, 60) for path in item["paths"]), reverse=True)
|
||||
eligible = [item for item in matrix if not any(root / path in hosted for path in item["paths"])]
|
||||
for item in eligible[:capacity]:
|
||||
eligible = [item for item in reversed(matrix) if not any(root / path in hosted for path in item["paths"])]
|
||||
for item in eligible[:nas_job_limit]:
|
||||
item["ugnas"] = True
|
||||
|
||||
discovered_paths = [path for entry in matrix for path in entry["paths"]]
|
||||
@@ -66,7 +69,10 @@ def main() -> None:
|
||||
if sorted(discovered_paths) != [path.relative_to(root).as_posix() for path in archivebox_tests]:
|
||||
raise SystemExit("Discovered test shard coverage does not match test files")
|
||||
|
||||
print(f"Discovered {len(discovered_paths)} test files exactly once across {len(matrix)} jobs")
|
||||
print(
|
||||
f"Discovered {len(discovered_paths)} test files exactly once across {len(matrix)} jobs; "
|
||||
f"{sum(item['ugnas'] for item in matrix)} selected for ugNAS, subject to the availability check",
|
||||
)
|
||||
print(json.dumps(matrix, separators=(",", ":")))
|
||||
|
||||
|
||||
|
||||
+53
-52
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"source_runs": [
|
||||
"https://github.com/ArchiveBox/ArchiveBox/actions/runs/36302511923",
|
||||
"https://github.com/ArchiveBox/ArchiveBox/actions/runs/36353196552"
|
||||
"https://github.com/ArchiveBox/ArchiveBox/actions/runs/36353196552",
|
||||
"https://github.com/ArchiveBox/ArchiveBox/actions/runs/36454992009"
|
||||
],
|
||||
"measurement": "Whole job seconds, including setup and excluding queue time. Unbatched files use the latest complete successful run; batched files retain their individual baseline measurement. Unknown files stay independent.",
|
||||
"measurement": "Whole job seconds, including setup and excluding queue time. Hosted unbatched files use the latest complete successful run; other files retain their individual baseline measurement. Unknown files stay independent.",
|
||||
"seconds": {
|
||||
"archivebox/tests/test_api_v1_auth_browser_session.py": 29,
|
||||
"archivebox/tests/test_api_v1_auth_check_api_token.py": 27,
|
||||
@@ -12,12 +13,12 @@
|
||||
"archivebox/tests/test_api_v1_cli_remove.py": 50,
|
||||
"archivebox/tests/test_api_v1_cli_schedule.py": 41,
|
||||
"archivebox/tests/test_api_v1_cli_search.py": 26,
|
||||
"archivebox/tests/test_api_v1_cli_update.py": 100,
|
||||
"archivebox/tests/test_api_v1_cli_update.py": 74,
|
||||
"archivebox/tests/test_api_v1_cli_workflow_add_search_update_remove.py": 64,
|
||||
"archivebox/tests/test_api_v1_core_any_id.py": 21,
|
||||
"archivebox/tests/test_api_v1_core_archiveresult_archiveresult_id.py": 55,
|
||||
"archivebox/tests/test_api_v1_core_archiveresults.py": 32,
|
||||
"archivebox/tests/test_api_v1_core_snapshot_snapshot_id.py": 203,
|
||||
"archivebox/tests/test_api_v1_core_snapshot_snapshot_id.py": 199,
|
||||
"archivebox/tests/test_api_v1_core_snapshots_rss.py": 27,
|
||||
"archivebox/tests/test_api_v1_core_snapshots.py": 28,
|
||||
"archivebox/tests/test_api_v1_core_tag_tag_id_rename.py": 23,
|
||||
@@ -45,23 +46,23 @@
|
||||
"archivebox/tests/test_api_v1_personas_sync.py": 29,
|
||||
"archivebox/tests/test_api_v1_workflow_core_token_auth_side_effects.py": 36,
|
||||
"archivebox/tests/test_api_v1_workflow_frozen_crawl_config_sources.py": 33,
|
||||
"archivebox/tests/test_archive_result_service.py": 122,
|
||||
"archivebox/tests/test_archive_result_service.py": 137,
|
||||
"archivebox/tests/test_auth_ldap.py": 44,
|
||||
"archivebox/tests/test_binary_service.py": 83,
|
||||
"archivebox/tests/test_cli_add_1.py": 179,
|
||||
"archivebox/tests/test_cli_add_2.py": 167,
|
||||
"archivebox/tests/test_cli_add_3.py": 194,
|
||||
"archivebox/tests/test_cli_add_4.py": 168,
|
||||
"archivebox/tests/test_cli_archiveresult.py": 145,
|
||||
"archivebox/tests/test_cli_add_1.py": 168,
|
||||
"archivebox/tests/test_cli_add_2.py": 203,
|
||||
"archivebox/tests/test_cli_add_3.py": 215,
|
||||
"archivebox/tests/test_cli_add_4.py": 201,
|
||||
"archivebox/tests/test_cli_archiveresult.py": 199,
|
||||
"archivebox/tests/test_cli_binary.py": 20,
|
||||
"archivebox/tests/test_cli_config.py": 141,
|
||||
"archivebox/tests/test_cli_crawl.py": 127,
|
||||
"archivebox/tests/test_cli_extract_input.py": 205,
|
||||
"archivebox/tests/test_cli_extract.py": 113,
|
||||
"archivebox/tests/test_cli_config.py": 148,
|
||||
"archivebox/tests/test_cli_crawl.py": 177,
|
||||
"archivebox/tests/test_cli_extract_input.py": 155,
|
||||
"archivebox/tests/test_cli_extract.py": 106,
|
||||
"archivebox/tests/test_cli_help.py": 30,
|
||||
"archivebox/tests/test_cli_init.py": 133,
|
||||
"archivebox/tests/test_cli_init.py": 135,
|
||||
"archivebox/tests/test_cli_install.py": 58,
|
||||
"archivebox/tests/test_cli_list.py": 177,
|
||||
"archivebox/tests/test_cli_list.py": 260,
|
||||
"archivebox/tests/test_cli_machine.py": 50,
|
||||
"archivebox/tests/test_cli_manage.py": 48,
|
||||
"archivebox/tests/test_cli_mcp.py": 51,
|
||||
@@ -70,22 +71,22 @@
|
||||
"archivebox/tests/test_cli_piping.py": 97,
|
||||
"archivebox/tests/test_cli_pluginmap.py": 21,
|
||||
"archivebox/tests/test_cli_process.py": 20,
|
||||
"archivebox/tests/test_cli_remove.py": 150,
|
||||
"archivebox/tests/test_cli_run_1.py": 165,
|
||||
"archivebox/tests/test_cli_run_2.py": 241,
|
||||
"archivebox/tests/test_cli_run_3.py": 162,
|
||||
"archivebox/tests/test_cli_run_4.py": 200,
|
||||
"archivebox/tests/test_cli_remove.py": 162,
|
||||
"archivebox/tests/test_cli_run_1.py": 195,
|
||||
"archivebox/tests/test_cli_run_2.py": 255,
|
||||
"archivebox/tests/test_cli_run_3.py": 167,
|
||||
"archivebox/tests/test_cli_run_4.py": 178,
|
||||
"archivebox/tests/test_cli_schedule.py": 87,
|
||||
"archivebox/tests/test_cli_search.py": 35,
|
||||
"archivebox/tests/test_cli_server.py": 251,
|
||||
"archivebox/tests/test_cli_server.py": 259,
|
||||
"archivebox/tests/test_cli_shell.py": 32,
|
||||
"archivebox/tests/test_cli_snapshot.py": 138,
|
||||
"archivebox/tests/test_cli_status.py": 105,
|
||||
"archivebox/tests/test_cli_snapshot.py": 181,
|
||||
"archivebox/tests/test_cli_status.py": 102,
|
||||
"archivebox/tests/test_cli_tag.py": 21,
|
||||
"archivebox/tests/test_cli_update_reindex_snapshots.py": 144,
|
||||
"archivebox/tests/test_cli_update.py": 148,
|
||||
"archivebox/tests/test_cli_version.py": 181,
|
||||
"archivebox/tests/test_config_DELETE_AFTER.py": 192,
|
||||
"archivebox/tests/test_cli_update_reindex_snapshots.py": 163,
|
||||
"archivebox/tests/test_cli_update.py": 142,
|
||||
"archivebox/tests/test_cli_version.py": 143,
|
||||
"archivebox/tests/test_config_DELETE_AFTER.py": 169,
|
||||
"archivebox/tests/test_config_MAX_limits.py": 34,
|
||||
"archivebox/tests/test_config_ONLY_NEW.py": 24,
|
||||
"archivebox/tests/test_config_SAVE_TITLE.py": 99,
|
||||
@@ -99,19 +100,19 @@
|
||||
"archivebox/tests/test_hook_result_lifecycle.py": 78,
|
||||
"archivebox/tests/test_hooks.py": 90,
|
||||
"archivebox/tests/test_machine_models.py": 52,
|
||||
"archivebox/tests/test_machine_service.py": 104,
|
||||
"archivebox/tests/test_machine_service.py": 89,
|
||||
"archivebox/tests/test_migrations_04_to_09.py": 36,
|
||||
"archivebox/tests/test_migrations_07_to_09.py": 140,
|
||||
"archivebox/tests/test_migrations_08_to_09.py": 257,
|
||||
"archivebox/tests/test_migrations_07_to_09.py": 183,
|
||||
"archivebox/tests/test_migrations_08_to_09.py": 244,
|
||||
"archivebox/tests/test_migrations_fresh.py": 99,
|
||||
"archivebox/tests/test_misc_checks.py": 22,
|
||||
"archivebox/tests/test_misc_monkey_patches.py": 20,
|
||||
"archivebox/tests/test_opencode_agent_1.py": 135,
|
||||
"archivebox/tests/test_opencode_agent_2.py": 138,
|
||||
"archivebox/tests/test_opencode_agent_3.py": 140,
|
||||
"archivebox/tests/test_opencode_browser_1.py": 290,
|
||||
"archivebox/tests/test_opencode_browser_2.py": 157,
|
||||
"archivebox/tests/test_opencode_browser_3.py": 129,
|
||||
"archivebox/tests/test_opencode_agent_1.py": 158,
|
||||
"archivebox/tests/test_opencode_agent_2.py": 145,
|
||||
"archivebox/tests/test_opencode_agent_3.py": 154,
|
||||
"archivebox/tests/test_opencode_browser_1.py": 262,
|
||||
"archivebox/tests/test_opencode_browser_2.py": 146,
|
||||
"archivebox/tests/test_opencode_browser_3.py": 148,
|
||||
"archivebox/tests/test_orm_helpers.py": 22,
|
||||
"archivebox/tests/test_permissions.py": 21,
|
||||
"archivebox/tests/test_persona_runtime.py": 56,
|
||||
@@ -119,18 +120,18 @@
|
||||
"archivebox/tests/test_process_runtime_paths.py": 28,
|
||||
"archivebox/tests/test_process_service.py": 49,
|
||||
"archivebox/tests/test_proof_previews.py": 54,
|
||||
"archivebox/tests/test_recursive_crawl_1.py": 396,
|
||||
"archivebox/tests/test_recursive_crawl_2.py": 193,
|
||||
"archivebox/tests/test_recursive_crawl_3.py": 144,
|
||||
"archivebox/tests/test_recursive_crawl_1.py": 339,
|
||||
"archivebox/tests/test_recursive_crawl_2.py": 171,
|
||||
"archivebox/tests/test_recursive_crawl_3.py": 176,
|
||||
"archivebox/tests/test_release_notes.py": 14,
|
||||
"archivebox/tests/test_release_source_fingerprint.py": 22,
|
||||
"archivebox/tests/test_release_workflow.py": 21,
|
||||
"archivebox/tests/test_resource_admission.py": 21,
|
||||
"archivebox/tests/test_runner_watch.py": 182,
|
||||
"archivebox/tests/test_search.py": 203,
|
||||
"archivebox/tests/test_runner_watch.py": 177,
|
||||
"archivebox/tests/test_search.py": 153,
|
||||
"archivebox/tests/test_serve_static.py": 31,
|
||||
"archivebox/tests/test_server_automatic_url.py": 36,
|
||||
"archivebox/tests/test_server_security_browser.py": 241,
|
||||
"archivebox/tests/test_server_security_browser.py": 278,
|
||||
"archivebox/tests/test_settings_signal_webhooks.py": 21,
|
||||
"archivebox/tests/test_setup_script.py": 21,
|
||||
"archivebox/tests/test_shutdown_util.py": 22,
|
||||
@@ -140,12 +141,12 @@
|
||||
"archivebox/tests/test_snapshot_service.py": 41,
|
||||
"archivebox/tests/test_snapshot_url_length.py": 26,
|
||||
"archivebox/tests/test_tag_service.py": 24,
|
||||
"archivebox/tests/test_takeover_util_1.py": 122,
|
||||
"archivebox/tests/test_takeover_util_2.py": 171,
|
||||
"archivebox/tests/test_takeover_util_3.py": 165,
|
||||
"archivebox/tests/test_takeover_util_4.py": 160,
|
||||
"archivebox/tests/test_takeover_util_1.py": 143,
|
||||
"archivebox/tests/test_takeover_util_2.py": 201,
|
||||
"archivebox/tests/test_takeover_util_3.py": 138,
|
||||
"archivebox/tests/test_takeover_util_4.py": 156,
|
||||
"archivebox/tests/test_test_harness.py": 22,
|
||||
"archivebox/tests/test_ui_add_view_runtime.py": 239,
|
||||
"archivebox/tests/test_ui_add_view_runtime.py": 221,
|
||||
"archivebox/tests/test_ui_add_view.py": 46,
|
||||
"archivebox/tests/test_ui_admin_apitoken.py": 23,
|
||||
"archivebox/tests/test_ui_admin_archiveresult.py": 30,
|
||||
@@ -154,14 +155,14 @@
|
||||
"archivebox/tests/test_ui_admin_links.py": 35,
|
||||
"archivebox/tests/test_ui_admin_machine.py": 29,
|
||||
"archivebox/tests/test_ui_admin_persona.py": 28,
|
||||
"archivebox/tests/test_ui_admin_snapshot.py": 288,
|
||||
"archivebox/tests/test_ui_admin_snapshot.py": 273,
|
||||
"archivebox/tests/test_ui_admin_tag.py": 26,
|
||||
"archivebox/tests/test_ui_admin_user.py": 28,
|
||||
"archivebox/tests/test_ui_config_views.py": 43,
|
||||
"archivebox/tests/test_ui_live_progress.py": 303,
|
||||
"archivebox/tests/test_ui_public_snapshot.py": 138,
|
||||
"archivebox/tests/test_ui_live_progress.py": 297,
|
||||
"archivebox/tests/test_ui_public_snapshot.py": 179,
|
||||
"archivebox/tests/test_ui_savepagenow.py": 48,
|
||||
"archivebox/tests/test_urls.py": 179,
|
||||
"archivebox/tests/test_urls.py": 220,
|
||||
"archivebox/tests/test_util.py": 20,
|
||||
"archivebox/tests/test_version_metadata.py": 24
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ jobs:
|
||||
organization: ArchiveBox
|
||||
primary-runner: self-hosted,ugnas-linux
|
||||
fallback-runner: ubuntu-24.04
|
||||
primaries-required: ${{ vars.UGNAS_CI_MAX_JOBS || 3 }}
|
||||
# Jobs queued for this pool must not be mistaken for idle runner slots:
|
||||
# requiring MAX_JOBS idle runners prevents assigning a second wave.
|
||||
primaries-required: ${{ vars.UGNAS_CI_MIN_IDLE || 1 }}
|
||||
fallback-on-error: true
|
||||
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
|
||||
+16
-43
@@ -125,6 +125,7 @@ FROM archivebox-runtime-base AS archivebox-builder
|
||||
WORKDIR "$CODE_DIR"
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-$TARGETARCH$TARGETVARIANT \
|
||||
--mount=type=bind,source=pyproject.toml,target=/app/pyproject.toml \
|
||||
--mount=type=bind,source=uv.lock,target=/app/uv.lock \
|
||||
<<'EOF'
|
||||
echo "[+] UV Installing ArchiveBox dependencies from pyproject.toml..."
|
||||
export UV_NO_CACHE=true
|
||||
@@ -137,48 +138,14 @@ apt-get install -qq -y --no-install-recommends x11-utils
|
||||
# Do not rewrite inherited native libraries merely to strip them again.
|
||||
/usr/bin/find /venv/lib/python3.*/site-packages -type f -name '*.so' -print > /tmp/archivebox-inherited-libraries
|
||||
|
||||
mkdir -p /tmp/archivebox-uv-project
|
||||
/venv/bin/python - <<'PY'
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
|
||||
source = Path("/app/pyproject.toml")
|
||||
target = Path("/tmp/archivebox-uv-project/pyproject.toml")
|
||||
text = source.read_text()
|
||||
text = text.replace(
|
||||
'environments = ["sys_platform == \'darwin\'", "sys_platform == \'linux\'"]',
|
||||
'environments = ["sys_platform == \'linux\'"]',
|
||||
)
|
||||
|
||||
# Docker builds need the just-published internal abx wheels immediately, but
|
||||
# PyPI simple can lag the version JSON endpoints by tens of minutes. Generate a
|
||||
# Docker-only dependency view from the version JSON so the published package
|
||||
# metadata stays normal while image builds remain resumable after a release.
|
||||
for package in ("abxbus", "abxpkg", "abx-plugins", "abx-dl"):
|
||||
match = re.search(
|
||||
rf'"{re.escape(package)}(?P<operator>==|>=)(?P<version>[^"]+)"',
|
||||
text,
|
||||
)
|
||||
if not match:
|
||||
continue
|
||||
version = match.group("version")
|
||||
with urllib.request.urlopen(f"https://pypi.org/pypi/{package}/{version}/json", timeout=20) as response:
|
||||
data = json.load(response)
|
||||
wheel_url = next(url["url"] for url in data["urls"] if url["filename"].endswith(".whl"))
|
||||
text = re.sub(
|
||||
rf'"{re.escape(package)}(?:==|>=)[^"]+"',
|
||||
f'"{package} @ {wheel_url}"',
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
|
||||
target.write_text(text)
|
||||
PY
|
||||
|
||||
# The old Docker-only dependency rewrite predated this checkout's lockfile:
|
||||
# it fetched version JSON to avoid stale PyPI Simple listings. That extra API
|
||||
# can itself lag or return 503s. The verified lock already records exact public
|
||||
# artifact URLs and hashes, including extras and platform markers. Consume it
|
||||
# directly, retaining the freshness check instead of resolving a second graph.
|
||||
/usr/bin/uv sync \
|
||||
--project /tmp/archivebox-uv-project \
|
||||
--project /app \
|
||||
--locked \
|
||||
--no-cache \
|
||||
--no-dev \
|
||||
--inexact \
|
||||
@@ -209,7 +176,7 @@ rm -rf "$builder_abxpkg_lib_dir"
|
||||
rm -rf /venv/lib/python3.*/site-packages/pip* \
|
||||
/venv/lib/python3.*/site-packages/wheel* \
|
||||
/venv/bin/pip /venv/bin/pip3 /venv/bin/pip3.* /venv/bin/wheel
|
||||
rm -rf /var/lib/apt/lists/* /tmp/archivebox-uv-project
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
EOF
|
||||
|
||||
COPY --chown=root:root --chmod=755 "." "$CODE_DIR/"
|
||||
@@ -291,7 +258,13 @@ RUN --mount=type=cache,target=/var/tmp/abxpkg-cache,sharing=locked,mode=1777,id=
|
||||
setpriv --reuid="$ARCHIVEBOX_USER" --regid="$ARCHIVEBOX_USER" --init-groups abx-dl install $ABX_DOCKER_PLUGINS \
|
||||
# pnpm includes musl variants that Debian's glibc runtime cannot use.
|
||||
&& find "$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules" -type l -name 'opencode-linux-*-musl' -delete \
|
||||
&& find "$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.pnpm" -maxdepth 1 -type d -name 'opencode-linux-*-musl@*' -exec rm -rf {} +
|
||||
&& find "$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.pnpm" -maxdepth 1 -type d -name 'opencode-linux-*-musl@*' -exec rm -rf {} + \
|
||||
# pnpm's explicit store lives under ABXPKG_LIB_DIR, regardless of XDG_CACHE_HOME.
|
||||
# Retaining its download copies made image size depend on whether the build
|
||||
# preserved hardlinks to installed files. Remove the store in this layer;
|
||||
# installed files retain their contents, and the offline check below proves
|
||||
# the complete catalog still works without the disposable package payloads.
|
||||
&& rm -rf "$ABXPKG_LIB_DIR/cache/pnpm"
|
||||
RUN --network=none export ABX_DOCKER_PLUGINS="$(/venv/bin/python3 -c 'from abx_dl.catalog import PluginCatalog; print(" ".join(PluginCatalog.discover(runtime="archivebox")))')" \
|
||||
&& setpriv --reuid="$ARCHIVEBOX_USER" --regid="$ARCHIVEBOX_USER" --init-groups \
|
||||
bash -c '"$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.bin/opencode" --version && abx-dl install $ABX_DOCKER_PLUGINS'
|
||||
|
||||
@@ -380,7 +380,7 @@ See <a href="#%EF%B8%8F-cli-usage">below</a> for usage examples using the CLI, W
|
||||
> *Warning: These are contributed by external volunteers and may lag behind the official Docker and `uv` channels.*
|
||||
|
||||
<ul>
|
||||
<li><s>TrueNAS: <a href="https://truecharts.org/charts/stable/archivebox/">Official ArchiveBox TrueChart</a> / <a href="https://dev.to/finloop/setting-up-archivebox-on-truenas-scale-1788">Custom App Guide</a></s> (<a href="https://truecharts.org/news/scale-deprecation/">TrueCharts is discontinued</a>, wait for <a href="https://forums.truenas.com/t/the-future-of-electric-eel-and-apps/5409/">Electric Eel</a>)</li>
|
||||
<li><a href="https://apps.truenas.com/catalog/archivebox_community/">TrueNAS</a></li>
|
||||
<li><a href="https://unraid.net/community/apps?q=archivebox#r">UnRaid</a></li>
|
||||
<li><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a></li>
|
||||
<li><a href="https://github.com/YunoHost-Apps/archivebox_ynh">Yunohost</a></li>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 6.1 on 2026-09-27 22:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0055_repair_legacy_retry_timestamps"),
|
||||
("crawls", "0019_crawlschedule_config"),
|
||||
("machine", "0022_networkinterface_identity_without_mac"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="archiveresult",
|
||||
index=models.Index(fields=["status", "plugin", "snapshot"], name="archiveresult_progress_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="snapshot",
|
||||
index=models.Index(fields=["crawl", "status", "downloaded_at", "output_size"], name="snapshot_progress_totals_idx"),
|
||||
),
|
||||
]
|
||||
@@ -14,7 +14,7 @@ class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("core", "0056_progress_covering_indexes"),
|
||||
("core", "0055_repair_legacy_retry_timestamps"),
|
||||
("machine", "0022_networkinterface_identity_without_mac"),
|
||||
]
|
||||
|
||||
|
||||
+23
-12
@@ -650,7 +650,6 @@ class Snapshot(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithConfig, ModelW
|
||||
indexes: ClassVar[list[models.Index]] = [
|
||||
models.Index(fields=["-bookmarked_at", "-created_at"], name="snapshot_public_order_idx"),
|
||||
models.Index(fields=["crawl", "status", "modified_at"], name="snapshot_progress_idx"),
|
||||
models.Index(fields=["crawl", "status", "downloaded_at", "output_size"], name="snapshot_progress_totals_idx"),
|
||||
]
|
||||
constraints: ClassVar[list[models.BaseConstraint]] = [
|
||||
# Allow same URL in different crawls, but not duplicates within same crawl
|
||||
@@ -896,7 +895,8 @@ class Snapshot(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithConfig, ModelW
|
||||
return False
|
||||
if self.status == self.StatusChoices.QUEUED:
|
||||
return bool(self.url) and self.start_processing()
|
||||
# abx-dl emits SnapshotCompletedEvent after the complete hook sequence;
|
||||
# SnapshotCompletedEvent seals completed or explicitly aborted attempts;
|
||||
# runner takeover preserves the active capture for its replacement.
|
||||
# ArchiveResult projection state never drives Snapshot completion.
|
||||
return False
|
||||
|
||||
@@ -4163,7 +4163,6 @@ class ArchiveResult(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithNotes):
|
||||
indexes: ClassVar[list[models.Index]] = [
|
||||
models.Index(fields=["snapshot", "status"], name="archiveresult_snap_status_idx"),
|
||||
models.Index(fields=["status", "snapshot"], name="archiveresult_status_snap_idx"),
|
||||
models.Index(fields=["status", "plugin", "snapshot"], name="archiveresult_progress_idx"),
|
||||
models.Index(fields=["-start_ts", "-id"], name="archiveresult_start_idx"),
|
||||
]
|
||||
constraints: ClassVar[list[models.BaseConstraint]] = [
|
||||
@@ -4438,6 +4437,10 @@ class ArchiveResult(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithNotes):
|
||||
return 0
|
||||
|
||||
def output_file_map(self) -> dict[str, dict[str, Any]]:
|
||||
# List views opt into this cache on their read-only, request-local rows.
|
||||
# Runner-owned results must still reflect metadata changed during a hook.
|
||||
if "_render_output_file_map" in self.__dict__:
|
||||
return self._render_output_file_map
|
||||
return self._normalize_output_files(self.output_files)
|
||||
|
||||
def output_file_paths(self) -> list[str]:
|
||||
@@ -4592,11 +4595,20 @@ class ArchiveResult(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithNotes):
|
||||
output_file_map: dict[str, dict[str, Any]] | None = None,
|
||||
) -> str | None:
|
||||
ignored = {"stdout.log", "stderr.log", "hook.pid", "listener.pid"}
|
||||
candidates = [
|
||||
path
|
||||
for path in output_file_paths
|
||||
if Path(path).name not in ignored and Path(path).suffix.lower() not in (".pid", ".log", ".sh")
|
||||
]
|
||||
# Responses/wget manifests can contain thousands of paths. Parse each
|
||||
# once instead of rebuilding Path objects for every preferred filename
|
||||
# and extension group, each time a list row selects a preview.
|
||||
candidates = []
|
||||
names = {}
|
||||
extensions = {}
|
||||
for path in output_file_paths:
|
||||
parsed_path = Path(path)
|
||||
name, extension = parsed_path.name, parsed_path.suffix.lower()
|
||||
if name in ignored or extension in (".pid", ".log", ".sh"):
|
||||
continue
|
||||
candidates.append(path)
|
||||
names.setdefault(name.lower(), path)
|
||||
extensions[path] = extension
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
@@ -4619,9 +4631,8 @@ class ArchiveResult(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithNotes):
|
||||
"article.json",
|
||||
]
|
||||
for preferred_name in preferred_names:
|
||||
for candidate in candidates:
|
||||
if Path(candidate).name.lower() == preferred_name:
|
||||
return candidate
|
||||
if preferred_name in names:
|
||||
return names[preferred_name]
|
||||
|
||||
from archivebox.plugins.discovery import get_plugin_output_extension_preference
|
||||
|
||||
@@ -4632,7 +4643,7 @@ class ArchiveResult(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithNotes):
|
||||
(".mp4", ".webm", ".mp3", ".opus", ".ogg", ".wav"),
|
||||
)
|
||||
for ext_group in ext_groups:
|
||||
group_candidates = [candidate for candidate in candidates if Path(candidate).suffix.lower() in ext_group]
|
||||
group_candidates = [candidate for candidate in candidates if extensions[candidate] in ext_group]
|
||||
if group_candidates:
|
||||
return max(
|
||||
group_candidates,
|
||||
|
||||
@@ -74,8 +74,9 @@ def recover_orchestrator_state(*, include_chrome: bool = False, crawl_id: str |
|
||||
**snapshot_filter,
|
||||
).update(retry_at=now, modified_at=now)
|
||||
|
||||
# Find rare uploads through hook_name's index before checking their parents;
|
||||
# a correlated upload probe otherwise visits every sealed snapshot.
|
||||
extension_upload_results = ArchiveResult.objects.filter(
|
||||
snapshot_id=OuterRef("pk"),
|
||||
hook_name=Snapshot.BROWSER_EXTENSION_UPLOAD_HOOK_NAME,
|
||||
)
|
||||
server_results = ArchiveResult.objects.filter(snapshot_id=OuterRef("pk")).exclude(
|
||||
@@ -83,15 +84,15 @@ def recover_orchestrator_state(*, include_chrome: bool = False, crawl_id: str |
|
||||
)
|
||||
extension_only_snapshots = (
|
||||
Snapshot.objects.filter(
|
||||
id__in=extension_upload_results.values("snapshot_id"),
|
||||
status=Snapshot.StatusChoices.SEALED,
|
||||
crawl__status__in=[Crawl.StatusChoices.QUEUED, Crawl.StatusChoices.STARTED, Crawl.StatusChoices.SEALED],
|
||||
**snapshot_filter,
|
||||
)
|
||||
.annotate(
|
||||
has_extension_upload=Exists(extension_upload_results),
|
||||
has_server_result=Exists(server_results),
|
||||
)
|
||||
.filter(has_extension_upload=True, has_server_result=False)
|
||||
.filter(has_server_result=False)
|
||||
)
|
||||
# Older browser-extension uploads could win a race with runner startup:
|
||||
# their successful external rows made the fresh Snapshot look finished
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
"""Shared foreground signal and child-process cleanup contracts.
|
||||
|
||||
Shutdown intent and process exit are separate: takeover must preserve the
|
||||
replacement owner's work, while a user abort must not later resume the crawl.
|
||||
Callers own that policy; this module preserves the received signal and stops
|
||||
only the process objects they explicitly supply.
|
||||
|
||||
Cooperative boundaries are essential, not just quieter exception handling.
|
||||
A real Linux CI regression interrupted Popen.poll() with SIGINT and stranded
|
||||
its wait lock. SIGKILL stopped the child (it became a zombie), but Popen.wait()
|
||||
still timed out. Raising a different exception or waiting longer cannot repair
|
||||
that interrupted critical section. Polling loops must record the signal first
|
||||
and honor it between operations; async owners may cancel their own task while
|
||||
their loop is running. Once exiting, the last-resort process cleanup must stay
|
||||
synchronous: the original loop may be closed, and creating a replacement loop
|
||||
cannot recover tasks or resources owned by it.
|
||||
|
||||
Keep immediate signal notices and repeated-signal force exit independent of
|
||||
the event bus: it may already be draining or closed during shutdown. Resumable
|
||||
pause/retry/skip belongs to the existing crawl controller and must not set
|
||||
sticky command-shutdown state. See foreground_shutdown_signals below for both
|
||||
cooperative and legacy blocking callers; do not change all callers' policy at
|
||||
once without supplying their interruption boundaries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -23,7 +48,7 @@ _active_shutdown_state: ShutdownSignalState | None = None
|
||||
|
||||
|
||||
def raise_if_shutdown_requested() -> None:
|
||||
"""Let long foreground loops honor a signal even if Python ignored it once."""
|
||||
"""Honor sticky shutdown intent at a caller-owned interruption boundary."""
|
||||
|
||||
if _active_shutdown_state and _active_shutdown_state.signal_name:
|
||||
raise KeyboardInterrupt
|
||||
@@ -48,7 +73,12 @@ def wait_popen_and_kill_children(
|
||||
timeout: float,
|
||||
kill_timeout: float = 2.0,
|
||||
) -> None:
|
||||
"""Wait for a Popen parent and then hard-kill any surviving descendants."""
|
||||
"""Reap our own Popen child and stop its explicitly captured descendants.
|
||||
|
||||
A dead child still needs its parent to reap it. Keep wait failures visible;
|
||||
bypassing Popen's lock would hide the interrupted-owner bug described above.
|
||||
Descendants must still be cleaned up when that wait fails.
|
||||
"""
|
||||
|
||||
try:
|
||||
try:
|
||||
@@ -62,6 +92,14 @@ def wait_popen_and_kill_children(
|
||||
except psutil.NoSuchProcess:
|
||||
state = {"pid": proc.pid, "status": "exited"}
|
||||
err.add_note(f"Process state after SIGKILL: {state}")
|
||||
# A locked Popen wait can time out even after the OS process exits.
|
||||
# Inspect CPython's lock only for diagnostics; never release it here.
|
||||
waitpid_lock = getattr(proc, "_waitpid_lock", None)
|
||||
err.add_note(
|
||||
f"Popen state: returncode={proc.returncode}, "
|
||||
f"waitpid_lock_locked={waitpid_lock.locked() if waitpid_lock is not None else None}, "
|
||||
f"threads={[thread.name for thread in threading.enumerate()]}",
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
# A parent that cannot be reaped must not leave its descendants running.
|
||||
@@ -114,11 +152,14 @@ def foreground_shutdown_signals(
|
||||
interrupt_handlers: dict[signal.Signals, Callable[[], None]] | None = None,
|
||||
raise_on_first_signal: bool = True,
|
||||
) -> Iterator[ShutdownSignalState]:
|
||||
"""Install foreground signal handlers that print an immediate exit notice.
|
||||
"""Keep shutdown intent visible until the owning foreground command unwinds.
|
||||
|
||||
Some log-tail loops intentionally swallow KeyboardInterrupt so that callers
|
||||
can centralize cleanup in finally blocks. The handler writes the signal name
|
||||
immediately, then raises KeyboardInterrupt to break out of the blocking read.
|
||||
Signal state was added because inner loops can swallow KeyboardInterrupt
|
||||
while their caller still needs to stop its children. Cooperative callers use
|
||||
raise_on_first_signal=False and check raise_if_shutdown_requested() between
|
||||
operations, or cancel their owned async task through on_signal. Raising from
|
||||
the handler can otherwise interrupt library locks or partially sent I/O.
|
||||
Blocking callers without a cooperative boundary retain immediate exceptions.
|
||||
"""
|
||||
|
||||
global _active_shutdown_state
|
||||
|
||||
+74
-19
@@ -17,7 +17,6 @@ from django.contrib.auth import HASH_SESSION_KEY, SESSION_KEY, get_user_model
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.core import signing
|
||||
from django.core.paginator import InvalidPage
|
||||
from django.db.models import Case, IntegerField, Q, Value, When
|
||||
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseForbidden, QueryDict
|
||||
from django.shortcuts import redirect, render
|
||||
@@ -48,7 +47,6 @@ from archivebox.config.configset import BaseConfigSet
|
||||
from archivebox.core.forms import AddLinkForm
|
||||
from archivebox.core.models import ArchiveResult, Snapshot, SnapshotTag
|
||||
from archivebox.core.permissions import (
|
||||
PERMISSIONS_PRIVATE,
|
||||
PERMISSIONS_PUBLIC,
|
||||
PERMISSIONS_UNLISTED,
|
||||
can_view_snapshot,
|
||||
@@ -92,6 +90,7 @@ from archivebox.plugins.discovery import (
|
||||
from archivebox.plugins.forms import get_plugin_config_binary_urls
|
||||
from archivebox.plugins.views import get_config_definition_link
|
||||
from archivebox.progressmonitor.views import live_progress_view
|
||||
from archivebox.progressmonitor.collection import collection_summary
|
||||
from archivebox.search.config import (
|
||||
get_search_mode,
|
||||
get_search_mode_backend,
|
||||
@@ -1213,6 +1212,18 @@ class OriginalDomainReplayView(View):
|
||||
|
||||
|
||||
class PublicIndexView(ListView):
|
||||
"""Keep browsing responsive while the same small server is archiving.
|
||||
|
||||
Performance takes priority over perfect initial totals: on a 1 vCPU / 1 GB
|
||||
host with hundreds of thousands of snapshots, even an indexed count can
|
||||
consume the entire roughly 0.5-0.7 second page-load budget. Benchmark with
|
||||
the runner active, since it shares the CPU, memory, and SQLite database.
|
||||
|
||||
The unfiltered list therefore renders rows first and accepts missing/stale
|
||||
display totals. A later ordinary reload can show the background-cached
|
||||
count; do not restore a synchronous COUNT or add polling just to fill it in.
|
||||
"""
|
||||
|
||||
template_name = "public_index.html"
|
||||
model = Snapshot
|
||||
ordering: ClassVar[list[str]] = ["-bookmarked_at", "-created_at"]
|
||||
@@ -1238,14 +1249,28 @@ class PublicIndexView(ListView):
|
||||
"status",
|
||||
"output_size",
|
||||
"permissions",
|
||||
# Modern replay URLs need the filesystem version and owner's name.
|
||||
# Deferring these caused four extra SELECTs per displayed snapshot.
|
||||
"fs_version",
|
||||
"crawl__created_by__username",
|
||||
)
|
||||
|
||||
def _ordered_public_page_from_order_index(self, *, page_number: int, page_size: int) -> list[Snapshot] | None:
|
||||
target_count = page_number * page_size
|
||||
def _ordered_public_page_from_order_index(self, *, page_number: int, page_size: int) -> list[Snapshot]:
|
||||
# One extra row determines Next without a full collection count. Cached
|
||||
# totals can be stale while the runner adds/removes snapshots, so they
|
||||
# must never decide whether the requested page exists.
|
||||
target_count = page_number * page_size + 1
|
||||
public_snapshots: list[Snapshot] = []
|
||||
scanned = 0
|
||||
chunk_size = max(self.public_page_scan_chunk_size, page_size)
|
||||
ordered_snapshots = Snapshot.objects.order_by(*self.ordering).only(*self._base_public_snapshot_fields())
|
||||
chunk_size = max(self.public_page_scan_chunk_size, page_size + 1)
|
||||
# Read in snapshot_public_order_idx order and filter visibility in small
|
||||
# batches. Combining the permissions predicate with ORDER BY can make
|
||||
# SQLite select the permissions index and sort the whole public archive
|
||||
# before LIMIT. This favors the common early pages without a new index;
|
||||
# deep pages or mostly private collections can still require more reads.
|
||||
ordered_snapshots = (
|
||||
Snapshot.objects.select_related("crawl__created_by").order_by(*self.ordering).only(*self._base_public_snapshot_fields())
|
||||
)
|
||||
|
||||
while len(public_snapshots) < target_count:
|
||||
chunk = list(ordered_snapshots[scanned : scanned + chunk_size])
|
||||
@@ -1261,20 +1286,39 @@ class PublicIndexView(ListView):
|
||||
if self.request.GET.get("q", default="").strip():
|
||||
return super().paginate_queryset(queryset, page_size)
|
||||
|
||||
public_count = self.get_exact_public_snapshot_count()
|
||||
paginator = self.get_paginator(range(public_count), page_size)
|
||||
page_kwarg = self.kwargs.get(self.page_kwarg)
|
||||
page_query = self.request.GET.get(self.page_kwarg)
|
||||
page_number = page_kwarg or page_query or 1
|
||||
|
||||
try:
|
||||
page = paginator.page(page_number)
|
||||
except InvalidPage as err:
|
||||
page_number = int(page_number)
|
||||
if page_number < 1:
|
||||
raise ValueError("Page must be positive")
|
||||
except (ValueError, TypeError) as err:
|
||||
raise Http404(f"Invalid page ({page_number}): {err}") from err
|
||||
|
||||
object_list = self._ordered_public_page_from_order_index(page_number=page.number, page_size=page_size)
|
||||
page.object_list = object_list
|
||||
return paginator, page, object_list, page.has_other_pages()
|
||||
snapshots = self._ordered_public_page_from_order_index(page_number=page_number, page_size=page_size)
|
||||
if not snapshots and page_number > 1:
|
||||
raise Http404(f"Invalid page ({page_number}): No snapshots")
|
||||
# Exhausting the rows gives an exact total for this read at no extra
|
||||
# cost. Otherwise the lookahead is only a lower bound: an absent count
|
||||
# is preferable to an on-request scan or a cached total smaller than
|
||||
# the rows we just observed. Cached overestimates are acceptable here.
|
||||
observed_count = (page_number - 1) * page_size + len(snapshots)
|
||||
self.public_snapshot_count = observed_count
|
||||
if len(snapshots) > page_size:
|
||||
summary = collection_summary(self.request.user, public=True)
|
||||
cached_count = summary["snapshots"] if summary is not None else None
|
||||
self.public_snapshot_count = cached_count if cached_count is not None and cached_count >= observed_count else None
|
||||
|
||||
# Give Django an in-memory lower bound, never the collection QuerySet
|
||||
# whose Paginator.count would issue COUNT(*). Navigation uses this read's
|
||||
# lookahead even when a stale total would hide a newly added page.
|
||||
# The template must use the separate display totals, not num_pages here.
|
||||
paginator = self.get_paginator(range(observed_count), page_size)
|
||||
page = paginator.page(page_number)
|
||||
page.object_list = snapshots[:page_size]
|
||||
return paginator, page, page.object_list, page.has_other_pages()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
runtime_config = self.__dict__.get("runtime_config")
|
||||
@@ -1303,10 +1347,16 @@ class PublicIndexView(ListView):
|
||||
and search_mode_backend
|
||||
and context["paginator"].count == 0,
|
||||
)
|
||||
context["public_snapshot_count"] = context["paginator"].count if query else self.public_snapshot_count
|
||||
count = context["public_snapshot_count"]
|
||||
context["public_page_count"] = (
|
||||
max(1, (count + context["paginator"].per_page - 1) // context["paginator"].per_page) if count is not None else None
|
||||
)
|
||||
snapshots = list(context.get("object_list") or ())
|
||||
from archivebox.plugins.output_groups import plugin_output_sizes
|
||||
|
||||
all_results_by_snapshot = {str(snapshot.id): [] for snapshot in snapshots}
|
||||
snapshots_by_id = {str(snapshot.id): snapshot for snapshot in snapshots}
|
||||
icons_by_snapshot: dict[str, dict[str, ArchiveResult]] = {str(snapshot.id): {} for snapshot in snapshots}
|
||||
tag_names_by_snapshot: dict[str, list[str]] = {str(snapshot.id): [] for snapshot in snapshots}
|
||||
list_icon_paths_by_snapshot: dict[str, list[str]] = {str(snapshot.id): [] for snapshot in snapshots}
|
||||
@@ -1348,6 +1398,12 @@ class PublicIndexView(ListView):
|
||||
# otherwise keeps SQLite's read lock throughout rendering.
|
||||
for result in list(results):
|
||||
snapshot_key = str(result.snapshot_id)
|
||||
# Thumbnail URLs traverse result.snapshot too; share the page's
|
||||
# already-loaded parent rather than fetching it once per result.
|
||||
result.snapshot = snapshots_by_id[snapshot_key]
|
||||
# These read-only page objects are shared by icons, sizes, and
|
||||
# thumbnails. Normalize each manifest once for this render.
|
||||
result._render_output_file_map = result.output_file_map()
|
||||
all_results_by_snapshot[snapshot_key].append(result)
|
||||
progress = progress_by_snapshot[snapshot_key]
|
||||
progress["total"] += 1
|
||||
@@ -1379,13 +1435,12 @@ class PublicIndexView(ListView):
|
||||
context["object_list"] = snapshots
|
||||
return context
|
||||
|
||||
def get_exact_public_snapshot_count(self) -> int:
|
||||
hidden_count = Snapshot.objects.filter(permissions=PERMISSIONS_PRIVATE).count()
|
||||
hidden_count += Snapshot.objects.filter(permissions=PERMISSIONS_UNLISTED).count()
|
||||
return Snapshot.objects.count() - hidden_count
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = public_snapshots_queryset(super().get_queryset(**kwargs)).only(*self._base_public_snapshot_fields())
|
||||
qs = (
|
||||
public_snapshots_queryset(super().get_queryset(**kwargs))
|
||||
.select_related("crawl__created_by")
|
||||
.only(*self._base_public_snapshot_fields())
|
||||
)
|
||||
query = self.request.GET.get("q", default="").strip()
|
||||
|
||||
if not query:
|
||||
|
||||
@@ -526,6 +526,8 @@ HISTORICAL_GHOST_MIGRATIONS: frozenset[tuple[str, str]] = frozenset(
|
||||
("core", "0053_remove_snapshottag_snapshot_old"),
|
||||
("core", "0054_alter_snapshot_timestamp"),
|
||||
("core", "0055_alter_tag_slug"),
|
||||
# Dev-only progress indexes removed in favor of existing-index queries.
|
||||
("core", "0056_progress_covering_indexes"),
|
||||
("core", "0056_remove_tag_uuid"),
|
||||
("core", "0057_rename_id_tag_old_id"),
|
||||
("core", "0058_alter_tag_old_id"),
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
"""Non-blocking, database-only collection totals for the progress monitor."""
|
||||
"""Cached display totals for progress polling and the public snapshot list.
|
||||
|
||||
Missing/stale statistics are intentional: returning useful rows and progress
|
||||
quickly matters more than exact initial counts. Each viewer may poll once per
|
||||
second; repeating aggregates per request would consume the small server's CPU
|
||||
and hold SQLite read locks needed by the archiving runner. Callers must accept
|
||||
None on a cold cache and must never wait for the refresh or count synchronously.
|
||||
|
||||
Background reads still take normal SQLite read locks. Existing indexes, short
|
||||
batches where needed, and throttled refreshes limit their cost; moving work to
|
||||
a thread does not make a scan free or require switching the database to WAL.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from threading import Lock, Thread
|
||||
@@ -9,14 +20,77 @@ from django.db import close_old_connections, connections
|
||||
from django.db.models import Count, Sum
|
||||
|
||||
|
||||
# This is only a process-local gate for background work, not a DB/file lock.
|
||||
# Every request tries it without waiting; a busy refresh means returning the
|
||||
# cached value (or None). Serializing refreshes also avoids competing scans for
|
||||
# different crawls/users when many viewer tabs open at once.
|
||||
_refresh_lock = Lock()
|
||||
|
||||
|
||||
def collection_summary(user):
|
||||
user_id = None if user.is_superuser else user.pk
|
||||
key = f"progress-collection:{'all' if user_id is None else user_id}"
|
||||
def crawl_summary(crawl_id, *, snapshot_id=None):
|
||||
"""Sample sealed snapshot metadata without adding table scans to polls."""
|
||||
key = f"progress-crawl:{crawl_id}:{snapshot_id or 'all'}"
|
||||
summary = cache.get(key)
|
||||
# Even an indexed SUM scans the index. Never make an HTTP request wait for
|
||||
if (summary is None or time() - summary["sampled_at"] >= 30) and _refresh_lock.acquire(blocking=False):
|
||||
if not cache.add(f"{key}:refresh", True, timeout=30):
|
||||
_refresh_lock.release()
|
||||
return summary
|
||||
|
||||
def refresh():
|
||||
try:
|
||||
close_old_connections()
|
||||
from archivebox.core.models import Snapshot
|
||||
|
||||
snapshots = Snapshot.objects.filter(crawl_id=crawl_id, status=Snapshot.StatusChoices.SEALED)
|
||||
if snapshot_id is not None:
|
||||
snapshots = snapshots.filter(pk=snapshot_id)
|
||||
snapshots = snapshots.order_by("modified_at").values_list("modified_at", "downloaded_at", "output_size")
|
||||
totals = {"snapshots": 0, "cancelled": 0, "bytes": 0}
|
||||
modified_at, offset = None, 0
|
||||
while True:
|
||||
batch_scope = snapshots.filter(modified_at__gte=modified_at) if modified_at is not None else snapshots
|
||||
# Use (crawl, status, modified_at), releasing the SQLite read
|
||||
# cursor after each small batch. Never hold a cursor over the
|
||||
# whole crawl, even in this background worker.
|
||||
batch = list(batch_scope[offset : offset + 100])
|
||||
for _, downloaded_at, size in batch:
|
||||
totals["snapshots"] += 1
|
||||
totals["cancelled"] += downloaded_at is None
|
||||
totals["bytes"] += size or 0
|
||||
if len(batch) < 100:
|
||||
break
|
||||
last_modified_at = batch[-1][0]
|
||||
# Only offset within ties; otherwise advance the indexed
|
||||
# timestamp range instead of rescanning previous pages.
|
||||
offset = (offset if last_modified_at == modified_at else 0) + sum(row[0] == last_modified_at for row in batch)
|
||||
modified_at = last_modified_at
|
||||
totals["sampled_at"] = time()
|
||||
cache.set(key, totals, timeout=3600)
|
||||
except Exception:
|
||||
logging.getLogger(__name__).exception("Could not refresh crawl totals")
|
||||
finally:
|
||||
connections.close_all()
|
||||
_refresh_lock.release()
|
||||
|
||||
try:
|
||||
Thread(target=refresh, name="crawl-summary", daemon=True).start()
|
||||
except RuntimeError:
|
||||
_refresh_lock.release()
|
||||
logging.getLogger(__name__).exception("Could not start crawl totals refresh")
|
||||
return summary
|
||||
|
||||
|
||||
def collection_summary(user, *, public=False):
|
||||
"""Return cached totals immediately and, when due, start one refresh.
|
||||
|
||||
Public pages need no extra AJAX/polling for these totals: an ordinary later
|
||||
page load reads the populated cache. Keep this best-effort display cache
|
||||
separate from pagination/authorization decisions, which need actual rows.
|
||||
"""
|
||||
user_id = None if user.is_superuser else user.pk
|
||||
key = "progress-collection:public" if public else f"progress-collection:{'all' if user_id is None else user_id}"
|
||||
summary = cache.get(key)
|
||||
# Even indexed COUNT/SUM aggregates scan the index. Never make HTTP wait for
|
||||
# that scan on a large collection. Serve stale totals while one worker refreshes;
|
||||
# a single non-blocking lock prevents a queue of scans from concurrent clients.
|
||||
if (summary is None or time() - summary["sampled_at"] >= 60) and _refresh_lock.acquire(blocking=False):
|
||||
@@ -32,13 +106,29 @@ def collection_summary(user):
|
||||
from archivebox.core.models import Snapshot
|
||||
|
||||
snapshots = Snapshot.objects.all()
|
||||
if user_id is not None:
|
||||
snapshots = snapshots.filter(crawl__created_by_id=user_id)
|
||||
# COUNT(*) + SUM(output_size) can use Snapshot's covering size index.
|
||||
# Snapshot.output_size is maintained when ArchiveResult sizes change.
|
||||
totals = snapshots.aggregate(snapshots=Count("*"), bytes=Sum("output_size"))
|
||||
totals["bytes"] = totals["bytes"] or 0
|
||||
if public:
|
||||
from archivebox.core.permissions import PERMISSIONS_PUBLIC
|
||||
|
||||
# SQLite can choose a table scan for equality after ANALYZE
|
||||
# on an all-public archive. Equal range bounds keep the count
|
||||
# on the existing permissions index. It is still O(n), so
|
||||
# public pages share this background cache, never await it.
|
||||
# Count the indexed, non-null permissions column so this
|
||||
# needs no Snapshot table reads or new covering-index migration.
|
||||
totals = snapshots.filter(permissions__gte=PERMISSIONS_PUBLIC, permissions__lte=PERMISSIONS_PUBLIC).aggregate(
|
||||
snapshots=Count("permissions"),
|
||||
)
|
||||
else:
|
||||
if user_id is not None:
|
||||
snapshots = snapshots.filter(crawl__created_by_id=user_id)
|
||||
# COUNT(*) + SUM(output_size) can use Snapshot's covering size index.
|
||||
# Snapshot.output_size is maintained when ArchiveResult sizes change.
|
||||
totals = snapshots.aggregate(snapshots=Count("*"), bytes=Sum("output_size"))
|
||||
totals["bytes"] = totals.get("bytes") or 0
|
||||
totals["sampled_at"] = time()
|
||||
# Retain old totals longer than the 60s refresh interval so a
|
||||
# slow/failed refresh does not force every tab back to a cold
|
||||
# count. Staleness is acceptable; blocking page loads is not.
|
||||
cache.set(key, totals, timeout=3600)
|
||||
except Exception:
|
||||
logging.getLogger(__name__).exception("Could not refresh collection totals")
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Literal
|
||||
from abx_dl.events import PROCESS_EXIT_SKIPPED
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError
|
||||
from django.db.models import CharField, Count, Q, Sum
|
||||
from django.db.models import CharField, Count, Q
|
||||
from django.db.models.functions import Cast
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.utils import timezone
|
||||
@@ -235,8 +235,8 @@ def live_progress_view(request, *, authorized_snapshot=None):
|
||||
ArchiveResult.StatusChoices.QUEUED,
|
||||
ArchiveResult.StatusChoices.STARTED,
|
||||
)
|
||||
# Start from the small set of open results and count from the covering
|
||||
# status/plugin index. Filtering by plugin first scans historical runs.
|
||||
# Start from open results using the existing status index. Filtering by
|
||||
# plugin first scans historical runs, including all completed results.
|
||||
result_counts = list(
|
||||
archiveresult_scope.filter(status__in=result_statuses).order_by().values("status", "plugin").annotate(count=Count("*")),
|
||||
)
|
||||
@@ -336,21 +336,15 @@ def live_progress_view(request, *, authorized_snapshot=None):
|
||||
persona_details_by_name[persona.name] = persona_details
|
||||
active_crawl_ids = [crawl["id"] for crawl in active_crawls_list]
|
||||
snapshot_counts_by_crawl: dict[str, dict[str, int]] = {str(crawl_id): {} for crawl_id in active_crawl_ids}
|
||||
cancelled_snapshot_counts_by_crawl: dict[str, int] = {str(crawl_id): 0 for crawl_id in active_crawl_ids}
|
||||
crawl_output_sizes_by_crawl: dict[str, int] = {str(crawl_id): 0 for crawl_id in active_crawl_ids}
|
||||
queued_snapshot_overflow_by_crawl: dict[str, int] = {str(crawl_id): 0 for crawl_id in active_crawl_ids}
|
||||
active_snapshot_scope = snapshot_scope.filter(crawl_id__in=active_crawl_ids)
|
||||
if active_crawl_ids:
|
||||
for row in (
|
||||
active_snapshot_scope.order_by()
|
||||
.values("crawl_id", "status")
|
||||
.annotate(count=Count("*"), downloaded=Count("downloaded_at"), size=Sum("output_size"))
|
||||
):
|
||||
# The existing (crawl, status, modified_at) index covers these counts.
|
||||
# Byte totals and cancellation details require table reads, so sample
|
||||
# those separately and share cached values between viewer tabs.
|
||||
for row in active_snapshot_scope.order_by().values("crawl_id", "status").annotate(count=Count("*")):
|
||||
crawl_id = str(row["crawl_id"])
|
||||
snapshot_counts_by_crawl[crawl_id][row["status"]] = row["count"]
|
||||
if row["status"] == Snapshot.StatusChoices.SEALED:
|
||||
cancelled_snapshot_counts_by_crawl[crawl_id] = row["count"] - row["downloaded"]
|
||||
crawl_output_sizes_by_crawl[crawl_id] = int(row["size"] or 0)
|
||||
|
||||
crawl_process_pids: dict[str, int] = {}
|
||||
snapshot_process_pids: dict[str, int] = {}
|
||||
@@ -569,7 +563,14 @@ def live_progress_view(request, *, authorized_snapshot=None):
|
||||
completed_snapshots = crawl_snapshot_counts.get(Snapshot.StatusChoices.SEALED, 0)
|
||||
started_snapshots = crawl_snapshot_counts.get(Snapshot.StatusChoices.STARTED, 0)
|
||||
pending_snapshots = crawl_snapshot_counts.get(Snapshot.StatusChoices.QUEUED, 0)
|
||||
cancelled_snapshots = cancelled_snapshot_counts_by_crawl.get(crawl_id, 0)
|
||||
from archivebox.progressmonitor.collection import crawl_summary
|
||||
|
||||
totals = (
|
||||
crawl_summary(crawl_id, snapshot_id=scoped_snapshot.pk if scoped_snapshot is not None else None)
|
||||
if completed_snapshots
|
||||
else {"snapshots": 0, "cancelled": 0, "bytes": 0}
|
||||
)
|
||||
cancelled_snapshots = totals["cancelled"] if totals is not None else None
|
||||
|
||||
# Count URLs in the crawl (for when snapshots haven't been created yet)
|
||||
urls_count = 0
|
||||
@@ -790,8 +791,8 @@ def live_progress_view(request, *, authorized_snapshot=None):
|
||||
persona_details = persona_details_by_id.get(str(crawl["persona_id"])) if crawl["persona_id"] else None
|
||||
persona_name = persona_details["name"] if persona_details else "Default"
|
||||
persona_details = persona_details or persona_details_by_name.get(persona_name)
|
||||
crawl_output_size = crawl_output_sizes_by_crawl.get(crawl_id, 0)
|
||||
avg_snapshot_size = int(crawl_output_size / completed_snapshots) if completed_snapshots else 0
|
||||
crawl_output_size = totals["bytes"] if totals is not None else None
|
||||
avg_snapshot_size = int(totals["bytes"] / max(totals["snapshots"], 1)) if totals is not None else None
|
||||
# These crawl-scope limits are frozen in Crawl.config. Rendering
|
||||
# them must not resolve personas, probe binaries, or reread config.
|
||||
max_urls = int(crawl["config"].get("CRAWL_MAX_URLS", request_config.CRAWL_MAX_URLS) or 0)
|
||||
@@ -841,8 +842,8 @@ def live_progress_view(request, *, authorized_snapshot=None):
|
||||
"max_snapshot_size_display": printable_filesize(snapshot_max_size) if snapshot_max_size else "unlimited",
|
||||
"crawl_output_size": crawl_output_size,
|
||||
"avg_snapshot_size": avg_snapshot_size,
|
||||
"crawl_output_size_display": printable_filesize(crawl_output_size) if crawl_output_size else "0 B",
|
||||
"avg_snapshot_size_display": printable_filesize(avg_snapshot_size) if avg_snapshot_size else "0 B",
|
||||
"crawl_output_size_display": printable_filesize(crawl_output_size or 0) if totals is not None else "…",
|
||||
"avg_snapshot_size_display": printable_filesize(avg_snapshot_size or 0) if totals is not None else "…",
|
||||
"tags": crawl_tags,
|
||||
"urls_count": urls_count,
|
||||
"total_snapshots": total_snapshots,
|
||||
|
||||
@@ -224,6 +224,7 @@ class CrawlRunner:
|
||||
self.snapshot_service = SnapshotService(
|
||||
self.bus,
|
||||
crawl_id=str(crawl.id),
|
||||
interrupted=self.execution_interrupted,
|
||||
)
|
||||
HookArchiveResultService(self.bus, emit_jsonl=False)
|
||||
ArchiveResultService(self.bus)
|
||||
@@ -258,6 +259,10 @@ class CrawlRunner:
|
||||
self._signal_abort_requested = False
|
||||
self._last_lease_heartbeat_at = 0.0
|
||||
|
||||
def execution_interrupted(self) -> bool:
|
||||
"""Owner loss preserves work; explicit user abort keeps its sealing policy."""
|
||||
return self._signal_abort_requested and not self._user_aborted
|
||||
|
||||
async def on_CrawlAbortEvent(self, event: CrawlAbortEvent) -> None:
|
||||
self._signal_abort_requested = True
|
||||
# An abort can arrive while the prompt transport is being established,
|
||||
@@ -1131,18 +1136,17 @@ class CrawlRunner:
|
||||
finally:
|
||||
cancel_watcher.cancel()
|
||||
await asyncio.gather(cancel_watcher, return_exceptions=True)
|
||||
completed_event = event.emit(
|
||||
CrawlCompletedEvent(
|
||||
url=snapshot["url"],
|
||||
snapshot_id=snapshot["id"],
|
||||
output_dir=str(output_dir),
|
||||
),
|
||||
)
|
||||
# Same signal lifecycle as CrawlCleanupEvent above: completion is a
|
||||
# normal bus event unless the interpreter is already unwinding from
|
||||
# SIGINT/SIGTERM/SIGHUP, where synchronous bus delivery is no
|
||||
# longer a dependable shutdown primitive.
|
||||
if not self._signal_abort_requested:
|
||||
# emit() schedules handlers immediately; guarding only the await
|
||||
# still lets completion projectors run during takeover. An aborted
|
||||
# execution owner must leave durable work for its replacement.
|
||||
if not self.execution_interrupted():
|
||||
completed_event = event.emit(
|
||||
CrawlCompletedEvent(
|
||||
url=snapshot["url"],
|
||||
snapshot_id=snapshot["id"],
|
||||
output_dir=str(output_dir),
|
||||
),
|
||||
)
|
||||
await _run_event_now(completed_event, CrawlCompletedEvent.model_fields["event_timeout"].default)
|
||||
|
||||
on_archivebox_CrawlStartEvent.__name__ = "on_archivebox_CrawlStartEvent__run_snapshots"
|
||||
@@ -1279,6 +1283,8 @@ class CrawlRunner:
|
||||
raise RuntimeError(f"Snapshot {snapshot_id} did not complete")
|
||||
await completed_snapshot.wait(timeout=snapshot_phase_timeout)
|
||||
await completed_snapshot.event_results_list()
|
||||
if self.execution_interrupted():
|
||||
return
|
||||
if snapshot["status"] == "sealed":
|
||||
await sync_to_async(run_snapshot_maintenance, thread_sensitive=True)(snapshot_id, output_dir=output_dir)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
@@ -73,10 +74,11 @@ def finalize_completed_snapshot(
|
||||
modified_at=timezone.now(),
|
||||
)
|
||||
|
||||
# SnapshotCompletedEvent is abx-dl's authoritative signal that the complete
|
||||
# snapshot hook sequence (including cleanup) finished. ArchiveResult rows
|
||||
# are projections of that work, never prerequisites used to decide whether
|
||||
# the Snapshot may seal.
|
||||
# SnapshotCompletedEvent finishes an attempt, including abort cleanup. An
|
||||
# explicit user abort deliberately seals that point-in-time capture; the
|
||||
# runner's interrupted-owner guard below keeps takeover work resumable.
|
||||
# ArchiveResult rows are projections of that work, never prerequisites used
|
||||
# to decide whether the Snapshot may seal.
|
||||
if not was_sealed and snapshot.status == Snapshot.StatusChoices.STARTED and snapshot.retry_at == owned_retry_at:
|
||||
snapshot.seal()
|
||||
snapshot.refresh_from_db()
|
||||
@@ -120,8 +122,9 @@ class SnapshotService(BaseService):
|
||||
LISTENS_TO = [SnapshotEvent, SnapshotCompletedEvent]
|
||||
EMITS = []
|
||||
|
||||
def __init__(self, bus, *, crawl_id: str):
|
||||
def __init__(self, bus, *, crawl_id: str, interrupted: Callable[[], bool] | None = None):
|
||||
self.crawl_id = crawl_id
|
||||
self.interrupted = interrupted
|
||||
self._run_ownership: dict[str, tuple[str, object, bool, list[str]]] = {}
|
||||
self._ownership_lock = asyncio.Lock()
|
||||
super().__init__(bus)
|
||||
@@ -175,6 +178,15 @@ class SnapshotService(BaseService):
|
||||
if ownership is None or ownership[0] != str(event.event_parent_id):
|
||||
return
|
||||
self._run_ownership.pop(snapshot_id, None)
|
||||
# abx-dl also emits completion after abort cleanup, even before the
|
||||
# first hook. Explicit user aborts seal a point-in-time capture so later
|
||||
# work cannot silently mix observations taken days apart; reopening it
|
||||
# is a separate user action. Takeover only replaces its execution owner.
|
||||
# Consult the runner's synchronous signal state: an exiting owner cannot
|
||||
# rely on another bus event arriving before this completion projector runs.
|
||||
# Leave its durable lease for the next elected runner's startup recovery.
|
||||
if self.interrupted is not None and self.interrupted():
|
||||
return
|
||||
_, owned_retry_at, was_sealed, retry_plugins = ownership
|
||||
await sync_to_async(finalize_completed_snapshot, thread_sensitive=True)(
|
||||
event.snapshot_id,
|
||||
|
||||
@@ -654,12 +654,19 @@
|
||||
</span>
|
||||
<input type="submit" value="Search">
|
||||
</form>
|
||||
{% comment %}
|
||||
Missing/stale totals are intentional on large collections: show rows
|
||||
immediately and let a later normal reload pick up the cached count.
|
||||
Do not fill this in with an extra count request or paginator.count:
|
||||
the unfiltered paginator only knows the rows needed for navigation.
|
||||
Use the optional public_* totals here and in the footer below.
|
||||
{% endcomment %}
|
||||
<div class="public-snapshot-count">
|
||||
{% if public_search_stream_pending %}
|
||||
<span class="archivebox-search-stream-status" role="status" aria-live="polite">Searching...</span>
|
||||
{% else %}
|
||||
{{ page_obj.start_index }}-{{ page_obj.end_index }} of {{ page_obj.paginator.count }}
|
||||
<span>(page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }})</span>
|
||||
{{ page_obj.start_index }}-{{ page_obj.end_index }}{% if public_snapshot_count != None %} of {{ public_snapshot_count }}{% endif %}
|
||||
<span>(page {{ page_obj.number }}{% if public_page_count != None %} of {{ public_page_count }}{% endif %})</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -669,7 +676,7 @@
|
||||
<tr>
|
||||
<th class="snapshot-time">Saved</th>
|
||||
<th class="snapshot-preview-cell">Preview</th>
|
||||
<th>Snapshot ({{ page_obj.paginator.count }})</th>
|
||||
<th>Snapshot{% if public_snapshot_count != None %} ({{ public_snapshot_count }}){% endif %}</th>
|
||||
<th class="snapshot-tags-cell">Tags</th>
|
||||
<th class="snapshot-status-cell">Status</th>
|
||||
<th class="snapshot-files-cell">Files</th>
|
||||
@@ -697,7 +704,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="public-pagination">
|
||||
Showing {{ page_obj.start_index }}-{{ page_obj.end_index }} of {{ page_obj.paginator.count }} total
|
||||
Showing {{ page_obj.start_index }}-{{ page_obj.end_index }}{% if public_snapshot_count != None %} of {{ public_snapshot_count }} total{% endif %}
|
||||
<br>
|
||||
<span class="step-links">
|
||||
{% if page_obj.has_previous %}
|
||||
@@ -706,14 +713,14 @@
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
Page {{ page_obj.number }}{% if public_page_count != None %} of {{ public_page_count }}{% endif %}
|
||||
</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?{% url_replace page=page_obj.next_page_number %}">next</a>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?{% url_replace page=page_obj.paginator.num_pages %}">last »</a>
|
||||
{% if page_obj.has_next and public_page_count != None %}
|
||||
<a href="?{% url_replace page=public_page_count %}">last »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -296,9 +296,11 @@ def test_archiveresult_event_create_uses_one_result_lookup():
|
||||
assert len(result_lookups) == 1
|
||||
|
||||
|
||||
def test_process_completed_projects_failed_archiveresult_from_shipped_hook(tmp_path, hermetic_lib_dir):
|
||||
def test_failed_shipped_title_hook_projects_failure_without_overwriting_title(tmp_path, hermetic_lib_dir):
|
||||
from archivebox.core.models import ArchiveResult
|
||||
|
||||
# Result linkage and the unchanged title are consequences of the same failed
|
||||
# hook. Check them together so this isolated runtime is installed only once.
|
||||
snapshot = _create_snapshot()
|
||||
process, result = _run_shipped_snapshot_hook(
|
||||
snapshot,
|
||||
@@ -310,22 +312,6 @@ def test_process_completed_projects_failed_archiveresult_from_shipped_hook(tmp_p
|
||||
assert result.status == ArchiveResult.StatusChoices.FAILED
|
||||
assert result.process_id == process.id
|
||||
assert "Chrome session" in result.output_str
|
||||
_cleanup_machine_process_rows()
|
||||
|
||||
|
||||
def test_failed_title_archiveresult_does_not_overwrite_snapshot_title(tmp_path, hermetic_lib_dir):
|
||||
from archivebox.core.models import ArchiveResult
|
||||
|
||||
snapshot = _create_snapshot()
|
||||
_, result = _run_shipped_snapshot_hook(
|
||||
snapshot,
|
||||
plugin="title",
|
||||
hook_name="on_Snapshot__54_title.js",
|
||||
lib_dir=hermetic_lib_dir,
|
||||
expected_exit_codes=(1,),
|
||||
)
|
||||
assert result.status == ArchiveResult.StatusChoices.FAILED
|
||||
assert "Chrome session" in result.output_str
|
||||
snapshot.refresh_from_db()
|
||||
assert snapshot.title in (None, "")
|
||||
assert snapshot.resolved_title == ""
|
||||
|
||||
@@ -36,15 +36,26 @@ from .test_cli_add_1 import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def progress_page(httpserver):
|
||||
# Exercise the real HTTP fetch and parser with fixed content. example.com's
|
||||
# outgoing IANA link has changed independently of ArchiveBox releases.
|
||||
httpserver.expect_request("/").respond_with_data(
|
||||
'<html><body><a href="https://iana.org/domains/example">Example domains</a></body></html>',
|
||||
content_type="text/html",
|
||||
)
|
||||
return httpserver.url_for("/")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("terminal_stream", ["stdout", "stderr"])
|
||||
def test_add_renders_live_progress_in_foreground_terminal(initialized_archive, terminal_stream):
|
||||
def test_add_renders_live_progress_in_foreground_terminal(initialized_archive, terminal_stream, progress_page):
|
||||
master, slave = pty.openpty()
|
||||
termios.tcsetwinsize(slave, (40, 160))
|
||||
output = bytearray()
|
||||
result = None
|
||||
try:
|
||||
result = run_archivebox_cmd(
|
||||
["add", "--plugins=parse_txt_urls", "https://example.com"],
|
||||
["add", "--plugins=parse_txt_urls", progress_page],
|
||||
cwd=initialized_archive,
|
||||
env=cli_env(USE_COLOR="True", SHOW_PROGRESS="True"),
|
||||
**{terminal_stream: slave},
|
||||
@@ -65,7 +76,7 @@ def test_add_renders_live_progress_in_foreground_terminal(initialized_archive, t
|
||||
with use_archivebox_db(initialized_archive):
|
||||
assert Crawl.objects.get().status == Crawl.StatusChoices.SEALED
|
||||
snapshot = Snapshot.objects.get()
|
||||
assert snapshot.url == "https://example.com"
|
||||
assert snapshot.url == progress_page
|
||||
parsed_urls = Path(snapshot.output_dir) / "parse_txt_urls" / "urls.jsonl"
|
||||
assert json.loads(parsed_urls.read_text())["url"] == "https://iana.org/domains/example"
|
||||
finally:
|
||||
@@ -76,9 +87,9 @@ def test_add_renders_live_progress_in_foreground_terminal(initialized_archive, t
|
||||
os.close(master)
|
||||
|
||||
|
||||
def test_add_redirected_progress_remains_plain_text(initialized_archive):
|
||||
def test_add_redirected_progress_remains_plain_text(initialized_archive, progress_page):
|
||||
result = run_archivebox_cmd(
|
||||
["add", "--plugins=parse_txt_urls", "https://example.com"],
|
||||
["add", "--plugins=parse_txt_urls", progress_page],
|
||||
cwd=initialized_archive,
|
||||
env=cli_env(),
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ from archivebox.tests.conftest import (
|
||||
cli_env,
|
||||
run_archivebox_cmd,
|
||||
run_queued_crawls,
|
||||
resolve_abxpkg_chrome_env,
|
||||
)
|
||||
|
||||
from archivebox.tests.test_orm_helpers import use_archivebox_db
|
||||
@@ -114,6 +115,18 @@ def test_add_interrupts_background_only_capture(initialized_archive, choice):
|
||||
|
||||
|
||||
def test_background_completions_render_above_interrupt_prompt(initialized_archive, recursive_test_site):
|
||||
env = cli_env(WGET_ARGS_EXTRA='["--limit-rate=20"]', INFINISCROLL_SCROLL_DELAY="10000", CHROME_HEADLESS="True")
|
||||
# The timed observation is for a running infinite-scroll hook. Resolve its
|
||||
# real browser and wget dependencies before the readiness window begins.
|
||||
installed = run_archivebox_cmd(
|
||||
["install", "wget", "chrome"],
|
||||
cwd=initialized_archive,
|
||||
env=env,
|
||||
timeout=600,
|
||||
)
|
||||
assert installed.returncode == 0, installed.stderr or installed.stdout
|
||||
env.update(resolve_abxpkg_chrome_env(Path(env["ABXPKG_LIB_DIR"]), env))
|
||||
|
||||
master, slave = pty.openpty()
|
||||
termios.tcsetwinsize(slave, (40, 200))
|
||||
output = bytearray()
|
||||
@@ -140,7 +153,7 @@ def test_background_completions_render_above_interrupt_prompt(initialized_archiv
|
||||
cwd=initialized_archive,
|
||||
# The small test-owned page still transfers slowly enough for wget
|
||||
# to finish while the real foreground hook's prompt is open.
|
||||
env=cli_env(WGET_ARGS_EXTRA='["--limit-rate=20"]', INFINISCROLL_SCROLL_DELAY="10000", CHROME_HEADLESS="True"),
|
||||
env=env,
|
||||
stdin=slave,
|
||||
stdout=slave,
|
||||
stderr=slave,
|
||||
|
||||
@@ -44,6 +44,19 @@ from .test_cli_add_1 import (
|
||||
|
||||
|
||||
def test_abort_stops_long_running_background_hook(initialized_archive):
|
||||
env = cli_env(INFINISCROLL_SCROLL_DELAY="10000", CHROME_HEADLESS="True")
|
||||
# Keep first-use dependency installation outside the timed hook-start
|
||||
# observation. This test is about interrupting a running hook; a cold
|
||||
# browser or forumdl install must not consume its readiness window.
|
||||
installed = run_archivebox_cmd(
|
||||
["install", "forumdl", "chrome"],
|
||||
cwd=initialized_archive,
|
||||
env=env,
|
||||
timeout=600,
|
||||
)
|
||||
assert installed.returncode == 0, installed.stderr or installed.stdout
|
||||
env.update(resolve_abxpkg_chrome_env(Path(env["ABXPKG_LIB_DIR"]), env))
|
||||
|
||||
master, slave = pty.openpty()
|
||||
termios.tcsetwinsize(slave, (40, 200))
|
||||
output = bytearray()
|
||||
@@ -66,14 +79,22 @@ def test_abort_stops_long_running_background_hook(initialized_archive):
|
||||
result = run_archivebox_cmd(
|
||||
["add", "--depth=1", "--plugins=forumdl,infiniscroll", "https://news.ycombinator.com"],
|
||||
cwd=initialized_archive,
|
||||
env=cli_env(INFINISCROLL_SCROLL_DELAY="10000", CHROME_HEADLESS="True"),
|
||||
env=env,
|
||||
stdin=slave,
|
||||
stdout=slave,
|
||||
stderr=slave,
|
||||
wait=False,
|
||||
start_new_session=True,
|
||||
)
|
||||
read_until(lambda: (scroll := result_for("infiniscroll")) is not None and scroll.status == "started")
|
||||
try:
|
||||
read_until(lambda: (scroll := result_for("infiniscroll")) is not None and scroll.status == "started")
|
||||
except AssertionError:
|
||||
# Capture state before teardown terminates the runner: otherwise a
|
||||
# missed readiness deadline loses the install/hook failure itself.
|
||||
with use_archivebox_db(initialized_archive):
|
||||
print("Hook readiness:", list(ArchiveResult.objects.values("plugin", "status", "output_str")))
|
||||
print("Process readiness:", list(Process.objects.values("process_type", "status", "exit_code", "stderr")))
|
||||
raise
|
||||
read_until(lambda: "running pid=" in re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", output.decode(errors="replace")))
|
||||
result.send_signal(signal.SIGINT)
|
||||
read_until(lambda: b"Choice [skip]:" in output)
|
||||
|
||||
@@ -269,6 +269,7 @@ def test_init_recovers_from_pre_squash_dev_history(tmp_path):
|
||||
("core", "0021_auto_20220724_1254"),
|
||||
("core", "0021_auto_20221128_1116"),
|
||||
("core", "0023_alter_archiveresult_options_archiveresult_abid_and_more"),
|
||||
("core", "0056_progress_covering_indexes"),
|
||||
("core", "0074_alter_snapshot_downloaded_at"),
|
||||
("core", "0075_crawl"),
|
||||
("core", "0075_archiveresult_retry_at"),
|
||||
|
||||
@@ -27,6 +27,38 @@ from .test_cli_run_1 import (
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestRecoverOrchestratorState:
|
||||
def test_extension_upload_recovery_uses_hook_index_before_snapshots(self):
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from archivebox.base_models.models import get_or_create_system_user_pk
|
||||
from archivebox.core.models import ArchiveResult, Snapshot
|
||||
from archivebox.core.recovery_util import recover_orchestrator_state
|
||||
from archivebox.crawls.models import Crawl
|
||||
|
||||
crawl = Crawl.objects.create(
|
||||
urls="https://example.com",
|
||||
created_by_id=get_or_create_system_user_pk(),
|
||||
status="sealed",
|
||||
retry_at=None,
|
||||
)
|
||||
snapshot = Snapshot.objects.create(url="https://example.com", crawl=crawl, status="sealed", retry_at=None)
|
||||
ArchiveResult.objects.create(snapshot=snapshot, plugin="title", hook_name="on_Snapshot__title", status="succeeded")
|
||||
with CaptureQueriesContext(connection) as queries:
|
||||
recovered = recover_orchestrator_state()
|
||||
assert recovered["snapshots_sealed_with_extension_uploads_only"] == 0
|
||||
snapshot.refresh_from_db()
|
||||
assert snapshot.status == "sealed" and snapshot.retry_at is None
|
||||
recovery_updates = [
|
||||
q["sql"] for q in queries if q["sql"].startswith("UPDATE") and Snapshot.BROWSER_EXTENSION_UPLOAD_HOOK_NAME in q["sql"]
|
||||
]
|
||||
assert len(recovery_updates) == 2
|
||||
if connection.vendor == "sqlite":
|
||||
for sql in recovery_updates:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("EXPLAIN QUERY PLAN " + sql)
|
||||
plan = " ".join(str(row[-1]) for row in cursor.fetchall())
|
||||
assert "hook_name=?" in plan, plan
|
||||
|
||||
def test_recover_orchestrator_state_unlocks_started_crawl_with_pending_snapshot(self):
|
||||
from archivebox.base_models.models import get_or_create_system_user_pk
|
||||
from archivebox.core.models import Snapshot
|
||||
|
||||
@@ -30,6 +30,7 @@ from archivebox.tests.conftest import (
|
||||
run_archivebox_cmd,
|
||||
start_archivebox_server,
|
||||
stop_archivebox_process,
|
||||
wait_for_log,
|
||||
wait_for_log_count,
|
||||
wait_for_pid_to_disappear,
|
||||
)
|
||||
@@ -813,24 +814,30 @@ def test_live_server_signal_exit_and_resume_uses_existing_supervisor_state(initi
|
||||
try:
|
||||
server = start_archivebox_server(initialized_archive, port=port, log_name=f"server-{stop_signal.name}.log", env=env)
|
||||
server_log = server.log_path
|
||||
# HTTP readiness can precede the foreground tail scope. Exercise its
|
||||
# actual signal policy, where interrupting Popen.poll stranded reaping.
|
||||
wait_for_log(server_log, "Tailing worker logs (Ctrl+C to stop)...")
|
||||
|
||||
os.kill(server.pid, stop_signal)
|
||||
server.wait(timeout=20)
|
||||
|
||||
if expected_notice:
|
||||
log_text = server_log.read_text(encoding="utf-8", errors="replace")
|
||||
assert server.returncode == 0, log_text
|
||||
assert expected_notice in log_text, log_text
|
||||
assert "ArchiveBox server shut down gracefully" in log_text, log_text
|
||||
assert_no_processes_for_data_dir(initialized_archive, timeout=12)
|
||||
|
||||
resumed = start_archivebox_server(initialized_archive, port=port, log_name=f"server-{stop_signal.name}-resumed.log", env=env)
|
||||
resumed_log = resumed.log_path
|
||||
wait_for_log(resumed_log, "Tailing worker logs (Ctrl+C to stop)...")
|
||||
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)
|
||||
resumed_text = resumed_log.read_text(encoding="utf-8", errors="replace")
|
||||
assert resumed.returncode == 0, resumed_text
|
||||
assert "Got SIGTERM" in resumed_text, resumed_text
|
||||
assert "ArchiveBox server shut down gracefully" in resumed_text, resumed_text
|
||||
assert_no_processes_for_data_dir(initialized_archive, timeout=12)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Takeover preserves a capture; an explicit user abort ends its attempt."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.parametrize("user_initiated", [False, True], ids=["takeover", "user-abort"])
|
||||
def test_abort_before_first_snapshot_hook_preserves_capture_intent(recursive_test_site, user_initiated):
|
||||
from abx_dl.events import CrawlAbortEvent, CrawlCompletedEvent, ProcessStartedEvent, SnapshotCompletedEvent, SnapshotEvent
|
||||
from archivebox.base_models.models import get_or_create_system_user_pk
|
||||
from archivebox.core.models import Snapshot
|
||||
from archivebox.crawls.models import Crawl
|
||||
from archivebox.services.runner import CrawlRunner
|
||||
|
||||
crawl = Crawl.objects.create(
|
||||
urls=recursive_test_site["root_url"],
|
||||
config={"PLUGINS": "wget"},
|
||||
created_by_id=get_or_create_system_user_pk(),
|
||||
)
|
||||
runner = CrawlRunner(crawl)
|
||||
completed = []
|
||||
started = []
|
||||
completed_crawls = []
|
||||
|
||||
async def abort_on_snapshot(event: SnapshotEvent) -> None:
|
||||
# Observe the real snapshot command after its DB owner is recorded but
|
||||
# before the hook service consumes it. This is the takeover window that
|
||||
# used to turn shutdown cleanup into a successful, empty capture.
|
||||
await event.emit(CrawlAbortEvent(user_initiated=user_initiated)).now()
|
||||
|
||||
runner.bus.on(SnapshotEvent, abort_on_snapshot)
|
||||
runner.bus.on(SnapshotCompletedEvent, lambda event: completed.append(event))
|
||||
runner.bus.on(ProcessStartedEvent, lambda event: started.append(event))
|
||||
runner.bus.on(CrawlCompletedEvent, lambda event: completed_crawls.append(event))
|
||||
if user_initiated:
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
asyncio.run(runner.run())
|
||||
else:
|
||||
asyncio.run(runner.run())
|
||||
|
||||
assert len(completed) == 1, "Aborting must still finish the real snapshot cleanup phase"
|
||||
assert not [event for event in started if event.hook_name.startswith("on_Snapshot")]
|
||||
snapshot = Snapshot.objects.get(crawl=crawl)
|
||||
crawl.refresh_from_db()
|
||||
assert len(completed_crawls) == int(user_initiated)
|
||||
assert snapshot.archiveresult_set.count() == 0
|
||||
if user_initiated:
|
||||
# Preserve the historical point-in-time boundary for an explicit abort.
|
||||
assert snapshot.status == Snapshot.StatusChoices.SEALED
|
||||
assert snapshot.retry_at is None
|
||||
assert snapshot.downloaded_at is not None
|
||||
assert crawl.status == Crawl.StatusChoices.SEALED
|
||||
assert crawl.retry_at is None
|
||||
else:
|
||||
assert snapshot.status in Snapshot.RUNNABLE_STATES, "Takeover must leave the capture resumable"
|
||||
assert snapshot.retry_at is not None
|
||||
assert snapshot.downloaded_at is None
|
||||
assert crawl.status in Crawl.RUNNABLE_STATES
|
||||
assert crawl.retry_at is not None
|
||||
@@ -91,7 +91,12 @@ def test_tlsnotary_preview_verifies_saved_evidence_without_remote_requests(snaps
|
||||
assert "Signed response byte range" in topology.inner_text()
|
||||
assert topology.locator('a[href*="response.http"]').count() > 0
|
||||
assert topology.locator('a[href*="receipt.json"]').count() > 0
|
||||
assert page.locator("a", has_text="Verify independently").get_attribute("href") == "https://tlsnotary.zervice.io/"
|
||||
# The installed plugin owns its verifier domain. Compare its template
|
||||
# with its declared default so a plugin release does not require a
|
||||
# simultaneous core test edit, while stale template links still fail.
|
||||
schema = json.loads((Path(get_plugins_dir()) / "tlsnotary" / "config.json").read_text())
|
||||
verifier_url = schema["properties"]["TLSNOTARY_VERIFIER_URL"]["default"]
|
||||
assert page.locator("a", has_text="Verify independently").get_attribute("href") == f"{verifier_url.rstrip('/')}/"
|
||||
assert all(urlsplit(request).hostname == hostname for request in requests)
|
||||
page.screenshot(path=str(tmp_path / "tlsnotary-desktop.png"), full_page=True)
|
||||
page.set_viewport_size({"width": 390, "height": 844})
|
||||
|
||||
@@ -594,7 +594,11 @@ class TestPublicIndexSearch:
|
||||
assert b"Public Example Website" in response.content
|
||||
assert b"No snapshots found." not in response.content
|
||||
|
||||
def test_public_index_shows_exact_total_count_and_page_count_for_100_plus_snapshots(self, client, crawl, public_snapshot):
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_public_index_shows_cached_total_count_and_page_count_for_100_plus_snapshots(self, client, crawl, public_snapshot):
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from archivebox.core.models import Snapshot
|
||||
|
||||
base_time = public_snapshot.bookmarked_at + timedelta(seconds=1)
|
||||
@@ -628,11 +632,21 @@ class TestPublicIndexSearch:
|
||||
],
|
||||
)
|
||||
|
||||
key = "progress-collection:public"
|
||||
cache.delete_many([key, f"{key}:refresh"])
|
||||
initial_response = client.get("/public/", HTTP_HOST=WEB_HOST)
|
||||
assert initial_response.status_code == 200
|
||||
assert initial_response.context["public_snapshot_count"] is None
|
||||
assert b"last »" not in initial_response.content
|
||||
deadline = time.monotonic() + 5
|
||||
while cache.get(key) is None and time.monotonic() < deadline:
|
||||
time.sleep(0.01)
|
||||
assert cache.get(key)["snapshots"] == 125
|
||||
response = client.get("/public/", HTTP_HOST=WEB_HOST)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.context["paginator"].count == 125
|
||||
assert response.context["paginator"].num_pages == 3
|
||||
assert response.context["public_snapshot_count"] == 125
|
||||
assert response.context["public_page_count"] == 3
|
||||
assert response.context["page_obj"].has_next() is True
|
||||
assert len(response.context["object_list"]) == 50
|
||||
content = response.content.decode()
|
||||
@@ -1329,7 +1343,7 @@ class TestSearchBackendsE2E:
|
||||
surface_name,
|
||||
search_mode,
|
||||
first_positive_elapsed,
|
||||
counts[:10],
|
||||
count_events[:10],
|
||||
)
|
||||
assert total_elapsed < 2.0, (surface_name, search_mode, total_elapsed, counts[-10:])
|
||||
|
||||
|
||||
@@ -83,6 +83,96 @@ def test_foreground_shutdown_can_request_cooperative_shutdown_without_raising():
|
||||
assert process.returncode == 130, (stdout, stderr)
|
||||
|
||||
|
||||
def test_cooperative_shutdown_during_child_poll_keeps_child_reapable():
|
||||
"""Interrupt real child polling at different points without stranding reaping.
|
||||
|
||||
Immediate KeyboardInterrupt stranded Popen's wait lock in real CI, leaving
|
||||
a zombie child that wait() could not reap. Exercise the cooperative policy
|
||||
used by the server with the same external signals and child cleanup.
|
||||
"""
|
||||
process = _start_signal_process(
|
||||
r"""
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from collections import Counter
|
||||
|
||||
from archivebox.core.shutdown_util import (
|
||||
foreground_shutdown_signals,
|
||||
raise_if_shutdown_requested,
|
||||
wait_popen_and_kill_children,
|
||||
)
|
||||
|
||||
print("READY", flush=True)
|
||||
interruption_sites = Counter()
|
||||
for attempt in range(512):
|
||||
child = subprocess.Popen(
|
||||
[sys.executable, "-c", "import time; time.sleep(60)"],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
interruption_site = None
|
||||
try:
|
||||
with foreground_shutdown_signals(first_signal_message=None, raise_on_first_signal=False) as state:
|
||||
try:
|
||||
os.write(sys.stdout.fileno(), b"POLLING\n")
|
||||
while True:
|
||||
raise_if_shutdown_requested()
|
||||
assert child.poll() is None
|
||||
except KeyboardInterrupt as interruption:
|
||||
assert state.signal_name == "SIGINT"
|
||||
frame = traceback.extract_tb(interruption.__traceback__)[-1]
|
||||
assert frame.name == "raise_if_shutdown_requested", frame
|
||||
interruption_site = f"{frame.name}:{frame.lineno}"
|
||||
interruption_sites[interruption_site] += 1
|
||||
# The child deliberately remains alive until this unchanged
|
||||
# kill-and-reap path asks it to stop.
|
||||
wait_popen_and_kill_children(child, [], timeout=0.001, kill_timeout=1.0)
|
||||
assert child.returncode == -signal.SIGKILL, (attempt, child.returncode)
|
||||
except BaseException as error:
|
||||
error.add_note(f"Interrupted child polling iteration: {attempt}, at {interruption_site}")
|
||||
raise
|
||||
finally:
|
||||
# Cleanup must also work if the assertion exposes a broken
|
||||
# Popen wait; preserve that failure while reaping the real PID.
|
||||
if child.returncode is None:
|
||||
try:
|
||||
os.kill(child.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
try:
|
||||
os.waitpid(child.pid, 0)
|
||||
except ChildProcessError:
|
||||
pass
|
||||
print(f"Interrupt locations: {dict(interruption_sites)}", file=sys.stderr, flush=True)
|
||||
print("REAPED:512", flush=True)
|
||||
""",
|
||||
)
|
||||
|
||||
try:
|
||||
# An external sender does not contend for the polling interpreter's
|
||||
# GIL. A Timer thread can bias delivery toward waitpid's GIL release,
|
||||
# missing other interruption points reached by real CLI Ctrl+C.
|
||||
assert process.stdout is not None
|
||||
sent = 0
|
||||
while sent < 512 and process.stdout.readline() == "POLLING\n":
|
||||
process.send_signal(signal.SIGINT)
|
||||
sent += 1
|
||||
stdout, stderr = process.communicate(timeout=30)
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=5)
|
||||
|
||||
assert process.returncode == 0, (stdout, stderr)
|
||||
assert sent == 512, (sent, stdout, stderr)
|
||||
assert stdout == "REAPED:512\n", (stdout, stderr)
|
||||
print(stderr)
|
||||
|
||||
|
||||
def test_daemon_runner_signal_exit_is_unexpected_for_supervisor():
|
||||
process = _start_signal_process(
|
||||
"""
|
||||
|
||||
@@ -209,10 +209,13 @@ def test_add_view_restarts_stopped_supervisord_runner(tmp_path, recursive_test_s
|
||||
_stop_worker(tmp_path, "worker_runner")
|
||||
assert _worker_state(tmp_path, "worker_runner") != "RUNNING"
|
||||
|
||||
session, csrf_token = _login_to_add_view(port)
|
||||
session, _add_page, csrf_token = _login_to_add_view(port)
|
||||
response = session.post(
|
||||
f"http://admin.archivebox.localhost:{port}/add/",
|
||||
headers={"Referer": f"http://admin.archivebox.localhost:{port}/add/"},
|
||||
f"http://127.0.0.1:{port}/add/",
|
||||
headers={
|
||||
"Host": f"admin.archivebox.localhost:{port}",
|
||||
"Referer": f"http://admin.archivebox.localhost:{port}/add/",
|
||||
},
|
||||
data={
|
||||
"url": recursive_test_site["root_url"],
|
||||
"depth": "0",
|
||||
@@ -246,19 +249,26 @@ def test_add_view_restarts_stopped_supervisord_runner(tmp_path, recursive_test_s
|
||||
stop_server(tmp_path)
|
||||
|
||||
|
||||
def _login_to_add_view(port: int) -> tuple[requests.Session, str]:
|
||||
def _login_to_add_view(port: int) -> tuple[requests.Session, requests.Response, str]:
|
||||
# Browsers resolve *.localhost themselves; system DNS in stock containers
|
||||
# need not. Loopback transport with the real Host/Referer preserves virtual
|
||||
# host routing, session cookies and CSRF checks without depending on DNS.
|
||||
session = requests.Session()
|
||||
get_http_response(port, host=f"admin.archivebox.localhost:{port}", path="/admin/login/")
|
||||
login_page = session.get(
|
||||
f"http://admin.archivebox.localhost:{port}/admin/login/",
|
||||
f"http://127.0.0.1:{port}/admin/login/",
|
||||
headers={"Host": f"admin.archivebox.localhost:{port}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert login_page.status_code == 200
|
||||
csrf_match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', login_page.text)
|
||||
assert csrf_match, login_page.text[:500]
|
||||
login_response = session.post(
|
||||
f"http://admin.archivebox.localhost:{port}/admin/login/",
|
||||
headers={"Referer": f"http://admin.archivebox.localhost:{port}/admin/login/"},
|
||||
f"http://127.0.0.1:{port}/admin/login/",
|
||||
headers={
|
||||
"Host": f"admin.archivebox.localhost:{port}",
|
||||
"Referer": f"http://admin.archivebox.localhost:{port}/admin/login/",
|
||||
},
|
||||
data={
|
||||
"username": "apitestadmin",
|
||||
"password": "testpass123",
|
||||
@@ -270,12 +280,13 @@ def _login_to_add_view(port: int) -> tuple[requests.Session, str]:
|
||||
)
|
||||
assert login_response.status_code in (302, 303), login_response.text
|
||||
add_page, csrf_token = _get_add_view(session, port, host=f"admin.archivebox.localhost:{port}")
|
||||
return session, csrf_token
|
||||
return session, add_page, csrf_token
|
||||
|
||||
|
||||
def _get_add_view(session: requests.Session, port: int, *, host: str) -> tuple[requests.Response, str]:
|
||||
add_page = session.get(
|
||||
f"http://{host}/add/",
|
||||
f"http://127.0.0.1:{port}/add/",
|
||||
headers={"Host": host},
|
||||
timeout=10,
|
||||
)
|
||||
assert add_page.status_code == 200
|
||||
@@ -612,33 +623,14 @@ def test_add_view_post_creates_schedule_over_server(tmp_path, recursive_test_sit
|
||||
|
||||
try:
|
||||
start_archivebox_server(tmp_path, env=env, port=port)
|
||||
session = requests.Session()
|
||||
get_http_response(port, host=f"admin.archivebox.localhost:{port}", path="/admin/login/")
|
||||
login_page = session.get(
|
||||
f"http://admin.archivebox.localhost:{port}/admin/login/",
|
||||
timeout=10,
|
||||
)
|
||||
assert login_page.status_code == 200
|
||||
csrf_match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', login_page.text)
|
||||
assert csrf_match, login_page.text[:500]
|
||||
login_response = session.post(
|
||||
f"http://admin.archivebox.localhost:{port}/admin/login/",
|
||||
headers={"Referer": f"http://admin.archivebox.localhost:{port}/admin/login/"},
|
||||
data={
|
||||
"username": "apitestadmin",
|
||||
"password": "testpass123",
|
||||
"csrfmiddlewaretoken": csrf_match.group(1),
|
||||
"next": "/add/",
|
||||
},
|
||||
timeout=10,
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert login_response.status_code in (302, 303), login_response.text
|
||||
_add_page, add_csrf_token = _get_add_view(session, port, host=f"admin.archivebox.localhost:{port}")
|
||||
session, _add_page, add_csrf_token = _login_to_add_view(port)
|
||||
|
||||
response = session.post(
|
||||
f"http://admin.archivebox.localhost:{port}/add/",
|
||||
headers={"Referer": f"http://admin.archivebox.localhost:{port}/add/"},
|
||||
f"http://127.0.0.1:{port}/add/",
|
||||
headers={
|
||||
"Host": f"admin.archivebox.localhost:{port}",
|
||||
"Referer": f"http://admin.archivebox.localhost:{port}/add/",
|
||||
},
|
||||
data={
|
||||
"url": recursive_test_site["root_url"],
|
||||
"depth": "0",
|
||||
@@ -686,37 +678,18 @@ def test_add_view_depth_two_crawl_renders_outputs_over_server(tmp_path, recursiv
|
||||
|
||||
try:
|
||||
start_archivebox_server(tmp_path, env=env, port=port)
|
||||
session = requests.Session()
|
||||
get_http_response(port, host=f"admin.archivebox.localhost:{port}", path="/admin/login/")
|
||||
login_page = session.get(
|
||||
f"http://admin.archivebox.localhost:{port}/admin/login/",
|
||||
timeout=10,
|
||||
)
|
||||
assert login_page.status_code == 200
|
||||
csrf_match = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', login_page.text)
|
||||
assert csrf_match, login_page.text[:500]
|
||||
login_response = session.post(
|
||||
f"http://admin.archivebox.localhost:{port}/admin/login/",
|
||||
headers={"Referer": f"http://admin.archivebox.localhost:{port}/admin/login/"},
|
||||
data={
|
||||
"username": "apitestadmin",
|
||||
"password": "testpass123",
|
||||
"csrfmiddlewaretoken": csrf_match.group(1),
|
||||
"next": "/add/",
|
||||
},
|
||||
timeout=10,
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert login_response.status_code in (302, 303), login_response.text
|
||||
add_page, add_csrf_token = _get_add_view(session, port, host=f"admin.archivebox.localhost:{port}")
|
||||
session, add_page, add_csrf_token = _login_to_add_view(port)
|
||||
assert add_page.status_code == 200
|
||||
assert 'name="depth"' in add_page.text
|
||||
assert 'name="url"' in add_page.text
|
||||
_stop_worker(tmp_path, "worker_runner")
|
||||
|
||||
response = session.post(
|
||||
f"http://admin.archivebox.localhost:{port}/add/",
|
||||
headers={"Referer": f"http://admin.archivebox.localhost:{port}/add/"},
|
||||
f"http://127.0.0.1:{port}/add/",
|
||||
headers={
|
||||
"Host": f"admin.archivebox.localhost:{port}",
|
||||
"Referer": f"http://admin.archivebox.localhost:{port}/add/",
|
||||
},
|
||||
data={
|
||||
"url": recursive_test_site["root_url"],
|
||||
"depth": "2",
|
||||
@@ -781,14 +754,16 @@ def test_add_view_depth_two_crawl_renders_outputs_over_server(tmp_path, recursiv
|
||||
assert "active_crawls" in progress.json()
|
||||
|
||||
index_page = requests.get(
|
||||
f"http://web.archivebox.localhost:{port}/public/",
|
||||
f"http://127.0.0.1:{port}/public/",
|
||||
headers={"Host": f"web.archivebox.localhost:{port}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert index_page.status_code == 200
|
||||
assert recursive_test_site["root_url"] in index_page.text
|
||||
|
||||
snapshot_admin = session.get(
|
||||
f"http://admin.archivebox.localhost:{port}/admin/core/snapshot/",
|
||||
f"http://127.0.0.1:{port}/admin/core/snapshot/",
|
||||
headers={"Host": f"admin.archivebox.localhost:{port}"},
|
||||
timeout=10,
|
||||
)
|
||||
assert snapshot_admin.status_code == 200
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
@@ -180,7 +181,7 @@ def real_crawl_setup_process(snapshot, hermetic_lib_dir):
|
||||
|
||||
|
||||
class TestLiveProgressView:
|
||||
def test_live_progress_uses_covering_indexes_for_totals(self, client, admin_user, crawl, snapshot):
|
||||
def test_live_progress_uses_existing_indexes_and_cached_totals(self, client, admin_user, crawl, snapshot):
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from archivebox.core.models import ArchiveResult, Snapshot
|
||||
@@ -192,23 +193,108 @@ class TestLiveProgressView:
|
||||
client.force_login(admin_user)
|
||||
with CaptureQueriesContext(connection) as queries:
|
||||
response = client.get(reverse("live_progress"), HTTP_HOST=ADMIN_TEST_HOST)
|
||||
query_sql = [query["sql"] for query in queries]
|
||||
assert response.status_code == 200, response.content
|
||||
payload = response.json()
|
||||
active_crawl = next(item for item in payload["active_crawls"] if item["id"] == str(crawl.pk))
|
||||
assert active_crawl["total_snapshots"] == 1
|
||||
assert active_crawl["crawl_output_size"] is None
|
||||
assert active_crawl["crawl_output_size_display"] == "…"
|
||||
deadline = time.monotonic() + 5
|
||||
while active_crawl["crawl_output_size"] is None and time.monotonic() < deadline:
|
||||
time.sleep(0.05)
|
||||
response = client.get(reverse("live_progress"), HTTP_HOST=ADMIN_TEST_HOST)
|
||||
assert response.status_code == 200, response.content
|
||||
active_crawl = next(item for item in response.json()["active_crawls"] if item["id"] == str(crawl.pk))
|
||||
assert active_crawl["cancelled_snapshots"] == 1
|
||||
assert active_crawl["crawl_output_size"] == 1234
|
||||
assert payload["indexing_queued"] == 1
|
||||
assert not any("SUM(" in sql or 'COUNT("core_snapshot"."downloaded_at")' in sql for sql in query_sql)
|
||||
if connection.vendor == "sqlite":
|
||||
aggregates = [query["sql"] for query in queries if "GROUP BY" in query["sql"]]
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='index'")
|
||||
indexes = {row[0] for row in cursor.fetchall()}
|
||||
assert "archiveresult_progress_idx" not in indexes
|
||||
assert "snapshot_progress_totals_idx" not in indexes
|
||||
aggregates = [sql for sql in query_sql if "GROUP BY" in sql]
|
||||
assert aggregates
|
||||
with connection.cursor() as cursor:
|
||||
for sql in aggregates:
|
||||
cursor.execute("EXPLAIN QUERY PLAN " + sql)
|
||||
plan = " ".join(str(row) for row in cursor.fetchall())
|
||||
assert "COVERING INDEX" in plan, plan
|
||||
if 'FROM "core_snapshot"' in sql:
|
||||
assert "COVERING INDEX" in plan, plan
|
||||
else:
|
||||
# Both existing status indexes can satisfy this lookup.
|
||||
# Require a status-keyed search, not one planner choice's
|
||||
# index name; a full scan must still fail below.
|
||||
assert re.search(r"SEARCH core_archiveresult USING (?:COVERING )?INDEX \S+ \(status=\?", plan), plan
|
||||
assert "SCAN " not in plan, plan
|
||||
|
||||
# Size/cancellation samples are shared by tabs while status counts stay
|
||||
# current on every request.
|
||||
Snapshot.objects.filter(pk=snapshot.pk).update(output_size=4321)
|
||||
with CaptureQueriesContext(connection) as cached_queries:
|
||||
for _ in range(5):
|
||||
response = client.get(reverse("live_progress"), HTTP_HOST=ADMIN_TEST_HOST)
|
||||
assert response.status_code == 200, response.content
|
||||
cached_crawl = next(item for item in response.json()["active_crawls"] if item["id"] == str(crawl.pk))
|
||||
assert cached_crawl["crawl_output_size"] == 1234
|
||||
assert not any("SUM(" in query["sql"] for query in cached_queries)
|
||||
|
||||
def test_live_progress_samples_batches_without_leaking_other_snapshots(self, client, admin_user, crawl, snapshot):
|
||||
from datetime import timedelta
|
||||
from archivebox.core.models import Snapshot
|
||||
from archivebox.crawls.models import Crawl
|
||||
|
||||
Crawl.objects.filter(pk=crawl.pk).update(status=Crawl.StatusChoices.STARTED)
|
||||
now = timezone.now()
|
||||
Snapshot.objects.bulk_create(
|
||||
[
|
||||
Snapshot(
|
||||
crawl=crawl,
|
||||
url=f"https://example.com/batch/{i}",
|
||||
timestamp=str(now.timestamp() + i + 1),
|
||||
status=Snapshot.StatusChoices.SEALED,
|
||||
downloaded_at=now if i % 2 else None,
|
||||
output_size=i + 1,
|
||||
)
|
||||
for i in range(305)
|
||||
],
|
||||
)
|
||||
# Exercise both a timestamp tie crossing batch boundaries and advancing
|
||||
# to a new timestamp. A started snapshot must not enter sealed totals.
|
||||
Snapshot.objects.filter(crawl=crawl, status=Snapshot.StatusChoices.SEALED).update(modified_at=now)
|
||||
Snapshot.objects.filter(crawl=crawl, output_size__gt=250).update(modified_at=now + timedelta(seconds=1))
|
||||
Snapshot.objects.filter(pk=snapshot.pk).update(output_size=9999)
|
||||
client.force_login(admin_user)
|
||||
deadline = time.monotonic() + 5
|
||||
while True:
|
||||
response = client.get(reverse("live_progress"), HTTP_HOST=ADMIN_TEST_HOST)
|
||||
assert response.status_code == 200, response.content
|
||||
totals = next(item for item in response.json()["active_crawls"] if item["id"] == str(crawl.pk))
|
||||
if totals["crawl_output_size"] is not None or time.monotonic() >= deadline:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert totals["crawl_output_size"] == sum(range(1, 306))
|
||||
assert totals["cancelled_snapshots"] == 153
|
||||
assert totals["total_snapshots"] == 306
|
||||
assert totals["completed_snapshots"] == 305
|
||||
|
||||
Snapshot.objects.filter(pk=snapshot.pk).update(status=Snapshot.StatusChoices.SEALED, config={"PERMISSIONS": "public"})
|
||||
client.logout()
|
||||
deadline = time.monotonic() + 5
|
||||
while True:
|
||||
response = client.get(reverse("live_progress"), {"snapshot_id": str(snapshot.pk)}, HTTP_HOST=ADMIN_TEST_HOST)
|
||||
assert response.status_code == 200, response.content
|
||||
scoped = response.json()["active_crawls"][0]
|
||||
if scoped["crawl_output_size"] is not None or time.monotonic() >= deadline:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
assert scoped["total_snapshots"] == 1
|
||||
assert scoped["crawl_output_size"] == 9999
|
||||
assert scoped["cancelled_snapshots"] == 1
|
||||
|
||||
def test_live_progress_does_not_inspect_archive_files(self, client, admin_user, crawl, snapshot):
|
||||
import sys
|
||||
from archivebox.core.models import Snapshot
|
||||
|
||||
@@ -421,6 +421,12 @@ class TestPublicIndex:
|
||||
response = client.get("/public/", HTTP_HOST=WEB_TEST_HOST)
|
||||
|
||||
result_queries = [query["sql"].lower() for query in captured_queries if "core_archiveresult" in query["sql"].lower()]
|
||||
row_lookups = [
|
||||
query["sql"]
|
||||
for query in captured_queries
|
||||
if re.search(r'WHERE "(?:core_snapshot|crawls_crawl|auth_user)"\."id" =', query["sql"])
|
||||
]
|
||||
assert row_lookups == [], "Public rows must reuse the page's snapshot, crawl, and owner metadata"
|
||||
assert len(result_queries) == 1
|
||||
assert "output_files" in result_queries[0]
|
||||
assert "output_str" in result_queries[0]
|
||||
@@ -435,6 +441,102 @@ class TestPublicIndex:
|
||||
assert b'data-tooltip="singlefile"' in response.content
|
||||
assert b'title="singlefile"' not in response.content
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_public_count_uses_permission_index_for_uniform_visibility(self, client, admin_user):
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import connection
|
||||
from django.db.backends.signals import connection_created
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from archivebox.core.models import Snapshot
|
||||
from archivebox.crawls.models import Crawl
|
||||
|
||||
crawl = Crawl.objects.create(urls="https://public-count.example", created_by=admin_user, config={"PERMISSIONS": "public"})
|
||||
Snapshot.objects.bulk_create(
|
||||
[
|
||||
Snapshot(
|
||||
url=f"https://public-count.example/{index}",
|
||||
timestamp=str(1800000000 + index),
|
||||
crawl=crawl,
|
||||
config={"PERMISSIONS": "public"},
|
||||
status="sealed",
|
||||
)
|
||||
for index in range(125)
|
||||
],
|
||||
)
|
||||
if connection.vendor == "sqlite":
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("ANALYZE core_snapshot")
|
||||
|
||||
key = "progress-collection:public"
|
||||
cache.delete_many([key, f"{key}:refresh"])
|
||||
background_queries = []
|
||||
|
||||
def observe(execute, sql, params, many, context):
|
||||
background_queries.append((sql, params))
|
||||
return execute(sql, params, many, context)
|
||||
|
||||
def observe_background_connection(sender, connection, **kwargs):
|
||||
connection.execute_wrappers.append(observe)
|
||||
|
||||
connection_created.connect(observe_background_connection)
|
||||
try:
|
||||
with CaptureQueriesContext(connection) as queries:
|
||||
response = client.get("/public/", HTTP_HOST=WEB_TEST_HOST)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert not any("COUNT(" in q["sql"] for q in queries), "Public page loads must not wait for a count"
|
||||
assert response.context["public_snapshot_count"] is None
|
||||
assert len(response.context["object_list"]) == 50
|
||||
assert response.context["page_obj"].has_next()
|
||||
assert b"of 125" not in response.content
|
||||
|
||||
deadline = time.monotonic() + 5
|
||||
while cache.get(key) is None and time.monotonic() < deadline:
|
||||
time.sleep(0.01)
|
||||
assert cache.get(key)["snapshots"] == 125
|
||||
|
||||
reloaded = client.get("/public/", HTTP_HOST=WEB_TEST_HOST)
|
||||
assert reloaded.context["public_snapshot_count"] == 125
|
||||
assert b"of 125" in reloaded.content
|
||||
last_page = client.get("/public/?page=3", HTTP_HOST=WEB_TEST_HOST)
|
||||
assert len(last_page.context["object_list"]) == 25
|
||||
assert not last_page.context["page_obj"].has_next()
|
||||
assert client.get("/public/?page=4", HTTP_HOST=WEB_TEST_HOST).status_code == 404
|
||||
|
||||
# The runner can add a page before the cached total refreshes.
|
||||
# Navigation follows actual rows, never the old cached page count.
|
||||
Snapshot.objects.bulk_create(
|
||||
[
|
||||
Snapshot(
|
||||
url=f"https://public-count.example/{index}",
|
||||
timestamp=str(1800000000 + index),
|
||||
crawl=crawl,
|
||||
config={"PERMISSIONS": "public"},
|
||||
status="sealed",
|
||||
)
|
||||
for index in range(125, 160)
|
||||
],
|
||||
)
|
||||
added_page = client.get("/public/?page=4", HTTP_HOST=WEB_TEST_HOST)
|
||||
assert added_page.status_code == 200
|
||||
assert len(added_page.context["object_list"]) == 10
|
||||
assert not added_page.context["page_obj"].has_next()
|
||||
assert added_page.context["public_snapshot_count"] == 160
|
||||
assert cache.get(key)["snapshots"] == 125
|
||||
|
||||
count_queries = [(sql, params) for sql, params in background_queries if "COUNT(" in sql and '"core_snapshot"' in sql]
|
||||
assert len(count_queries) == 1, "Reloads must reuse the cached total"
|
||||
if connection.vendor == "sqlite":
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("EXPLAIN QUERY PLAN " + count_queries[0][0], count_queries[0][1])
|
||||
plan = " ".join(str(row[-1]) for row in cursor.fetchall())
|
||||
assert "SEARCH core_snapshot USING INDEX" in plan and "permissions>" in plan, plan
|
||||
finally:
|
||||
connection_created.disconnect(observe_background_connection)
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
def test_upgraded_07_snapshot_icon_target_renders_at_extensionless_live_url(self, client, admin_user):
|
||||
from archivebox.core.models import ArchiveResult, Snapshot
|
||||
|
||||
@@ -11,6 +11,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from contextlib import ExitStack
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
@@ -1460,6 +1461,7 @@ def tail_multiple_worker_logs(log_files: list[str], follow=True, proc=None, keep
|
||||
next_ownership_check = 0.0
|
||||
try:
|
||||
while follow:
|
||||
raise_if_shutdown_requested()
|
||||
now = time.monotonic()
|
||||
if keep_running is not None and now >= next_ownership_check:
|
||||
if not keep_running():
|
||||
@@ -1476,6 +1478,9 @@ def tail_multiple_worker_logs(log_files: list[str], follow=True, proc=None, keep
|
||||
# Read ALL available lines from all files (not just one per iteration)
|
||||
for log_path, f in file_handles:
|
||||
while True:
|
||||
# A noisy worker may never reach EOF; shutdown must not
|
||||
# depend on returning to the outer poll loop.
|
||||
raise_if_shutdown_requested()
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break # No more lines available in this file
|
||||
@@ -1489,10 +1494,8 @@ def tail_multiple_worker_logs(log_files: list[str], follow=True, proc=None, keep
|
||||
if not had_output:
|
||||
time.sleep(0.05)
|
||||
|
||||
except (KeyboardInterrupt, BrokenPipeError, OSError):
|
||||
except (KeyboardInterrupt, SystemExit, OSError):
|
||||
return "interrupted" # Let the caller handle the cleanup message
|
||||
except SystemExit:
|
||||
return "interrupted"
|
||||
finally:
|
||||
# Close all file handles
|
||||
for _, f in file_handles:
|
||||
@@ -1674,42 +1677,54 @@ def start_server_workers(
|
||||
):
|
||||
from archivebox.config.common import get_config
|
||||
|
||||
require_server_worker_memory()
|
||||
config = get_config()
|
||||
shutdown_state = None
|
||||
tail_result = "stopped"
|
||||
try:
|
||||
supervisor = get_or_create_supervisord_process(daemonize=daemonize)
|
||||
workers, log_files, components = build_server_worker_plan(
|
||||
config=config,
|
||||
host=host,
|
||||
port=port,
|
||||
debug=debug,
|
||||
reload=reload,
|
||||
nothreading=nothreading,
|
||||
supervisor=supervisor,
|
||||
)
|
||||
component_list = format_runtime_components(components)
|
||||
if resumed_from_pid:
|
||||
print(
|
||||
"[yellow][*] Other newer archivebox process "
|
||||
f"(pid={resumed_from_pid}) exited, taking over {component_list} in this process again...[/yellow]",
|
||||
)
|
||||
else:
|
||||
print(f"[*] Starting {component_list} in this process (pid={os.getpid()})...")
|
||||
|
||||
print()
|
||||
sync_supervisord_workers(supervisor, workers, prune=True)
|
||||
print()
|
||||
|
||||
if daemonize:
|
||||
return None
|
||||
|
||||
from django.db import connections
|
||||
|
||||
connections.close_all()
|
||||
# A real takeover regression sent SIGTERM after a standby server resumed,
|
||||
# while it was still syncing workers. Immediate exceptions can interrupt
|
||||
# subprocess polling or waits during startup and cleanup, so keep the
|
||||
# existing cooperative policy active for the whole foreground lifecycle.
|
||||
# Daemon startup retains its existing signal policy.
|
||||
with ExitStack() as shutdown_context:
|
||||
if not daemonize:
|
||||
shutdown_state = shutdown_context.enter_context(foreground_shutdown_signals(raise_on_first_signal=False))
|
||||
try:
|
||||
with foreground_shutdown_signals() as shutdown_state:
|
||||
require_server_worker_memory()
|
||||
config = get_config()
|
||||
supervisor = get_or_create_supervisord_process(daemonize=daemonize)
|
||||
workers, log_files, components = build_server_worker_plan(
|
||||
config=config,
|
||||
host=host,
|
||||
port=port,
|
||||
debug=debug,
|
||||
reload=reload,
|
||||
nothreading=nothreading,
|
||||
supervisor=supervisor,
|
||||
)
|
||||
component_list = format_runtime_components(components)
|
||||
if resumed_from_pid:
|
||||
print(
|
||||
"[yellow][*] Other newer archivebox process "
|
||||
f"(pid={resumed_from_pid}) exited, taking over {component_list} in this process again...[/yellow]",
|
||||
)
|
||||
else:
|
||||
print(f"[*] Starting {component_list} in this process (pid={os.getpid()})...")
|
||||
|
||||
print()
|
||||
sync_supervisord_workers(supervisor, workers, prune=True)
|
||||
print()
|
||||
|
||||
if daemonize:
|
||||
return None
|
||||
|
||||
from django.db import connections
|
||||
|
||||
try:
|
||||
# Finish the current startup operation before honoring a
|
||||
# shutdown received during supervisor/worker synchronization.
|
||||
# The tail also checks intent between operations, including
|
||||
# while draining busy logs. Cleanup remains synchronous.
|
||||
raise_if_shutdown_requested()
|
||||
connections.close_all()
|
||||
# Tail worker logs while supervisord runs.
|
||||
sys.stdout.write("Tailing worker logs (Ctrl+C to stop)...\n\n")
|
||||
sys.stdout.flush()
|
||||
@@ -1719,26 +1734,24 @@ def start_server_workers(
|
||||
proc=_supervisord_proc, # Stop tailing when supervisord exits
|
||||
keep_running=keep_running,
|
||||
)
|
||||
except (KeyboardInterrupt, BrokenPipeError, OSError):
|
||||
if daemonize:
|
||||
raise
|
||||
if not shutdown_state or not shutdown_state.signal_name:
|
||||
print("\n[🛑] Got CTRL+C, stopping gracefully...")
|
||||
except SystemExit:
|
||||
if daemonize:
|
||||
raise
|
||||
except BaseException as e:
|
||||
if daemonize:
|
||||
raise
|
||||
STDERR.print(f"\n[🛑] Got {e.__class__.__name__} exception, stopping gracefully...")
|
||||
finally:
|
||||
signal_shutdown_requested = bool(shutdown_state and shutdown_state.signal_name)
|
||||
if not daemonize and (signal_shutdown_requested or should_stop_supervisord is None or should_stop_supervisord()):
|
||||
# Ensure supervisord and all children are stopped only while this
|
||||
# foreground parent is still the active server parent. Standby
|
||||
# parents must not tear down a newer leader's services. If this
|
||||
# foreground parent itself received an OS shutdown signal, always
|
||||
# stop the supervisord child it owns; stop_own_supervisord_process()
|
||||
# does not target supervisord processes owned by other parents.
|
||||
stop_own_supervisord_process(record_exit=not signal_shutdown_requested)
|
||||
return tail_result
|
||||
except (KeyboardInterrupt, OSError):
|
||||
if not shutdown_state.signal_name:
|
||||
print("\n[🛑] Got CTRL+C, stopping gracefully...")
|
||||
except SystemExit:
|
||||
pass
|
||||
except BaseException as e:
|
||||
STDERR.print(f"\n[🛑] Got {e.__class__.__name__} exception, stopping gracefully...")
|
||||
finally:
|
||||
if not daemonize:
|
||||
signal_shutdown_requested = bool(shutdown_state and shutdown_state.signal_name)
|
||||
stop_owned = signal_shutdown_requested or should_stop_supervisord is None or should_stop_supervisord()
|
||||
# The ownership callback can itself receive the first signal.
|
||||
signal_shutdown_requested = bool(shutdown_state and shutdown_state.signal_name)
|
||||
if stop_owned or signal_shutdown_requested:
|
||||
# A standby parent must never tear down the replacement
|
||||
# leader's services. Signals stop only our own Popen child,
|
||||
# even if leadership changed; they avoid DB exit writes.
|
||||
stop_own_supervisord_process(record_exit=not signal_shutdown_requested)
|
||||
# A signal can arrive during the final ownership/process-exit check. Do not
|
||||
# send the outer server loop back into standby or restart after shutdown.
|
||||
return "interrupted" if shutdown_state and shutdown_state.signal_name else tail_result
|
||||
@@ -10,8 +10,9 @@ import sys
|
||||
import time
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
from playwright.sync_api import expect, sync_playwright
|
||||
|
||||
|
||||
def main():
|
||||
@@ -96,8 +97,49 @@ def main():
|
||||
response = page.goto(snapshot["replay_url"], wait_until="domcontentloaded")
|
||||
assert response and response.ok, snapshot["replay_url"]
|
||||
page.locator('.thumb-card[data-plugin-name="screenshot"]').wait_for(state="attached")
|
||||
page.locator("#main-frame").wait_for(state="attached")
|
||||
page.screenshot(path=str(evidence / f"{snapshot['id']}.png"), full_page=True)
|
||||
try:
|
||||
# The shell creates an about:blank iframe before replay starts.
|
||||
# Its presence previously accepted blank screenshots as proof of
|
||||
# replay. Exercise the default viewer and its real captured page,
|
||||
# using the same readiness contract as the WACZ browser tests.
|
||||
expect(page.locator(".header-title-text")).to_contain_text(re.compile(r"\S"))
|
||||
preview = page.locator("#main-frame")
|
||||
expect(preview).to_have_attribute("src", re.compile(r"/archivewebpage/archivewebpage\.wacz\?preview=1"))
|
||||
replay_root = preview.content_frame.locator("#replay-root")
|
||||
expect(replay_root).to_have_attribute("data-url", snapshot["url"])
|
||||
preview_frame = preview.element_handle().content_frame()
|
||||
assert preview_frame is not None
|
||||
preview_frame.wait_for_function(
|
||||
"""() => {
|
||||
if (window.archiveboxReplayError) throw new Error(window.archiveboxReplayError);
|
||||
return Boolean(window.archiveboxReplayReady);
|
||||
}""",
|
||||
)
|
||||
# Extracted display titles need not match the captured document's
|
||||
# <title>: HedgeDoc's note heading and HTML title differ. Identify
|
||||
# the archived document by its replay origin/path and original URL,
|
||||
# as the ArchiveWebpage browser tests do. Wrapper frames and other
|
||||
# output thumbnails cannot satisfy this check.
|
||||
replay_base = replay_root.get_attribute("data-replaybase")
|
||||
assert replay_base
|
||||
replay_prefix = urljoin(preview_frame.url, f"{replay_base}w/")
|
||||
frames = list(preview_frame.child_frames)
|
||||
frame_titles = {}
|
||||
while frames:
|
||||
replay = frames.pop()
|
||||
frame_titles[replay.url] = replay.title().strip()
|
||||
if replay.url.startswith(replay_prefix) and replay.url.endswith(f"/{snapshot['url']}"):
|
||||
body = replay.locator("body")
|
||||
expect(body).to_be_visible()
|
||||
# Both acceptance URLs contain substantial article/profile
|
||||
# text. Reject a blank frame or a short viewer error message.
|
||||
assert len(" ".join(body.inner_text().split())) > 1000, f"Missing captured page content: {replay.url}"
|
||||
break
|
||||
frames.extend(replay.child_frames)
|
||||
else:
|
||||
raise AssertionError(f"Captured URL {snapshot['url']!r} not found in replay frames: {frame_titles}")
|
||||
finally:
|
||||
page.screenshot(path=str(evidence / f"{snapshot['id']}.png"), full_page=True)
|
||||
browser.close()
|
||||
# Recheck identity and host OOM history after the browser flow too.
|
||||
final = remote("capture")
|
||||
|
||||
@@ -118,10 +118,22 @@ trap cleanup EXIT INT TERM
|
||||
|
||||
mkdir -p "$DATA_DIR" "$OUTPUT_DIR" "$PUBLIC_OUTPUT_DIR"
|
||||
|
||||
PLUGIN_SCREENSHOT_PLAN="$CAPTURE_ROOT/plugin-screenshots.json"
|
||||
uv run --no-cache --project "$REPO_DIR" "$REPO_DIR/bin/generate_ui_screenshot_gallery.py" plugin-plan >"$PLUGIN_SCREENSHOT_PLAN"
|
||||
PLUGIN_ENABLED_CONFIG=()
|
||||
while IFS= read -r enabled_config; do
|
||||
[[ -z "$enabled_config" ]] || PLUGIN_ENABLED_CONFIG+=("$enabled_config")
|
||||
done < <(uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'import json,sys; print("\n".join(f"{plugin.upper()}_ENABLED=True" for plugin in json.load(open(sys.argv[1]))["enabled"]))' \
|
||||
"$PLUGIN_SCREENSHOT_PLAN")
|
||||
|
||||
echo "[*] Initializing the ArchiveBox collection at $DATA_DIR"
|
||||
(
|
||||
cd "$DATA_DIR"
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox init --quick
|
||||
if [[ "${#PLUGIN_ENABLED_CONFIG[@]}" != "0" ]]; then
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox config --set "${PLUGIN_ENABLED_CONFIG[@]}"
|
||||
fi
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox install
|
||||
)
|
||||
|
||||
@@ -209,11 +221,7 @@ PYPORT
|
||||
)"
|
||||
(
|
||||
cd "$DATA_DIR"
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox config --set \
|
||||
OPENCODE_ENABLED=True "OPENCODE_PORT=$OPENCODE_PORT" TLSNOTARY_ENABLED=True \
|
||||
HASHES_ENABLED=True OPENTIMESTAMPS_ENABLED=True
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox install opencode --binproviders=env,pnpm
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox install opentimestamps
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox config --set "OPENCODE_PORT=$OPENCODE_PORT"
|
||||
)
|
||||
|
||||
echo "[*] Starting ArchiveBox on port $PORT"
|
||||
@@ -262,43 +270,16 @@ VIEWS=(
|
||||
"Login|$ADMIN_BASE_URL/admin/login/|/admin/login/|archivebox/templates/admin/login.html"
|
||||
"Public snapshot list|$PUBLIC_BASE_URL/public/|/public/|archivebox/core/views.py"
|
||||
)
|
||||
REQUIRED_TEMPLATE_PLUGINS=(
|
||||
accessibility
|
||||
chrome
|
||||
consolelog
|
||||
defuddle
|
||||
dns
|
||||
hashes
|
||||
headers
|
||||
htmltotext
|
||||
liteparse
|
||||
opendataloader
|
||||
parse_dom_outlinks
|
||||
parse_html_urls
|
||||
parse_jsonl_urls
|
||||
parse_netscape_urls
|
||||
parse_rss_urls
|
||||
parse_txt_urls
|
||||
redirects
|
||||
seo
|
||||
sslcerts
|
||||
title
|
||||
trafilatura
|
||||
)
|
||||
DISCOVERED_TEMPLATE_PLUGINS=()
|
||||
|
||||
add_supplementary_template_capture() {
|
||||
local plugin_name="$1"
|
||||
local plugin_names="$1"
|
||||
local source_url="$2"
|
||||
local capture_plugins="$plugin_name"
|
||||
local capture_log="$CAPTURE_ROOT/${plugin_name}-supplementary-capture.log"
|
||||
local supplementary_plugins=("$plugin_name")
|
||||
|
||||
if [[ "$plugin_name" == "opendataloader" ]]; then
|
||||
capture_plugins="wget"
|
||||
supplementary_plugins=(liteparse opendataloader)
|
||||
fi
|
||||
echo "[*] Capturing real $plugin_name source for template coverage"
|
||||
local prepare_plugins="$3"
|
||||
local capture_plugins="${prepare_plugins:-$plugin_names}"
|
||||
local capture_log="$CAPTURE_ROOT/${plugin_names}-supplementary-capture.log"
|
||||
local supplementary_plugins=()
|
||||
IFS=',' read -r -a supplementary_plugins <<<"$plugin_names"
|
||||
echo "[*] Capturing real $plugin_names source for template coverage"
|
||||
if ! (
|
||||
cd "$DATA_DIR"
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox add \
|
||||
@@ -308,7 +289,7 @@ add_supplementary_template_capture() {
|
||||
--plugins="$capture_plugins" \
|
||||
"$source_url"
|
||||
) >"$capture_log" 2>&1; then
|
||||
echo "[!] Supplementary $plugin_name capture failed" >&2
|
||||
echo "[!] Supplementary $plugin_names capture failed" >&2
|
||||
tail -100 "$capture_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -321,37 +302,37 @@ add_supplementary_template_capture() {
|
||||
'import os; from urllib.parse import urlsplit; from archivebox.core.models import Snapshot; from archivebox.core.routes_util import build_snapshot_detail_url; snapshot=Snapshot.objects.filter(url=os.environ["UI_SCREENSHOT_SOURCE_URL"]).order_by("-bookmarked_at").first(); url=build_snapshot_detail_url(snapshot.archive_path_from_db) if snapshot else ""; print(f"{snapshot.id}\t{url}\t{urlsplit(url).path}" if snapshot else "")'
|
||||
) | tail -1)"
|
||||
if [[ -z "$snapshot_record" ]]; then
|
||||
echo "[!] Supplementary $plugin_name capture did not create a snapshot" >&2
|
||||
echo "[!] Supplementary $plugin_names capture did not create a snapshot" >&2
|
||||
tail -100 "$capture_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local snapshot_id snapshot_view_url snapshot_view_path
|
||||
IFS=$'\t' read -r snapshot_id snapshot_view_url snapshot_view_path <<<"$snapshot_record"
|
||||
if [[ "$plugin_name" == "opendataloader" ]]; then
|
||||
if [[ -n "$prepare_plugins" ]]; then
|
||||
if ! (
|
||||
cd "$DATA_DIR"
|
||||
LITEPARSE_ENABLED=True OPENDATALOADER_ENABLED=True uv run --no-cache --project "$REPO_DIR" archivebox extract \
|
||||
--plugins=liteparse,opendataloader \
|
||||
uv run --no-cache --project "$REPO_DIR" archivebox extract \
|
||||
--plugins="$plugin_names" \
|
||||
"$snapshot_id"
|
||||
) >>"$capture_log" 2>&1; then
|
||||
echo "[!] LiteParse/OpenDataLoader extraction failed for the captured wget PDF" >&2
|
||||
echo "[!] $plugin_names extraction failed for the prepared snapshot" >&2
|
||||
tail -100 "$capture_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
stop_background_runner
|
||||
fi
|
||||
|
||||
local discovery_report="$CAPTURE_ROOT/${plugin_name}-output-discovery.json"
|
||||
local discovery_report="$CAPTURE_ROOT/${plugin_names}-output-discovery.json"
|
||||
NODE_PATH="$ABXPKG_LIB_DIR/pnpm/packages/chrome/node_modules" \
|
||||
CHROME_BINARY="$SCREENSHOT_CHROME_BINARY" \
|
||||
SCREENSHOT_USER_DATA_DIR="$PERSONAS_DIR/$ACTIVE_PERSONA/chrome_profile" \
|
||||
SCREENSHOT_SNAPSHOT_HEADER=collapsed \
|
||||
SCREENSHOT_WIDTH=1600 \
|
||||
SCREENSHOT_HEIGHT=1000 \
|
||||
node "$REPO_DIR/bin/take_screenshot.js" "$snapshot_view_url" "$CAPTURE_ROOT/${plugin_name}-output-discovery.png" >"$discovery_report"
|
||||
node "$REPO_DIR/bin/take_screenshot.js" "$snapshot_view_url" "$CAPTURE_ROOT/${plugin_names}-output-discovery.png" >"$discovery_report"
|
||||
|
||||
local supplementary_plugin discovered_preview_url plugin_already_discovered discovered_plugin
|
||||
local supplementary_plugin discovered_preview_url capture_mode
|
||||
for supplementary_plugin in "${supplementary_plugins[@]}"; do
|
||||
discovered_preview_url="$(
|
||||
UI_SCREENSHOT_DISCOVERY_REPORT="$discovery_report" UI_SCREENSHOT_EXPECT_PLUGIN="$supplementary_plugin" \
|
||||
@@ -365,17 +346,10 @@ add_supplementary_template_capture() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
plugin_already_discovered=0
|
||||
for discovered_plugin in "${DISCOVERED_TEMPLATE_PLUGINS[@]}"; do
|
||||
if [[ "$discovered_plugin" == "$supplementary_plugin" ]]; then
|
||||
plugin_already_discovered=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$plugin_already_discovered" == "0" ]]; then
|
||||
DISCOVERED_TEMPLATE_PLUGINS+=("$supplementary_plugin")
|
||||
VIEWS+=("Snapshot View ($supplementary_plugin)|$snapshot_view_url#$supplementary_plugin|$snapshot_view_path|archivebox/templates/core/snapshot.html")
|
||||
fi
|
||||
capture_mode="$(uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'import json,sys; recipe=json.load(open(sys.argv[1]))["overrides"][sys.argv[2]]; text=recipe.get("wait_for_text", ""); print("wait-preview-text:" + text if text else "")' \
|
||||
"$PLUGIN_SCREENSHOT_PLAN" "$supplementary_plugin")"
|
||||
VIEWS+=("Snapshot View ($supplementary_plugin)|$snapshot_view_url#$supplementary_plugin|$snapshot_view_path|archivebox/templates/core/snapshot.html|$capture_mode")
|
||||
done
|
||||
}
|
||||
|
||||
@@ -387,9 +361,12 @@ while [[ "$capture_index" -lt "${#VIEWS[@]}" ]]; do
|
||||
slug="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '-' | sed 's/^-//; s/-$//')"
|
||||
echo "[*] $name: $url"
|
||||
snapshot_header=expanded
|
||||
[[ "$name" != "Snapshot View ("* ]] || snapshot_header=collapsed
|
||||
[[ "$capture_mode" != "snapshot-collapsed" ]] || snapshot_header=collapsed
|
||||
wait_for_text=""
|
||||
[[ "$capture_mode" != wait-text:* ]] || wait_for_text="${capture_mode#wait-text:}"
|
||||
preview_wait_for_text=""
|
||||
[[ "$capture_mode" != wait-preview-text:* ]] || preview_wait_for_text="${capture_mode#wait-preview-text:}"
|
||||
view_timing_report=""
|
||||
while IFS='|' read -r profile _viewport_width _viewport_height; do
|
||||
filename="$slug-$profile.png"
|
||||
@@ -515,6 +492,7 @@ PY
|
||||
SCREENSHOT_COLLAPSE_FILTERS=1 \
|
||||
SCREENSHOT_SNAPSHOT_HEADER="$snapshot_header" \
|
||||
SCREENSHOT_WAIT_FOR_TEXT="$wait_for_text" \
|
||||
SCREENSHOT_PREVIEW_WAIT_FOR_TEXT="$preview_wait_for_text" \
|
||||
node "$REPO_DIR/bin/take_screenshot.js" "$url" "$screenshot_path" >"$capture_dir/report.json"
|
||||
view_timing_report="$capture_dir/report.json"
|
||||
timing_report_path="$view_timing_report"
|
||||
@@ -674,54 +652,23 @@ PY
|
||||
SCREENSHOT_WIDTH=1600 \
|
||||
SCREENSHOT_HEIGHT=1000 \
|
||||
node "$REPO_DIR/bin/take_screenshot.js" "$LIVE_SNAPSHOT_VIEW_URL" "$CAPTURE_ROOT/snapshot-output-discovery.png" >"$SNAPSHOT_DISCOVERY_REPORT"
|
||||
SNAPSHOT_OUTPUT_PLUGINS="$(UI_SCREENSHOT_DISCOVERY_REPORT="$SNAPSHOT_DISCOVERY_REPORT" uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'import json, os; from urllib.parse import urlsplit; report=json.load(open(os.environ["UI_SCREENSHOT_DISCOVERY_REPORT"])); print("\n".join("{}\t{}\t{}".format(output["plugin"], output["outputPath"], "wait-replay:Nick Sweeting" if urlsplit(output["previewUrl"]).path.endswith(".wacz") else "") for output in report["checks"]["snapshotOutputs"]))')"
|
||||
SNAPSHOT_OUTPUT_PLUGINS="$(UI_SCREENSHOT_PLAN="$PLUGIN_SCREENSHOT_PLAN" UI_SCREENSHOT_DISCOVERY_REPORT="$SNAPSHOT_DISCOVERY_REPORT" uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'import json, os; from urllib.parse import urlsplit; report=json.load(open(os.environ["UI_SCREENSHOT_DISCOVERY_REPORT"])); overrides=json.load(open(os.environ["UI_SCREENSHOT_PLAN"]))["overrides"]; print("\n".join("{}\t{}\t{}".format(output["plugin"], output["outputPath"], "wait-replay:Nick Sweeting" if urlsplit(output["previewUrl"]).path.endswith(".wacz") else "") for output in report["checks"]["snapshotOutputs"] if output["plugin"] not in overrides))')"
|
||||
if [[ -z "$SNAPSHOT_OUTPUT_PLUGINS" ]]; then
|
||||
echo "[!] The Sweeting.me snapshot detail page exposed no selectable outputs" >&2
|
||||
exit 1
|
||||
fi
|
||||
for required_plugin in tlsnotary opentimestamps; do
|
||||
if ! printf '%s\n' "$SNAPSHOT_OUTPUT_PLUGINS" | cut -f1 | grep -qx "$required_plugin"; then
|
||||
echo "[!] Live capture did not expose the required $required_plugin output" >&2
|
||||
tail -200 "$CAPTURE_ROOT/sweeting-live-capture.log" >&2
|
||||
cat "$SNAPSHOT_DISCOVERY_REPORT" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
while IFS=$'\t' read -r plugin_name output_path output_capture_mode; do
|
||||
[[ -z "$plugin_name" ]] && continue
|
||||
DISCOVERED_TEMPLATE_PLUGINS+=("$plugin_name")
|
||||
VIEWS+=("Snapshot View ($plugin_name)|$LIVE_SNAPSHOT_VIEW_URL#$output_path|$LIVE_SNAPSHOT_VIEW_PATH|archivebox/templates/core/snapshot.html|$output_capture_mode")
|
||||
done <<<"$SNAPSHOT_OUTPUT_PLUGINS"
|
||||
add_supplementary_template_capture \
|
||||
parse_jsonl_urls \
|
||||
https://raw.githubusercontent.com/noho/dayu-agent/f4343e490dd096e81a2f4e715c5592a10e126a26/utils/web_ci_urls.jsonl
|
||||
add_supplementary_template_capture \
|
||||
parse_rss_urls \
|
||||
https://github.com/ArchiveBox/ArchiveBox/releases.atom
|
||||
add_supplementary_template_capture \
|
||||
parse_netscape_urls \
|
||||
https://gist.githubusercontent.com/jgarber623/cdc8e2fa1cbcb6889872/raw/2081aac716fd9aeb2dc443cd7cace57b8e830c57/delicious.html
|
||||
add_supplementary_template_capture \
|
||||
opendataloader \
|
||||
https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf
|
||||
while IFS='|' read -r example_plugins example_url example_prepare; do
|
||||
[[ -z "$example_plugins" ]] && continue
|
||||
add_supplementary_template_capture "$example_plugins" "$example_url" "$example_prepare"
|
||||
done < <(uv run --no-cache --project "$REPO_DIR" python -c \
|
||||
'import json,sys; plan=json.load(open(sys.argv[1])); print("\n".join("|".join((",".join(example["plugins"]), example["url"], ",".join(example["prepare_plugins"]))) for example in plan["examples"]))' \
|
||||
"$PLUGIN_SCREENSHOT_PLAN")
|
||||
|
||||
missing_template_plugins=()
|
||||
for required_plugin in "${REQUIRED_TEMPLATE_PLUGINS[@]}"; do
|
||||
plugin_was_discovered=0
|
||||
for discovered_plugin in "${DISCOVERED_TEMPLATE_PLUGINS[@]}"; do
|
||||
if [[ "$discovered_plugin" == "$required_plugin" ]]; then
|
||||
plugin_was_discovered=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
[[ "$plugin_was_discovered" == "0" ]] && missing_template_plugins+=("$required_plugin")
|
||||
done
|
||||
if [[ "$MAX_VIEWS" == "0" && "${#missing_template_plugins[@]}" != "0" ]]; then
|
||||
echo "[!] Real snapshot UI discovery is missing template coverage for: ${missing_template_plugins[*]}" >&2
|
||||
cat "$SNAPSHOT_DISCOVERY_REPORT" >&2
|
||||
exit 1
|
||||
fi
|
||||
VIEWS+=("Snapshot View (header collapsed)|$LIVE_SNAPSHOT_VIEW_URL|$LIVE_SNAPSHOT_VIEW_PATH|archivebox/templates/core/snapshot.html|snapshot-collapsed")
|
||||
fi
|
||||
if [[ "$MAX_VIEWS" != "0" && "$capture_index" -ge "$MAX_VIEWS" ]]; then
|
||||
|
||||
@@ -32,7 +32,6 @@ REQUIRED_VIEW_NAMES = {
|
||||
"Snapshot admin detail",
|
||||
"Snapshot View (capture in progress)",
|
||||
"Snapshot View (header collapsed)",
|
||||
"Snapshot View (opentimestamps)",
|
||||
"Snapshot files",
|
||||
"Archive results",
|
||||
"Archive result detail",
|
||||
@@ -61,6 +60,41 @@ REQUIRED_VIEW_NAMES = {
|
||||
}
|
||||
|
||||
|
||||
def plugin_screenshot_plan() -> dict:
|
||||
"""Discover default coverage and plugin-owned examples from the installed package."""
|
||||
from abx_plugins import get_plugins_dir
|
||||
|
||||
required = []
|
||||
examples = {}
|
||||
overrides = {}
|
||||
enabled_plugins = []
|
||||
for plugin_dir in sorted(get_plugins_dir().iterdir()):
|
||||
config_path = plugin_dir / "config.json"
|
||||
if not config_path.is_file():
|
||||
continue
|
||||
config = json.loads(config_path.read_text())
|
||||
recipe = json.loads((plugin_dir / config["screenshot"]).read_text()) if config.get("screenshot") else {}
|
||||
plugin = plugin_dir.name
|
||||
enabled = config.get("properties", {}).get(f"{plugin.upper()}_ENABLED", {}).get("default", False)
|
||||
if recipe.get("enabled") or recipe.get("url"):
|
||||
enabled_plugins.append(plugin)
|
||||
enabled = True
|
||||
if recipe.get("view"):
|
||||
required.append(recipe["view"])
|
||||
elif enabled and (
|
||||
recipe.get("url")
|
||||
or (plugin_dir / "templates/full.html").is_file()
|
||||
or ((plugin_dir / "templates/card.html").is_file() and not config.get("card_hidden"))
|
||||
):
|
||||
required.append(f"Snapshot View ({plugin})")
|
||||
if recipe.get("url"):
|
||||
overrides[plugin] = recipe
|
||||
key = (recipe["url"], tuple(recipe.get("prepare_plugins", [])))
|
||||
example = examples.setdefault(key, {"url": key[0], "prepare_plugins": list(key[1]), "plugins": []})
|
||||
example["plugins"].append(plugin)
|
||||
return {"required": required, "examples": list(examples.values()), "overrides": overrides, "enabled": enabled_plugins}
|
||||
|
||||
|
||||
def build_provenance() -> dict[str, str]:
|
||||
version = tomllib.loads((REPO_DIR / "pyproject.toml").read_text(encoding="utf-8"))["project"]["version"]
|
||||
revision = os.environ.get("GITHUB_SHA", "").strip()
|
||||
@@ -213,19 +247,9 @@ def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -
|
||||
|
||||
if not allow_partial:
|
||||
captured_names = set(captures_by_name)
|
||||
missing = sorted(REQUIRED_VIEW_NAMES - captured_names)
|
||||
missing = sorted((REQUIRED_VIEW_NAMES | set(plugin_screenshot_plan()["required"])) - captured_names)
|
||||
if missing:
|
||||
raise SystemExit(f"required UI screenshot coverage is missing: {', '.join(missing)}")
|
||||
snapshot_output_views = [
|
||||
name
|
||||
for name in captured_names
|
||||
if name.startswith("Snapshot View (")
|
||||
and name not in {"Snapshot View (capture in progress)", "Snapshot View (header collapsed)"}
|
||||
]
|
||||
if len(snapshot_output_views) < 20:
|
||||
raise SystemExit(
|
||||
f"expected at least 20 rendered Sweeting.me snapshot outputs, got {len(snapshot_output_views)}",
|
||||
)
|
||||
markdown_sections = []
|
||||
html_sections = []
|
||||
for capture in grouped_captures:
|
||||
@@ -276,7 +300,7 @@ def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -
|
||||
),
|
||||
)
|
||||
html_sections.append(
|
||||
f"<article><h2>{html.escape(capture['name'])}</h2>"
|
||||
f'<article id="{re.sub(r"[^a-z0-9]+", "-", capture["name"].lower()).strip("-")}"><h2>{html.escape(capture["name"])}</h2>'
|
||||
f'<p><a href="{html.escape(capture["url"])}"><code>{html.escape(route)}</code></a> · '
|
||||
f'<a href="{html.escape(source_url)}">View code</a>{html.escape(ttfb_text)}</p>'
|
||||
f'<div class="shots">{"".join(html_figures)}</div></article>',
|
||||
@@ -345,6 +369,12 @@ def refresh_gallery(html_path: Path) -> None:
|
||||
if not header or not sections:
|
||||
raise SystemExit("Published screenshot gallery is missing its capture header or content")
|
||||
content = sections[1]
|
||||
content = re.sub(
|
||||
r"<article><h2>(.*?)</h2>",
|
||||
lambda match: f'<article id="{re.sub(r"[^a-z0-9]+", "-", html.unescape(match[1]).lower()).strip("-")}"><h2>{match[1]}</h2>',
|
||||
content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
for profile in CAPTURE_PROFILES:
|
||||
# Galleries published before the switcher used classes alone.
|
||||
content = content.replace(
|
||||
@@ -355,6 +385,9 @@ def refresh_gallery(html_path: Path) -> None:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if sys.argv[1:] == ["plugin-plan"]:
|
||||
print(json.dumps(plugin_screenshot_plan()))
|
||||
return
|
||||
if len(sys.argv) == 3 and sys.argv[1] == "refresh":
|
||||
refresh_gallery(Path(sys.argv[2]))
|
||||
return
|
||||
|
||||
@@ -27,6 +27,7 @@ Environment:
|
||||
SCREENSHOT_SCROLL_SELECTOR Scroll this selector into view before capture
|
||||
SCREENSHOT_WAIT_SELECTOR Wait for this selector before capture
|
||||
SCREENSHOT_WAIT_FOR_TEXT Wait for this visible page text before capture
|
||||
SCREENSHOT_PREVIEW_WAIT_FOR_TEXT Wait for this text in the selected snapshot preview iframe
|
||||
SCREENSHOT_REQUIRE_LIVE_PROGRESS Require active snapshot progress and a nonblank decoded screencast within 60s
|
||||
SCREENSHOT_CLICK_SELECTOR Click this selector before capture
|
||||
SCREENSHOT_AFTER_CLICK_WAIT_SELECTOR Wait for this selector after clicking
|
||||
@@ -353,6 +354,16 @@ async function main() {
|
||||
if (process.env.SCREENSHOT_WAIT_SELECTOR) {
|
||||
await page.waitForSelector(process.env.SCREENSHOT_WAIT_SELECTOR, { timeout: 45000 });
|
||||
}
|
||||
if (process.env.SCREENSHOT_PREVIEW_WAIT_FOR_TEXT) {
|
||||
const previewHandle = await page.waitForSelector('#main-frame', {timeout: 45000});
|
||||
const preview = await previewHandle.contentFrame();
|
||||
if (!preview) throw new Error('Snapshot preview iframe has no document');
|
||||
await preview.waitForFunction(
|
||||
(text) => document.body?.innerText.includes(text),
|
||||
{timeout: 45000},
|
||||
process.env.SCREENSHOT_PREVIEW_WAIT_FOR_TEXT,
|
||||
);
|
||||
}
|
||||
if (process.env.SCREENSHOT_CLICK_SELECTOR) {
|
||||
await page.waitForSelector(process.env.SCREENSHOT_CLICK_SELECTOR, { timeout: 45000 });
|
||||
await page.click(process.env.SCREENSHOT_CLICK_SELECTOR);
|
||||
@@ -373,6 +384,13 @@ async function main() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
}
|
||||
|
||||
// Selecting a hash opens its output stack after the saved header preference
|
||||
// is read. Apply the requested collapse through the real toggle afterwards.
|
||||
if (process.env.SCREENSHOT_SNAPSHOT_HEADER === 'collapsed') {
|
||||
const toggle = await page.$('.header-toggle[aria-expanded="true"]');
|
||||
if (toggle) await toggle.click();
|
||||
}
|
||||
|
||||
const checks = await page.evaluate(() => ({
|
||||
url: location.href,
|
||||
title: document.title,
|
||||
|
||||
@@ -373,6 +373,16 @@ def run_snippets(snippet_ids: tuple[str, ...]) -> None:
|
||||
"PATH": f"{Path(sys.executable).parent}:{env['PATH']}",
|
||||
},
|
||||
)
|
||||
# A runnable Python alias can still select an interpreter without the
|
||||
# project's packages. Keep its first hop visible when a real hook fails;
|
||||
# resolving every symlink here would hide the venv path we need to check.
|
||||
if env.get("PYTHON_BINARY"):
|
||||
python_override = Path(env["PYTHON_BINARY"])
|
||||
print(
|
||||
f"Documentation Python: harness={sys.executable}, override={python_override}, "
|
||||
f"target={python_override.readlink() if python_override.is_symlink() else python_override}",
|
||||
flush=True,
|
||||
)
|
||||
Path(env["HOME"]).mkdir()
|
||||
scenarios = {records[snippet_id]["scenario"] for snippet_id in snippet_ids}
|
||||
workdirs = {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "archivebox",
|
||||
"version": "0.9.71rc25",
|
||||
"version": "0.9.71",
|
||||
"repository": "github:ArchiveBox/ArchiveBox",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4V3Z"></path></svg></span>
|
||||
<h3>يحفظ علامات التبويب في ثوان ، يخزنها منذ عقود</h3>
|
||||
<p>جمع الإشارات المرجعية للقراءة لاحقًا ، أو الاحتفاظ بالأدلة للحالات القانونية التي يتم التحقق منها من عمليات التقاط من موقعنا <a href="https://tlsnotary.zervice.io/">ملحق تسجيل TLSNotary</a>.</p>
|
||||
<p>جمع الإشارات المرجعية للقراءة لاحقًا ، أو الاحتفاظ بالأدلة للحالات القانونية التي يتم التحقق منها من عمليات التقاط من موقعنا <a href="https://tlsnotary.archivebox.io/">ملحق تسجيل TLSNotary</a>.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><circle cx="10" cy="10" r="7"></circle><path d="m15 15 6 6"></path></svg></span>
|
||||
@@ -204,7 +204,7 @@
|
||||
<ul>
|
||||
<li>تخزين لقطات الشاشة ، ملفات PDFs ، الرؤوس ، ملفات WARC ، واستخراج النص.</li>
|
||||
<li>ضع علامة على المصادر واستعراضها من خلال واجهة الويب المستضافة ذاتيًا.</li>
|
||||
<li>استخدام ZK proofs مع <a href="https://tlsnotary.zervice.io/">TLSNotary</a> لإثبات صحة المحتوى.</li>
|
||||
<li>استخدام ZK proofs مع <a href="https://tlsnotary.archivebox.io/">TLSNotary</a> لإثبات صحة المحتوى.</li>
|
||||
</ul>
|
||||
<figure class="card-shot"><a href="/screenshots/snapshot-view-tlsnotary-desktop.png"><img alt="مخرج TLSNotary للتحقق من استجابة تم أرشيفة في صفحة تفاصيل اللقطة" height="1000" loading="lazy" src="/screenshots/snapshot-view-tlsnotary-desktop.png" width="1600"/></a></figure>
|
||||
</article>
|
||||
@@ -370,8 +370,8 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<div aria-labelledby="install-tab-platforms" class="install-panel" id="install-platforms">
|
||||
<figure class="install-art install-art--server"><div class="install-art-stage"><svg aria-hidden="true" class="install-server-rack" viewbox="0 0 160 180"><rect height="156" rx="12" width="124" x="18" y="12"></rect><rect height="34" rx="5" width="96" x="32" y="28"></rect><rect height="34" rx="5" width="96" x="32" y="73"></rect><rect height="34" rx="5" width="96" x="32" y="118"></rect><path d="M47 45h43m-43 45h43m-43 45h43"></path><circle cx="111" cy="45" r="4"></circle><circle cx="111" cy="90" r="4"></circle><circle cx="111" cy="135" r="4"></circle></svg><div class="install-art-tile"><img alt="" class="" loading="lazy" src="../assets/install/archivebox-server.png"/><span>ArchiveBox</span></div><div class="install-art-chips"><span>NAS</span><span>خادم منزلي</span></div></div><figcaption>على الأجهزة الخاصة بك</figcaption></figure>
|
||||
<div class="command-card">
|
||||
<div class="step"><span>1</span><div><h4>اختر منصتك</h4><p>قد تتأخر تكاملات المجتمع عن إصدارات Docker وPython.</p><div class="mini-links"><a href="https://unraid.net/community/apps?q=archivebox#r">نونريد</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>تثبيت وفتح ArchiveBox</h4><p>اتبع دليل تثبيت المنصة ، قم بتكوين التخزين المستمر ، ثم افتح التطبيق لإنهاء الإعداد.</p><p>بالنسبة إلى TrueNAS، راجع <a href="https://github.com/ArchiveBox/ArchiveBox#quickstart">ملاحظات منصة README</a> قبل اختيار التكامل.</p></div></div>
|
||||
<div class="step"><span>1</span><div><h4>اختر منصتك</h4><p>قد تتأخر تكاملات المجتمع عن إصدارات Docker وPython.</p><div class="mini-links"><a href="https://apps.truenas.com/catalog/archivebox_community/">TrueNAS</a><a href="https://unraid.net/community/apps?q=archivebox#r">نونريد</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>تثبيت وفتح ArchiveBox</h4><p>اتبع دليل تثبيت المنصة ، قم بتكوين التخزين المستمر ، ثم افتح التطبيق لإنهاء الإعداد.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-labelledby="install-tab-hosting" class="install-panel" id="install-hosting">
|
||||
@@ -408,7 +408,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<li><a aria-label="ملحق أحادي الملف" href="https://plugins.archivebox.io/#singlefile" title="ملف فردي"><img alt="" height="44" loading="lazy" src="../assets/plugins/singlefile.png" width="44"/></a></li>
|
||||
<li><a aria-label="يوتيوب/yt-dlp البرنامج المساعد" href="https://plugins.archivebox.io/#ytdlp" title="يوتيوب/yt-dlp"><img alt="" height="44" loading="lazy" src="../assets/plugins/youtube.svg" width="44"/></a></li>
|
||||
<li><a aria-label="ذيل" href="https://tailscale.com/" title="ذيل"><img alt="" height="44" loading="lazy" src="../assets/plugins/tailscale.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSNotary" href="https://tlsnotary.zervice.io/" title="TLSNotary"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSNotary" href="https://tlsnotary.archivebox.io/" title="TLSNotary"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
</ul>
|
||||
<p class="configuration-description">يمكن إعداد ArchiveBox عبر واجهة الإدارة أو واجهة سطر الأوامر، <code>ArchiveBox.conf</code>، أو عن طريق استيراد الإعدادات وملفات الكوكيز من Chrome أو متصفحات أخرى. يعمل نموذج التكوين نفسه في Docker وDocker Compose، وفي عمليات التثبيت على الأجهزة الحقيقية، وفي المهام المجدولة، وكذلك في التشغيلات المؤقتة عبر واجهة سطر الأوامر.</p>
|
||||
</div>
|
||||
@@ -627,7 +627,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<!-- ARCHIVEBOX:FOOTER-START -->
|
||||
<footer aria-label="ArchiveBox ecosystem" class="abx-footer">
|
||||
<div class="abx-footer-intro"><div><a class="abx-brand" href="https://archivebox.io/"><svg aria-hidden="true" class="abx-logo" viewbox="0 0 40 40"><rect fill="#9b2854" height="40" rx="10" width="40"></rect><path d="M10 14h20v17H10z" fill="#fff4ed"></path><path d="M8 9h24v6H8z" fill="#fff4ed"></path><path d="M16 19h8v3h-8z" fill="#9b2854"></path><path d="M12 31V17" stroke="#9b2854" stroke-width="2"></path></svg><span>ArchiveBox</span></a><p class="abx-tagline">أدوات مفتوحة المصدر للاحتفاظ بنسخة دائمة من الويب.<br/>بياناتك. أجهزتك. أرشيفك.</p></div><p class="abx-footer-motto">مكان للويب<br/>الذي تريد الاحتفاظ به.</p></div>
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>التطبيقات والأدوات</h2><ul><li><a href="https://archivebox.io/">خادم ArchiveBox</a><a aria-label="خادم ArchiveBox — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox لنظامَي macOS وiOS</a><a aria-label="ArchiveBox لنظامَي macOS وiOS — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox لنظام Android</a><a aria-label="ArchiveBox لنظام Android — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox لسطح المكتب</a><a aria-label="ArchiveBox لسطح المكتب — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">إضافة ArchiveBox للمتصفح</a><a aria-label="إضافة ArchiveBox للمتصفح — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">مكتبة إضافات ArchiveBox</a><a aria-label="مكتبة إضافات ArchiveBox — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-Line truncated
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>التطبيقات والأدوات</h2><ul><li><a href="https://archivebox.io/">خادم ArchiveBox</a><a aria-label="خادم ArchiveBox — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox لنظامَي macOS وiOS</a><a aria-label="ArchiveBox لنظامَي macOS وiOS — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox لنظام Android</a><a aria-label="ArchiveBox لنظام Android — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox لسطح المكتب</a><a aria-label="ArchiveBox لسطح المكتب — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">إضافة ArchiveBox للمتصفح</a><a aria-label="إضافة ArchiveBox للمتصفح — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">مكتبة إضافات ArchiveBox</a><a aria-label="مكتبة إضافات ArchiveBox — الشيفرة المصدرية على GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="عرض الشيفرة المصدرية على GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.56.1.77-.24.77-Line truncated
|
||||
<!-- /ARCHIVEBOX:FOOTER-START -->
|
||||
<div class="abx-footer-local"><p>أرشفة ويب مفتوحة المصدر ومستضافة ذاتيًا. بترخيص MIT وبدعم غير ربحي ومصممة لمجموعات تدوم طويلًا.</p>
|
||||
<div>
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4V3Z"></path></svg></span>
|
||||
<h3>Guarda pestañas en segundos, las almacena durante décadas</h3>
|
||||
<p>Recopile marcadores para leer más tarde o conserve pruebas para casos legales con capturas verificables de nuestra <a href="https://tlsnotary.zervice.io/">Complemento de firma TLSNotary</a>.</p>
|
||||
<p>Recopile marcadores para leer más tarde o conserve pruebas para casos legales con capturas verificables de nuestra <a href="https://tlsnotary.archivebox.io/">Complemento de firma TLSNotary</a>.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><circle cx="10" cy="10" r="7"></circle><path d="m15 15 6 6"></path></svg></span>
|
||||
@@ -204,7 +204,7 @@
|
||||
<ul>
|
||||
<li>Almacene capturas de pantalla, archivos PDF, encabezados, archivos WARC y extracción de texto.</li>
|
||||
<li>Etiqueta y revisa fuentes a través de la interfaz web autohospedada.</li>
|
||||
<li>Utilice pruebas ZK con <a href="https://tlsnotary.zervice.io/">TLSNotario</a> para demostrar la autenticidad del contenido.</li>
|
||||
<li>Utilice pruebas ZK con <a href="https://tlsnotary.archivebox.io/">TLSNotario</a> para demostrar la autenticidad del contenido.</li>
|
||||
</ul>
|
||||
<figure class="card-shot"><a href="/screenshots/snapshot-view-tlsnotary-desktop.png"><img alt="Salida de TLSNotary que verifica una respuesta archivada en la página de detalles de la instantánea" height="1000" loading="lazy" src="/screenshots/snapshot-view-tlsnotary-desktop.png" width="1600"/></a></figure>
|
||||
</article>
|
||||
@@ -370,8 +370,8 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<div aria-labelledby="install-tab-platforms" class="install-panel" id="install-platforms">
|
||||
<figure class="install-art install-art--server"><div class="install-art-stage"><svg aria-hidden="true" class="install-server-rack" viewbox="0 0 160 180"><rect height="156" rx="12" width="124" x="18" y="12"></rect><rect height="34" rx="5" width="96" x="32" y="28"></rect><rect height="34" rx="5" width="96" x="32" y="73"></rect><rect height="34" rx="5" width="96" x="32" y="118"></rect><path d="M47 45h43m-43 45h43m-43 45h43"></path><circle cx="111" cy="45" r="4"></circle><circle cx="111" cy="90" r="4"></circle><circle cx="111" cy="135" r="4"></circle></svg><div class="install-art-tile"><img alt="" class="" loading="lazy" src="../assets/install/archivebox-server.png"/><span>ArchiveBox</span></div><div class="install-art-chips"><span>NAS</span><span>Servidor doméstico</span></div></div><figcaption>En tu propio hardware</figcaption></figure>
|
||||
<div class="command-card">
|
||||
<div class="step"><span>1</span><div><h4>Elige tu plataforma</h4><p>Las integraciones comunitarias pueden ir por detrás de las versiones de Docker y Python.</p><div class="mini-links"><a href="https://unraid.net/community/apps?q=archivebox#r">sin asaltar</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Instalar y abrir ArchiveBox</h4><p>Siga la guía de instalación de la plataforma, configure el almacenamiento persistente y luego abra la aplicación para finalizar la configuración.</p><p>Para TrueNAS, consulte la <a href="https://github.com/ArchiveBox/ArchiveBox#quickstart">Notas de la plataforma README</a> antes de elegir una integración.</p></div></div>
|
||||
<div class="step"><span>1</span><div><h4>Elige tu plataforma</h4><p>Las integraciones comunitarias pueden ir por detrás de las versiones de Docker y Python.</p><div class="mini-links"><a href="https://apps.truenas.com/catalog/archivebox_community/">TrueNAS</a><a href="https://unraid.net/community/apps?q=archivebox#r">sin asaltar</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Instalar y abrir ArchiveBox</h4><p>Siga la guía de instalación de la plataforma, configure el almacenamiento persistente y luego abra la aplicación para finalizar la configuración.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-labelledby="install-tab-hosting" class="install-panel" id="install-hosting">
|
||||
@@ -408,7 +408,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<li><a aria-label="Complemento de archivo único" href="https://plugins.archivebox.io/#singlefile" title="Archivo único"><img alt="" height="44" loading="lazy" src="../assets/plugins/singlefile.png" width="44"/></a></li>
|
||||
<li><a aria-label="Complemento YouTube/yt-dlp" href="https://plugins.archivebox.io/#ytdlp" title="YouTube/yt-dlp"><img alt="" height="44" loading="lazy" src="../assets/plugins/youtube.svg" width="44"/></a></li>
|
||||
<li><a aria-label="Escala de cola" href="https://tailscale.com/" title="Escala de cola"><img alt="" height="44" loading="lazy" src="../assets/plugins/tailscale.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSNotario" href="https://tlsnotary.zervice.io/" title="TLSNotario"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSNotario" href="https://tlsnotary.archivebox.io/" title="TLSNotario"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
</ul>
|
||||
<p class="configuration-description">ArchiveBox se puede configurar a través de la interfaz de usuario de administración, CLI, <code>ArchiveBox.conf</code>, o importando configuraciones y cookies desde Chrome u otros navegadores. El mismo modelo de configuración funciona en Docker, Docker Compose, instalaciones básicas, trabajos programados y ejecuciones CLI únicas.</p>
|
||||
</div>
|
||||
@@ -627,7 +627,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<!-- ARCHIVEBOX:FOOTER-START -->
|
||||
<footer aria-label="ArchiveBox ecosystem" class="abx-footer">
|
||||
<div class="abx-footer-intro"><div><a class="abx-brand" href="https://archivebox.io/"><svg aria-hidden="true" class="abx-logo" viewbox="0 0 40 40"><rect fill="#9b2854" height="40" rx="10" width="40"></rect><path d="M10 14h20v17H10z" fill="#fff4ed"></path><path d="M8 9h24v6H8z" fill="#fff4ed"></path><path d="M16 19h8v3h-8z" fill="#9b2854"></path><path d="M12 31V17" stroke="#9b2854" stroke-width="2"></path></svg><span>ArchiveBox</span></a><p class="abx-tagline">Herramientas de código abierto para conservar una copia duradera de la web.<br/>Tus datos. Tus dispositivos. Tu archivo.</p></div><p class="abx-footer-motto">Un hogar para la web<br/>que quieres conservar.</p></div>
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Aplicaciones y herramientas</h2><ul><li><a href="https://archivebox.io/">Servidor ArchiveBox</a><a aria-label="Servidor ArchiveBox — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox para macOS e iOS</a><a aria-label="ArchiveBox para macOS e iOS — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox para Android</a><a aria-label="ArchiveBox para Android — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox para escritorio</a><a aria-label="ArchiveBox para escritorio — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">Extensión de navegador ArchiveBox</a><a aria-label="Extensión de navegador ArchiveBox — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">Biblioteca de complementos ArchiveBox</a><a aria-label="Biblioteca de complementos ArchiveBox — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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 2Line truncated
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Aplicaciones y herramientas</h2><ul><li><a href="https://archivebox.io/">Servidor ArchiveBox</a><a aria-label="Servidor ArchiveBox — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox para macOS e iOS</a><a aria-label="ArchiveBox para macOS e iOS — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox para Android</a><a aria-label="ArchiveBox para Android — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox para escritorio</a><a aria-label="ArchiveBox para escritorio — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">Extensión de navegador ArchiveBox</a><a aria-label="Extensión de navegador ArchiveBox — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">Biblioteca de complementos ArchiveBox</a><a aria-label="Biblioteca de complementos ArchiveBox — código fuente en GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="Ver código fuente en GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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 2Line truncated
|
||||
<!-- /ARCHIVEBOX:FOOTER-START -->
|
||||
<div class="abx-footer-local"><p>Archivado web de código abierto y autoalojado. Con licencia MIT, apoyo de organizaciones sin ánimo de lucro y diseñado para colecciones duraderas.</p>
|
||||
<div>
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4V3Z"></path></svg></span>
|
||||
<h3>Enregistre les onglets en quelques secondes, les stocke pendant des décennies</h3>
|
||||
<p>Collectez des signets pour les lire plus tard ou conservez des preuves pour des affaires juridiques avec des captures vérifiables de notre <a href="https://tlsnotary.zervice.io/">Plugin de signature TLSNotary</a>.</p>
|
||||
<p>Collectez des signets pour les lire plus tard ou conservez des preuves pour des affaires juridiques avec des captures vérifiables de notre <a href="https://tlsnotary.archivebox.io/">Plugin de signature TLSNotary</a>.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><circle cx="10" cy="10" r="7"></circle><path d="m15 15 6 6"></path></svg></span>
|
||||
@@ -204,7 +204,7 @@
|
||||
<ul>
|
||||
<li>Stockez des captures d'écran, des PDF, des en-têtes, des fichiers WARC et des extractions de texte.</li>
|
||||
<li>Marquez et examinez les sources via l’interface Web auto-hébergée.</li>
|
||||
<li>Utilisez des preuves ZK avec <a href="https://tlsnotary.zervice.io/">TLSNotaire</a> pour prouver l'authenticité du contenu.</li>
|
||||
<li>Utilisez des preuves ZK avec <a href="https://tlsnotary.archivebox.io/">TLSNotaire</a> pour prouver l'authenticité du contenu.</li>
|
||||
</ul>
|
||||
<figure class="card-shot"><a href="/screenshots/snapshot-view-tlsnotary-desktop.png"><img alt="Sortie TLSNotary vérifiant une réponse archivée sur la page de détails de l'instantané" height="1000" loading="lazy" src="/screenshots/snapshot-view-tlsnotary-desktop.png" width="1600"/></a></figure>
|
||||
</article>
|
||||
@@ -370,8 +370,8 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<div aria-labelledby="install-tab-platforms" class="install-panel" id="install-platforms">
|
||||
<figure class="install-art install-art--server"><div class="install-art-stage"><svg aria-hidden="true" class="install-server-rack" viewbox="0 0 160 180"><rect height="156" rx="12" width="124" x="18" y="12"></rect><rect height="34" rx="5" width="96" x="32" y="28"></rect><rect height="34" rx="5" width="96" x="32" y="73"></rect><rect height="34" rx="5" width="96" x="32" y="118"></rect><path d="M47 45h43m-43 45h43m-43 45h43"></path><circle cx="111" cy="45" r="4"></circle><circle cx="111" cy="90" r="4"></circle><circle cx="111" cy="135" r="4"></circle></svg><div class="install-art-tile"><img alt="" class="" loading="lazy" src="../assets/install/archivebox-server.png"/><span>ArchiveBox</span></div><div class="install-art-chips"><span>NAS</span><span>Serveur domestique</span></div></div><figcaption>Sur votre propre matériel</figcaption></figure>
|
||||
<div class="command-card">
|
||||
<div class="step"><span>1</span><div><h4>Choisissez votre plateforme</h4><p>Les intégrations communautaires peuvent être en retard par rapport aux versions Docker et Python.</p><div class="mini-links"><a href="https://unraid.net/community/apps?q=archivebox#r">Sans raid</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Installer et ouvrir ArchiveBox</h4><p>Suivez le guide d'installation de la plateforme, configurez le stockage persistant, puis ouvrez l'application pour terminer la configuration.</p><p>Pour TrueNAS, consultez le <a href="https://github.com/ArchiveBox/ArchiveBox#quickstart">Notes de la plateforme README</a> avant de choisir une intégration.</p></div></div>
|
||||
<div class="step"><span>1</span><div><h4>Choisissez votre plateforme</h4><p>Les intégrations communautaires peuvent être en retard par rapport aux versions Docker et Python.</p><div class="mini-links"><a href="https://apps.truenas.com/catalog/archivebox_community/">TrueNAS</a><a href="https://unraid.net/community/apps?q=archivebox#r">Sans raid</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Installer et ouvrir ArchiveBox</h4><p>Suivez le guide d'installation de la plateforme, configurez le stockage persistant, puis ouvrez l'application pour terminer la configuration.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-labelledby="install-tab-hosting" class="install-panel" id="install-hosting">
|
||||
@@ -408,7 +408,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<li><a aria-label="Plugin SingleFile" href="https://plugins.archivebox.io/#singlefile" title="Fichier unique"><img alt="" height="44" loading="lazy" src="../assets/plugins/singlefile.png" width="44"/></a></li>
|
||||
<li><a aria-label="Plugin YouTube/yt-dlp" href="https://plugins.archivebox.io/#ytdlp" title="YouTube / yt-dlp"><img alt="" height="44" loading="lazy" src="../assets/plugins/youtube.svg" width="44"/></a></li>
|
||||
<li><a aria-label="Échelle arrière" href="https://tailscale.com/" title="Échelle arrière"><img alt="" height="44" loading="lazy" src="../assets/plugins/tailscale.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSNotaire" href="https://tlsnotary.zervice.io/" title="TLSNotaire"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSNotaire" href="https://tlsnotary.archivebox.io/" title="TLSNotaire"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
</ul>
|
||||
<p class="configuration-description">ArchiveBox peut être configuré via l'interface utilisateur d'administration, la CLI, <code>ArchiveBox.conf</code>, ou en important la configuration et les cookies depuis Chrome/d'autres navigateurs. Le même modèle de configuration fonctionne dans Docker, Docker Compose, les installations nues, les tâches planifiées et les exécutions CLI ponctuelles.</p>
|
||||
</div>
|
||||
@@ -627,7 +627,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<!-- ARCHIVEBOX:FOOTER-START -->
|
||||
<footer aria-label="ArchiveBox ecosystem" class="abx-footer">
|
||||
<div class="abx-footer-intro"><div><a class="abx-brand" href="https://archivebox.io/"><svg aria-hidden="true" class="abx-logo" viewbox="0 0 40 40"><rect fill="#9b2854" height="40" rx="10" width="40"></rect><path d="M10 14h20v17H10z" fill="#fff4ed"></path><path d="M8 9h24v6H8z" fill="#fff4ed"></path><path d="M16 19h8v3h-8z" fill="#9b2854"></path><path d="M12 31V17" stroke="#9b2854" stroke-width="2"></path></svg><span>ArchiveBox</span></a><p class="abx-tagline">Des outils open source pour conserver durablement une copie du Web.<br/>Vos données. Vos appareils. Vos archives.</p></div><p class="abx-footer-motto">Un espace pour le Web<br/>que vous souhaitez conserver.</p></div>
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Applications et outils</h2><ul><li><a href="https://archivebox.io/">Serveur ArchiveBox</a><a aria-label="Serveur ArchiveBox — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox pour macOS et iOS</a><a aria-label="ArchiveBox pour macOS et iOS — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox pour Android</a><a aria-label="ArchiveBox pour Android — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox pour ordinateur</a><a aria-label="ArchiveBox pour ordinateur — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">Extension de navigateur ArchiveBox</a><a aria-label="Extension de navigateur ArchiveBox — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">Bibliothèque de plugins ArchiveBox</a><a aria-label="Bibliothèque de plugins ArchiveBox — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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.9Line truncated
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Applications et outils</h2><ul><li><a href="https://archivebox.io/">Serveur ArchiveBox</a><a aria-label="Serveur ArchiveBox — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox pour macOS et iOS</a><a aria-label="ArchiveBox pour macOS et iOS — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox pour Android</a><a aria-label="ArchiveBox pour Android — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox pour ordinateur</a><a aria-label="ArchiveBox pour ordinateur — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">Extension de navigateur ArchiveBox</a><a aria-label="Extension de navigateur ArchiveBox — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">Bibliothèque de plugins ArchiveBox</a><a aria-label="Bibliothèque de plugins ArchiveBox — code source sur GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="Voir le code source sur GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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.9Line truncated
|
||||
<!-- /ARCHIVEBOX:FOOTER-START -->
|
||||
<div class="abx-footer-local"><p>Archivage web open source et auto-hébergé. Sous licence MIT, soutenu par des organismes à but non lucratif et conçu pour des collections durables.</p>
|
||||
<div>
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<article>
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4V3Z"/></svg></span>
|
||||
<h3>Saves tabs in seconds, stores them for decades</h3>
|
||||
<p>Collect bookmarks to read later, or preserve evidence for legal cases with verifiable captures from our <a href="https://tlsnotary.zervice.io/">TLSNotary signing plugin</a>.</p>
|
||||
<p>Collect bookmarks to read later, or preserve evidence for legal cases with verifiable captures from our <a href="https://tlsnotary.archivebox.io/">TLSNotary signing plugin</a>.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span class="card-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="10" cy="10" r="7"/><path d="m15 15 6 6"/></svg></span>
|
||||
@@ -196,7 +196,7 @@
|
||||
<ul>
|
||||
<li>Store screenshots, PDFs, headers, WARC files, and text extraction.</li>
|
||||
<li>Tag and review sources through the self-hosted web interface.</li>
|
||||
<li>Use ZK proofs with <a href="https://tlsnotary.zervice.io/">TLSNotary</a> to prove content authenticity.</li>
|
||||
<li>Use ZK proofs with <a href="https://tlsnotary.archivebox.io/">TLSNotary</a> to prove content authenticity.</li>
|
||||
</ul>
|
||||
<figure class="card-shot"><a href="/screenshots/snapshot-view-tlsnotary-desktop.png"><img src="/screenshots/snapshot-view-tlsnotary-desktop.png" width="1600" height="1000" loading="lazy" alt="TLSNotary output verifying an archived response on the snapshot details page"></a></figure>
|
||||
</article>
|
||||
@@ -363,8 +363,8 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<div class="install-panel" id="install-platforms" aria-labelledby="install-tab-platforms">
|
||||
<figure class="install-art install-art--server"><div class="install-art-stage"><svg class="install-server-rack" viewBox="0 0 160 180" aria-hidden="true"><rect x="18" y="12" width="124" height="156" rx="12"/><rect x="32" y="28" width="96" height="34" rx="5"/><rect x="32" y="73" width="96" height="34" rx="5"/><rect x="32" y="118" width="96" height="34" rx="5"/><path d="M47 45h43m-43 45h43m-43 45h43"/><circle cx="111" cy="45" r="4"/><circle cx="111" cy="90" r="4"/><circle cx="111" cy="135" r="4"/></svg><div class="install-art-tile"><img src="./assets/install/archivebox-server.png" alt="" class="" loading="lazy"><span>ArchiveBox</span></div><div class="install-art-chips"><span>NAS</span><span>Home server</span></div></div><figcaption>On your own hardware</figcaption></figure>
|
||||
<div class="command-card">
|
||||
<div class="step"><span>1</span><div><h4>Choose your platform</h4><p>Community integrations may lag behind the Docker and Python releases.</p><div class="mini-links"><a href="https://unraid.net/community/apps?q=archivebox#r">Unraid</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Install and open ArchiveBox</h4><p>Follow the platform’s installation guide, configure persistent storage, then open the app to finish setup.</p><p>For TrueNAS, see the <a href="https://github.com/ArchiveBox/ArchiveBox#quickstart">README’s platform notes</a> before choosing an integration.</p></div></div>
|
||||
<div class="step"><span>1</span><div><h4>Choose your platform</h4><p>Community integrations may lag behind the Docker and Python releases.</p><div class="mini-links"><a href="https://apps.truenas.com/catalog/archivebox_community/">TrueNAS</a><a href="https://unraid.net/community/apps?q=archivebox#r">Unraid</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Install and open ArchiveBox</h4><p>Follow the platform’s installation guide, configure persistent storage, then open the app to finish setup.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="install-panel" id="install-hosting" aria-labelledby="install-tab-hosting">
|
||||
@@ -402,7 +402,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<li><a href="https://plugins.archivebox.io/#singlefile" aria-label="SingleFile plugin" title="SingleFile"><img src="./assets/plugins/singlefile.png" width="44" height="44" loading="lazy" alt=""></a></li>
|
||||
<li><a href="https://plugins.archivebox.io/#ytdlp" aria-label="YouTube / yt-dlp plugin" title="YouTube / yt-dlp"><img src="./assets/plugins/youtube.svg" width="44" height="44" loading="lazy" alt=""></a></li>
|
||||
<li><a href="https://tailscale.com/" aria-label="Tailscale" title="Tailscale"><img src="./assets/plugins/tailscale.svg" width="44" height="44" loading="lazy" alt=""></a></li>
|
||||
<li><a href="https://tlsnotary.zervice.io/" aria-label="TLSNotary" title="TLSNotary"><img src="./assets/plugins/tlsnotary.svg" width="44" height="44" loading="lazy" alt=""></a></li>
|
||||
<li><a href="https://tlsnotary.archivebox.io/" aria-label="TLSNotary" title="TLSNotary"><img src="./assets/plugins/tlsnotary.svg" width="44" height="44" loading="lazy" alt=""></a></li>
|
||||
</ul>
|
||||
<p class="configuration-description">ArchiveBox can be configured via the admin UI, CLI, <code>ArchiveBox.conf</code>, or by importing config and cookies from Chrome / other browsers. The same configuration model works in Docker, Docker Compose, bare-metal installs, scheduled jobs, and one-off CLI runs.</p>
|
||||
</div>
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4V3Z"></path></svg></span>
|
||||
<h3>Сохраняет вкладки за считанные секунды и хранит их десятилетиями.</h3>
|
||||
<p>Собирайте закладки, чтобы прочитать их позже, или сохраняйте доказательства по юридическим делам с помощью поддающихся проверке записей из наших <a href="https://tlsnotary.zervice.io/">Плагин подписи TLSNotary</a>.</p>
|
||||
<p>Собирайте закладки, чтобы прочитать их позже, или сохраняйте доказательства по юридическим делам с помощью поддающихся проверке записей из наших <a href="https://tlsnotary.archivebox.io/">Плагин подписи TLSNotary</a>.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><circle cx="10" cy="10" r="7"></circle><path d="m15 15 6 6"></path></svg></span>
|
||||
@@ -204,7 +204,7 @@
|
||||
<ul>
|
||||
<li>Храните снимки экрана, PDF-файлы, заголовки, файлы WARC и извлечение текста.</li>
|
||||
<li>Отмечайте и просматривайте источники через автономный веб-интерфейс.</li>
|
||||
<li>Используйте доказательства ZK с <a href="https://tlsnotary.zervice.io/">TLSНотариус</a> для подтверждения подлинности контента.</li>
|
||||
<li>Используйте доказательства ZK с <a href="https://tlsnotary.archivebox.io/">TLSНотариус</a> для подтверждения подлинности контента.</li>
|
||||
</ul>
|
||||
<figure class="card-shot"><a href="/screenshots/snapshot-view-tlsnotary-desktop.png"><img alt="Вывод TLSNotary, подтверждающий заархивированный ответ на странице сведений о снимке" height="1000" loading="lazy" src="/screenshots/snapshot-view-tlsnotary-desktop.png" width="1600"/></a></figure>
|
||||
</article>
|
||||
@@ -370,8 +370,8 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<div aria-labelledby="install-tab-platforms" class="install-panel" id="install-platforms">
|
||||
<figure class="install-art install-art--server"><div class="install-art-stage"><svg aria-hidden="true" class="install-server-rack" viewbox="0 0 160 180"><rect height="156" rx="12" width="124" x="18" y="12"></rect><rect height="34" rx="5" width="96" x="32" y="28"></rect><rect height="34" rx="5" width="96" x="32" y="73"></rect><rect height="34" rx="5" width="96" x="32" y="118"></rect><path d="M47 45h43m-43 45h43m-43 45h43"></path><circle cx="111" cy="45" r="4"></circle><circle cx="111" cy="90" r="4"></circle><circle cx="111" cy="135" r="4"></circle></svg><div class="install-art-tile"><img alt="" class="" loading="lazy" src="../assets/install/archivebox-server.png"/><span>ArchiveBox</span></div><div class="install-art-chips"><span>NAS</span><span>Домашний сервер</span></div></div><figcaption>На вашем собственном оборудовании</figcaption></figure>
|
||||
<div class="command-card">
|
||||
<div class="step"><span>1</span><div><h4>Выберите свою платформу</h4><p>Интеграция сообщества может отставать от выпусков Docker и Python.</p><div class="mini-links"><a href="https://unraid.net/community/apps?q=archivebox#r">Не набег</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Установите и откройте ArchiveBox.</h4><p>Следуйте инструкциям по установке платформы, настройте постоянное хранилище, затем откройте приложение, чтобы завершить настройку.</p><p>Информацию о TrueNAS см. <a href="https://github.com/ArchiveBox/ArchiveBox#quickstart">Примечания к платформе README</a> прежде чем выбрать интеграцию.</p></div></div>
|
||||
<div class="step"><span>1</span><div><h4>Выберите свою платформу</h4><p>Интеграция сообщества может отставать от выпусков Docker и Python.</p><div class="mini-links"><a href="https://apps.truenas.com/catalog/archivebox_community/">TrueNAS</a><a href="https://unraid.net/community/apps?q=archivebox#r">Не набег</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>Установите и откройте ArchiveBox.</h4><p>Следуйте инструкциям по установке платформы, настройте постоянное хранилище, затем откройте приложение, чтобы завершить настройку.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-labelledby="install-tab-hosting" class="install-panel" id="install-hosting">
|
||||
@@ -408,7 +408,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<li><a aria-label="Плагин SingleFile" href="https://plugins.archivebox.io/#singlefile" title="Один файл"><img alt="" height="44" loading="lazy" src="../assets/plugins/singlefile.png" width="44"/></a></li>
|
||||
<li><a aria-label="Плагин YouTube/yt-dlp" href="https://plugins.archivebox.io/#ytdlp" title="YouTube / yt-dlp"><img alt="" height="44" loading="lazy" src="../assets/plugins/youtube.svg" width="44"/></a></li>
|
||||
<li><a aria-label="Хвостовая чешуя" href="https://tailscale.com/" title="Хвостовая чешуя"><img alt="" height="44" loading="lazy" src="../assets/plugins/tailscale.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSНотариус" href="https://tlsnotary.zervice.io/" title="TLSНотариус"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLSНотариус" href="https://tlsnotary.archivebox.io/" title="TLSНотариус"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
</ul>
|
||||
<p class="configuration-description">ArchiveBox можно настроить через пользовательский интерфейс администратора, CLI, <code>ArchiveBox.conf</code>или импортировав конфигурацию и файлы cookie из Chrome/других браузеров. Одна и та же модель конфигурации работает в Docker, Docker Compose, при установке на «голое железо», запланированных заданиях и одноразовых запусках CLI.</p>
|
||||
</div>
|
||||
@@ -627,7 +627,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<!-- ARCHIVEBOX:FOOTER-START -->
|
||||
<footer aria-label="ArchiveBox ecosystem" class="abx-footer">
|
||||
<div class="abx-footer-intro"><div><a class="abx-brand" href="https://archivebox.io/"><svg aria-hidden="true" class="abx-logo" viewbox="0 0 40 40"><rect fill="#9b2854" height="40" rx="10" width="40"></rect><path d="M10 14h20v17H10z" fill="#fff4ed"></path><path d="M8 9h24v6H8z" fill="#fff4ed"></path><path d="M16 19h8v3h-8z" fill="#9b2854"></path><path d="M12 31V17" stroke="#9b2854" stroke-width="2"></path></svg><span>ArchiveBox</span></a><p class="abx-tagline">Открытые инструменты для долговременного сохранения копий веб-страниц.<br/>Ваши данные. Ваши устройства. Ваш архив.</p></div><p class="abx-footer-motto">Место для веб-страниц<br/>которые вы хотите сохранить.</p></div>
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Приложения и инструменты</h2><ul><li><a href="https://archivebox.io/">Сервер ArchiveBox</a><a aria-label="Сервер ArchiveBox — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox для macOS и iOS</a><a aria-label="ArchiveBox для macOS и iOS — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox для Android</a><a aria-label="ArchiveBox для Android — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox для компьютера</a><a aria-label="ArchiveBox для компьютера — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">Расширение ArchiveBox для браузера</a><a aria-label="Расширение ArchiveBox для браузера — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">Библиотека плагинов ArchiveBox</a><a aria-label="Библиотека плагинов ArchiveBox — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.Line truncated
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>Приложения и инструменты</h2><ul><li><a href="https://archivebox.io/">Сервер ArchiveBox</a><a aria-label="Сервер ArchiveBox — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">ArchiveBox для macOS и iOS</a><a aria-label="ArchiveBox для macOS и iOS — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">ArchiveBox для Android</a><a aria-label="ArchiveBox для Android — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox для компьютера</a><a aria-label="ArchiveBox для компьютера — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">Расширение ArchiveBox для браузера</a><a aria-label="Расширение ArchiveBox для браузера — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">Библиотека плагинов ArchiveBox</a><a aria-label="Библиотека плагинов ArchiveBox — исходный код на GitHub" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="Смотреть исходный код на GitHub"><svg aria-hidden="true" viewbox="0 0 24 24"><path d="M12 .75a11.25 11.25 0 0 0-3.56 21.92c.Line truncated
|
||||
<!-- /ARCHIVEBOX:FOOTER-START -->
|
||||
<div class="abx-footer-local"><p>Открытое веб-архивирование на собственном сервере. Лицензия MIT, поддержка некоммерческих организаций и ориентация на долговременные коллекции.</p>
|
||||
<div>
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4V3Z"></path></svg></span>
|
||||
<h3>在几秒钟内保存选项卡,将其存储数十年</h3>
|
||||
<p>收集书签以供稍后阅读,或通过我们的可验证捕获保存法律案件的证据 <a href="https://tlsnotary.zervice.io/">TLSNotary 签名插件</a>.</p>
|
||||
<p>收集书签以供稍后阅读,或通过我们的可验证捕获保存法律案件的证据 <a href="https://tlsnotary.archivebox.io/">TLSNotary 签名插件</a>.</p>
|
||||
</article>
|
||||
<article>
|
||||
<span aria-hidden="true" class="card-icon"><svg viewbox="0 0 24 24"><circle cx="10" cy="10" r="7"></circle><path d="m15 15 6 6"></path></svg></span>
|
||||
@@ -204,7 +204,7 @@
|
||||
<ul>
|
||||
<li>存储屏幕截图、PDF、标题、WARC 文件和文本提取。</li>
|
||||
<li>通过自托管 Web 界面标记和审查源。</li>
|
||||
<li>使用 ZK 证明 <a href="https://tlsnotary.zervice.io/">TLS公证人</a> 以证明内容的真实性。</li>
|
||||
<li>使用 ZK 证明 <a href="https://tlsnotary.archivebox.io/">TLS公证人</a> 以证明内容的真实性。</li>
|
||||
</ul>
|
||||
<figure class="card-shot"><a href="/screenshots/snapshot-view-tlsnotary-desktop.png"><img alt="TLSNotary 输出验证快照详细信息页面上的存档响应" height="1000" loading="lazy" src="/screenshots/snapshot-view-tlsnotary-desktop.png" width="1600"/></a></figure>
|
||||
</article>
|
||||
@@ -370,8 +370,8 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<div aria-labelledby="install-tab-platforms" class="install-panel" id="install-platforms">
|
||||
<figure class="install-art install-art--server"><div class="install-art-stage"><svg aria-hidden="true" class="install-server-rack" viewbox="0 0 160 180"><rect height="156" rx="12" width="124" x="18" y="12"></rect><rect height="34" rx="5" width="96" x="32" y="28"></rect><rect height="34" rx="5" width="96" x="32" y="73"></rect><rect height="34" rx="5" width="96" x="32" y="118"></rect><path d="M47 45h43m-43 45h43m-43 45h43"></path><circle cx="111" cy="45" r="4"></circle><circle cx="111" cy="90" r="4"></circle><circle cx="111" cy="135" r="4"></circle></svg><div class="install-art-tile"><img alt="" class="" loading="lazy" src="../assets/install/archivebox-server.png"/><span>ArchiveBox</span></div><div class="install-art-chips"><span>NAS</span><span>家庭服务器</span></div></div><figcaption>在您自己的硬件上</figcaption></figure>
|
||||
<div class="command-card">
|
||||
<div class="step"><span>1</span><div><h4>选择您的平台</h4><p>社区集成可能落后于 Docker 和 Python 版本。</p><div class="mini-links"><a href="https://unraid.net/community/apps?q=archivebox#r">解除突袭</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>安装并打开 ArchiveBox</h4><p>按照平台的安装指南,配置持久存储,然后打开应用程序完成设置。</p><p>对于 TrueNAS,请参阅 <a href="https://github.com/ArchiveBox/ArchiveBox#quickstart">README 的平台说明</a> 在选择集成之前。</p></div></div>
|
||||
<div class="step"><span>1</span><div><h4>选择您的平台</h4><p>社区集成可能落后于 Docker 和 Python 版本。</p><div class="mini-links"><a href="https://apps.truenas.com/catalog/archivebox_community/">TrueNAS</a><a href="https://unraid.net/community/apps?q=archivebox#r">解除突袭</a><a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=archivebox">Proxmox</a><a href="https://github.com/YunoHost-Apps/archivebox_ynh">YunoHost</a><a href="https://www.cloudron.io/store/io.archivebox.cloudronapp.html">Cloudron</a><a href="https://docs.saltbox.dev/sandbox/apps/archivebox/">Saltbox</a><a href="https://portainer-templates.as93.net/archivebox">Portainer</a><a href="https://runtipi.io/docs/apps-available">Runtipi</a></div></div></div>
|
||||
<div class="step"><span>2</span><div><h4>安装并打开 ArchiveBox</h4><p>按照平台的安装指南,配置持久存储,然后打开应用程序完成设置。</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div aria-labelledby="install-tab-hosting" class="install-panel" id="install-hosting">
|
||||
@@ -408,7 +408,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<li><a aria-label="单文件插件" href="https://plugins.archivebox.io/#singlefile" title="单一文件"><img alt="" height="44" loading="lazy" src="../assets/plugins/singlefile.png" width="44"/></a></li>
|
||||
<li><a aria-label="YouTube / yt-dlp 插件" href="https://plugins.archivebox.io/#ytdlp" title="YouTube / yt-dlp"><img alt="" height="44" loading="lazy" src="../assets/plugins/youtube.svg" width="44"/></a></li>
|
||||
<li><a aria-label="尾鳞" href="https://tailscale.com/" title="尾鳞"><img alt="" height="44" loading="lazy" src="../assets/plugins/tailscale.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLS公证人" href="https://tlsnotary.zervice.io/" title="TLS公证人"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
<li><a aria-label="TLS公证人" href="https://tlsnotary.archivebox.io/" title="TLS公证人"><img alt="" height="44" loading="lazy" src="../assets/plugins/tlsnotary.svg" width="44"/></a></li>
|
||||
</ul>
|
||||
<p class="configuration-description">ArchiveBox 可以通过管理 UI、CLI 进行配置, <code>ArchiveBox.conf</code>,或通过从 Chrome/其他浏览器导入配置和 cookie。相同的配置模型适用于 Docker、Docker Compose、裸机安装、计划作业和一次性 CLI 运行。</p>
|
||||
</div>
|
||||
@@ -627,7 +627,7 @@ archivebox server 0.0.0.0:5797</code></pre></div></div></div></div>
|
||||
<!-- ARCHIVEBOX:FOOTER-START -->
|
||||
<footer aria-label="ArchiveBox ecosystem" class="abx-footer">
|
||||
<div class="abx-footer-intro"><div><a class="abx-brand" href="https://archivebox.io/"><svg aria-hidden="true" class="abx-logo" viewbox="0 0 40 40"><rect fill="#9b2854" height="40" rx="10" width="40"></rect><path d="M10 14h20v17H10z" fill="#fff4ed"></path><path d="M8 9h24v6H8z" fill="#fff4ed"></path><path d="M16 19h8v3h-8z" fill="#9b2854"></path><path d="M12 31V17" stroke="#9b2854" stroke-width="2"></path></svg><span>ArchiveBox</span></a><p class="abx-tagline">用于长期保存网页副本的开源工具。<br/>你的数据。你的设备。你的归档。</p></div><p class="abx-footer-motto">为网页安个家<br/>保存你想保留的内容。</p></div>
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>应用与工具</h2><ul><li><a href="https://archivebox.io/">ArchiveBox 服务器</a><a aria-label="ArchiveBox 服务器 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">macOS 和 iOS 版 ArchiveBox</a><a aria-label="macOS 和 iOS 版 ArchiveBox — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">Android 版 ArchiveBox</a><a aria-label="Android 版 ArchiveBox — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox 桌面版</a><a aria-label="ArchiveBox 桌面版 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">ArchiveBox 浏览器扩展</a><a aria-label="ArchiveBox 浏览器扩展 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">ArchiveBox 插件库</a><a aria-label="ArchiveBox 插件库 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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">Line truncated
|
||||
<div class="abx-footer-grid"><section class="abx-footer-column"><h2>应用与工具</h2><ul><li><a href="https://archivebox.io/">ArchiveBox 服务器</a><a aria-label="ArchiveBox 服务器 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/ArchiveBox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://app.archivebox.io/#get-started">macOS 和 iOS 版 ArchiveBox</a><a aria-label="macOS 和 iOS 版 ArchiveBox — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/ios-archivebox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://android.archivebox.io/">Android 版 ArchiveBox</a><a aria-label="Android 版 ArchiveBox — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/android-archivebox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://electron.archivebox.io/">ArchiveBox 桌面版</a><a aria-label="ArchiveBox 桌面版 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/electron-archivebox" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://extension.archivebox.io/">ArchiveBox 浏览器扩展</a><a aria-label="ArchiveBox 浏览器扩展 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/archivebox-browser-extension" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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"></path></svg></a></li><li><a href="https://plugins.archivebox.io/">ArchiveBox 插件库</a><a aria-label="ArchiveBox 插件库 — GitHub 源代码" class="abx-source" href="https://github.com/ArchiveBox/abx-plugins" title="在 GitHub 查看源代码"><svg aria-hidden="true" viewbox="0 0 24 24"><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">Line truncated
|
||||
<!-- /ARCHIVEBOX:FOOTER-START -->
|
||||
<div class="abx-footer-local"><p>开源、自托管的网页归档。采用 MIT 许可证,由非营利组织支持,为长期保存的收藏而设计。</p>
|
||||
<div>
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "archivebox"
|
||||
version = "0.9.71rc25"
|
||||
version = "0.9.71"
|
||||
requires-python = ">=3.13"
|
||||
description = "Self-hosted internet archiving solution."
|
||||
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
|
||||
@@ -77,10 +77,10 @@ dependencies = [
|
||||
"w3lib>=2.2.1", # used for parsing content-type encoding from http response headers & html tags
|
||||
### Extractor dependencies (runtime binaries resolved through abxpkg)
|
||||
### Binary/Package Management
|
||||
"abxbus==2.5.65", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abxpkg==1.13.14", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abx-plugins[opencode]==1.13.100", # optional AI runtime clients are owned by the plugin
|
||||
"abx-dl==1.13.68", # shared ArchiveBox downloader package
|
||||
"abxbus==2.5.70", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abxpkg==1.13.21", # direct imports only; version constrained by abx-dl -> abx-plugins
|
||||
"abx-plugins[opencode]==1.13.109", # optional AI runtime clients are owned by the plugin
|
||||
"abx-dl==1.13.79", # 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",
|
||||
@@ -343,7 +343,7 @@ Donate = "https://github.com/ArchiveBox/ArchiveBox/wiki/Donations"
|
||||
|
||||
|
||||
[tool.bumpver]
|
||||
current_version = "v0.9.71rc25"
|
||||
current_version = "v0.9.71"
|
||||
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-23T01:33:48.67282448Z"
|
||||
exclude-newer = "2026-09-23T19:26:02.6709672Z"
|
||||
exclude-newer-span = "P5D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
abxbus = { timestamp = "2026-09-28T01:33:47.672836753Z", span = "PT1S" }
|
||||
abxbus = { timestamp = "2026-09-28T19:26:01.670984315Z", 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.13.68"
|
||||
version = "1.13.79"
|
||||
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/06/b6/68753701d58aac8a54f6d08d51317a8f8fec63195c69c704bd4f58e393c8/abx_dl-1.13.68.tar.gz", hash = "sha256:2d48ee38ccc319d098ca51b84707b44d8778f0714e3f9b141e48a79c2d12da5c", size = 97386 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/59/e178d330d244351f01026a2c261c7c0d8067c0efcbef9cf095635fa97579/abx_dl-1.13.79.tar.gz", hash = "sha256:8825b06570df48f3ecdd525e8fca09a2d8dac77b8ba059efaac7d0df845b578a", size = 97588, upload-time = "2026-09-28T19:23:47.984Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/1c/6ca86ff1b741099abfcd86988a6881c763d3b2e3b6ea0f429cd046123a6f/abx_dl-1.13.68-py3-none-any.whl", hash = "sha256:8d4e5d6180078100408564ca4b1d11cea6b44bfe2c921b83e4cc07419a73ede9", size = 102835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/6f/f7de1fd42bbcdf56769a7277ca261007525d2a482544fe03d509b3cc6805/abx_dl-1.13.79-py3-none-any.whl", hash = "sha256:3b9b8426da6c0fcd55583d0f26c037561b40982866c4a12f481c8f481fbe56b2", size = 102934, upload-time = "2026-09-28T19:23:49.287Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "abx-plugins"
|
||||
version = "1.13.100"
|
||||
version = "1.13.109"
|
||||
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/e2/0d/f1c7a91fdb3fcb99a335811bac9313656ddf0ec13d2f413936ae74b568c3/abx_plugins-1.13.100.tar.gz", hash = "sha256:1412abfc97a213a670f501208b189cc3d41a0adf448b40e5a927988195152d9a", size = 459065, upload-time = "2026-09-28T01:22:11.148Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/22/e236ab70ff3c24c3be7f511c202e9943b4acf7988ca180f6593a959cf190/abx_plugins-1.13.109.tar.gz", hash = "sha256:056556249fbe5e1aa5eca3cca84923fab9a4a1fcfd0d3f82c4911de97f2e49f5", size = 461059, upload-time = "2026-09-28T19:08:05.06Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/b4/c13098a9cd16c0c25a888ecd70e378e9f655bad84d742b610bee03bbf8a4/abx_plugins-1.13.100-py3-none-any.whl", hash = "sha256:17c9395c8658a74c6278648f99139a33db78eb05289b0f4793133ee23667edb5", size = 669219, upload-time = "2026-09-28T01:22:08.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/c9/6331394242f8c5afa1fb03abfd805e509add8a7d03c14b9e126e6724af20/abx_plugins-1.13.109-py3-none-any.whl", hash = "sha256:5bdecde68a9bc2265a26b95205c9214a07e39328c981c8700234f577a57d0ba7", size = 673893, upload-time = "2026-09-28T19:08:06.808Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -68,21 +68,21 @@ opencode = [
|
||||
|
||||
[[package]]
|
||||
name = "abxbus"
|
||||
version = "2.5.65"
|
||||
version = "2.5.70"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "uuid7", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/87/389195fe7d4c864cc1866f8a2b8292ec39c23200fb5f865985d24ad7a56f/abxbus-2.5.65.tar.gz", hash = "sha256:7edbb6036a1c01f13ea5c61f746c54233e709701e53cb674b3159d3727611ef2", size = 134216, upload-time = "2026-09-23T19:18:02.517Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/80/43ab38e5a88a6c8864c2460e0685f946f9fcd6cb4776033611107d70ecd6/abxbus-2.5.70.tar.gz", hash = "sha256:e2a6593d67ed619833a6a2b2a282c58312404f166436a0991f5017d51f45fbe5", size = 134824, upload-time = "2026-09-28T18:36:05.082Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/21/a1b144425294d71ae6d3874dc27bceb0e62c5fc15d72ca15bab11e0dc724/abxbus-2.5.65-py3-none-any.whl", hash = "sha256:8e9cc083f84ba190e843a3dad13b333bdb857204a2db7e0411d3574d45861131", size = 127915, upload-time = "2026-09-23T19:18:01.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/0b/67ebd55f417b641f05b9ba07d738b75b33924026cda220c9d4f771d6e16e/abxbus-2.5.70-py3-none-any.whl", hash = "sha256:a33727a432ec6729d3bd0674d36bb82d87a1faac66480a3eeddaee23e6960a25", size = 128460, upload-time = "2026-09-28T18:36:06.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "abxpkg"
|
||||
version = "1.13.14"
|
||||
version = "1.13.21"
|
||||
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/19/a1/50c7f7454f249cc84f880dcfd62aa629e4e1c88972c292e8a4cf3107bdc8/abxpkg-1.13.14.tar.gz", hash = "sha256:4000d712bb06cd878abb1eca02b282e069d303856f8b74a24373e23bff2e0b5d", size = 242796, upload-time = "2026-09-27T22:15:46.04Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/bb/56f863aa00e8e9311c0c4af059e7f0ebed2242fd40be9f3b4bbb0e70d646/abxpkg-1.13.21.tar.gz", hash = "sha256:9d5e05a0534836ea637e1c94f4555558c5d6a09368e3ee24e5a056812afc2342", size = 243339, upload-time = "2026-09-28T18:51:45.417Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/c5/be44f98e98595460a7462b1c424dc264d216ea127c8778742d491609b7b9/abxpkg-1.13.14-py3-none-any.whl", hash = "sha256:d8803ae7316f843c655978821b2c26f1463ca1e1b1700f92e8a1a3a88ae182fd", size = 254220, upload-time = "2026-09-27T22:15:47.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/9c/e60dc8ef5ec6dbe9b39c6bd06165f9d018660cba891295ff625d7cf577b1/abxpkg-1.13.21-py3-none-any.whl", hash = "sha256:b796b99dbbdfdafa3ab05f881ad505539deabbe14eb7b358811f12c47c5a47ad", size = 254735, upload-time = "2026-09-28T18:51:43.415Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -127,7 +127,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "archivebox"
|
||||
version = "0.9.71rc25"
|
||||
version = "0.9.71"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -227,10 +227,10 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "abx-dl", specifier = "==1.13.68" },
|
||||
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.13.100" },
|
||||
{ name = "abxbus", specifier = "==2.5.65" },
|
||||
{ name = "abxpkg", specifier = "==1.13.14" },
|
||||
{ name = "abx-dl", specifier = "==1.13.79" },
|
||||
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.13.109" },
|
||||
{ name = "abxbus", specifier = "==2.5.70" },
|
||||
{ name = "abxpkg", specifier = "==1.13.21" },
|
||||
{ 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