fixes multiple race condition issues

This commit is contained in:
otsmr 2026-04-24 23:14:48 +02:00
parent 919aec464e
commit e8d8e8b160
5 changed files with 128 additions and 90 deletions

View file

@ -15,6 +15,7 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
@ -158,7 +159,8 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
}
}
Mutex protectDownload = Mutex();
Mutex _protectDownload = Mutex();
Mutex _protectDecryption = Mutex();
Future<void> startDownloadMedia(MediaFile media, bool force) async {
final mediaService = MediaFileService(media);
@ -175,7 +177,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
return;
}
final isBlocked = await protectDownload.protect<bool>(() async {
final isBlocked = await _protectDownload.protect<bool>(() async {
final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
if (msg == null || msg.downloadState != DownloadState.pending) {
@ -285,66 +287,86 @@ Future<void> requestMediaReupload(String mediaId) async {
}
Future<void> handleEncryptedFile(String mediaId) async {
final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) {
Log.error('Media file not found in database.');
return;
}
await exclusiveAccess(
lockName: 'decryption-$mediaId',
mutex: _protectDecryption,
action: () async {
final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) {
Log.error('Media file not found in database.');
return;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.downloaded),
),
if (mediaService.mediaFile.downloadState == DownloadState.ready) {
Log.info('Decryption of $mediaId already finished.');
return;
}
if (!mediaService.encryptedPath.existsSync()) {
Log.warn(
'Encrypted media file $mediaId does not exist anymore. Decryption probably already finished.',
);
return;
}
late Uint8List encryptedBytes;
try {
encryptedBytes = await mediaService.encryptedPath.readAsBytes();
} catch (e) {
Log.error('Could not read encrypted media file: $e');
await requestMediaReupload(mediaId);
return;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.downloaded),
),
);
try {
final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData(
mediaService.mediaFile.encryptionKey!,
);
final secretBox = SecretBox(
encryptedBytes,
nonce: mediaService.mediaFile.encryptionNonce!,
mac: Mac(mediaService.mediaFile.encryptionMac!),
);
final plaintextBytes = await chacha20.decrypt(
secretBox,
secretKey: secretKeyData,
);
final rawMediaBytes = Uint8List.fromList(plaintextBytes);
await mediaService.tempPath.writeAsBytes(rawMediaBytes);
} catch (e) {
Log.error(
'Could not decrypt the media file. Requesting a new upload.',
);
await requestMediaReupload(mediaId);
return;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.ready),
),
);
Log.info('Decryption of $mediaId was successful');
mediaService.encryptedPath.deleteSync();
unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!));
},
);
late Uint8List encryptedBytes;
try {
encryptedBytes = await mediaService.encryptedPath.readAsBytes();
} catch (e) {
Log.error('Could not read encrypted media file: $e');
await requestMediaReupload(mediaId);
return;
}
try {
final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData(mediaService.mediaFile.encryptionKey!);
final secretBox = SecretBox(
encryptedBytes,
nonce: mediaService.mediaFile.encryptionNonce!,
mac: Mac(mediaService.mediaFile.encryptionMac!),
);
final plaintextBytes = await chacha20.decrypt(
secretBox,
secretKey: secretKeyData,
);
final rawMediaBytes = Uint8List.fromList(plaintextBytes);
await mediaService.tempPath.writeAsBytes(rawMediaBytes);
} catch (e) {
Log.error(
'Could not decrypt the media file. Requesting a new upload.',
);
await requestMediaReupload(mediaId);
return;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
downloadState: Value(DownloadState.ready),
),
);
Log.info('Decryption of $mediaId was successful');
mediaService.encryptedPath.deleteSync();
unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!));
}
Future<void> makeMigrationToVersion91() async {

View file

@ -6,7 +6,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/main.dart';
import 'package:twonly/src/constants/keyvalue.keys.dart';
import 'package:twonly/src/services/api/mediafiles/upload.api.dart';
import 'package:twonly/src/utils/exclusive_access.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:workmanager/workmanager.dart';

View file

@ -8,7 +8,7 @@ Future<T> exclusiveAccess<T>({
required Future<T> Function() action,
required Mutex mutex,
}) async {
final lockFile = File('${AppEnvironment.supportDir}/$lockName.lock');
final lockFile = File('${AppEnvironment.cacheDir}/$lockName.lock');
return mutex.protect(() async {
var lockAcquired = false;
@ -24,8 +24,8 @@ Future<T> exclusiveAccess<T>({
try {
final stat = lockFile.statSync();
if (stat.type != FileSystemEntityType.notFound) {
final age = DateTime.now().difference(stat.modified).inMilliseconds;
if (age > 1000) {
final age = DateTime.now().difference(stat.modified).inSeconds;
if (age > 10) {
lockFile.deleteSync();
continue;
}

View file

@ -1,14 +1,27 @@
import 'dart:convert';
import 'dart:io';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
import 'package:twonly/src/utils/log.dart';
class KeyValueStore {
static final Mutex _mutex = Mutex();
static Future<File> _getFilePath(String key) async {
return File('${AppEnvironment.supportDir}/keyvalue/$key.json');
}
static Future<void> delete(String key) async {
static Future<T> _exclusive<T>(String key, Future<T> Function() action) {
return exclusiveAccess(
lockName: 'keyvalue-$key',
mutex: _mutex,
action: action,
);
}
static Future<void> delete(String key) => _exclusive(key, () async {
try {
final file = await _getFilePath(key);
if (file.existsSync()) {
@ -17,30 +30,33 @@ class KeyValueStore {
} catch (e) {
Log.error('Error deleting file: $e');
}
}
});
static Future<Map<String, dynamic>?> get(String key) async {
try {
final file = await _getFilePath(key);
if (file.existsSync()) {
final contents = await file.readAsString();
return jsonDecode(contents) as Map<String, dynamic>;
} else {
return null;
}
} catch (e) {
Log.warn('Error reading file: $e');
return null;
}
}
static Future<Map<String, dynamic>?> get(String key) =>
_exclusive(key, () async {
final file = await _getFilePath(key);
try {
if (file.existsSync()) {
final contents = await file.readAsString();
return jsonDecode(contents) as Map<String, dynamic>;
} else {
return null;
}
} catch (e) {
Log.warn('Error reading file. Deleting it.: $e');
file.deleteSync();
return null;
}
});
static Future<void> put(String key, Map<String, dynamic> value) async {
try {
final file = await _getFilePath(key);
await file.parent.create(recursive: true);
await file.writeAsString(jsonEncode(value));
} catch (e) {
Log.error('Error writing file: $e');
}
}
static Future<void> put(String key, Map<String, dynamic> value) =>
_exclusive(key, () async {
try {
final file = await _getFilePath(key);
await file.parent.create(recursive: true);
await file.writeAsString(jsonEncode(value));
} catch (e) {
Log.error('Error writing file: $e');
}
});
}

View file

@ -6,7 +6,7 @@ import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/exclusive_access.dart';
import 'package:twonly/src/utils/exclusive_access.utils.dart';
class Log {
static bool _isInitialized = false;