249761f29f
A developer.apple.com crawl came back at 19 GB, 18 of it assets, most of that the site's own videos, .dmg/.pkg installers, and PDF manuals pulled from a couple dozen hosts including unrelated third parties. None of it helps read the docs offline. This changes what kage localizes by default. Bulk media, installers, archives, and PDFs are left pointing at their live URL instead of downloaded. The decision is made from the URL alone, so the rewritten HTML simply keeps the remote link. --keep-media restores the old behavior and --skip-ext adds more extensions to leave remote. Assets are localized only from the seed's registrable domain by default, so www.apple.com and images.apple.com still come along but a separate brand domain or an off-topic third party does not. --all-asset-hosts goes back to downloading from any host. The size cap was also truncating instead of skipping: it wrapped the body in a LimitReader, so an over-cap file was saved as exactly the first N MB of itself, a corrupt fragment that would never play or run. On the apple crawl that was around a gigabyte of half-downloaded WWDC videos. kage now checks the response size and leaves an over-cap asset out of the mirror.
145 lines
5.0 KiB
Go
145 lines
5.0 KiB
Go
// Package clone is kage's engine: it ties the Chrome pool, the JavaScript
|
|
// stripper, the asset localiser, and the URL↔path mapper into one resumable,
|
|
// polite crawl that turns a live site into a browsable offline folder.
|
|
package clone
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/tamnd/kage/urlx"
|
|
)
|
|
|
|
// DefaultOutDir is where mirrors land unless --out overrides it: a per-user data
|
|
// directory ($HOME/data/kage) so clones from anywhere collect in one place,
|
|
// falling back to a local kage-out when the home directory cannot be resolved.
|
|
func DefaultOutDir() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil || home == "" {
|
|
return "kage-out"
|
|
}
|
|
return filepath.Join(home, "data", "kage")
|
|
}
|
|
|
|
// Config is the full set of knobs for a clone run. DefaultConfig fills the
|
|
// baseline; the CLI overlays flags on top.
|
|
type Config struct {
|
|
OutDir string // output root; the mirror lands in <OutDir>/<host>/
|
|
Reserved string // reserved dir name for assets and state (default "_kage")
|
|
|
|
Workers int // page render workers
|
|
AssetWorkers int // HTTP asset download workers
|
|
BrowserPages int // Chrome page-pool size
|
|
MaxPages int // stop after N pages (0 = unlimited)
|
|
MaxDepth int // BFS/DFS depth cap (0 = unlimited)
|
|
Traversal string
|
|
MaxAssetBytes int64
|
|
|
|
// AssetSameDomain, when set, localizes only assets whose host shares the
|
|
// seed's registrable domain (apple.com covers developer.apple.com and
|
|
// www.apple.com but not cdn-apple.com or an unrelated third party). An
|
|
// off-domain asset is left pointing at its live URL instead of downloaded.
|
|
AssetSameDomain bool
|
|
// SkipAssetExts lists asset extensions (".mp4", ".pdf", ".dmg", …) that are
|
|
// left on the live web rather than downloaded, so bulk media, installers, and
|
|
// archives do not bloat the mirror. The reference keeps its remote URL.
|
|
SkipAssetExts map[string]bool
|
|
|
|
Timeout time.Duration // per HTTP request
|
|
Settle time.Duration // network-idle quiet period
|
|
RenderTimeout time.Duration // hard cap per page render
|
|
Scroll bool
|
|
|
|
UserAgent string
|
|
IncludeSubdomains bool
|
|
ScopePrefix string
|
|
ExcludePaths []string
|
|
|
|
RespectRobots bool
|
|
FollowSitemap bool
|
|
Headless bool
|
|
KeepNoscript bool
|
|
ChromeBin string
|
|
ControlURL string
|
|
|
|
// Resume loads the prior run's visited set and skips pages already written,
|
|
// so an interrupted or repeated clone picks up where it left off instead of
|
|
// refetching. Refresh forces every page to be re-rendered in place (the
|
|
// mirror is kept, files are overwritten) to pull in changed content. Force
|
|
// deletes the mirror first for a clean-slate clone. Persist writes the
|
|
// visited set back to state.json when the run ends.
|
|
Resume bool
|
|
Refresh bool
|
|
Force bool
|
|
Persist bool
|
|
}
|
|
|
|
// DefaultUserAgent is a current desktop Chrome UA, used by the asset fetcher and
|
|
// the robots fetch so a site treats kage like the browser it drives.
|
|
const DefaultUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
|
|
|
|
// DefaultSkipAssetExts returns the asset extensions kage leaves on the live web
|
|
// by default: bulk media, installers, and archives that rarely matter for
|
|
// reading a site offline but dominate its download size (a docs site's WWDC
|
|
// videos, .dmg/.pkg installers, and PDF manuals can be most of the bytes).
|
|
// Page-rendering assets (images, fonts, CSS) are deliberately absent, so the
|
|
// offline pages still look right.
|
|
func DefaultSkipAssetExts() map[string]bool {
|
|
exts := []string{
|
|
// Video and audio.
|
|
".mp4", ".m4v", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv",
|
|
".m3u8", ".ts", ".mp3", ".wav", ".flac", ".aac", ".ogg", ".oga",
|
|
// Installers and disk images.
|
|
".dmg", ".pkg", ".exe", ".msi", ".deb", ".rpm", ".appimage", ".iso",
|
|
// Archives.
|
|
".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z", ".rar",
|
|
// Documents that download rather than render.
|
|
".pdf",
|
|
}
|
|
m := make(map[string]bool, len(exts))
|
|
for _, e := range exts {
|
|
m[e] = true
|
|
}
|
|
return m
|
|
}
|
|
|
|
// DefaultConfig returns the baseline configuration.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
OutDir: DefaultOutDir(),
|
|
Reserved: urlx.DefaultReserved,
|
|
Workers: 4,
|
|
AssetWorkers: 8,
|
|
BrowserPages: 4,
|
|
MaxAssetBytes: 25 << 20,
|
|
AssetSameDomain: true,
|
|
SkipAssetExts: DefaultSkipAssetExts(),
|
|
Traversal: "bfs",
|
|
Timeout: 30 * time.Second,
|
|
Settle: 1500 * time.Millisecond,
|
|
RenderTimeout: 30 * time.Second,
|
|
UserAgent: DefaultUserAgent,
|
|
RespectRobots: true,
|
|
FollowSitemap: true,
|
|
Headless: true,
|
|
Resume: true,
|
|
Persist: true,
|
|
}
|
|
}
|
|
|
|
// HostDir returns the mirror root for a seed host: <OutDir>/<host>.
|
|
func (c Config) HostDir(host string) string {
|
|
return filepath.Join(c.OutDir, host)
|
|
}
|
|
|
|
// scope builds the urlx scope config from the run config.
|
|
func (c Config) scope() urlx.ScopeConfig {
|
|
return urlx.ScopeConfig{
|
|
IncludeSubdomains: c.IncludeSubdomains,
|
|
ScopePrefix: c.ScopePrefix,
|
|
ExcludePaths: c.ExcludePaths,
|
|
}
|
|
}
|