Store PDF paths as filenames instead of full URLs
This fixes issues where the full URL of our directory path changes, also it moves files into documents instead of caches, and ensures PDFs are downloaded before opening.
This commit is contained in:
@ -21,15 +21,14 @@ import Utils
|
||||
let url: URL
|
||||
}
|
||||
|
||||
let pdfURL: URL
|
||||
let viewModel: PDFViewerViewModel
|
||||
|
||||
@StateObject var pdfStateObject = PDFStateObject()
|
||||
@State var readerView: Bool = false
|
||||
@State private var shareLink: ShareLink?
|
||||
@State private var errorMessage: String?
|
||||
|
||||
init(remoteURL: URL, viewModel: PDFViewerViewModel) {
|
||||
self.pdfURL = viewModel.pdfItem.localPdfURL ?? remoteURL
|
||||
init(viewModel: PDFViewerViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
@ -135,12 +134,20 @@ import Utils
|
||||
.sheet(item: $shareLink) {
|
||||
ShareSheet(activityItems: [$0.url])
|
||||
}
|
||||
} else if let errorMessage = errorMessage {
|
||||
Text(errorMessage)
|
||||
} else {
|
||||
ProgressView()
|
||||
.task {
|
||||
let document = HighlightedDocument(url: pdfURL, viewModel: viewModel)
|
||||
pdfStateObject.document = document
|
||||
pdfStateObject.coordinator = PDFViewCoordinator(document: document, viewModel: viewModel)
|
||||
// NOTE: the issue here is the PDF is downloaded, but saved to a URL we don't know about
|
||||
// because it is changed.
|
||||
if let pdfURL = await viewModel.downloadPDF(dataService: dataService) {
|
||||
let document = HighlightedDocument(url: pdfURL, viewModel: viewModel)
|
||||
pdfStateObject.document = document
|
||||
pdfStateObject.coordinator = PDFViewCoordinator(document: document, viewModel: viewModel)
|
||||
} else {
|
||||
errorMessage = "Unable to download PDF."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ public final class PDFViewerViewModel: ObservableObject {
|
||||
@Published public var readerView: Bool = false
|
||||
|
||||
public let pdfItem: PDFItem
|
||||
private var storedURL: URL?
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
@ -81,4 +80,22 @@ public final class PDFViewerViewModel: ObservableObject {
|
||||
|
||||
return components?.url
|
||||
}
|
||||
|
||||
public var itemDownloaded: Bool {
|
||||
if let localPdfURL = pdfItem.localPdfURL, FileManager.default.fileExists(atPath: localPdfURL.path) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func downloadPDF(dataService: DataService) async -> URL? {
|
||||
do {
|
||||
if let localURL = try await dataService.fetchPDFData(slug: pdfItem.slug, pageURLString: pdfItem.originalArticleURL) {
|
||||
return localURL
|
||||
}
|
||||
} catch {
|
||||
print("error downloading PDF", error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,7 +300,7 @@ struct LinkItemDetailView: View {
|
||||
@ViewBuilder private var fixedNavBarReader: some View {
|
||||
if let pdfItem = viewModel.pdfItem, let pdfURL = pdfItem.pdfURL {
|
||||
#if os(iOS)
|
||||
PDFViewer(remoteURL: pdfURL, viewModel: PDFViewerViewModel(pdfItem: pdfItem))
|
||||
PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#elseif os(macOS)
|
||||
PDFWrapperView(pdfURL: pdfURL)
|
||||
|
||||
@ -143,7 +143,7 @@ import Utils
|
||||
public var body: some View {
|
||||
if let item = viewModel.item, item.isReadyToRead {
|
||||
if let pdfItem = PDFItem.make(item: item), let urlStr = item.pageURLString, let remoteUrl = URL(string: urlStr) {
|
||||
PDFViewer(remoteURL: remoteUrl, viewModel: PDFViewerViewModel(pdfItem: pdfItem))
|
||||
PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem))
|
||||
.navigationBarHidden(true)
|
||||
.navigationViewStyle(.stack)
|
||||
.accentColor(.appGrayTextContrast)
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="imageURLString" optional="YES" attributeType="String"/>
|
||||
<attribute name="isArchived" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="localPdfURL" optional="YES" attributeType="String"/>
|
||||
<attribute name="localPDF" optional="YES" attributeType="String"/>
|
||||
<attribute name="onDeviceImageURLString" optional="YES" attributeType="String"/>
|
||||
<attribute name="originalHtml" optional="YES" attributeType="String"/>
|
||||
<attribute name="pageURLString" attributeType="String"/>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import CoreData
|
||||
import Foundation
|
||||
import Utils
|
||||
|
||||
public struct HomeFeedData { // TODO: rename this
|
||||
public let items: [NSManagedObjectID]
|
||||
@ -46,11 +47,7 @@ public extension LinkedItem {
|
||||
var isReadyToRead: Bool {
|
||||
if isPDF {
|
||||
// If its a PDF we verify the local file is available
|
||||
if let localPdfURL = localPdfURL, let url = URL(string: localPdfURL), FileManager.default.fileExists(atPath: url.path) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return PDFUtils.exists(filename: localPDF)
|
||||
}
|
||||
// Check the state and whether we have HTML
|
||||
return state == "SUCCEEDED"
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import CoreData
|
||||
import Foundation
|
||||
import Utils
|
||||
|
||||
public struct PDFItem {
|
||||
public let objectID: NSManagedObjectID
|
||||
public let itemID: String
|
||||
public let pdfURL: URL?
|
||||
public let localPdfURL: URL?
|
||||
public let localPDF: String?
|
||||
public let title: String
|
||||
public let slug: String
|
||||
public let readingProgress: Double
|
||||
@ -22,7 +23,7 @@ public struct PDFItem {
|
||||
objectID: item.objectID,
|
||||
itemID: item.unwrappedID,
|
||||
pdfURL: URL(string: item.unwrappedPageURLString),
|
||||
localPdfURL: item.localPdfURL.flatMap { URL(string: $0) },
|
||||
localPDF: item.localPDF,
|
||||
title: item.unwrappedID,
|
||||
slug: item.unwrappedSlug,
|
||||
readingProgress: item.readingProgress,
|
||||
@ -33,4 +34,11 @@ public struct PDFItem {
|
||||
highlights: item.highlights.asArray(of: Highlight.self)
|
||||
)
|
||||
}
|
||||
|
||||
public var localPdfURL: URL? {
|
||||
if let localPDF = localPDF {
|
||||
return PDFUtils.localPdfURL(filename: localPDF)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,11 +148,8 @@ public final class DataService: ObservableObject {
|
||||
switch pageScrape.contentType {
|
||||
case let .pdf(localUrl):
|
||||
linkedItem.contentReader = "PDF"
|
||||
linkedItem.localPdfURL = localUrl.absoluteString
|
||||
linkedItem.title = PDFUtils.titleFromPdfFile(pageScrape.url)
|
||||
// let thumbnailUrl = PDFUtils.thumbnailUrl(localUrl: localUrl)
|
||||
// linkedItem.imageURLString = await PDFUtils.createThumbnailFor(inputUrl: localUrl, at: thumbnailUrl)
|
||||
|
||||
linkedItem.localPDF = try PDFUtils.moveToLocal(url: localUrl)
|
||||
case let .html(html: html, title: title, iconURL: iconURL):
|
||||
linkedItem.contentReader = "WEB"
|
||||
linkedItem.originalHtml = html
|
||||
|
||||
@ -95,21 +95,6 @@ public extension DataService {
|
||||
}
|
||||
}
|
||||
|
||||
func uploadFileInBackground(id: String, localPdfURL: String?, url: URL, usingSession session: URLSession) -> URLSessionTask? {
|
||||
if let localPdfURL = localPdfURL, let localUrl = URL(string: localPdfURL) {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "PUT"
|
||||
request.setValue("application/pdf", forHTTPHeaderField: "content-type")
|
||||
request.setValue(id, forHTTPHeaderField: "clientRequestId")
|
||||
|
||||
let task = session.uploadTask(with: request, fromFile: localUrl)
|
||||
return task
|
||||
} else {
|
||||
// TODO: How should we handle this scenario?
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func saveFilePublisher(requestId: String, uploadFileId: String, url: String) async throws -> String? {
|
||||
enum MutationResult {
|
||||
case saved(requestId: String, url: String)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import Utils
|
||||
|
||||
public extension DataService {
|
||||
internal func syncOfflineItemsWithServerIfNeeded() async throws {
|
||||
@ -55,7 +56,6 @@ public extension DataService {
|
||||
let uploadRequest = try await uploadFileRequest(id: id, url: url)
|
||||
if let urlString = uploadRequest.urlString, let uploadUrl = URL(string: urlString) {
|
||||
try await uploadFile(id: id, localPdfURL: localPdfURL, url: uploadUrl)
|
||||
// try await services.dataService.saveFilePublisher(requestId: requestId, uploadFileId: uploadFileID, url: url)
|
||||
} else {
|
||||
throw SaveArticleError.badData
|
||||
}
|
||||
@ -112,10 +112,8 @@ public extension DataService {
|
||||
switch item.contentReader {
|
||||
case "PDF":
|
||||
let id = item.unwrappedID
|
||||
let localPdfURL = item.localPdfURL
|
||||
let url = item.unwrappedPageURLString
|
||||
|
||||
if let pdfUrlStr = localPdfURL, let localPdfURL = URL(string: pdfUrlStr) {
|
||||
if let localPDF = item.localPDF, let localPdfURL = PDFUtils.localPdfURL(filename: localPDF) {
|
||||
Task {
|
||||
try await createPageFromPdf(id: id, localPdfURL: localPdfURL, url: url)
|
||||
}
|
||||
|
||||
@ -2,14 +2,15 @@ import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
import Utils
|
||||
|
||||
extension DataService {
|
||||
struct PendingLink {
|
||||
public extension DataService {
|
||||
internal struct PendingLink {
|
||||
let itemID: String
|
||||
let retryCount: Int
|
||||
}
|
||||
|
||||
public func prefetchPages(itemIDs: [String], username: String) async {
|
||||
func prefetchPages(itemIDs: [String], username: String) async {
|
||||
// TODO: make this concurrent
|
||||
// TODO: make a non-pending page option for BG tasks
|
||||
for itemID in itemIDs {
|
||||
@ -17,7 +18,7 @@ extension DataService {
|
||||
}
|
||||
}
|
||||
|
||||
func prefetchPage(pendingLink: PendingLink, username: String) async {
|
||||
internal func prefetchPage(pendingLink: PendingLink, username: String) async {
|
||||
let content = try? await articleContent(username: username, itemID: pendingLink.itemID, useCache: false)
|
||||
|
||||
if content?.contentStatus == .processing, pendingLink.retryCount < 7 {
|
||||
@ -40,7 +41,7 @@ extension DataService {
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchArticleContent(
|
||||
func fetchArticleContent(
|
||||
itemID: String,
|
||||
username: String? = nil,
|
||||
requestCount: Int = 1
|
||||
@ -69,7 +70,7 @@ extension DataService {
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public func articleContent(
|
||||
func articleContent(
|
||||
username: String,
|
||||
itemID: String,
|
||||
useCache: Bool
|
||||
@ -193,7 +194,7 @@ extension DataService {
|
||||
return articleContent
|
||||
}
|
||||
|
||||
func persistArticleContent(item: InternalLinkedItem, htmlContent: String, highlights: [InternalHighlight]) async throws {
|
||||
internal func persistArticleContent(item: InternalLinkedItem, htmlContent: String, highlights: [InternalHighlight]) async throws {
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
@ -243,8 +244,10 @@ extension DataService {
|
||||
}
|
||||
}
|
||||
|
||||
func fetchPDFData(slug: String, pageURLString: String) async throws {
|
||||
guard let url = URL(string: pageURLString) else { return }
|
||||
func fetchPDFData(slug: String, pageURLString: String) async throws -> URL? {
|
||||
guard let url = URL(string: pageURLString) else {
|
||||
throw BasicError.message(messageText: "No PDF URL found")
|
||||
}
|
||||
let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url)
|
||||
guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
|
||||
throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.")
|
||||
@ -253,6 +256,11 @@ extension DataService {
|
||||
throw BasicError.message(messageText: "pdfFetch failed. no data received.")
|
||||
}
|
||||
|
||||
var localPdfURL: URL?
|
||||
let tempPath = FileManager.default
|
||||
.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(UUID().uuidString + ".pdf")
|
||||
|
||||
try await backgroundContext.perform { [weak self] in
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug)
|
||||
@ -263,15 +271,11 @@ extension DataService {
|
||||
throw BasicError.message(messageText: errorMessage)
|
||||
}
|
||||
|
||||
let subPath = UUID().uuidString + ".pdf" // linkedItem.title.isEmpty ? UUID().uuidString : linkedItem.title
|
||||
|
||||
let path = FileManager.default
|
||||
.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(subPath)
|
||||
|
||||
do {
|
||||
try data.write(to: path)
|
||||
linkedItem.localPdfURL = path.absoluteString
|
||||
try data.write(to: tempPath)
|
||||
let localPDF = try PDFUtils.moveToLocal(url: tempPath)
|
||||
localPdfURL = PDFUtils.localPdfURL(filename: localPDF)
|
||||
linkedItem.localPDF = localPDF
|
||||
try self?.backgroundContext.save()
|
||||
} catch {
|
||||
self?.backgroundContext.rollback()
|
||||
@ -279,9 +283,11 @@ extension DataService {
|
||||
throw BasicError.message(messageText: errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return localPdfURL
|
||||
}
|
||||
|
||||
func cachedArticleContent(itemID: String) async -> ArticleContent? {
|
||||
internal func cachedArticleContent(itemID: String) async -> ArticleContent? {
|
||||
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
linkedItemFetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", itemID
|
||||
@ -307,7 +313,7 @@ extension DataService {
|
||||
}
|
||||
}
|
||||
|
||||
public func syncUnsyncedArticleContent(itemID: String) async {
|
||||
func syncUnsyncedArticleContent(itemID: String) async {
|
||||
let linkedItemFetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
linkedItemFetchRequest.predicate = NSPredicate(
|
||||
format: "id == %@", itemID
|
||||
|
||||
@ -11,6 +11,30 @@ import QuickLookThumbnailing
|
||||
import UIKit
|
||||
|
||||
public enum PDFUtils {
|
||||
public static func moveToLocal(url: URL) throws -> String {
|
||||
let subPath = UUID().uuidString + ".pdf"
|
||||
let dest = FileManager.default
|
||||
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(subPath)
|
||||
|
||||
try FileManager.default.moveItem(at: url, to: dest)
|
||||
return subPath
|
||||
}
|
||||
|
||||
public static func localPdfURL(filename: String) -> URL? {
|
||||
let url = FileManager.default
|
||||
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent(filename)
|
||||
return url
|
||||
}
|
||||
|
||||
public static func exists(filename: String?) -> Bool {
|
||||
if let filename = filename, let localPdfURL = localPdfURL(filename: filename) {
|
||||
return FileManager.default.fileExists(atPath: localPdfURL.absoluteString)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public static func titleFromPdfFile(_ urlStr: String) -> String {
|
||||
let url = URL(string: urlStr)
|
||||
if let url = url {
|
||||
|
||||
Reference in New Issue
Block a user