Skip to content

No way to see what changed between two mirrors#721

Open
xroche wants to merge 16 commits into
masterfrom
feat/change-report
Open

No way to see what changed between two mirrors#721
xroche wants to merge 16 commits into
masterfrom
feat/change-report

Conversation

@xroche

@xroche xroche commented Jul 26, 2026

Copy link
Copy Markdown
Owner

--update already works out which resources are new, which changed and which the server reports unchanged, then drops it: the flags reach file_notify() and go no further than a log line, and deletions exist only as a side effect of purging. --changes (-%d) keeps all of it and writes hts-changes.json next to the log, with the four lists, their counts and a one-line summary in the log.

"Changed" means the bytes differ, not that the server re-sent the resource. Comparing the mirrored files does not work for that: HTTrack stamps every parsed page with the crawl date through the footer, so those bytes move on every run and the whole site reads as changed. The comparison is on payloads instead. For parsed pages the previous payload is the body the cache kept; everywhere else the mirrored file is the payload verbatim, and the previous copy is hashed just before it gets overwritten. With no cache and nothing to hash, parsed pages fall back to the 200-versus-304 signal. The cache also carries the mirror's file index, so --cache=0 leaves no previous mirror to subtract from: nothing is reported gone, and first_crawl comes out null rather than guessed.

Accounting is keyed on the mirror-relative path, not the URL, so a redirect and its target that share a save name are one entry, and only the first notify for a file samples its pre-run state, so a retried transfer is not counted twice. What counts as already mirrored comes from the previous run's file index rather than from the file being on disk: a partial left by this crawl's own failed attempt is on disk but was never part of the previous mirror. A resource the crawl tried and failed to fetch drops out of that index too, but its copy is untouched, so it is reported unchanged rather than gone. The deleted set is now computed whether or not purging is on; unlinking still happens only under --purge-old.

The accumulator is locked on both sides. FTP transfers notify from a thread the crawl never joins, so one can arrive while the report is being resolved and written, and the report path used to run unlocked over an array realloct() was moving underneath it. Writing the report also seals the accumulator: a notify that lands afterwards is dropped instead of starting a second report nobody will write. With the feature off the hooks return before doing any work, so an --update without --changes performs the same cache reads as master and produces the same mirror.

htsopt.h gains two tail-appended fields on httrackp (changes, changes_state), so this is an installed-header change and anything embedding the struct needs a rebuild. I have not touched VERSION_INFO. Content diffs and keeping the previous copy of a changed page are out of scope on purpose: both change what a mirror directory holds and run into the purge invariant.

tests/93_local-changes.test is a three-pass crawl whose routes all answer 200 with no Last-Modified and no ETag, so a naive implementation calls the whole site changed; it asserts each bucket exactly, covers a redirect, a retried direct-to-disk transfer, a transfer that never completes, a pass with purging on and a mirror crawled with the cache off. httrack -#test=changes covers the bucket rules and the JSON escaping, invalid UTF-8 included, and -#test=changes-race runs notifier threads against the report path for the FTP shape (clean under ThreadSanitizer, fifteen races with the lock removed). Every rule above was graded against a build with that rule mutated, to check the tests go red. The file format is documented in html/changes.html, and the WebHTTrack toggle, man page and command-line guide are updated; WinHTTrack and Android only need the flag added to the argv they already build.

Closes #714

xroche and others added 16 commits July 26, 2026 17:57
--update already knows which resources were new, which changed and which the
server called unchanged, and throws it away: the flags reach file_notify() and
go no further than a log line, while deletions exist only as a side effect of
purging. --changes (-%d) keeps all of it and writes hts-changes.json plus a
one-line summary in the log.

"Changed" means the bytes differ, not that the server re-sent the resource.
Comparing the mirrored files directly would not work: HTTrack stamps every
parsed page with the crawl date via the footer, so those bytes differ on every
run. Payloads are compared instead, the previous one coming from the cache for
parsed pages and from the local copy sampled just before it is overwritten for
everything else.

The mirror-relative path, not the URL, is the accumulator's key, so a redirect
and its target that share a save name are one entry; and only the first notify
for a file samples its pre-run state, so a retried transfer is not counted
twice. What counts as already mirrored comes from the previous run's file
index rather than from the file's presence on disk: a partial left by this
crawl's own failed attempt is on disk but was never part of the previous
mirror.

The deleted set is now computed whether or not purging is enabled; unlinking
still happens only under --purge-old.

Closes #714

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
A crawl that mirrored nothing created no accumulator, so hts_changes_close_opt
returned without writing and the previous run's report stayed on disk as if it
described this one. Write it whenever --changes is on. The no-data rollback is
the deliberate exception, and is now documented: it restores the previous cache
generation, so leaving the matching report alone is the consistent behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
The size shortcut compared rendered on-disk sizes even for parsed pages, whose
payload digests describe something else entirely, so it decided the outcome
before the payload comparison could run: a page whose payload never changed but
whose rewritten links moved read as changed. It now only applies when both
digests describe the file on disk.

file_notify() reaches the accumulator from the FTP download thread as well as
the main one, and the lazy allocation, the coucal write and the entries realloc
were all unguarded. Every entry point now takes a mutex, and the HTML hook does
its cache read before taking it, since that read can itself re-enter
file_notify() and move the array.

Two fixtures cover what nothing did: a gzipped direct-to-disk body that changes
at constant length (without the pre-sample before the decoded temp is renamed,
it reads as unchanged), and a page with a fixed payload behind a redirect whose
target is renamed, so its file on disk changes length while its bytes do not.
Both were checked against builds with the respective fix reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Both sides appended to tests/Makefile.am's TESTS; kept master's
86_local-proxytrack-cache-longfields.test alongside 88_local-changes.test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Records the second parent: the previous commit carried master's content but
was committed after a stash cleared MERGE_HEAD, so git never saw it as a merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>

# Conflicts:
#	tests/Makefile.am
…740)

The feature PR added the two LANG_CHANGES* entries to lang.def with English and
Francais only; every other locale fell back to English in the WebHTTrack form.
Each file is written in its own declared charset.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflict in tests/Makefile.am only: master took 87 and 90, PR #718 holds 88,
so the change-report test moves to 89.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Lock the report path against the FTP thread the crawl never joins, seal the
accumulator once the report is written, key entries off the project directory
so the report survives --cache=0, stop calling a file gone when the crawl only
failed to re-fetch it, and skip the hook's work entirely when --changes is off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Adds a changes-race self-test (the FTP shape: notifier threads against the
report path), fixtures for a transfer the crawl never completes, for a leftover
file at a name the crawl mirrors fresh, and for a cache-off mirror, plus a pass
with purging on. Registers the web GUI's --changes box with the clearing
companion master's #725 now requires, and documents the degraded mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Conflict in tests/Makefile.am only: master took 89, so the change-report test
moves to 93 (PR #720 holds 92).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
A connection killed before the status line surfaces differently on macOS, where
it truncates the mirrored file to zero (#748). Assert only what holds on both:
it is never reported gone, and its file survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Conflict in tests/Makefile.am only: master added 88 and 92; the change-report
test keeps 93.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No way to see what changed between two mirrors

1 participant