#!/usr/bin/env python3 import os import re from pathlib import Path from urllib.parse import quote from xml.sax.saxutils import escape # ====== CONFIGURATION ====== BASE_URL = "https://www.cdn.ay1.net" FILES_LIST = "files.txt" SITEMAP_BASENAME = "sitemap" MAX_URLS_PER_SITEMAP = 10000 MAX_SITEMAP_BYTES = 50 * 1024 * 1024 # The directory containing this script. ROOT = Path(__file__).resolve().parent INPUT = ROOT / FILES_LIST SITEMAPS_DIR = ROOT # This command generates files.txt while intentionally including directories # and make-sitemaps.py.txt, but no other files directly under ./: # # find -L . -mindepth 1 \( -type d -o \( -type f \( -path './make-sitemaps.py.txt' -o -path './*/*' \) \) \) ! -name 'sitemap-*.xml' ! -name 'files.txt' ! -name 'du.txt' ! -name 'robots.txt' ! -name 'DMCA-take-down-requests.txt' ! -name '50GB-zero-filled-file-test.img' -print | LC_ALL=C sort -u > files.txt # # du -Lh . > du.txt # =========================== def chunks(items, size): """Yield successive chunks containing no more than size items.""" for start in range(0, len(items), size): yield items[start:start + size] def temporary_path(final_path): """Return a temporary filename in the same directory.""" return final_path.with_name(f".{final_path.name}.tmp") def write_sitemap(final_path, path_chunk): """Write one sitemap atomically.""" temp_path = temporary_path(final_path) try: with temp_path.open("w", encoding="utf-8", newline="\n") as f: f.write('\n') f.write( '\n' ) for rel_path in path_chunk: encoded_path = quote(rel_path.lstrip("/"), safe="/") url = BASE_URL.rstrip("/") + "/" + encoded_path f.write(" \n") f.write(f" {escape(url)}\n") f.write(" \n") f.write("\n") sitemap_size = temp_path.stat().st_size if sitemap_size > MAX_SITEMAP_BYTES: raise RuntimeError( f"{final_path.name} would be {sitemap_size} bytes, " f"which exceeds the 50 MiB sitemap limit" ) os.replace(temp_path, final_path) finally: try: temp_path.unlink() except FileNotFoundError: pass def write_sitemap_index(final_path, sitemap_names): """Write the sitemap index atomically.""" temp_path = temporary_path(final_path) try: with temp_path.open("w", encoding="utf-8", newline="\n") as f: f.write('\n') f.write( '\n' ) for sitemap_name in sitemap_names: loc = BASE_URL.rstrip("/") + "/" + sitemap_name f.write(" \n") f.write(f" {escape(loc)}\n") f.write(" \n") f.write("\n") os.replace(temp_path, final_path) finally: try: temp_path.unlink() except FileNotFoundError: pass if not INPUT.is_file(): raise SystemExit(f"ERROR: Input file does not exist: {INPUT}") paths = [] with INPUT.open("r", encoding="utf-8") as f: for line in f: # Remove only the line ending. Do not remove spaces that may # legitimately be part of a filename. path = line.rstrip("\r\n") if not path: continue if path.startswith("./"): path = path[2:] # Directory URLs are canonical with a trailing slash. Using the final # URL directly avoids an unnecessary redirect when search engines crawl # entries from the sitemap. if (ROOT / path).is_dir() and not path.endswith("/"): path += "/" paths.append(path) total = len(paths) print(f"Found {total} paths") # An empty files.txt could indicate that find failed. Do not replace valid # sitemaps with an empty sitemap index in that situation. if total == 0: raise SystemExit( "ERROR: files.txt contains no paths; existing sitemaps were not changed" ) out_files = [] for number, path_chunk in enumerate( chunks(paths, MAX_URLS_PER_SITEMAP), start=1 ): filename = f"{SITEMAP_BASENAME}-{number}.xml" output_path = SITEMAPS_DIR / filename write_sitemap(output_path, path_chunk) out_files.append(filename) print(f"Wrote {filename} with {len(path_chunk)} URLs") index_name = f"{SITEMAP_BASENAME}-index.xml" index_path = SITEMAPS_DIR / index_name write_sitemap_index(index_path, out_files) print( f"Wrote {index_name} referencing " f"{len(out_files)} sitemap files" ) # Delete obsolete numbered sitemap files. For example, if the site once # required sitemap-3.xml but now only requires sitemap-1.xml and sitemap-2.xml. expected_files = set(out_files) numbered_sitemap = re.compile( rf"^{re.escape(SITEMAP_BASENAME)}-[0-9]+\.xml$" ) for old_path in SITEMAPS_DIR.glob(f"{SITEMAP_BASENAME}-*.xml"): if ( numbered_sitemap.fullmatch(old_path.name) and old_path.name not in expected_files ): old_path.unlink() print(f"Removed obsolete sitemap file: {old_path.name}")