mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 03:42:13 +00:00
fixes multiple race condition issues
This commit is contained in:
parent
919aec464e
commit
e8d8e8b160
5 changed files with 128 additions and 90 deletions
|
|
@ -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/model/protobuf/client/generated/messages.pbserver.dart';
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.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/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.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 {
|
Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
||||||
final mediaService = MediaFileService(media);
|
final mediaService = MediaFileService(media);
|
||||||
|
|
@ -175,7 +177,7 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isBlocked = await protectDownload.protect<bool>(() async {
|
final isBlocked = await _protectDownload.protect<bool>(() async {
|
||||||
final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
|
final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
|
||||||
|
|
||||||
if (msg == null || msg.downloadState != DownloadState.pending) {
|
if (msg == null || msg.downloadState != DownloadState.pending) {
|
||||||
|
|
@ -285,66 +287,86 @@ Future<void> requestMediaReupload(String mediaId) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleEncryptedFile(String mediaId) async {
|
Future<void> handleEncryptedFile(String mediaId) async {
|
||||||
final mediaService = await MediaFileService.fromMediaId(mediaId);
|
await exclusiveAccess(
|
||||||
if (mediaService == null) {
|
lockName: 'decryption-$mediaId',
|
||||||
Log.error('Media file not found in database.');
|
mutex: _protectDecryption,
|
||||||
return;
|
action: () async {
|
||||||
}
|
final mediaService = await MediaFileService.fromMediaId(mediaId);
|
||||||
|
if (mediaService == null) {
|
||||||
|
Log.error('Media file not found in database.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
if (mediaService.mediaFile.downloadState == DownloadState.ready) {
|
||||||
mediaId,
|
Log.info('Decryption of $mediaId already finished.');
|
||||||
const MediaFilesCompanion(
|
return;
|
||||||
downloadState: Value(DownloadState.downloaded),
|
}
|
||||||
),
|
|
||||||
|
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 {
|
Future<void> makeMigrationToVersion91() async {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/main.dart';
|
import 'package:twonly/main.dart';
|
||||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||||
import 'package:twonly/src/services/api/mediafiles/upload.api.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/keyvalue.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ Future<T> exclusiveAccess<T>({
|
||||||
required Future<T> Function() action,
|
required Future<T> Function() action,
|
||||||
required Mutex mutex,
|
required Mutex mutex,
|
||||||
}) async {
|
}) async {
|
||||||
final lockFile = File('${AppEnvironment.supportDir}/$lockName.lock');
|
final lockFile = File('${AppEnvironment.cacheDir}/$lockName.lock');
|
||||||
return mutex.protect(() async {
|
return mutex.protect(() async {
|
||||||
var lockAcquired = false;
|
var lockAcquired = false;
|
||||||
|
|
||||||
|
|
@ -24,8 +24,8 @@ Future<T> exclusiveAccess<T>({
|
||||||
try {
|
try {
|
||||||
final stat = lockFile.statSync();
|
final stat = lockFile.statSync();
|
||||||
if (stat.type != FileSystemEntityType.notFound) {
|
if (stat.type != FileSystemEntityType.notFound) {
|
||||||
final age = DateTime.now().difference(stat.modified).inMilliseconds;
|
final age = DateTime.now().difference(stat.modified).inSeconds;
|
||||||
if (age > 1000) {
|
if (age > 10) {
|
||||||
lockFile.deleteSync();
|
lockFile.deleteSync();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,27 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
import 'package:twonly/src/utils/exclusive_access.utils.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
class KeyValueStore {
|
class KeyValueStore {
|
||||||
|
static final Mutex _mutex = Mutex();
|
||||||
|
|
||||||
static Future<File> _getFilePath(String key) async {
|
static Future<File> _getFilePath(String key) async {
|
||||||
return File('${AppEnvironment.supportDir}/keyvalue/$key.json');
|
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 {
|
try {
|
||||||
final file = await _getFilePath(key);
|
final file = await _getFilePath(key);
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
|
|
@ -17,30 +30,33 @@ class KeyValueStore {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Error deleting file: $e');
|
Log.error('Error deleting file: $e');
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
static Future<Map<String, dynamic>?> get(String key) async {
|
static Future<Map<String, dynamic>?> get(String key) =>
|
||||||
try {
|
_exclusive(key, () async {
|
||||||
final file = await _getFilePath(key);
|
final file = await _getFilePath(key);
|
||||||
if (file.existsSync()) {
|
try {
|
||||||
final contents = await file.readAsString();
|
if (file.existsSync()) {
|
||||||
return jsonDecode(contents) as Map<String, dynamic>;
|
final contents = await file.readAsString();
|
||||||
} else {
|
return jsonDecode(contents) as Map<String, dynamic>;
|
||||||
return null;
|
} else {
|
||||||
}
|
return null;
|
||||||
} catch (e) {
|
}
|
||||||
Log.warn('Error reading file: $e');
|
} catch (e) {
|
||||||
return null;
|
Log.warn('Error reading file. Deleting it.: $e');
|
||||||
}
|
file.deleteSync();
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
static Future<void> put(String key, Map<String, dynamic> value) async {
|
static Future<void> put(String key, Map<String, dynamic> value) =>
|
||||||
try {
|
_exclusive(key, () async {
|
||||||
final file = await _getFilePath(key);
|
try {
|
||||||
await file.parent.create(recursive: true);
|
final file = await _getFilePath(key);
|
||||||
await file.writeAsString(jsonEncode(value));
|
await file.parent.create(recursive: true);
|
||||||
} catch (e) {
|
await file.writeAsString(jsonEncode(value));
|
||||||
Log.error('Error writing file: $e');
|
} catch (e) {
|
||||||
}
|
Log.error('Error writing file: $e');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||||
import 'package:twonly/globals.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 {
|
class Log {
|
||||||
static bool _isInitialized = false;
|
static bool _isInitialized = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue