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:
Jackson Harper
2022-06-08 16:08:32 -07:00
parent c86c18507e
commit 7b81ad253d
12 changed files with 98 additions and 59 deletions

View File

@ -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."
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -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"/>

View File

@ -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"

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -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 {