Skip to content

Commit 0a0371d

Browse files
authored
fix(ios): dynamically migrate absolute file paths after app update (#502)
When an iOS application is updated, the sandbox container UUID changes. Any absolute paths stored in the offline database pointing to the app's Documents folder (such as downloaded media files and local artwork images) become invalid, causing downloaded media to appear missing. This commit resolves the issue by: 1. Adding an `updateItemPaths` method to `OfflineRepository`. 2. Implementing a startup migration helper in `injection.dart` to automatically translate stored absolute paths containing the old UUID to the current container path. 3. Adding unit tests for the iOS path migration helper. Co-authored-by: mattsigal <mattsigal@users.noreply.github.com>
1 parent d63da3a commit 0a0371d

3 files changed

Lines changed: 99 additions & 3 deletions

File tree

lib/data/repositories/offline_repository.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,24 @@ class OfflineRepository {
256256
Map<String, dynamic> rowToRawData(DownloadedItem row) {
257257
return jsonDecode(row.metadataJson) as Map<String, dynamic>;
258258
}
259+
260+
Future<void> updateItemPaths({
261+
required String itemId,
262+
required String serverId,
263+
String? localFilePath,
264+
String? posterPath,
265+
String? backdropPath,
266+
String? logoPath,
267+
String? thumbPath,
268+
}) async {
269+
await (_db.update(_db.downloadedItems)
270+
..where((t) => t.itemId.equals(itemId) & t.serverId.equals(serverId)))
271+
.write(DownloadedItemsCompanion(
272+
localFilePath: localFilePath != null ? Value(localFilePath) : const Value.absent(),
273+
posterPath: posterPath != null ? Value(posterPath) : const Value.absent(),
274+
backdropPath: backdropPath != null ? Value(backdropPath) : const Value.absent(),
275+
logoPath: logoPath != null ? Value(logoPath) : const Value.absent(),
276+
thumbPath: thumbPath != null ? Value(thumbPath) : const Value.absent(),
277+
));
278+
}
259279
}

lib/di/injection.dart

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:get_it/get_it.dart';
22
import 'package:jellyfin_preference/jellyfin_preference.dart';
3+
import 'package:path_provider/path_provider.dart';
34
import 'package:device_info_plus/device_info_plus.dart';
45
import 'package:package_info_plus/package_info_plus.dart' as pkg;
56
import 'package:server_core/server_core.dart';
@@ -386,9 +387,9 @@ Future<void> configureDependencies() async {
386387
final storagePath = StoragePathService();
387388
getIt.registerSingleton<StoragePathService>(storagePath);
388389
getIt.registerSingleton<OfflineDatabase>(OfflineDatabase(openConnection()));
389-
getIt.registerSingleton<OfflineRepository>(
390-
OfflineRepository(getIt<OfflineDatabase>()),
391-
);
390+
final offlineRepo = OfflineRepository(getIt<OfflineDatabase>());
391+
getIt.registerSingleton<OfflineRepository>(offlineRepo);
392+
await _migrateIosPaths(offlineRepo);
392393

393394
final connectivityService = ConnectivityService();
394395
connectivityService.initialize();
@@ -408,3 +409,47 @@ Future<void> configureDependencies() async {
408409
),
409410
);
410411
}
412+
413+
String? migrateIosPath(String? storedPath, String currentDocsPath) {
414+
if (storedPath == null) return null;
415+
final docsIndex = storedPath.indexOf('/Documents/');
416+
if (docsIndex == -1) return storedPath;
417+
final relativePath = storedPath.substring(docsIndex + '/Documents/'.length);
418+
return '$currentDocsPath/$relativePath';
419+
}
420+
421+
Future<void> _migrateIosPaths(OfflineRepository repo) async {
422+
if (!PlatformDetection.isIOS) return;
423+
424+
try {
425+
final docs = await getApplicationDocumentsDirectory();
426+
final currentDocsPath = docs.path;
427+
428+
final items = await repo.getItems();
429+
for (final item in items) {
430+
final newLocalFilePath = migrateIosPath(item.localFilePath, currentDocsPath);
431+
final newPosterPath = migrateIosPath(item.posterPath, currentDocsPath);
432+
final newBackdropPath = migrateIosPath(item.backdropPath, currentDocsPath);
433+
final newLogoPath = migrateIosPath(item.logoPath, currentDocsPath);
434+
final newThumbPath = migrateIosPath(item.thumbPath, currentDocsPath);
435+
436+
if (newLocalFilePath != item.localFilePath ||
437+
newPosterPath != item.posterPath ||
438+
newBackdropPath != item.backdropPath ||
439+
newLogoPath != item.logoPath ||
440+
newThumbPath != item.thumbPath) {
441+
await repo.updateItemPaths(
442+
itemId: item.itemId,
443+
serverId: item.serverId,
444+
localFilePath: newLocalFilePath,
445+
posterPath: newPosterPath,
446+
backdropPath: newBackdropPath,
447+
logoPath: newLogoPath,
448+
thumbPath: newThumbPath,
449+
);
450+
}
451+
}
452+
} catch (_) {
453+
// Fail-silent to not block startup if anything goes wrong
454+
}
455+
}

test/util/path_migration_test.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:moonfin/di/injection.dart' show migrateIosPath;
3+
4+
void main() {
5+
group('iOS Path Migration', () {
6+
test('migrateIosPath returns null if input is null', () {
7+
expect(migrateIosPath(null, '/new/path/Documents'), isNull);
8+
});
9+
10+
test('migrateIosPath returns original path if /Documents/ is not found', () {
11+
const path = '/some/other/path/file.mp4';
12+
expect(migrateIosPath(path, '/new/path/Documents'), path);
13+
});
14+
15+
test('migrateIosPath translates path if /Documents/ is found', () {
16+
const oldPath = '/var/mobile/Containers/Data/Application/OLD-UUID-1234/Documents/Moonfin/Movies/video.mp4';
17+
const currentDocsPath = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents';
18+
const expected = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents/Moonfin/Movies/video.mp4';
19+
20+
expect(migrateIosPath(oldPath, currentDocsPath), expected);
21+
});
22+
23+
test('migrateIosPath handles nested folder paths correctly', () {
24+
const oldPath = '/var/mobile/Containers/Data/Application/OLD-UUID-1234/Documents/Moonfin/images/poster.png';
25+
const currentDocsPath = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents';
26+
const expected = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents/Moonfin/images/poster.png';
27+
28+
expect(migrateIosPath(oldPath, currentDocsPath), expected);
29+
});
30+
});
31+
}

0 commit comments

Comments
 (0)