Skip to content

Commit 9ba4048

Browse files
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>
1 parent 58a0709 commit 9ba4048

File tree

6 files changed

+99
-27
lines changed

6 files changed

+99
-27
lines changed

ElementX/Sources/Application/AppCoordinator.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
125125

126126
if let previousVersion = appSettings.lastVersionLaunched.flatMap(Version.init) {
127127
performMigrationsIfNecessary(from: previousVersion, to: currentVersion)
128+
129+
// Manual clean to handle the potential case where the app crashes before moving a shared file.
130+
cleanAppGroupTemporaryDirectory()
128131
} else {
129132
// The app has been deleted since the previous run. Reset everything.
130133
wipeUserData(includingSettings: true)
@@ -252,12 +255,17 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
252255
} else {
253256
handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias))
254257
}
255-
case .share:
258+
case .share(let payload):
256259
guard isExternalURL else {
257260
MXLog.error("Received unexpected internal share route")
258261
break
259262
}
260-
handleAppRoute(route)
263+
264+
do {
265+
try handleAppRoute(.share(payload.withDefaultTemporaryDirectory()))
266+
} catch {
267+
MXLog.error("Failed moving payload out of the app group container: \(error)")
268+
}
261269
default:
262270
break
263271
}
@@ -408,6 +416,31 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
408416
userSessionStore.reset()
409417
}
410418

