Files
kage/clone/config.go
T
Duc-Tam Nguyen 249761f29f Leave bulk and off-domain assets remote, skip over-cap downloads
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.
2026-06-15 20:27:47 +07:00

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,
}
}