2/// Save a web page as a Safari webarchive.
4/// Usage: save_safari_webarchive [URL] [OUTPUT_PATH]
6/// This will save the page to the desired file, but may fail for
9/// - the web page can't be loaded
10/// - the web page returns a non-200 status code
11/// - there's already a file at that path (it won't overwrite an existing
14/// For a detailed explanation of the code in this script, see
15/// https://alexwlchan.net/2024/creating-a-safari-webarchive/
17/// The canonical copy of this script lives in GitHub, see
18/// https://github.com/alexwlchan/safari-webarchiver
22let SCRIPT_VERSION = "1.0.1"
24/// Print an error message and terminate the process if there are
25/// any errors while loading a page.
26class ExitOnFailureDelegate: NSObject, WKNavigationDelegate {
29 init(_ urlString: String) {
30 self.urlString = urlString
35 didFail: WKNavigation!,
36 withError error: Error
38 fputs("Failed to load \(self.urlString): \(error.localizedDescription)\n", stderr)
44 didFailProvisionalNavigation: WKNavigation!,
45 withError error: Error
47 fputs("Failed to load \(self.urlString): \(error.localizedDescription)\n", stderr)
53 decidePolicyFor navigationResponse: WKNavigationResponse,
54 decisionHandler: (WKNavigationResponsePolicy) -> Void
56 if let httpUrlResponse = (navigationResponse.response as? HTTPURLResponse) {
57 if httpUrlResponse.statusCode != 200 {
58 fputs("Failed to load \(self.urlString): got status code \(httpUrlResponse.statusCode)\n", stderr)
63 decisionHandler(.allow)
69 /// Load the given URL in the web view.
71 /// This method will block until the URL has finished loading.
72 func load(_ urlString: String) {
73 let delegate = ExitOnFailureDelegate(urlString)
74 webView.navigationDelegate = delegate
76 if let url = URL(string: urlString) {
77 let request = URLRequest(url: url)
80 while (self.isLoading) {
81 RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
84 fputs("Unable to use \(urlString) as a URL\n", stderr)
89 /// Save a copy of the web view's contents as a webarchive file.
91 /// This method will block until the webarchive has been saved,
92 /// or the save has failed for some reason.
93 func saveAsWebArchive(savePath: URL) {
96 self.createWebArchiveData(completionHandler: { result in
98 let data = try result.get()
101 options: [Data.WritingOptions.withoutOverwriting]
105 fputs("Unable to save webarchive file: \(error.localizedDescription)\n", stderr)
111 RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
116if CommandLine.arguments.count == 2 && CommandLine.arguments[1] == "--version" {
117 let filename = (CommandLine.arguments[0] as NSString).lastPathComponent
118 print("\(filename) \(SCRIPT_VERSION)")
122guard CommandLine.arguments.count == 3 else {
123 fputs("Usage: \(CommandLine.arguments[0]) <URL> <OUTPUT_PATH>\n", stderr)
127let urlString = CommandLine.arguments[1]
128let savePath = URL(fileURLWithPath: CommandLine.arguments[2])
130let webView = WKWebView()
132webView.load(urlString)
133webView.saveAsWebArchive(savePath: savePath)
135print("Saved webarchive to \(savePath)")