Skip to main content

save_safari_webarchive.swift

1#!/usr/bin/env swift
2/// Save a web page as a Safari webarchive.
3///
4/// Usage: save_safari_webarchive [URL] [OUTPUT_PATH]
5///
6/// This will save the page to the desired file, but may fail for
7/// several reasons:
8///
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
12/// webarchive)
13///
14/// For a detailed explanation of the code in this script, see
15/// https://alexwlchan.net/2024/creating-a-safari-webarchive/
16///
17/// The canonical copy of this script lives in GitHub, see
18/// https://github.com/alexwlchan/safari-webarchiver
20import WebKit
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 {
27 var urlString: String
29 init(_ urlString: String) {
30 self.urlString = urlString
31 }
33 func webView(
34 _: WKWebView,
35 didFail: WKNavigation!,
36 withError error: Error
37 ) {
38 fputs("Failed to load \(self.urlString): \(error.localizedDescription)\n", stderr)
39 exit(1)
40 }
42 func webView(
43 _: WKWebView,
44 didFailProvisionalNavigation: WKNavigation!,
45 withError error: Error
46 ) {
47 fputs("Failed to load \(self.urlString): \(error.localizedDescription)\n", stderr)
48 exit(1)
49 }
51 func webView(
52 _: WKWebView,
53 decidePolicyFor navigationResponse: WKNavigationResponse,
54 decisionHandler: (WKNavigationResponsePolicy) -> Void
55 ) {
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)
59 exit(1)
60 }
61 }
63 decisionHandler(.allow)
64 }
67extension WKWebView {
69 /// Load the given URL in the web view.
70 ///
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)
78 self.load(request)
80 while (self.isLoading) {
81 RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
82 }
83 } else {
84 fputs("Unable to use \(urlString) as a URL\n", stderr)
85 exit(1)
86 }
87 }
89 /// Save a copy of the web view's contents as a webarchive file.
90 ///
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) {
94 var isSaving = true
96 self.createWebArchiveData(completionHandler: { result in
97 do {
98 let data = try result.get()
99 try data.write(
100 to: savePath,
101 options: [Data.WritingOptions.withoutOverwriting]
102 )
103 isSaving = false
104 } catch {
105 fputs("Unable to save webarchive file: \(error.localizedDescription)\n", stderr)
106 exit(1)
107 }
108 })
110 while (isSaving) {
111 RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
112 }
113 }
116if CommandLine.arguments.count == 2 && CommandLine.arguments[1] == "--version" {
117 let filename = (CommandLine.arguments[0] as NSString).lastPathComponent
118 print("\(filename) \(SCRIPT_VERSION)")
119 exit(0)
122guard CommandLine.arguments.count == 3 else {
123 fputs("Usage: \(CommandLine.arguments[0]) <URL> <OUTPUT_PATH>\n", stderr)
124 exit(1)
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)")