mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-04 00:18:41 +00:00
113 lines
4.4 KiB
Swift
113 lines
4.4 KiB
Swift
import Foundation
|
|
|
|
enum DownloadURLParser {
|
|
static func parse(_ text: String) -> [URL] {
|
|
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
|
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
|
let detected = detector?.matches(in: text, range: range).compactMap(\.url) ?? []
|
|
|
|
let tokenized = text
|
|
.components(separatedBy: CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ",;")))
|
|
.compactMap { token -> URL? in
|
|
let trimmed = token.trimmingCharacters(in: CharacterSet(charactersIn: "\"'<>[]()"))
|
|
guard let url = URL(string: trimmed),
|
|
let scheme = url.scheme?.lowercased(),
|
|
["http", "https", "ftp", "sftp"].contains(scheme) else {
|
|
return nil
|
|
}
|
|
return url
|
|
}
|
|
|
|
var seen = Set<String>()
|
|
return (detected + tokenized).filter { url in
|
|
guard let scheme = url.scheme?.lowercased(),
|
|
["http", "https", "ftp", "sftp"].contains(scheme) else {
|
|
return false
|
|
}
|
|
return seen.insert(url.absoluteString).inserted
|
|
}
|
|
}
|
|
}
|
|
|
|
enum DownloadMetadataFetcher {
|
|
static func fetch(for url: URL, settings: AppSettings) async -> PendingDownload {
|
|
let initialName = FileClassifier.fileName(from: url)
|
|
let initialCategory = FileClassifier.category(forFileName: initialName)
|
|
let initialDirectory = await settings.destinationDirectory(for: initialCategory)
|
|
var pending = PendingDownload(
|
|
url: url,
|
|
fileName: initialName,
|
|
category: initialCategory,
|
|
defaultDirectory: initialDirectory,
|
|
state: .loading
|
|
)
|
|
|
|
guard url.scheme?.lowercased().hasPrefix("http") == true else {
|
|
pending.state = .loaded
|
|
return pending
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "HEAD"
|
|
request.timeoutInterval = 12
|
|
request.setValue("Firelink/0.1", forHTTPHeaderField: "User-Agent")
|
|
|
|
do {
|
|
let (_, response) = try await URLSession.shared.data(for: request)
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
pending.state = .loaded
|
|
return pending
|
|
}
|
|
|
|
if let disposition = httpResponse.value(forHTTPHeaderField: "Content-Disposition"),
|
|
let fileName = fileName(fromContentDisposition: disposition) {
|
|
pending.fileName = fileName
|
|
pending.category = FileClassifier.category(forFileName: fileName)
|
|
pending.defaultDirectory = await settings.destinationDirectory(for: pending.category)
|
|
}
|
|
|
|
if let contentLength = httpResponse.value(forHTTPHeaderField: "Content-Length"),
|
|
let bytes = Int64(contentLength) {
|
|
pending.sizeBytes = bytes
|
|
} else if response.expectedContentLength > 0 {
|
|
pending.sizeBytes = response.expectedContentLength
|
|
}
|
|
|
|
pending.mimeType = httpResponse.mimeType
|
|
pending.state = .loaded
|
|
} catch {
|
|
pending.state = .failed(error.localizedDescription)
|
|
}
|
|
|
|
return pending
|
|
}
|
|
|
|
private static func fileName(fromContentDisposition header: String) -> String? {
|
|
let parts = header.components(separatedBy: ";")
|
|
for part in parts {
|
|
let trimmed = part.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if trimmed.lowercased().hasPrefix("filename*="),
|
|
let value = trimmed.components(separatedBy: "''").last?.removingPercentEncoding {
|
|
return value.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
|
|
}
|
|
|
|
if trimmed.lowercased().hasPrefix("filename=") {
|
|
return String(trimmed.dropFirst("filename=".count))
|
|
.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
enum ByteFormatter {
|
|
static func string(_ bytes: Int64?) -> String {
|
|
guard let bytes else { return "Unknown" }
|
|
let formatter = ByteCountFormatter()
|
|
formatter.countStyle = .file
|
|
formatter.includesUnit = true
|
|
formatter.includesCount = true
|
|
return formatter.string(fromByteCount: bytes)
|
|
}
|
|
}
|