feat(ios): use UUID-based filenames for shared images (Phase 1B)
Store shared images as <shareId>.<ext> in the App Group container while keeping the original filename in metadata, preventing on-disk collisions without changing retrieval, deletion, or JS consumer behavior.
This commit is contained in:
@@ -10,12 +10,12 @@ The iOS share target uses a **Share Extension** (`TimeSafariShareExtension`) tha
|
||||
|
||||
| Key | Storage | Written by | Purpose |
|
||||
|-----|---------|------------|---------|
|
||||
| `sharedPhotoFilePath` | UserDefaults (suite) | Share Extension | Relative filename of image file in container |
|
||||
| `sharedPhotoFileName` | UserDefaults (suite) | Share Extension | Display/original filename |
|
||||
| `sharedPhotoFilePath` | UserDefaults (suite) | Share Extension | On-disk filename in container (`<shareId>.<ext>`) |
|
||||
| `sharedPhotoFileName` | UserDefaults (suite) | Share Extension | Original display filename from source app |
|
||||
| `sharedPhotoShareId` | UserDefaults (suite) | Share Extension | Unique UUID per incoming share (Phase 1A) |
|
||||
| `sharedPhotoReady` | UserDefaults (suite) | Share Extension | Boolean signal that a new share is available |
|
||||
| `sharedPhotoBase64` | UserDefaults (suite) | *(legacy, not written)* | Removed on write for cleanup |
|
||||
| Image file | App Group filesystem | Share Extension | Raw image bytes at `{container}/{sharedPhotoFilePath}` |
|
||||
| Image file | App Group filesystem | Share Extension | Raw image bytes at `{container}/{sharedPhotoFilePath}` (`<shareId>.<ext>`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -324,6 +324,51 @@ Example log sequence for a single share:
|
||||
|
||||
---
|
||||
|
||||
## Unique Stored Filenames
|
||||
|
||||
**Implemented:** 2026-06-23 (Phase 1B)
|
||||
|
||||
Phase 1B eliminates on-disk filename collisions by storing each shared image under a UUID-based filename while preserving the original filename as metadata for consumers.
|
||||
|
||||
### On-Disk vs Metadata
|
||||
|
||||
| Field | UserDefaults key | Example | Purpose |
|
||||
|-------|------------------|---------|---------|
|
||||
| Stored filename | `sharedPhotoFilePath` | `A1B2C3D4-E5F6-7890-ABCD-EF1234567890.jpg` | Unique file in App Group container |
|
||||
| Original filename | `sharedPhotoFileName` | `vacation-photo.jpg` | Returned to JS as `fileName` |
|
||||
| Share ID | `sharedPhotoShareId` | `A1B2C3D4-E5F6-7890-ABCD-EF1234567890` | Correlates logs across extension and main app |
|
||||
|
||||
Stored filename format: `<shareId>.<extension>`, where extension is taken from the original filename (defaults to `jpg` when absent).
|
||||
|
||||
### Implementation
|
||||
|
||||
| File | Method | Change |
|
||||
|------|--------|--------|
|
||||
| `ShareViewController.swift` | `fileExtension(from:)` | Extracts extension from original filename |
|
||||
| `ShareViewController.swift` | `storedFileName(shareId:originalFileName:)` | Builds `<shareId>.<ext>` |
|
||||
| `ShareViewController.swift` | `storeImageData` | Writes to stored filename; saves original in `sharedPhotoFileName` |
|
||||
| `SharedImageUtility.swift` | `getSharedImageData` | Reads file via `sharedPhotoFilePath`; returns original `sharedPhotoFileName` |
|
||||
|
||||
When a new share arrives before the previous one is retrieved, `storeImageData` removes the file at the previous `sharedPhotoFilePath` before writing, preserving single-pending-share semantics.
|
||||
|
||||
### Logging (Phase 1B)
|
||||
|
||||
Store and retrieve events include all three identifiers:
|
||||
|
||||
```
|
||||
[ShareTarget] file stored shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
|
||||
[ShareTarget] metadata stored shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
|
||||
[ShareTarget] share retrieved shareId=<id> originalFilename=<name> storedFilename=<shareId>.<ext>
|
||||
```
|
||||
|
||||
### Phase 1B Scope (Intentionally Unchanged)
|
||||
|
||||
- `getSharedImageData()` still returns only `base64` and original `fileName` to JavaScript
|
||||
- Retrieval timing, deletion keys, and `hasSharedImage()` behavior are unchanged
|
||||
- Android code is unchanged
|
||||
|
||||
---
|
||||
|
||||
## Configuration References
|
||||
|
||||
| Resource | Value |
|
||||
|
||||
@@ -48,9 +48,7 @@ public class SharedImageUtility {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let shareId = shareId {
|
||||
print("[ShareTarget] share retrieved shareId=\(shareId)")
|
||||
}
|
||||
print("[ShareTarget] share retrieved shareId=\(shareId ?? "unknown") originalFilename=\(fileName) storedFilename=\(filePath)")
|
||||
|
||||
// Convert file data to base64 for JavaScript consumption
|
||||
let base64String = imageData.base64EncodedString()
|
||||
|
||||
@@ -150,6 +150,17 @@ class ShareViewController: UIViewController {
|
||||
return "shared-image.\(newExtension)"
|
||||
}
|
||||
|
||||
/// Extract file extension from original filename, defaulting to jpg when absent
|
||||
private func fileExtension(from fileName: String) -> String {
|
||||
let ext = (fileName as NSString).pathExtension
|
||||
return ext.isEmpty ? "jpg" : ext.lowercased()
|
||||
}
|
||||
|
||||
/// Build unique on-disk filename: <shareId>.<extension>
|
||||
private func storedFileName(shareId: String, originalFileName: String) -> String {
|
||||
return "\(shareId).\(fileExtension(from: originalFileName))"
|
||||
}
|
||||
|
||||
/// Store image data as a file in the App Group container
|
||||
/// All images are stored as files regardless of size for consistency and simplicity
|
||||
/// Returns true if successful, false otherwise
|
||||
@@ -158,13 +169,18 @@ class ShareViewController: UIViewController {
|
||||
return false
|
||||
}
|
||||
|
||||
// Create file URL in the container using the actual filename
|
||||
// Extract extension from fileName if present, otherwise use sharedImageFileName
|
||||
let actualFileName = fileName.isEmpty ? sharedImageFileName : fileName
|
||||
let fileURL = containerURL.appendingPathComponent(actualFileName)
|
||||
let originalFileName = fileName.isEmpty ? "\(sharedImageFileName).jpg" : fileName
|
||||
let storedFileName = storedFileName(shareId: shareId, originalFileName: originalFileName)
|
||||
let fileURL = containerURL.appendingPathComponent(storedFileName)
|
||||
|
||||
// Remove old file if it exists
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
// Remove previously pending share file (metadata tracks one share at a time)
|
||||
if let userDefaults = UserDefaults(suiteName: appGroupIdentifier),
|
||||
let previousPath = userDefaults.string(forKey: sharedPhotoFilePathKey) {
|
||||
let previousURL = containerURL.appendingPathComponent(previousPath)
|
||||
if previousURL != fileURL {
|
||||
try? FileManager.default.removeItem(at: previousURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Write image data to file
|
||||
do {
|
||||
@@ -172,23 +188,23 @@ class ShareViewController: UIViewController {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
print("[ShareTarget] file stored shareId=\(shareId)")
|
||||
print("[ShareTarget] file stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)")
|
||||
|
||||
// Store file path and filename in UserDefaults (small data, safe to store)
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Store relative path, filename, and share identifier
|
||||
userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
|
||||
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
|
||||
// sharedPhotoFilePath = on-disk name; sharedPhotoFileName = original display name
|
||||
userDefaults.set(storedFileName, forKey: sharedPhotoFilePathKey)
|
||||
userDefaults.set(originalFileName, forKey: sharedPhotoFileNameKey)
|
||||
userDefaults.set(shareId, forKey: sharedPhotoShareIdKey)
|
||||
|
||||
// Clean up any old base64 data that might exist
|
||||
userDefaults.removeObject(forKey: "sharedPhotoBase64")
|
||||
|
||||
userDefaults.synchronize()
|
||||
print("[ShareTarget] metadata stored shareId=\(shareId)")
|
||||
print("[ShareTarget] metadata stored shareId=\(shareId) originalFilename=\(originalFileName) storedFilename=\(storedFileName)")
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user