419+
/// Manually cleans up any files in the app group's `tmp` directory.
420+
///
421+
/// **Note:** If there is a single file we consider it to be an active share payload and ignore it.
422+
private func cleanAppGroupTemporaryDirectory() {
423+
let fileURLs: [URL]
424+
do {
425+
fileURLs = try FileManager.default.contentsOfDirectory(at: URL.appGroupTemporaryDirectory, includingPropertiesForKeys: nil, options: [])
426+
} catch {
427+
MXLog.warning("Failed to enumerate app group temporary directory: \(error)")
428+
return
429+
}
430+
431+
guard fileURLs.count > 1 else {
432+
return // If there is only a single item in here, there's likely a pending share payload that is yet to be processed.
433+
}
434+
435+
for url in fileURLs {
436+
do {
437+
try FileManager.default.removeItem(at: url)
438+
} catch {
439+
MXLog.warning("Failed to remove file from app group temporary directory: \(error)")
440+
}
441+
}
442+
}
443+
411444
private func setupStateMachine() {
412445
stateMachine.addTransitionHandler { [weak self] context in
413446
guard let self else { return }

ElementX/Sources/Other/Extensions/FileManager.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ extension FileManager {
3737
}
3838

3939
@discardableResult
40-
func writeDataToTemporaryDirectory(data: Data, fileName: String) throws -> URL {
41-
let newURL = URL.appGroupTemporaryDirectory.appendingPathComponent(fileName)
40+
func writeDataToTemporaryDirectory(data: Data, fileName: String, withinAppGroupContainer: Bool = false) throws -> URL {
41+
let baseURL: URL = withinAppGroupContainer ? .appGroupTemporaryDirectory : .temporaryDirectory
42+
let newURL = baseURL.appendingPathComponent(fileName)
4243

4344
try data.write(to: newURL)
4445

ElementX/Sources/Other/Extensions/NSItemProvider.swift

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,23 @@ extension NSItemProvider {
2828
try? await loadItem(forTypeIdentifier: UTType.text.identifier) as? String
2929
}
3030

31-
func storeData() async -> URL? {
31+
/// Stores the item's data from the provider within the temporary directory, returning the URL on success.
32+
/// - Parameter withinAppGroupContainer: Whether the data needs to be shared between bundles.
33+
/// If passing `true` you will need to manually clean up the file once you have the data in the receiving bundle.
34+
func storeData(withinAppGroupContainer: Bool = false) async -> URL? {
3235
guard let contentType = preferredContentType else {
3336
MXLog.error("Invalid NSItemProvider: \(self)")
3437
return nil
3538
}
3639

3740
if contentType.type.identifier == UTType.image.identifier {
38-
return await generateURLForUIImage(contentType)
41+
return await generateURLForUIImage(contentType, withinAppGroupContainer: withinAppGroupContainer)
3942
} else {
40-
return await generateURLForGenericData(contentType)
43+
return await generateURLForGenericData(contentType, withinAppGroupContainer: withinAppGroupContainer)
4144
}
4245
}
4346

44-
private func generateURLForUIImage(_ contentType: PreferredContentType) async -> URL? {
47+
private func generateURLForUIImage(_ contentType: PreferredContentType, withinAppGroupContainer: Bool) async -> URL? {
4548
guard let uiImage = try? await loadItem(forTypeIdentifier: contentType.type.identifier) as? UIImage else {
4649
MXLog.error("Failed casting UIImage, invalid NSItemProvider: \(self)")
4750
return nil
@@ -52,22 +55,25 @@ extension NSItemProvider {
5255
return nil
5356
}
5457

58+
let filename = if let suggestedName = suggestedName as NSString?,
59+
// Suggestions are nice but their extension is `jpeg`
60+
let filename = (suggestedName.deletingPathExtension as NSString).appendingPathExtension(contentType.fileExtension) {
61+
filename
62+
} else {
63+
"\(UUID().uuidString).\(contentType.fileExtension)"
64+
}
65+
5566
do {
56-
if let suggestedName = suggestedName as? NSString,
57-
// Suggestions are nice but their extension is `jpeg`
58-
let filename = (suggestedName.deletingPathExtension as NSString).appendingPathExtension(contentType.fileExtension) {
59-
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename)
60-
} else {
61-
let filename = "\(UUID().uuidString).\(contentType.fileExtension)"
62-
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename)
63-
}
67+
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData,
68+
fileName: filename,
69+
withinAppGroupContainer: withinAppGroupContainer)
6470
} catch {
6571
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
6672
return nil
6773
}
6874
}
6975

70-
private func generateURLForGenericData(_ contentType: PreferredContentType) async -> URL? {
76+
private func generateURLForGenericData(_ contentType: PreferredContentType, withinAppGroupContainer: Bool) async -> URL? {
7177
let providerDescription = description
7278
let shareData: Data? = await withCheckedContinuation { continuation in
7379
_ = loadDataRepresentation(for: contentType.type) { data, error in
@@ -92,15 +98,19 @@ extension NSItemProvider {
9298
return nil
9399
}
94100

101+
let filename = if let suggestedName = suggestedName as NSString?,
102+
suggestedName.hasPathExtension {
103+
suggestedName as String
104+
} else if let suggestedName {
105+
"\(suggestedName).\(contentType.fileExtension)"
106+
} else {
107+
"\(UUID().uuidString).\(contentType.fileExtension)"
108+
}
109+
95110
do {
96-
if let filename = suggestedName {
97-
let hasExtension = !(filename as NSString).pathExtension.isEmpty
98-
let filename = hasExtension ? filename : "\(filename).\(contentType.fileExtension)"
99-
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
100-
} else {
101-
let filename = "\(UUID().uuidString).\(contentType.fileExtension)"
102-
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
103-
}
111+
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData,
112+
fileName: filename,
113+
withinAppGroupContainer: withinAppGroupContainer)
104114
} catch {
105115
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
106116
return nil
@@ -164,3 +174,7 @@ extension NSItemProvider {
164174
return mimeType.hasPrefix("image/") || mimeType.hasPrefix("video/") || mimeType.hasPrefix("application/")
165175
}
166176
}
177+
178+
private extension NSString {
179+
var hasPathExtension: Bool { !pathExtension.isEmpty }
180+
}

ElementX/Sources/Other/Extensions/URL.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ extension URL: @retroactive ExpressibleByStringLiteral {
7575
return url
7676
}
7777

78-
/// The app group temporary directory
78+
/// The app group temporary directory (useful for transferring files between different bundles).
79+
///
80+
/// **Note:** This `tmp` directory doesn't appear to behave as expected as it isn't being tidied up by the system.
81+
/// Make sure to manually tidy up any files you place in here once you've transferred them from one bundle to another.
7982
static var appGroupTemporaryDirectory: URL {
8083
let url = appGroupContainerDirectory
8184
.appendingPathComponent("tmp", isDirectory: true)

ElementX/Sources/ShareExtension/ShareExtensionModels.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,30 @@ enum ShareExtensionPayload: Hashable, Codable {
2222
roomID
2323
}
2424
}
25+
26+
/// Moves any files in the payload from our `appGroupTemporaryDirectory` to the
27+
/// system's `temporaryDirectory` returning a modified payload with updated file URLs.
28+
func withDefaultTemporaryDirectory() throws -> Self {
29+
switch self {
30+
case .mediaFile(let roomID, let mediaFile):
31+
let path = mediaFile.url.path.replacing(URL.appGroupTemporaryDirectory.path, with: "").trimmingPrefix("/")
32+
let newURL = URL.temporaryDirectory.appending(path: path)
33+
34+
try? FileManager.default.removeItem(at: newURL)
35+
try FileManager.default.moveItem(at: mediaFile.url, to: newURL)
36+
37+
return .mediaFile(roomID: roomID, mediaFile: mediaFile.replacingURL(with: newURL))
38+
case .text:
39+
return self
40+
}
41+
}
2542
}
2643

2744
struct ShareExtensionMediaFile: Hashable, Codable {
2845
let url: URL
2946
let suggestedName: String?
47+
48+
fileprivate func replacingURL(with newURL: URL) -> ShareExtensionMediaFile {
49+
ShareExtensionMediaFile(url: newURL, suggestedName: suggestedName)
50+
}
3051
}

ShareExtension/Sources/ShareExtensionViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ShareExtensionViewController: UIViewController {
4444

4545
let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier
4646

47-
if let fileURL = await itemProvider.storeData() {
47+
if let fileURL = await itemProvider.storeData(withinAppGroupContainer: true) {
4848
return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
4949
} else if let url = await itemProvider.loadTransferable(type: URL.self) {
5050
return .text(roomID: roomID, text: url.absoluteString)

0 commit comments

Comments
 (0)