Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,23 @@ package final class WireDriveLocalAssetRepository: WireDriveLocalAssetRepository

let (tempURL, _) = try await download.value

let filename = node.path.split(separator: "/").last.flatMap(String.init) ?? "-"

var asset = try verifyAsset(nodeID: nodeID, eTag: eTag)
try await fileCache.saveFile(at: tempURL, key: asset.cacheKey)

let extensionComponents = asset.cacheKey.split(separator: ".")
let pathWithoutExtension: String = if extensionComponents.count > 1 {
String(extensionComponents.dropLast().joined(separator: "."))
} else {
asset.cacheKey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to cover the else branch with the unit tests?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that you mention it, I think the else case will never trigger, actually. Because the cacheKey is built always with the pattern:
uuid + "_" + some_other_id + "." + file_extension
and then the new key becomes uuid + "_" + some_other_id + "/" + filename_with_extension
The else case is just a defensive fallback.
We can't test it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I don't know what would happen if we had files without a file extension.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a unit test for files without extensions. Thanks for the suggestion.

}

let key = pathWithoutExtension + "/" + filename

try await fileCache.saveFile(at: tempURL, key: key)

asset = try verifyAsset(nodeID: nodeID, eTag: eTag)
asset.downloadState = .downloaded(cacheKey: asset.cacheKey)
asset.downloadState = .downloaded(cacheKey: key)
try store.upsertAsset(asset)
} catch {
// We don't care about the eTag when setting download state to failed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
)
nodesAPI.getNodeNodeID_MockValue = node

// when

Check warning on line 98 in WireMessaging/Tests/WireMessagingTests/WireDrive/ImplementationTests/WireDriveLocalAssetRepositoryTests.swift

View workflow job for this annotation

GitHub Actions / Test Results

Result of call to 'refreshAssetMetadata(nodeID:)' is unused

Result of call to 'refreshAssetMetadata(nodeID:)' is unused
try await sut.refreshAssetMetadata(nodeID: nodeID)

// then the store is updated with the new metadata
Expand Down Expand Up @@ -148,7 +148,7 @@
downloadURL: URL(string: "https://example.com/file.png")!
)

// when

Check warning on line 151 in WireMessaging/Tests/WireMessagingTests/WireDrive/ImplementationTests/WireDriveLocalAssetRepositoryTests.swift

View workflow job for this annotation

GitHub Actions / Test Results

Result of call to 'refreshAssetMetadata(nodeID:)' is unused

Result of call to 'refreshAssetMetadata(nodeID:)' is unused
try await sut.refreshAssetMetadata(nodeID: nodeID)

// then the store is updated with the new metadata
Expand Down Expand Up @@ -215,7 +215,7 @@
path: "path/file.png",
contentType: "image/png",
size: 1234,
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc.png")
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc/file.png")
)
)

Expand Down Expand Up @@ -251,7 +251,84 @@
path: "path/file.png",
contentType: "image/png",
size: 1234,
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc.png")
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc/file.png")
)
]
)
}

@Test
func downloadAsset_withoutFileExtension() async throws {
// given
storeBacking[nodeID] = nil

let node = WireDriveNode.fixture(
uuid: nodeID,
path: "path/fileWithoutExtension",
size: 1234,
eTag: "abc",
mimeType: "image/png",
downloadURL: URL(string: "https://example.com/fileWithoutExtension")!
)
nodesAPI.getNodeNodeID_MockValue = node

let (progressStream, progressContinuation) = AsyncStream.makeStream(of: Double.self)
fileDownloader.downloadFrom_MockValue = (progress: progressStream, download: Task.fixture())

Task {
progressContinuation.yield(0.5)
progressContinuation.yield(1)
progressContinuation.finish()
}

// when
try await sut.downloadAsset(nodeID: nodeID)

// then
#expect(
try store.asset(nodeID: nodeID) == WireDriveLocalAsset(
nodeID: nodeID,
eTag: "abc",
path: "path/fileWithoutExtension",
contentType: "image/png",
size: 1234,
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc/fileWithoutExtension")
)
)

#expect(
store.upsertAsset_Invocations == [
WireDriveLocalAsset(
nodeID: nodeID,
eTag: "abc",
path: "path/fileWithoutExtension",
contentType: "image/png",
size: 1234,
downloadState: .pending,
),
WireDriveLocalAsset(
nodeID: nodeID,
eTag: "abc",
path: "path/fileWithoutExtension",
contentType: "image/png",
size: 1234,
downloadState: .downloading(progress: 0.5)
),
WireDriveLocalAsset(
nodeID: nodeID,
eTag: "abc",
path: "path/fileWithoutExtension",
contentType: "image/png",
size: 1234,
downloadState: .downloading(progress: 1.0)
),
WireDriveLocalAsset(
nodeID: nodeID,
eTag: "abc",
path: "path/fileWithoutExtension",
contentType: "image/png",
size: 1234,
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc/fileWithoutExtension")
)
]
)
Expand Down Expand Up @@ -314,7 +391,7 @@
path: "path/file.png",
contentType: "image/png",
size: 1234,
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc.png")
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc/file.png")
)
}
)
Expand Down Expand Up @@ -351,7 +428,7 @@
path: "path/file.png",
contentType: "image/png",
size: 1234,
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc.png")
downloadState: .downloaded(cacheKey: "\(nodeID.uuidString)-abc/file.png")
)
]
)
Expand Down
20 changes: 15 additions & 5 deletions wire-ios-data-model/Source/Model/Message/FileAssetCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -945,14 +945,19 @@ private struct FileCache: Cache {
movingOriginal: Bool,
createdAt creationDate: Date = Date()
) {

guard fromUrl.scheme == NSURLFileScheme else { fatal("Can't save remote URL to cache: \(fromUrl)") }

let toUrl = URLForKey(key)

let destinationFolder = toUrl.deletingLastPathComponent()
if !FileManager.default.fileExists(atPath: destinationFolder.path) {
try? FileManager.default.createDirectory(at: destinationFolder, withIntermediateDirectories: true)
}

let coordinator = NSFileCoordinator()

var error: NSError?
coordinator.coordinate(writingItemAt: toUrl, options: .forReplacing, error: &error) { url in
coordinator.coordinate(writingItemAt: toUrl, options: .forMoving, error: &error) { url in
do {
if movingOriginal {
try FileManager.default.moveItem(at: fromUrl, to: url)
Expand All @@ -973,7 +978,7 @@ private struct FileCache: Cache {
}

if let error {
WireLogger.assets.error("Failed to copy asset data from \(fromUrl) for key = \(key): \(error)")
WireLogger.assets.error("Failed to copy asset data from \(fromUrl) for key = \(key): \(error)")
}
}

Expand Down Expand Up @@ -1012,8 +1017,13 @@ private struct FileCache: Cache {
fileprivate func URLForKey(_ key: String) -> URL {
guard key != ".", key != ".." else { fatal("Can't use \(key) as cache key") }
var safeKey = key
for c in ":\\/%\"" { // see https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
safeKey = safeKey.replacingOccurrences(of: "\(c)", with: "_")

/// see https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
/// however we allow the slash character `/` to be able to have directories in the key
let reservedCharacters = ":\\%\""

for character in reservedCharacters {
safeKey = safeKey.replacingOccurrences(of: "\(character)", with: "_")
}
return cacheFolderURL.appendingPathComponent(safeKey)
}
Expand Down
Loading