Only use the appGroupTemporaryDirectory to access a file from the share extension. (#4002)
… and switch back to the plain `URL.temporaryDirectory` for everything else. * Fix documentation Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
This commit is contained in:
@@ -125,6 +125,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
if let previousVersion = appSettings.lastVersionLaunched.flatMap(Version.init) {
|
if let previousVersion = appSettings.lastVersionLaunched.flatMap(Version.init) {
|
||||||
performMigrationsIfNecessary(from: previousVersion, to: currentVersion)
|
performMigrationsIfNecessary(from: previousVersion, to: currentVersion)
|
||||||
|
|
||||||
|
// Manual clean to handle the potential case where the app crashes before moving a shared file.
|
||||||
|
cleanAppGroupTemporaryDirectory()
|
||||||
} else {
|
} else {
|
||||||
// The app has been deleted since the previous run. Reset everything.
|
// The app has been deleted since the previous run. Reset everything.
|
||||||
wipeUserData(includingSettings: true)
|
wipeUserData(includingSettings: true)
|
||||||
@@ -252,12 +255,17 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
} else {
|
} else {
|
||||||
handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias))
|
handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias))
|
||||||
}
|
}
|
||||||
case .share:
|
case .share(let payload):
|
||||||
guard isExternalURL else {
|
guard isExternalURL else {
|
||||||
MXLog.error("Received unexpected internal share route")
|
MXLog.error("Received unexpected internal share route")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
handleAppRoute(route)
|
|
||||||
|
do {
|
||||||
|
try handleAppRoute(.share(payload.withDefaultTemporaryDirectory()))
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed moving payload out of the app group container: \(error)")
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -408,6 +416,31 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
userSessionStore.reset()
|
userSessionStore.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Manually cleans up any files in the app group's `tmp` directory.
|
||||||
|
///
|
||||||
|
/// **Note:** If there is a single file we consider it to be an active share payload and ignore it.
|
||||||
|
private func cleanAppGroupTemporaryDirectory() {
|
||||||
|
let fileURLs: [URL]
|
||||||
|
do {
|
||||||
|
fileURLs = try FileManager.default.contentsOfDirectory(at: URL.appGroupTemporaryDirectory, includingPropertiesForKeys: nil, options: [])
|
||||||
|
} catch {
|
||||||
|
MXLog.warning("Failed to enumerate app group temporary directory: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard fileURLs.count > 1 else {
|
||||||
|
return // If there is only a single item in here, there's likely a pending share payload that is yet to be processed.
|
||||||
|
}
|
||||||
|
|
||||||
|
for url in fileURLs {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
} catch {
|
||||||
|
MXLog.warning("Failed to remove file from app group temporary directory: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func setupStateMachine() {
|
private func setupStateMachine() {
|
||||||
stateMachine.addTransitionHandler { [weak self] context in
|
stateMachine.addTransitionHandler { [weak self] context in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ extension FileManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func writeDataToTemporaryDirectory(data: Data, fileName: String) throws -> URL {
|
func writeDataToTemporaryDirectory(data: Data, fileName: String, withinAppGroupContainer: Bool = false) throws -> URL {
|
||||||
let newURL = URL.appGroupTemporaryDirectory.appendingPathComponent(fileName)
|
let baseURL: URL = withinAppGroupContainer ? .appGroupTemporaryDirectory : .temporaryDirectory
|
||||||
|
let newURL = baseURL.appendingPathComponent(fileName)
|
||||||
|
|
||||||
try data.write(to: newURL)
|
try data.write(to: newURL)
|
||||||
|
|
||||||
|
|||||||
@@ -28,20 +28,23 @@ extension NSItemProvider {
|
|||||||
try? await loadItem(forTypeIdentifier: UTType.text.identifier) as? String
|
try? await loadItem(forTypeIdentifier: UTType.text.identifier) as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
func storeData() async -> URL? {
|
/// Stores the item's data from the provider within the temporary directory, returning the URL on success.
|
||||||
|
/// - Parameter withinAppGroupContainer: Whether the data needs to be shared between bundles.
|
||||||
|
/// If passing `true` you will need to manually clean up the file once you have the data in the receiving bundle.
|
||||||
|
func storeData(withinAppGroupContainer: Bool = false) async -> URL? {
|
||||||
guard let contentType = preferredContentType else {
|
guard let contentType = preferredContentType else {
|
||||||
MXLog.error("Invalid NSItemProvider: \(self)")
|
MXLog.error("Invalid NSItemProvider: \(self)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if contentType.type.identifier == UTType.image.identifier {
|
if contentType.type.identifier == UTType.image.identifier {
|
||||||
return await generateURLForUIImage(contentType)
|
return await generateURLForUIImage(contentType, withinAppGroupContainer: withinAppGroupContainer)
|
||||||
} else {
|
} else {
|
||||||
return await generateURLForGenericData(contentType)
|
return await generateURLForGenericData(contentType, withinAppGroupContainer: withinAppGroupContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateURLForUIImage(_ contentType: PreferredContentType) async -> URL? {
|
private func generateURLForUIImage(_ contentType: PreferredContentType, withinAppGroupContainer: Bool) async -> URL? {
|
||||||
guard let uiImage = try? await loadItem(forTypeIdentifier: contentType.type.identifier) as? UIImage else {
|
guard let uiImage = try? await loadItem(forTypeIdentifier: contentType.type.identifier) as? UIImage else {
|
||||||
MXLog.error("Failed casting UIImage, invalid NSItemProvider: \(self)")
|
MXLog.error("Failed casting UIImage, invalid NSItemProvider: \(self)")
|
||||||
return nil
|
return nil
|
||||||
@@ -52,22 +55,25 @@ extension NSItemProvider {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filename = if let suggestedName = suggestedName as NSString?,
|
||||||
|
// Suggestions are nice but their extension is `jpeg`
|
||||||
|
let filename = (suggestedName.deletingPathExtension as NSString).appendingPathExtension(contentType.fileExtension) {
|
||||||
|
filename
|
||||||
|
} else {
|
||||||
|
"\(UUID().uuidString).\(contentType.fileExtension)"
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if let suggestedName = suggestedName as? NSString,
|
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData,
|
||||||
// Suggestions are nice but their extension is `jpeg`
|
fileName: filename,
|
||||||
let filename = (suggestedName.deletingPathExtension as NSString).appendingPathExtension(contentType.fileExtension) {
|
withinAppGroupContainer: withinAppGroupContainer)
|
||||||
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename)
|
|
||||||
} else {
|
|
||||||
let filename = "\(UUID().uuidString).\(contentType.fileExtension)"
|
|
||||||
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
|
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateURLForGenericData(_ contentType: PreferredContentType) async -> URL? {
|
private func generateURLForGenericData(_ contentType: PreferredContentType, withinAppGroupContainer: Bool) async -> URL? {
|
||||||
let providerDescription = description
|
let providerDescription = description
|
||||||
let shareData: Data? = await withCheckedContinuation { continuation in
|
let shareData: Data? = await withCheckedContinuation { continuation in
|
||||||
_ = loadDataRepresentation(for: contentType.type) { data, error in
|
_ = loadDataRepresentation(for: contentType.type) { data, error in
|
||||||
@@ -92,15 +98,19 @@ extension NSItemProvider {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filename = if let suggestedName = suggestedName as NSString?,
|
||||||
|
suggestedName.hasPathExtension {
|
||||||
|
suggestedName as String
|
||||||
|
} else if let suggestedName {
|
||||||
|
"\(suggestedName).\(contentType.fileExtension)"
|
||||||
|
} else {
|
||||||
|
"\(UUID().uuidString).\(contentType.fileExtension)"
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if let filename = suggestedName {
|
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData,
|
||||||
let hasExtension = !(filename as NSString).pathExtension.isEmpty
|
fileName: filename,
|
||||||
let filename = hasExtension ? filename : "\(filename).\(contentType.fileExtension)"
|
withinAppGroupContainer: withinAppGroupContainer)
|
||||||
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
|
|
||||||
} else {
|
|
||||||
let filename = "\(UUID().uuidString).\(contentType.fileExtension)"
|
|
||||||
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
|
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
|
||||||
return nil
|
return nil
|
||||||
@@ -164,3 +174,7 @@ extension NSItemProvider {
|
|||||||
return mimeType.hasPrefix("image/") || mimeType.hasPrefix("video/") || mimeType.hasPrefix("application/")
|
return mimeType.hasPrefix("image/") || mimeType.hasPrefix("video/") || mimeType.hasPrefix("application/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension NSString {
|
||||||
|
var hasPathExtension: Bool { !pathExtension.isEmpty }
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ extension URL: @retroactive ExpressibleByStringLiteral {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The app group temporary directory
|
/// The app group temporary directory (useful for transferring files between different bundles).
|
||||||
|
///
|
||||||
|
/// **Note:** This `tmp` directory doesn't appear to behave as expected as it isn't being tidied up by the system.
|
||||||
|
/// Make sure to manually tidy up any files you place in here once you've transferred them from one bundle to another.
|
||||||
static var appGroupTemporaryDirectory: URL {
|
static var appGroupTemporaryDirectory: URL {
|
||||||
let url = appGroupContainerDirectory
|
let url = appGroupContainerDirectory
|
||||||
.appendingPathComponent("tmp", isDirectory: true)
|
.appendingPathComponent("tmp", isDirectory: true)
|
||||||
|
|||||||
@@ -22,9 +22,30 @@ enum ShareExtensionPayload: Hashable, Codable {
|
|||||||
roomID
|
roomID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves any files in the payload from our `appGroupTemporaryDirectory` to the
|
||||||
|
/// system's `temporaryDirectory` returning a modified payload with updated file URLs.
|
||||||
|
func withDefaultTemporaryDirectory() throws -> Self {
|
||||||
|
switch self {
|
||||||
|
case .mediaFile(let roomID, let mediaFile):
|
||||||
|
let path = mediaFile.url.path.replacing(URL.appGroupTemporaryDirectory.path, with: "").trimmingPrefix("/")
|
||||||
|
let newURL = URL.temporaryDirectory.appending(path: path)
|
||||||
|
|
||||||
|
try? FileManager.default.removeItem(at: newURL)
|
||||||
|
try FileManager.default.moveItem(at: mediaFile.url, to: newURL)
|
||||||
|
|
||||||
|
return .mediaFile(roomID: roomID, mediaFile: mediaFile.replacingURL(with: newURL))
|
||||||
|
case .text:
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ShareExtensionMediaFile: Hashable, Codable {
|
struct ShareExtensionMediaFile: Hashable, Codable {
|
||||||
let url: URL
|
let url: URL
|
||||||
let suggestedName: String?
|
let suggestedName: String?
|
||||||
|
|
||||||
|
fileprivate func replacingURL(with newURL: URL) -> ShareExtensionMediaFile {
|
||||||
|
ShareExtensionMediaFile(url: newURL, suggestedName: suggestedName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ShareExtensionViewController: UIViewController {
|
|||||||
|
|
||||||
let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier
|
let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier
|
||||||
|
|
||||||
if let fileURL = await itemProvider.storeData() {
|
if let fileURL = await itemProvider.storeData(withinAppGroupContainer: true) {
|
||||||
return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
|
return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
|
||||||
} else if let url = await itemProvider.loadTransferable(type: URL.self) {
|
} else if let url = await itemProvider.loadTransferable(type: URL.self) {
|
||||||
return .text(roomID: roomID, text: url.absoluteString)
|
return .text(roomID: roomID, text: url.absoluteString)
|
||||||
|
|||||||
Reference in New Issue
Block a user