Skip to main content

Initial commit

ID
72bf80c
date
2024-05-16 20:06:38+00:00
author
Alex Chan <alex@alexwlchan.net>
message
Initial commit
changed files
2 files, 125 additions

Changed files

.gitattributes (0) → .gitattributes (47)

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..c226feb
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+save_safari_webarchive linguist-language=Swift

save_safari_webarchive (0) → save_safari_webarchive (3400)

diff --git a/save_safari_webarchive b/save_safari_webarchive
new file mode 100755
index 0000000..4e60955
--- /dev/null
+++ b/save_safari_webarchive
@@ -0,0 +1,124 @@
+#!/usr/bin/env swift
+/// Save a web page as a Safari webarchive.
+///
+/// Usage: save_safari_webarchive [URL] [OUTPUT_PATH]
+///
+/// This will save the page to the desired file, but may fail for
+/// several reasons:
+///
+///   - the web page can't be loaded
+///   - the web page returns a non-200 status code
+///   - there's already a file at that path (it won't overwrite an existing
+///     webarchive)
+///
+/// For a detailed explanation of the code in this script, see
+/// https://alexwlchan.net/2024/creating-a-safari-webarchive/
+
+import WebKit
+
+/// Print an error message and terminate the process if there are
+/// any errors while loading a page.
+class ExitOnFailureDelegate: NSObject, WKNavigationDelegate {
+  var urlString: String
+
+  init(_ urlString: String) {
+    self.urlString = urlString
+  }
+
+  func webView(
+    _ webView: WKWebView,
+    didFail: WKNavigation!,
+    withError error: Error
+  ) {
+    fputs("Failed to load \(self.urlString) (1): \(error.localizedDescription)\n", stderr)
+    exit(1)
+  }
+
+  func webView(
+    _ webView: WKWebView,
+    didFailProvisionalNavigation: WKNavigation!,
+    withError error: Error
+  ) {
+    fputs("Failed to load \(self.urlString) (2): \(error.localizedDescription)\n", stderr)
+    exit(1)
+  }
+
+  func webView(
+    _ webView: WKWebView,
+    decidePolicyFor navigationResponse: WKNavigationResponse,
+    decisionHandler: (WKNavigationResponsePolicy) -> Void
+  ) {
+    if let httpUrlResponse = (navigationResponse.response as? HTTPURLResponse) {
+      if httpUrlResponse.statusCode != 200 {
+        fputs("Failed to load \(self.urlString): got status code \(httpUrlResponse.statusCode)\n", stderr)
+        exit(1)
+      }
+    }
+
+    decisionHandler(.allow)
+  }
+}
+
+let webView = WKWebView()
+
+extension WKWebView {
+
+  /// Load the given URL in the web view.
+  ///
+  /// This method will block until the URL has finished loading.
+  func load(_ urlString: String) {
+    let delegate = ExitOnFailureDelegate(urlString)
+    webView.navigationDelegate = delegate
+
+    if let url = URL(string: urlString) {
+      let request = URLRequest(url: url)
+      self.load(request)
+
+      while (self.isLoading) {
+        RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
+      }
+    } else {
+      fputs("Unable to use \(urlString) as a URL\n", stderr)
+      exit(1)
+    }
+  }
+
+  /// Save a copy of the web view's contents as a webarchive file.
+  ///
+  /// This method will block until the webarchive has been saved,
+  /// or the save has failed for some reason.
+  func saveAsWebArchive(savePath: URL) {
+    var isSaving = true
+
+    self.createWebArchiveData(completionHandler: { result in
+      do {
+        let data = try result.get()
+        try data.write(
+          to: savePath,
+          options: [Data.WritingOptions.withoutOverwriting]
+        )
+        isSaving = false
+      } catch {
+        fputs("Unable to save webarchive file: \(error.localizedDescription)\n", stderr)
+        exit(1)
+      }
+    })
+
+    while (isSaving) {
+      RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
+    }
+  }
+}
+
+guard CommandLine.arguments.count == 3 else {
+    print("Usage: \(CommandLine.arguments[0]) <URL> <OUTPUT_PATH>")
+    exit(1)
+}
+
+let urlString = CommandLine.arguments[1]
+let savePath = URL(fileURLWithPath: CommandLine.arguments[2])
+
+webView.load(urlString)
+webView.saveAsWebArchive(savePath: savePath)
+
+print("Saved webarchive to \(savePath)")