Files
Firelink/Sources/Firelink/DownloadMetadataFetcher.swift
T
2026-06-01 05:08:07 +03:30

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)
}
}