mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 07:48:40 +00:00
fixing issues with media_download
This commit is contained in:
parent
0207aaf074
commit
f5cbcf154b
12 changed files with 332 additions and 447 deletions
|
|
@ -45,6 +45,8 @@ void main() async {
|
|||
apiService = ApiService();
|
||||
twonlyDB = TwonlyDB();
|
||||
|
||||
await initFileDownloader();
|
||||
|
||||
// await twonlyDB.messagesDao.resetPendingDownloadState();
|
||||
// await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days();
|
||||
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
|
||||
|
|
@ -56,8 +58,6 @@ void main() async {
|
|||
|
||||
// unawaited(performTwonlySafeBackup());
|
||||
|
||||
await initFileDownloader();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
|
|
|
|||
|
|
@ -51,4 +51,10 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllMediaFilesPendingDownload() async {
|
||||
return (select(mediaFiles)
|
||||
..where((t) => t.downloadState.equals(DownloadState.pending.name)))
|
||||
.get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,7 +177,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
if (msg.mediaId != null) {
|
||||
await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!)))
|
||||
.go();
|
||||
await removeMediaFile(msg.mediaId!);
|
||||
|
||||
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
|
||||
if (mediaService != null) {
|
||||
mediaService.fullMediaRemoval();
|
||||
}
|
||||
}
|
||||
await (delete(messageHistories)
|
||||
..where((t) => t.messageId.equals(messageId)))
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ enum UploadState {
|
|||
receiverNotified,
|
||||
}
|
||||
|
||||
enum DownloadState { pending, downloading, reuploadRequested }
|
||||
enum DownloadState {
|
||||
pending,
|
||||
downloading,
|
||||
downloaded,
|
||||
ready,
|
||||
reuploadRequested
|
||||
}
|
||||
|
||||
@DataClassName('MediaFile')
|
||||
class MediaFiles extends Table {
|
||||
|
|
@ -31,8 +37,7 @@ class MediaFiles extends Table {
|
|||
BoolColumn get reopenByContact =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
|
||||
BoolColumn get storedByContact =>
|
||||
boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get stored => boolean().withDefault(const Constant(false))();
|
||||
|
||||
TextColumn get reuploadRequestedBy =>
|
||||
text().map(IntListTypeConverter()).nullable()();
|
||||
|
|
|
|||
|
|
@ -1473,15 +1473,14 @@ class $MediaFilesTable extends MediaFiles
|
|||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("reopen_by_contact" IN (0, 1))'),
|
||||
defaultValue: const Constant(false));
|
||||
static const VerificationMeta _storedByContactMeta =
|
||||
const VerificationMeta('storedByContact');
|
||||
static const VerificationMeta _storedMeta = const VerificationMeta('stored');
|
||||
@override
|
||||
late final GeneratedColumn<bool> storedByContact = GeneratedColumn<bool>(
|
||||
'stored_by_contact', aliasedName, false,
|
||||
late final GeneratedColumn<bool> stored = GeneratedColumn<bool>(
|
||||
'stored', aliasedName, false,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("stored_by_contact" IN (0, 1))'),
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'),
|
||||
defaultValue: const Constant(false));
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<List<int>?, String>
|
||||
|
|
@ -1536,7 +1535,7 @@ class $MediaFilesTable extends MediaFiles
|
|||
downloadState,
|
||||
requiresAuthentication,
|
||||
reopenByContact,
|
||||
storedByContact,
|
||||
stored,
|
||||
reuploadRequestedBy,
|
||||
displayLimitInMilliseconds,
|
||||
downloadToken,
|
||||
|
|
@ -1573,11 +1572,9 @@ class $MediaFilesTable extends MediaFiles
|
|||
reopenByContact.isAcceptableOrUnknown(
|
||||
data['reopen_by_contact']!, _reopenByContactMeta));
|
||||
}
|
||||
if (data.containsKey('stored_by_contact')) {
|
||||
context.handle(
|
||||
_storedByContactMeta,
|
||||
storedByContact.isAcceptableOrUnknown(
|
||||
data['stored_by_contact']!, _storedByContactMeta));
|
||||
if (data.containsKey('stored')) {
|
||||
context.handle(_storedMeta,
|
||||
stored.isAcceptableOrUnknown(data['stored']!, _storedMeta));
|
||||
}
|
||||
if (data.containsKey('display_limit_in_milliseconds')) {
|
||||
context.handle(
|
||||
|
|
@ -1638,8 +1635,8 @@ class $MediaFilesTable extends MediaFiles
|
|||
data['${effectivePrefix}requires_authentication'])!,
|
||||
reopenByContact: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!,
|
||||
storedByContact: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool, data['${effectivePrefix}stored_by_contact'])!,
|
||||
stored: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.bool, data['${effectivePrefix}stored'])!,
|
||||
reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn
|
||||
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||
data['${effectivePrefix}reupload_requested_by'])),
|
||||
|
|
@ -1690,7 +1687,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
final DownloadState? downloadState;
|
||||
final bool requiresAuthentication;
|
||||
final bool reopenByContact;
|
||||
final bool storedByContact;
|
||||
final bool stored;
|
||||
final List<int>? reuploadRequestedBy;
|
||||
final int? displayLimitInMilliseconds;
|
||||
final Uint8List? downloadToken;
|
||||
|
|
@ -1705,7 +1702,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
this.downloadState,
|
||||
required this.requiresAuthentication,
|
||||
required this.reopenByContact,
|
||||
required this.storedByContact,
|
||||
required this.stored,
|
||||
this.reuploadRequestedBy,
|
||||
this.displayLimitInMilliseconds,
|
||||
this.downloadToken,
|
||||
|
|
@ -1731,7 +1728,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
}
|
||||
map['requires_authentication'] = Variable<bool>(requiresAuthentication);
|
||||
map['reopen_by_contact'] = Variable<bool>(reopenByContact);
|
||||
map['stored_by_contact'] = Variable<bool>(storedByContact);
|
||||
map['stored'] = Variable<bool>(stored);
|
||||
if (!nullToAbsent || reuploadRequestedBy != null) {
|
||||
map['reupload_requested_by'] = Variable<String>($MediaFilesTable
|
||||
.$converterreuploadRequestedByn
|
||||
|
|
@ -1769,7 +1766,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
: Value(downloadState),
|
||||
requiresAuthentication: Value(requiresAuthentication),
|
||||
reopenByContact: Value(reopenByContact),
|
||||
storedByContact: Value(storedByContact),
|
||||
stored: Value(stored),
|
||||
reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(reuploadRequestedBy),
|
||||
|
|
@ -1807,7 +1804,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
requiresAuthentication:
|
||||
serializer.fromJson<bool>(json['requiresAuthentication']),
|
||||
reopenByContact: serializer.fromJson<bool>(json['reopenByContact']),
|
||||
storedByContact: serializer.fromJson<bool>(json['storedByContact']),
|
||||
stored: serializer.fromJson<bool>(json['stored']),
|
||||
reuploadRequestedBy:
|
||||
serializer.fromJson<List<int>?>(json['reuploadRequestedBy']),
|
||||
displayLimitInMilliseconds:
|
||||
|
|
@ -1832,7 +1829,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
$MediaFilesTable.$converterdownloadStaten.toJson(downloadState)),
|
||||
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
|
||||
'reopenByContact': serializer.toJson<bool>(reopenByContact),
|
||||
'storedByContact': serializer.toJson<bool>(storedByContact),
|
||||
'stored': serializer.toJson<bool>(stored),
|
||||
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
|
||||
'displayLimitInMilliseconds':
|
||||
serializer.toJson<int?>(displayLimitInMilliseconds),
|
||||
|
|
@ -1851,7 +1848,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
Value<DownloadState?> downloadState = const Value.absent(),
|
||||
bool? requiresAuthentication,
|
||||
bool? reopenByContact,
|
||||
bool? storedByContact,
|
||||
bool? stored,
|
||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
|
|
@ -1868,7 +1865,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
requiresAuthentication:
|
||||
requiresAuthentication ?? this.requiresAuthentication,
|
||||
reopenByContact: reopenByContact ?? this.reopenByContact,
|
||||
storedByContact: storedByContact ?? this.storedByContact,
|
||||
stored: stored ?? this.stored,
|
||||
reuploadRequestedBy: reuploadRequestedBy.present
|
||||
? reuploadRequestedBy.value
|
||||
: this.reuploadRequestedBy,
|
||||
|
|
@ -1901,9 +1898,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
reopenByContact: data.reopenByContact.present
|
||||
? data.reopenByContact.value
|
||||
: this.reopenByContact,
|
||||
storedByContact: data.storedByContact.present
|
||||
? data.storedByContact.value
|
||||
: this.storedByContact,
|
||||
stored: data.stored.present ? data.stored.value : this.stored,
|
||||
reuploadRequestedBy: data.reuploadRequestedBy.present
|
||||
? data.reuploadRequestedBy.value
|
||||
: this.reuploadRequestedBy,
|
||||
|
|
@ -1935,7 +1930,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
..write('downloadState: $downloadState, ')
|
||||
..write('requiresAuthentication: $requiresAuthentication, ')
|
||||
..write('reopenByContact: $reopenByContact, ')
|
||||
..write('storedByContact: $storedByContact, ')
|
||||
..write('stored: $stored, ')
|
||||
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
||||
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
||||
..write('downloadToken: $downloadToken, ')
|
||||
|
|
@ -1955,7 +1950,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
downloadState,
|
||||
requiresAuthentication,
|
||||
reopenByContact,
|
||||
storedByContact,
|
||||
stored,
|
||||
reuploadRequestedBy,
|
||||
displayLimitInMilliseconds,
|
||||
$driftBlobEquality.hash(downloadToken),
|
||||
|
|
@ -1973,7 +1968,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
|
|||
other.downloadState == this.downloadState &&
|
||||
other.requiresAuthentication == this.requiresAuthentication &&
|
||||
other.reopenByContact == this.reopenByContact &&
|
||||
other.storedByContact == this.storedByContact &&
|
||||
other.stored == this.stored &&
|
||||
other.reuploadRequestedBy == this.reuploadRequestedBy &&
|
||||
other.displayLimitInMilliseconds == this.displayLimitInMilliseconds &&
|
||||
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
|
||||
|
|
@ -1991,7 +1986,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
final Value<DownloadState?> downloadState;
|
||||
final Value<bool> requiresAuthentication;
|
||||
final Value<bool> reopenByContact;
|
||||
final Value<bool> storedByContact;
|
||||
final Value<bool> stored;
|
||||
final Value<List<int>?> reuploadRequestedBy;
|
||||
final Value<int?> displayLimitInMilliseconds;
|
||||
final Value<Uint8List?> downloadToken;
|
||||
|
|
@ -2007,7 +2002,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
this.downloadState = const Value.absent(),
|
||||
this.requiresAuthentication = const Value.absent(),
|
||||
this.reopenByContact = const Value.absent(),
|
||||
this.storedByContact = const Value.absent(),
|
||||
this.stored = const Value.absent(),
|
||||
this.reuploadRequestedBy = const Value.absent(),
|
||||
this.displayLimitInMilliseconds = const Value.absent(),
|
||||
this.downloadToken = const Value.absent(),
|
||||
|
|
@ -2024,7 +2019,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
this.downloadState = const Value.absent(),
|
||||
required bool requiresAuthentication,
|
||||
this.reopenByContact = const Value.absent(),
|
||||
this.storedByContact = const Value.absent(),
|
||||
this.stored = const Value.absent(),
|
||||
this.reuploadRequestedBy = const Value.absent(),
|
||||
this.displayLimitInMilliseconds = const Value.absent(),
|
||||
this.downloadToken = const Value.absent(),
|
||||
|
|
@ -2042,7 +2037,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
Expression<String>? downloadState,
|
||||
Expression<bool>? requiresAuthentication,
|
||||
Expression<bool>? reopenByContact,
|
||||
Expression<bool>? storedByContact,
|
||||
Expression<bool>? stored,
|
||||
Expression<String>? reuploadRequestedBy,
|
||||
Expression<int>? displayLimitInMilliseconds,
|
||||
Expression<Uint8List>? downloadToken,
|
||||
|
|
@ -2060,7 +2055,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
if (requiresAuthentication != null)
|
||||
'requires_authentication': requiresAuthentication,
|
||||
if (reopenByContact != null) 'reopen_by_contact': reopenByContact,
|
||||
if (storedByContact != null) 'stored_by_contact': storedByContact,
|
||||
if (stored != null) 'stored': stored,
|
||||
if (reuploadRequestedBy != null)
|
||||
'reupload_requested_by': reuploadRequestedBy,
|
||||
if (displayLimitInMilliseconds != null)
|
||||
|
|
@ -2081,7 +2076,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
Value<DownloadState?>? downloadState,
|
||||
Value<bool>? requiresAuthentication,
|
||||
Value<bool>? reopenByContact,
|
||||
Value<bool>? storedByContact,
|
||||
Value<bool>? stored,
|
||||
Value<List<int>?>? reuploadRequestedBy,
|
||||
Value<int?>? displayLimitInMilliseconds,
|
||||
Value<Uint8List?>? downloadToken,
|
||||
|
|
@ -2098,7 +2093,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
requiresAuthentication:
|
||||
requiresAuthentication ?? this.requiresAuthentication,
|
||||
reopenByContact: reopenByContact ?? this.reopenByContact,
|
||||
storedByContact: storedByContact ?? this.storedByContact,
|
||||
stored: stored ?? this.stored,
|
||||
reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy,
|
||||
displayLimitInMilliseconds:
|
||||
displayLimitInMilliseconds ?? this.displayLimitInMilliseconds,
|
||||
|
|
@ -2136,8 +2131,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
if (reopenByContact.present) {
|
||||
map['reopen_by_contact'] = Variable<bool>(reopenByContact.value);
|
||||
}
|
||||
if (storedByContact.present) {
|
||||
map['stored_by_contact'] = Variable<bool>(storedByContact.value);
|
||||
if (stored.present) {
|
||||
map['stored'] = Variable<bool>(stored.value);
|
||||
}
|
||||
if (reuploadRequestedBy.present) {
|
||||
map['reupload_requested_by'] = Variable<String>($MediaFilesTable
|
||||
|
|
@ -2178,7 +2173,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
|
|||
..write('downloadState: $downloadState, ')
|
||||
..write('requiresAuthentication: $requiresAuthentication, ')
|
||||
..write('reopenByContact: $reopenByContact, ')
|
||||
..write('storedByContact: $storedByContact, ')
|
||||
..write('stored: $stored, ')
|
||||
..write('reuploadRequestedBy: $reuploadRequestedBy, ')
|
||||
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
|
||||
..write('downloadToken: $downloadToken, ')
|
||||
|
|
@ -7107,7 +7102,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
|
|||
Value<DownloadState?> downloadState,
|
||||
required bool requiresAuthentication,
|
||||
Value<bool> reopenByContact,
|
||||
Value<bool> storedByContact,
|
||||
Value<bool> stored,
|
||||
Value<List<int>?> reuploadRequestedBy,
|
||||
Value<int?> displayLimitInMilliseconds,
|
||||
Value<Uint8List?> downloadToken,
|
||||
|
|
@ -7124,7 +7119,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
|
|||
Value<DownloadState?> downloadState,
|
||||
Value<bool> requiresAuthentication,
|
||||
Value<bool> reopenByContact,
|
||||
Value<bool> storedByContact,
|
||||
Value<bool> stored,
|
||||
Value<List<int>?> reuploadRequestedBy,
|
||||
Value<int?> displayLimitInMilliseconds,
|
||||
Value<Uint8List?> downloadToken,
|
||||
|
|
@ -7190,9 +7185,8 @@ class $$MediaFilesTableFilterComposer
|
|||
column: $table.reopenByContact,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<bool> get storedByContact => $composableBuilder(
|
||||
column: $table.storedByContact,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
ColumnFilters<bool> get stored => $composableBuilder(
|
||||
column: $table.stored, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<List<int>?, List<int>, String>
|
||||
get reuploadRequestedBy => $composableBuilder(
|
||||
|
|
@ -7271,9 +7265,8 @@ class $$MediaFilesTableOrderingComposer
|
|||
column: $table.reopenByContact,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<bool> get storedByContact => $composableBuilder(
|
||||
column: $table.storedByContact,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
ColumnOrderings<bool> get stored => $composableBuilder(
|
||||
column: $table.stored, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get reuploadRequestedBy => $composableBuilder(
|
||||
column: $table.reuploadRequestedBy,
|
||||
|
|
@ -7332,8 +7325,8 @@ class $$MediaFilesTableAnnotationComposer
|
|||
GeneratedColumn<bool> get reopenByContact => $composableBuilder(
|
||||
column: $table.reopenByContact, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<bool> get storedByContact => $composableBuilder(
|
||||
column: $table.storedByContact, builder: (column) => column);
|
||||
GeneratedColumn<bool> get stored =>
|
||||
$composableBuilder(column: $table.stored, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<List<int>?, String>
|
||||
get reuploadRequestedBy => $composableBuilder(
|
||||
|
|
@ -7408,7 +7401,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
Value<DownloadState?> downloadState = const Value.absent(),
|
||||
Value<bool> requiresAuthentication = const Value.absent(),
|
||||
Value<bool> reopenByContact = const Value.absent(),
|
||||
Value<bool> storedByContact = const Value.absent(),
|
||||
Value<bool> stored = const Value.absent(),
|
||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
|
|
@ -7425,7 +7418,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
downloadState: downloadState,
|
||||
requiresAuthentication: requiresAuthentication,
|
||||
reopenByContact: reopenByContact,
|
||||
storedByContact: storedByContact,
|
||||
stored: stored,
|
||||
reuploadRequestedBy: reuploadRequestedBy,
|
||||
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
||||
downloadToken: downloadToken,
|
||||
|
|
@ -7442,7 +7435,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
Value<DownloadState?> downloadState = const Value.absent(),
|
||||
required bool requiresAuthentication,
|
||||
Value<bool> reopenByContact = const Value.absent(),
|
||||
Value<bool> storedByContact = const Value.absent(),
|
||||
Value<bool> stored = const Value.absent(),
|
||||
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
|
||||
Value<int?> displayLimitInMilliseconds = const Value.absent(),
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
|
|
@ -7459,7 +7452,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
|
|||
downloadState: downloadState,
|
||||
requiresAuthentication: requiresAuthentication,
|
||||
reopenByContact: reopenByContact,
|
||||
storedByContact: storedByContact,
|
||||
stored: stored,
|
||||
reuploadRequestedBy: reuploadRequestedBy,
|
||||
displayLimitInMilliseconds: displayLimitInMilliseconds,
|
||||
downloadToken: downloadToken,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
|
|
@ -10,24 +8,23 @@ import 'package:drift/drift.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/media_upload.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart';
|
||||
|
||||
Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
|
||||
// This is called when WebSocket is newly connected, so allow all downloads to be restarted.
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getAllMessagesPendingDownloading();
|
||||
final mediaFiles =
|
||||
await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload();
|
||||
|
||||
for (final message in messages) {
|
||||
await startDownloadMedia(message, force);
|
||||
for (final mediaFile in mediaFiles) {
|
||||
await startDownloadMedia(mediaFile, force);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +41,7 @@ Map<String, List<String>> defaultAutoDownloadOptions = {
|
|||
],
|
||||
};
|
||||
|
||||
Future<bool> isAllowedToDownload(bool isVideo) async {
|
||||
Future<bool> isAllowedToDownload({required bool isVideo}) async {
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
|
||||
final user = await getUser();
|
||||
|
|
@ -76,7 +73,7 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
|
|||
}
|
||||
|
||||
Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
||||
final messageId = int.parse(update.task.taskId.replaceAll('download_', ''));
|
||||
final mediaId = update.task.taskId.replaceAll('download_', '');
|
||||
var failed = false;
|
||||
|
||||
if (update.status == TaskStatus.failed ||
|
||||
|
|
@ -93,83 +90,45 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
|
|||
);
|
||||
}
|
||||
} else {
|
||||
Log.info('Got ${update.status} for $messageId');
|
||||
Log.info('Got ${update.status} for $mediaId');
|
||||
return;
|
||||
}
|
||||
await handleDownloadStatusUpdateInternal(messageId, failed);
|
||||
}
|
||||
|
||||
Future<void> handleDownloadStatusUpdateInternal(
|
||||
int messageId,
|
||||
bool failed,
|
||||
) async {
|
||||
if (failed) {
|
||||
Log.error('Download failed for $messageId');
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(messageId)
|
||||
.getSingleOrNull();
|
||||
if (message != null && message.downloadState != DownloadState.downloaded) {
|
||||
await handleMediaError(message);
|
||||
}
|
||||
await requestMediaReupload(mediaId);
|
||||
} else {
|
||||
Log.info('Download was successfully for $messageId');
|
||||
await handleEncryptedFile(messageId);
|
||||
await handleEncryptedFile(mediaId);
|
||||
}
|
||||
}
|
||||
|
||||
Mutex protectDownload = Mutex();
|
||||
|
||||
Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
||||
Log.info(
|
||||
'Download blocked for ${message.messageId} because of network state.',
|
||||
);
|
||||
if (message.contentJson == null) {
|
||||
Log.error('Content of ${message.messageId} not found.');
|
||||
await handleMediaError(message);
|
||||
final mediaService = await MediaFileService.fromMedia(media);
|
||||
|
||||
if (mediaService.encryptedPath.existsSync()) {
|
||||
await handleEncryptedFile(media.mediaId);
|
||||
return;
|
||||
}
|
||||
|
||||
final content = MessageContent.fromJson(
|
||||
message.kind,
|
||||
jsonDecode(message.contentJson!) as Map,
|
||||
);
|
||||
|
||||
if (content is! MediaMessageContent) {
|
||||
Log.error('Content of ${message.messageId} is not media file.');
|
||||
await handleMediaError(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.downloadToken == null) {
|
||||
Log.error('Download token not defined for ${message.messageId}.');
|
||||
await handleMediaError(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && !await isAllowedToDownload(content.isVideo)) {
|
||||
if (!force &&
|
||||
!await isAllowedToDownload(isVideo: media.type == MediaType.video)) {
|
||||
Log.warn(
|
||||
'Download blocked for ${message.messageId} because of network state.',
|
||||
'Download blocked for ${media.mediaId} because of network state.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final isBlocked = await protectDownload.protect<bool>(() async {
|
||||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(message.messageId)
|
||||
.getSingleOrNull();
|
||||
final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
|
||||
|
||||
if (msg == null) return true;
|
||||
|
||||
if (msg.downloadState != DownloadState.pending) {
|
||||
Log.error(
|
||||
'${message.messageId} is already downloaded or is downloading.',
|
||||
);
|
||||
if (msg == null || msg.downloadState != DownloadState.pending) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
message.messageId,
|
||||
const MessagesCompanion(
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
msg.mediaId,
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.downloading),
|
||||
),
|
||||
);
|
||||
|
|
@ -178,11 +137,16 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
|||
});
|
||||
|
||||
if (isBlocked) {
|
||||
Log.info('Download for ${message.messageId} already started.');
|
||||
Log.info('Download for ${media.mediaId} already started.');
|
||||
return;
|
||||
}
|
||||
|
||||
final downloadToken = uint8ListToHex(content.downloadToken!);
|
||||
if (media.downloadToken == null) {
|
||||
Log.info('Download token for ${media.mediaId} not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
final downloadToken = uint8ListToHex(media.downloadToken!);
|
||||
|
||||
final apiUrl =
|
||||
'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken';
|
||||
|
|
@ -190,20 +154,20 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
|||
try {
|
||||
final task = DownloadTask(
|
||||
url: apiUrl,
|
||||
taskId: 'download_${message.messageId}',
|
||||
directory: 'media/received/',
|
||||
baseDirectory: BaseDirectory.applicationSupport,
|
||||
filename: '${message.messageId}.encrypted',
|
||||
taskId: 'download_${media.mediaId}',
|
||||
directory: mediaService.encryptedPath.parent.path,
|
||||
baseDirectory: BaseDirectory.root,
|
||||
filename: basename(mediaService.encryptedPath.path),
|
||||
priority: 0,
|
||||
retries: 10,
|
||||
);
|
||||
|
||||
Log.info(
|
||||
'Got media file. Starting download: ${downloadToken.substring(0, 10)}',
|
||||
'Downloading ${media.mediaId} to ${mediaService.encryptedPath}',
|
||||
);
|
||||
|
||||
try {
|
||||
await downloadFileFast(message.messageId, apiUrl);
|
||||
await downloadFileFast(media, apiUrl, mediaService.encryptedPath);
|
||||
} catch (e) {
|
||||
Log.error('Fast download failed: $e');
|
||||
await FileDownloader().enqueue(task);
|
||||
|
|
@ -214,269 +178,114 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
|
|||
}
|
||||
|
||||
Future<void> downloadFileFast(
|
||||
int messageId,
|
||||
MediaFile media,
|
||||
String apiUrl,
|
||||
File filePath,
|
||||
) async {
|
||||
final directoryPath =
|
||||
'${(await getApplicationSupportDirectory()).path}/media/received/';
|
||||
final filename = '$messageId.encrypted';
|
||||
|
||||
final directory = Directory(directoryPath);
|
||||
if (!directory.existsSync()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
|
||||
final filePath = '${directory.path}/$filename';
|
||||
|
||||
final response =
|
||||
await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
await File(filePath).writeAsBytes(response.bodyBytes);
|
||||
await filePath.writeAsBytes(response.bodyBytes);
|
||||
Log.info('Fast Download successful: $filePath');
|
||||
await handleDownloadStatusUpdateInternal(messageId, false);
|
||||
await handleEncryptedFile(media.mediaId);
|
||||
return;
|
||||
} else {
|
||||
if (response.statusCode == 404 || response.statusCode == 403) {
|
||||
await handleDownloadStatusUpdateInternal(messageId, true);
|
||||
if (response.statusCode == 404 ||
|
||||
response.statusCode == 403 ||
|
||||
response.statusCode == 400) {
|
||||
// Message was deleted from the server. Requesting it again from the sender to upload it again...
|
||||
await requestMediaReupload(media.mediaId);
|
||||
return;
|
||||
}
|
||||
// can be tried again
|
||||
// Will be tried again using the slow method...
|
||||
throw Exception('Fast download failed with status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleEncryptedFile(int messageId) async {
|
||||
final msg = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(messageId)
|
||||
.getSingleOrNull();
|
||||
if (msg == null) {
|
||||
Log.error('Not message for downloaded file found: $messageId');
|
||||
Future<void> requestMediaReupload(String mediaId) async {
|
||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
|
||||
if (messages.length != 1 || messages.first.senderId == null) {
|
||||
Log.error(
|
||||
'Media file has none or more than one sender. That is not possible');
|
||||
return;
|
||||
}
|
||||
|
||||
final encryptedBytes = await readMediaFile(msg.messageId, 'encrypted');
|
||||
await sendCipherText(
|
||||
messages.first.senderId!,
|
||||
EncryptedContent(
|
||||
mediaUpdate: EncryptedContent_MediaUpdate(
|
||||
type: EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR,
|
||||
targetMessageId: mediaId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (encryptedBytes == null) {
|
||||
Log.error('encrypted bytes are not found for ${msg.messageId}');
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.reuploadRequested),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleEncryptedFile(String mediaId) async {
|
||||
final mediaService = await MediaFileService.fromMediaId(mediaId);
|
||||
if (mediaService == null) {
|
||||
Log.error('Media file $mediaId not found in database.');
|
||||
return;
|
||||
}
|
||||
|
||||
final content =
|
||||
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map);
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.downloaded),
|
||||
),
|
||||
);
|
||||
|
||||
late Uint8List encryptedBytes;
|
||||
try {
|
||||
encryptedBytes = await mediaService.encryptedPath.readAsBytes();
|
||||
} catch (e) {
|
||||
Log.error('Could not read encrypted media file: $mediaId. $e');
|
||||
await requestMediaReupload(mediaId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
final secretKeyData = SecretKeyData(content.encryptionKey!);
|
||||
final secretKeyData = SecretKeyData(mediaService.mediaFile.encryptionKey!);
|
||||
|
||||
final secretBox = SecretBox(
|
||||
encryptedBytes,
|
||||
nonce: content.encryptionNonce!,
|
||||
mac: Mac(content.encryptionMac!),
|
||||
nonce: mediaService.mediaFile.encryptionNonce!,
|
||||
mac: Mac(mediaService.mediaFile.encryptionMac!),
|
||||
);
|
||||
|
||||
// try {
|
||||
final plaintextBytes =
|
||||
await chacha20.decrypt(secretBox, secretKey: secretKeyData);
|
||||
var imageBytes = Uint8List.fromList(plaintextBytes);
|
||||
|
||||
if (content.isVideo) {
|
||||
final extractedBytes = extractUint8Lists(imageBytes);
|
||||
imageBytes = extractedBytes[0];
|
||||
await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]);
|
||||
}
|
||||
final rawMediaBytes = Uint8List.fromList(plaintextBytes);
|
||||
|
||||
await writeMediaFile(msg.messageId, 'png', imageBytes);
|
||||
// } catch (e) {
|
||||
// Log.error(
|
||||
// "could not decrypt the media file in the second try. reporting error to user: $e");
|
||||
// handleMediaError(msg);
|
||||
// return;
|
||||
// }
|
||||
await mediaService.tempPath.writeAsBytes(rawMediaBytes);
|
||||
} catch (e) {
|
||||
Log.error('$e');
|
||||
|
||||
/// legacy support
|
||||
final chacha20 = Xchacha20.poly1305Aead();
|
||||
final secretKeyData = SecretKeyData(content.encryptionKey!);
|
||||
|
||||
final secretBox = SecretBox(
|
||||
encryptedBytes,
|
||||
nonce: content.encryptionNonce!,
|
||||
mac: Mac(content.encryptionMac!),
|
||||
Log.error(
|
||||
'Could not decrypt the media file. Requesting a new upload.',
|
||||
);
|
||||
|
||||
try {
|
||||
final plaintextBytes =
|
||||
await chacha20.decrypt(secretBox, secretKey: secretKeyData);
|
||||
var imageBytes = Uint8List.fromList(plaintextBytes);
|
||||
|
||||
if (content.isVideo) {
|
||||
final extractedBytes = extractUint8Lists(imageBytes);
|
||||
imageBytes = extractedBytes[0];
|
||||
await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]);
|
||||
}
|
||||
|
||||
await writeMediaFile(msg.messageId, 'png', imageBytes);
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
'could not decrypt the media file in the second try. reporting error to user: $e',
|
||||
);
|
||||
await handleMediaError(msg);
|
||||
return;
|
||||
}
|
||||
await requestMediaReupload(mediaId);
|
||||
return;
|
||||
}
|
||||
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
msg.messageId,
|
||||
const MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.ready),
|
||||
),
|
||||
);
|
||||
|
||||
Log.info('Download and decryption of ${msg.messageId} was successful');
|
||||
Log.info('Decryption of $mediaId was successful');
|
||||
|
||||
await deleteMediaFile(msg.messageId, 'encrypted');
|
||||
mediaService.encryptedPath.deleteSync();
|
||||
|
||||
unawaited(apiService.downloadDone(content.downloadToken!));
|
||||
unawaited(apiService.downloadDone(mediaService.mediaFile.downloadToken!));
|
||||
}
|
||||
|
||||
Future<Uint8List?> getImageBytes(int mediaId) async {
|
||||
return readMediaFile(mediaId, 'png');
|
||||
}
|
||||
|
||||
Future<File?> getVideoPath(int mediaId) async {
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
return File('$basePath.mp4');
|
||||
}
|
||||
|
||||
/// --- helper functions ---
|
||||
|
||||
Future<Uint8List?> readMediaFile(int mediaId, String type) async {
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
Log.info('Reading: $file');
|
||||
if (!file.existsSync()) {
|
||||
return null;
|
||||
}
|
||||
return file.readAsBytes();
|
||||
}
|
||||
|
||||
Future<bool> existsMediaFile(int mediaId, String type) async {
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
return file.existsSync();
|
||||
}
|
||||
|
||||
Future<void> writeMediaFile(int mediaId, String type, Uint8List data) async {
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
await file.writeAsBytes(data);
|
||||
}
|
||||
|
||||
Future<void> deleteMediaFile(int mediaId, String type) async {
|
||||
final basePath = await getMediaFilePath(mediaId, 'received');
|
||||
final file = File('$basePath.$type');
|
||||
try {
|
||||
if (file.existsSync()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Error deleting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> purgeReceivedMediaFiles() async {
|
||||
final basedir = await getApplicationSupportDirectory();
|
||||
final directory = Directory(join(basedir.path, 'media', 'received'));
|
||||
await purgeMediaFiles(directory);
|
||||
}
|
||||
|
||||
Future<void> purgeMediaFiles(Directory directory) async {
|
||||
// Check if the directory exists
|
||||
if (directory.existsSync()) {
|
||||
// List all files in the directory
|
||||
final files = directory.listSync();
|
||||
|
||||
// Iterate over each file
|
||||
for (final file in files) {
|
||||
// Get the filename
|
||||
final filename = file.uri.pathSegments.last;
|
||||
|
||||
// Use a regular expression to extract the integer part
|
||||
final match = RegExp(r'(\d+)').firstMatch(filename);
|
||||
if (match != null) {
|
||||
// Parse the integer and add it to the list
|
||||
final fileId = int.parse(match.group(0)!);
|
||||
|
||||
try {
|
||||
if (directory.path.endsWith('send')) {
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaUploadId(fileId);
|
||||
var canBeDeleted = true;
|
||||
|
||||
for (final message in messages) {
|
||||
try {
|
||||
final content = MediaMessageContent.fromJson(
|
||||
jsonDecode(message.contentJson!) as Map,
|
||||
);
|
||||
|
||||
final oneDayAgo =
|
||||
DateTime.now().subtract(const Duration(days: 1));
|
||||
final twoDaysAgo =
|
||||
DateTime.now().subtract(const Duration(days: 1));
|
||||
|
||||
if ((message.openedAt == null ||
|
||||
oneDayAgo.isBefore(message.openedAt!)) &&
|
||||
!message.errorWhileSending) {
|
||||
canBeDeleted = false;
|
||||
} else if (message.mediaStored) {
|
||||
if (!file.path.contains('.original.') &&
|
||||
!file.path.contains('.encrypted')) {
|
||||
canBeDeleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// In case the image is not yet opened but successfully uploaded
|
||||
/// to the server preserve the image for two days in case of an receiving error will happen
|
||||
/// and then delete them as well.
|
||||
if (message.acknowledgeByServer &&
|
||||
twoDaysAgo.isAfter(message.sendAt)) {
|
||||
// Preserve images which can be stored by the other person...
|
||||
if (content.maxShowTime != gMediaShowInfinite) {
|
||||
canBeDeleted = true;
|
||||
}
|
||||
// Encrypted or upload data can be removed when acknowledgeByServer
|
||||
if (file.path.contains('.upload') ||
|
||||
file.path.contains('.encrypted')) {
|
||||
canBeDeleted = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
if (canBeDeleted) {
|
||||
Log.info('purged media file ${file.path} ');
|
||||
file.deleteSync();
|
||||
}
|
||||
} else {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(fileId)
|
||||
.getSingleOrNull();
|
||||
if ((message == null) ||
|
||||
(message.openedAt != null &&
|
||||
!message.mediaStored &&
|
||||
message.acknowledgeByServer) ||
|
||||
message.errorWhileSending) {
|
||||
file.deleteSync();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// /data/user/0/eu.twonly.testing/files/media/received/27.encrypted
|
||||
// /data/user/0/eu.twonly.testing/app_flutter/data/user/0/eu.twonly.testing/files/media/received/27.encrypted
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
|
|
@ -8,7 +7,6 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
|||
import 'package:twonly/src/services/api/media_download.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
import 'package:twonly/src/services/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/thumbnail.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> handleMedia(
|
||||
|
|
@ -33,7 +31,10 @@ Future<void> handleMedia(
|
|||
}
|
||||
|
||||
// in case there was already a downloaded file delete it...
|
||||
await removeMediaFile(message.mediaId!);
|
||||
final mediaService = await MediaFileService.fromMediaId(message.mediaId!);
|
||||
if (mediaService != null) {
|
||||
mediaService.tempPath.deleteSync();
|
||||
}
|
||||
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
message.mediaId!,
|
||||
|
|
@ -81,6 +82,7 @@ Future<void> handleMedia(
|
|||
);
|
||||
|
||||
if (mediaFile == null) {
|
||||
Log.error('Could not insert media file into database');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +123,11 @@ Future<void> handleMediaUpdate(
|
|||
if (message == null || message.mediaId == null) return;
|
||||
final mediaFile =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!);
|
||||
if (mediaFile == null) return;
|
||||
if (mediaFile == null) {
|
||||
Log.info(
|
||||
'Got media file update, but media file was not found ${message.mediaId}');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mediaUpdate.type) {
|
||||
case EncryptedContent_MediaUpdate_Type.REOPENED:
|
||||
|
|
@ -137,11 +143,12 @@ Future<void> handleMediaUpdate(
|
|||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
const MediaFilesCompanion(
|
||||
storedByContact: Value(true),
|
||||
stored: Value(true),
|
||||
),
|
||||
);
|
||||
|
||||
unawaited(createThumbnailForMediaFile(mediaFile));
|
||||
final mediaService = await MediaFileService.fromMedia(mediaFile);
|
||||
unawaited(mediaService.createThumbnail());
|
||||
|
||||
case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
|
||||
Log.info('Got media file decryption error ${mediaFile.mediaId}');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,124 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/thumbnail.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
Future<void> removeMediaFile(String mediaId) async {
|
||||
Log.error('TODO removeMediaFile: $mediaId');
|
||||
class MediaFileService {
|
||||
MediaFileService(this.mediaFile, {required this.applicationSupportDirectory});
|
||||
MediaFile mediaFile;
|
||||
|
||||
final Directory applicationSupportDirectory;
|
||||
|
||||
static Future<MediaFileService> fromMedia(MediaFile media) async {
|
||||
return MediaFileService(
|
||||
media,
|
||||
applicationSupportDirectory: await getApplicationSupportDirectory(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<MediaFileService?> fromMediaId(String mediaId) async {
|
||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
||||
if (mediaFile == null) return null;
|
||||
return MediaFileService(
|
||||
mediaFile,
|
||||
applicationSupportDirectory: await getApplicationSupportDirectory(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateFromDB() async {
|
||||
final updated =
|
||||
await twonlyDB.mediaFilesDao.getMediaFileById(mediaFile.mediaId);
|
||||
if (updated != null) {
|
||||
mediaFile = updated;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createThumbnail() async {
|
||||
if (!storedPath.existsSync()) {
|
||||
Log.error('Could not create Thumbnail as stored media does not exists.');
|
||||
return;
|
||||
}
|
||||
switch (mediaFile.type) {
|
||||
case MediaType.image:
|
||||
await createThumbnailsForImage(storedPath, thumbnailPath);
|
||||
case MediaType.video:
|
||||
await createThumbnailsForVideo(storedPath, thumbnailPath);
|
||||
case MediaType.gif:
|
||||
Log.error('Thumbnail for .gif is not implemented yet');
|
||||
}
|
||||
}
|
||||
|
||||
void fullMediaRemoval() {
|
||||
if (tempPath.existsSync()) {
|
||||
tempPath.deleteSync();
|
||||
}
|
||||
if (encryptedPath.existsSync()) {
|
||||
encryptedPath.deleteSync();
|
||||
}
|
||||
if (storedPath.existsSync()) {
|
||||
storedPath.deleteSync();
|
||||
}
|
||||
if (thumbnailPath.existsSync()) {
|
||||
thumbnailPath.deleteSync();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> storeMediaFile() async {
|
||||
Log.info('Storing media file ${mediaFile.mediaId}');
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
const MediaFilesCompanion(
|
||||
stored: Value(true),
|
||||
),
|
||||
);
|
||||
await tempPath.copy(storedPath.path);
|
||||
await updateFromDB();
|
||||
}
|
||||
|
||||
File _buildFilePath(
|
||||
String directory, {
|
||||
String namePrefix = '',
|
||||
String extensionParam = '',
|
||||
}) {
|
||||
final mediaBaseDir = Directory(join(
|
||||
applicationSupportDirectory.path,
|
||||
'mediafiles',
|
||||
directory,
|
||||
));
|
||||
if (!mediaBaseDir.existsSync()) {
|
||||
mediaBaseDir.createSync(recursive: true);
|
||||
}
|
||||
var extension = extensionParam;
|
||||
if (extension == '') {
|
||||
switch (mediaFile.type) {
|
||||
case MediaType.image:
|
||||
extension = 'webp';
|
||||
case MediaType.video:
|
||||
extension = 'mp4';
|
||||
case MediaType.gif:
|
||||
extension = 'gif';
|
||||
}
|
||||
}
|
||||
return File(
|
||||
join(mediaBaseDir.path, '${mediaFile.mediaId}$namePrefix.$extension'),
|
||||
);
|
||||
}
|
||||
|
||||
File get tempPath => _buildFilePath('tmp');
|
||||
File get storedPath => _buildFilePath('stored');
|
||||
File get thumbnailPath => _buildFilePath(
|
||||
'stored',
|
||||
namePrefix: '.thumbnail',
|
||||
extensionParam: 'webp',
|
||||
);
|
||||
File get encryptedPath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.encrypted',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,13 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
|
||||
Future<void> createThumbnailForMediaFile(MediaFile media) async {
|
||||
|
||||
switch (media.type) {
|
||||
case MediaType.image:
|
||||
TODO
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Future<void> createThumbnailsForImage(File file) async {
|
||||
final fileExtension = file.path.split('.').last.toLowerCase();
|
||||
Future<void> createThumbnailsForImage(
|
||||
File sourceFile,
|
||||
File destinationFile,
|
||||
) async {
|
||||
final fileExtension = sourceFile.path.split('.').last.toLowerCase();
|
||||
if (fileExtension != 'png') {
|
||||
Log.error('Could not create thumbnail for image. $fileExtension != .png');
|
||||
return;
|
||||
|
|
@ -30,7 +17,7 @@ Future<void> createThumbnailsForImage(File file) async {
|
|||
final imageBytesCompressed = await FlutterImageCompress.compressWithFile(
|
||||
minHeight: 800,
|
||||
minWidth: 450,
|
||||
file.path,
|
||||
sourceFile.path,
|
||||
format: CompressFormat.webp,
|
||||
quality: 50,
|
||||
);
|
||||
|
|
@ -39,16 +26,17 @@ Future<void> createThumbnailsForImage(File file) async {
|
|||
Log.error('Could not compress the image');
|
||||
return;
|
||||
}
|
||||
|
||||
final thumbnailFile = getThumbnailPath(file);
|
||||
await thumbnailFile.writeAsBytes(imageBytesCompressed);
|
||||
await destinationFile.writeAsBytes(imageBytesCompressed);
|
||||
} catch (e) {
|
||||
Log.error('Could not compress the image got :$e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createThumbnailsForVideo(File file) async {
|
||||
final fileExtension = file.path.split('.').last.toLowerCase();
|
||||
Future<void> createThumbnailsForVideo(
|
||||
File sourceFile,
|
||||
File destinationFile,
|
||||
) async {
|
||||
final fileExtension = sourceFile.path.split('.').last.toLowerCase();
|
||||
if (fileExtension != 'mp4') {
|
||||
Log.error('Could not create thumbnail for video. $fileExtension != .mp4');
|
||||
return;
|
||||
|
|
@ -56,8 +44,8 @@ Future<void> createThumbnailsForVideo(File file) async {
|
|||
|
||||
try {
|
||||
await VideoThumbnail.thumbnailFile(
|
||||
video: file.path,
|
||||
thumbnailPath: getThumbnailPath(file).path,
|
||||
video: sourceFile.path,
|
||||
thumbnailPath: destinationFile.path,
|
||||
maxWidth: 450,
|
||||
quality: 75,
|
||||
);
|
||||
|
|
@ -65,15 +53,3 @@ Future<void> createThumbnailsForVideo(File file) async {
|
|||
Log.error('Could not create the video thumbnail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
File getThumbnailPath(File file) {
|
||||
final originalFileName = file.uri.pathSegments.last;
|
||||
final fileNameWithoutExtension = originalFileName.split('.').first;
|
||||
var fileExtension = originalFileName.split('.').last;
|
||||
if (fileExtension == 'mp4') {
|
||||
fileExtension = 'png';
|
||||
}
|
||||
final newFileName = '$fileNameWithoutExtension.thumbnail.$fileExtension';
|
||||
Directory(file.parent.path).createSync();
|
||||
return File(join(file.parent.path, newFileName));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
|
||||
import 'package:twonly/src/services/api/media_upload.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
|
@ -48,20 +48,19 @@ Future<void> performTwonlySafeBackup({bool force = false}) async {
|
|||
final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
|
||||
await backupDir.create(recursive: true);
|
||||
|
||||
final backupDatabaseFile =
|
||||
File(join(backupDir.path, 'twonly_database.backup.sqlite'));
|
||||
final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
|
||||
|
||||
final backupDatabaseFileCleaned =
|
||||
File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite'));
|
||||
File(join(backupDir.path, 'twonly.backup.cleaned.sqlite'));
|
||||
|
||||
// copy database
|
||||
final originalDatabase = File(join(baseDir, 'twonly_database.sqlite'));
|
||||
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
|
||||
await originalDatabase.copy(backupDatabaseFile.path);
|
||||
|
||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||
final backupDB = TwonlyDatabase(
|
||||
final backupDB = TwonlyDB(
|
||||
driftDatabase(
|
||||
name: 'twonly_database.backup',
|
||||
name: 'twonly.backup',
|
||||
native: DriftNativeOptions(
|
||||
databaseDirectory: () async {
|
||||
return backupDir;
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ import 'package:http/http.dart' as http;
|
|||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/tables/messages_table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/backup/backup.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -90,44 +88,9 @@ Future<void> handleBackupData(
|
|||
);
|
||||
|
||||
final baseDir = (await getApplicationSupportDirectory()).path;
|
||||
final originalDatabase = File(join(baseDir, 'twonly_database.sqlite'));
|
||||
final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
|
||||
await originalDatabase.writeAsBytes(backupContent.twonlyDatabase);
|
||||
|
||||
/// When restoring the last message ID must be increased otherwise
|
||||
/// receivers would mark them as duplicates as they where already
|
||||
/// send.
|
||||
final database = TwonlyDatabase();
|
||||
var lastMessageSend = 0;
|
||||
int? randomUserId;
|
||||
|
||||
final contacts = await database.contactsDao.getAllNotBlockedContacts();
|
||||
for (final contact in contacts) {
|
||||
randomUserId = contact.userId;
|
||||
final days = DateTime.now().difference(contact.lastMessageExchange).inDays;
|
||||
if (days < lastMessageSend) {
|
||||
lastMessageSend = days;
|
||||
}
|
||||
}
|
||||
|
||||
if (randomUserId != null) {
|
||||
// for each day add 400 message ids
|
||||
final dummyMessagesCounter = (lastMessageSend + 1) * 400;
|
||||
Log.info(
|
||||
'Creating $dummyMessagesCounter dummy messages to increase message counter as last message was $lastMessageSend days ago.',
|
||||
);
|
||||
for (var i = 0; i < dummyMessagesCounter; i++) {
|
||||
await database.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
contactId: Value(randomUserId),
|
||||
kind: const Value(MessageKind.ack),
|
||||
acknowledgeByServer: const Value(true),
|
||||
errorWhileSending: const Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
await database.messagesDao.deleteAllMessagesByContactId(randomUserId);
|
||||
}
|
||||
|
||||
const storage = FlutterSecureStorage();
|
||||
|
||||
final secureStorage = jsonDecode(backupContent.secureStorageJson);
|
||||
|
|
|
|||
|
|
@ -340,6 +340,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
Future<Uint8List?> getMergedImage() async {
|
||||
Uint8List? image;
|
||||
|
||||
|
||||
TODO: When changed then create a new mediaID!!!!!!
|
||||
As storedMediaId would overwrite it....
|
||||
|
||||
if (layers.length > 1 || widget.videoFilePath != null) {
|
||||
for (final x in layers) {
|
||||
x.showCustomButtons = false;
|
||||
|
|
|
|||
Loading…
Reference in a new issue