Authenticated and remote images in TipTap now load via a custom edgeever-res scheme after native fetch, so the file:// editor can display protected blobs. Bottom create control sits under the tab separator at a smaller size, and list decode skips corrupt memo rows instead of blanking the feed.
76 lines
2.9 KiB
Swift
76 lines
2.9 KiB
Swift
import Foundation
|
|
import CryptoKit
|
|
|
|
actor ResourceCache {
|
|
private let directory: URL
|
|
private let maxBytes: Int64 = 200 * 1024 * 1024
|
|
|
|
init() {
|
|
let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
directory = base.appendingPathComponent("resource-cache", isDirectory: true)
|
|
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
}
|
|
|
|
func fileURL(for resourceId: String) -> URL {
|
|
let safe = resourceId.replacingOccurrences(of: "/", with: "_")
|
|
return directory.appendingPathComponent(safe)
|
|
}
|
|
|
|
func dataURL(for resourceId: String, data: Data, mimeType: String) throws -> String {
|
|
let url = fileURL(for: resourceId)
|
|
try data.write(to: url, options: .atomic)
|
|
let base64 = data.base64EncodedString()
|
|
return "data:\(mimeType);base64,\(base64)"
|
|
}
|
|
|
|
func cachedData(for resourceId: String) -> Data? {
|
|
let url = fileURL(for: resourceId)
|
|
return try? Data(contentsOf: url)
|
|
}
|
|
|
|
static func resourceId(from source: String) -> String? {
|
|
// /api/v1/resources/:id or .../blob
|
|
guard let url = URL(string: source, relativeTo: URL(string: "https://placeholder.local")) else { return nil }
|
|
let parts = url.path.split(separator: "/").map(String.init)
|
|
if let idx = parts.firstIndex(of: "resources"), idx + 1 < parts.count {
|
|
return parts[idx + 1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Match Android `normalizeProtectedResourcePath` — ensure blob route.
|
|
static func normalizeProtectedResourcePath(_ source: String, baseURL: URL?) -> String {
|
|
var path = source
|
|
if let base = baseURL?.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")),
|
|
path.hasPrefix(base + "/")
|
|
{
|
|
path = String(path.dropFirst(base.count))
|
|
}
|
|
if path.hasPrefix("http://") || path.hasPrefix("https://"),
|
|
let url = URL(string: path)
|
|
{
|
|
path = url.path
|
|
}
|
|
guard path.hasPrefix("/api/v1/resources/") else { return path }
|
|
if path.range(of: #"/blob(?:$|[?#])"#, options: .regularExpression) != nil {
|
|
return path
|
|
}
|
|
if let match = path.range(of: #"^/api/v1/resources/[^/?#]+"#, options: .regularExpression) {
|
|
return String(path[match]) + "/blob"
|
|
}
|
|
return path
|
|
}
|
|
|
|
static func isProtectedResourceSource(_ source: String, baseURL: URL?) -> Bool {
|
|
// Match Android / editor JS: any path that points at the protected resources API.
|
|
if source.hasPrefix("/api/v1/resources/") { return true }
|
|
if source.contains("/api/v1/resources/") { return true }
|
|
if let base = baseURL?.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/")),
|
|
source.hasPrefix(base + "/api/v1/resources/")
|
|
{
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|