krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v5.0.0: DomainDig/DomainViewModel+Export.swift · raw

  1import Foundation
  2
  3/// Export and data-portability surface of `DomainViewModel`: rendering the
  4/// current report, batch results, tracked domains, timelines, and workflow runs
  5/// into the shareable formats, plus the full-backup / portable-slice exporters
  6/// and the import + app-settings persistence entry points.
  7///
  8/// These are thin adapters over `DomainReportExporter` and
  9/// `DomainDataPortabilityService`. The report-projection helpers they call
 10/// (`currentBatchReports`, `reports(for:)`, `timelineReports`, `workflowReports`)
 11/// remain on the main type  they belong to the report layer shared with
 12/// inspection, not to export.
 13extension DomainViewModel {
 14    func exportJSONData() -> Data? {
 15        guard let currentReport else { return nil }
 16        return try? DomainReportExporter.data(for: currentReport, format: .json)
 17    }
 18
 19    func exportJSONString() -> String? {
 20        guard let data = exportJSONData() else { return nil }
 21        return String(data: data, encoding: .utf8)
 22    }
 23
 24    func exportSingleReportData(format: DomainExportFormat) -> Data? {
 25        guard let currentReport else { return nil }
 26        return try? DomainReportExporter.data(for: currentReport, format: format)
 27    }
 28
 29    func exportBatchReportData(format: DomainExportFormat) -> Data? {
 30        try? DomainReportExporter.data(
 31            for: currentBatchReports(),
 32            format: format,
 33            title: batchLookupSource == .watchlistRefresh ? "Tracked Domains Export" : "Batch Results Export"
 34        )
 35    }
 36
 37    func exportTrackedDomainsData(domains: [TrackedDomain], format: DomainExportFormat) -> Data? {
 38        try? DomainReportExporter.data(
 39            for: reports(for: domains),
 40            format: format,
 41            title: "Tracked Domains Export"
 42        )
 43    }
 44
 45    func exportTimelineText(domain: String, includeDiffSummary: Bool) -> String {
 46        DomainReportExporter.timelineText(
 47            for: timelineReports(for: domain),
 48            domain: domain,
 49            includeDiffSummary: includeDiffSummary
 50        )
 51    }
 52
 53    func exportTimelineJSONData(domain: String, includeDiffSummary: Bool) -> Data? {
 54        try? DomainReportExporter.timelineData(
 55            for: timelineReports(for: domain),
 56            domain: domain,
 57            includeDiffSummary: includeDiffSummary
 58        )
 59    }
 60
 61    func exportFullBackupData() -> Data? {
 62        try? DomainDataPortabilityService.backupData()
 63    }
 64
 65    func exportPortableTrackedDomainsJSONData() -> Data? {
 66        try? DomainDataPortabilityService.trackedDomainsExportData()
 67    }
 68
 69    func exportPortableTrackedDomainsCSV() -> String {
 70        DomainDataPortabilityService.trackedDomainsCSV()
 71    }
 72
 73    func exportPortableWorkflowsJSONData() -> Data? {
 74        try? DomainDataPortabilityService.workflowsExportData()
 75    }
 76
 77    func exportPortableWorkflowsCSV() -> String {
 78        DomainDataPortabilityService.workflowsCSV()
 79    }
 80
 81    func exportPortableHistoryJSONData() -> Data? {
 82        try? DomainDataPortabilityService.historyExportData()
 83    }
 84
 85    func prepareDataImport(
 86        data: Data,
 87        fileName: String,
 88        mode: DataPortabilityImportMode
 89    ) throws -> DataImportPreview {
 90        try DomainDataPortabilityService.prepareImport(data: data, fileName: fileName, mode: mode)
 91    }
 92
 93    func applyDataImport(_ preview: DataImportPreview, mode: DataPortabilityImportMode) throws -> DataImportResult {
 94        let result = try DomainDataPortabilityService.applyImport(preview, mode: mode)
 95        refreshPersistedData()
 96        portabilityStatusMessage = result.summary
 97        CloudSyncService.shared.markAppSettingsChanged()
 98        CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: monitoringSettings.isEnabled)
 99        CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .imported)
100        return result
101    }
102
103    func persistCurrentAppSettings(resolverURLString: String, appDensityRawValue: String) {
104        DomainDataPortabilityService.saveAppSettings(
105            AppSettingsSnapshot(
106                recentSearches: recentSearches,
107                savedDomains: savedDomains,
108                resolverURLString: resolverURLString,
109                appDensityRawValue: appDensityRawValue
110            )
111        )
112        CloudSyncService.shared.markAppSettingsChanged()
113        refreshDataLifecycleSummary()
114    }
115
116    func exportWorkflowText(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
117        let reports = workflowReports(from: summary, changedOnly: changedOnly)
118        let base = DomainReportExporter.batchText(
119            for: reports,
120            title: "\(summary.workflowName) Workflow Export"
121        )
122        guard !summary.workflowInsights.isEmpty else { return base }
123        let insightLines = summary.workflowInsights.map {
124            "- \($0.description): \($0.domainsInvolved.joined(separator: ", "))"
125        }
126        return ([ "\(summary.workflowName) Workflow Insights", String(repeating: "-", count: 32) ] + insightLines + ["", base]).joined(separator: "\n")
127    }
128
129    func exportWorkflowCSV(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
130        DomainReportExporter.csv(
131            for: workflowReports(from: summary, changedOnly: changedOnly),
132            workflowInsights: summary.workflowInsights
133        )
134    }
135
136    func exportWorkflowJSONData(summary: WorkflowRunSummary, changedOnly: Bool) -> Data? {
137        let payload = WorkflowExportPayload(
138            workflowName: summary.workflowName,
139            generatedAt: summary.generatedAt,
140            workflowInsights: summary.workflowInsights,
141            reports: workflowReports(from: summary, changedOnly: changedOnly)
142        )
143        let encoder = JSONEncoder()
144        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
145        encoder.dateEncodingStrategy = .iso8601
146        return try? encoder.encode(payload)
147    }
148
149    func exportWorkflowMarkdown(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
150        let reports = workflowReports(from: summary, changedOnly: changedOnly)
151        let base = DomainReportExporter.batchMarkdown(for: reports, title: "\(summary.workflowName) Workflow Export")
152        guard !summary.workflowInsights.isEmpty else { return base }
153        let insightLines = summary.workflowInsights.map {
154            "- \($0.description): \($0.domainsInvolved.joined(separator: ", "))"
155        }
156        return (["## \(summary.workflowName) Workflow Insights"] + insightLines + ["", base]).joined(separator: "\n")
157    }
158
159    func exportWorkflowData(summary: WorkflowRunSummary, changedOnly: Bool, format: DomainExportFormat) -> Data? {
160        switch format {
161        case .text:
162            return Data(exportWorkflowText(summary: summary, changedOnly: changedOnly).utf8)
163        case .csv:
164            return Data(exportWorkflowCSV(summary: summary, changedOnly: changedOnly).utf8)
165        case .json:
166            return exportWorkflowJSONData(summary: summary, changedOnly: changedOnly)
167        case .markdown:
168            return Data(exportWorkflowMarkdown(summary: summary, changedOnly: changedOnly).utf8)
169        case .pdf:
170            return DomainReportExporter.pdfData(fromMarkdown: exportWorkflowMarkdown(summary: summary, changedOnly: changedOnly))
171        }
172    }
173}
174
175private struct WorkflowExportPayload: Codable {
176    let workflowName: String
177    let generatedAt: Date
178    let workflowInsights: [WorkflowInsight]
179    let reports: [DomainReport]
180}