8bc9c8bea1
2. add --craw-delay flag to specify/override robots directive.
147 lines
5.1 KiB
Go
147 lines
5.1 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
|
|
CrawlDelay time.Duration // override robots.txt Crawl-delay when > 0
|
|
FollowSitemap bool
|
|
Headless bool
|
|
KeepNoscript bool
|
|
MobileReadable 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,
|
|
}
|
|
}
|