fixing issues with media_download

This commit is contained in:
otsmr 2025-10-22 00:01:55 +02:00
parent 0207aaf074
commit f5cbcf154b
12 changed files with 332 additions and 447 deletions

View file

@ -45,6 +45,8 @@ void main() async {
apiService = ApiService(); apiService = ApiService();
twonlyDB = TwonlyDB(); twonlyDB = TwonlyDB();
await initFileDownloader();
// await twonlyDB.messagesDao.resetPendingDownloadState(); // await twonlyDB.messagesDao.resetPendingDownloadState();
// await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days(); // await twonlyDB.messagesDao.handleMediaFilesOlderThan30Days();
// await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions(); // await twonlyDB.messageRetransmissionDao.purgeOldRetransmissions();
@ -56,8 +58,6 @@ void main() async {
// unawaited(performTwonlySafeBackup()); // unawaited(performTwonlySafeBackup());
await initFileDownloader();
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [

View file

@ -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();
}
} }

View file

@ -177,7 +177,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
if (msg.mediaId != null) { if (msg.mediaId != null) {
await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!))) await (delete(mediaFiles)..where((t) => t.mediaId.equals(msg.mediaId!)))
.go(); .go();
await removeMediaFile(msg.mediaId!);
final mediaService = await MediaFileService.fromMediaId(msg.mediaId!);
if (mediaService != null) {
mediaService.fullMediaRemoval();
}
} }
await (delete(messageHistories) await (delete(messageHistories)
..where((t) => t.messageId.equals(messageId))) ..where((t) => t.messageId.equals(messageId)))

View file

@ -16,7 +16,13 @@ enum UploadState {
receiverNotified, receiverNotified,
} }
enum DownloadState { pending, downloading, reuploadRequested } enum DownloadState {
pending,
downloading,
downloaded,
ready,
reuploadRequested
}
@DataClassName('MediaFile') @DataClassName('MediaFile')
class MediaFiles extends Table { class MediaFiles extends Table {
@ -31,8 +37,7 @@ class MediaFiles extends Table {
BoolColumn get reopenByContact => BoolColumn get reopenByContact =>
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();
BoolColumn get storedByContact => BoolColumn get stored => boolean().withDefault(const Constant(false))();
boolean().withDefault(const Constant(false))();
TextColumn get reuploadRequestedBy => TextColumn get reuploadRequestedBy =>
text().map(IntListTypeConverter()).nullable()(); text().map(IntListTypeConverter()).nullable()();

View file

@ -1473,15 +1473,14 @@ class $MediaFilesTable extends MediaFiles
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("reopen_by_contact" IN (0, 1))'), 'CHECK ("reopen_by_contact" IN (0, 1))'),
defaultValue: const Constant(false)); defaultValue: const Constant(false));
static const VerificationMeta _storedByContactMeta = static const VerificationMeta _storedMeta = const VerificationMeta('stored');
const VerificationMeta('storedByContact');
@override @override
late final GeneratedColumn<bool> storedByContact = GeneratedColumn<bool>( late final GeneratedColumn<bool> stored = GeneratedColumn<bool>(
'stored_by_contact', aliasedName, false, 'stored', aliasedName, false,
type: DriftSqlType.bool, type: DriftSqlType.bool,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints:
'CHECK ("stored_by_contact" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'),
defaultValue: const Constant(false)); defaultValue: const Constant(false));
@override @override
late final GeneratedColumnWithTypeConverter<List<int>?, String> late final GeneratedColumnWithTypeConverter<List<int>?, String>
@ -1536,7 +1535,7 @@ class $MediaFilesTable extends MediaFiles
downloadState, downloadState,
requiresAuthentication, requiresAuthentication,
reopenByContact, reopenByContact,
storedByContact, stored,
reuploadRequestedBy, reuploadRequestedBy,
displayLimitInMilliseconds, displayLimitInMilliseconds,
downloadToken, downloadToken,
@ -1573,11 +1572,9 @@ class $MediaFilesTable extends MediaFiles
reopenByContact.isAcceptableOrUnknown( reopenByContact.isAcceptableOrUnknown(
data['reopen_by_contact']!, _reopenByContactMeta)); data['reopen_by_contact']!, _reopenByContactMeta));
} }
if (data.containsKey('stored_by_contact')) { if (data.containsKey('stored')) {
context.handle( context.handle(_storedMeta,
_storedByContactMeta, stored.isAcceptableOrUnknown(data['stored']!, _storedMeta));
storedByContact.isAcceptableOrUnknown(
data['stored_by_contact']!, _storedByContactMeta));
} }
if (data.containsKey('display_limit_in_milliseconds')) { if (data.containsKey('display_limit_in_milliseconds')) {
context.handle( context.handle(
@ -1638,8 +1635,8 @@ class $MediaFilesTable extends MediaFiles
data['${effectivePrefix}requires_authentication'])!, data['${effectivePrefix}requires_authentication'])!,
reopenByContact: attachedDatabase.typeMapping.read( reopenByContact: attachedDatabase.typeMapping.read(
DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!,
storedByContact: attachedDatabase.typeMapping.read( stored: attachedDatabase.typeMapping
DriftSqlType.bool, data['${effectivePrefix}stored_by_contact'])!, .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!,
reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
data['${effectivePrefix}reupload_requested_by'])), data['${effectivePrefix}reupload_requested_by'])),
@ -1690,7 +1687,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
final DownloadState? downloadState; final DownloadState? downloadState;
final bool requiresAuthentication; final bool requiresAuthentication;
final bool reopenByContact; final bool reopenByContact;
final bool storedByContact; final bool stored;
final List<int>? reuploadRequestedBy; final List<int>? reuploadRequestedBy;
final int? displayLimitInMilliseconds; final int? displayLimitInMilliseconds;
final Uint8List? downloadToken; final Uint8List? downloadToken;
@ -1705,7 +1702,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
this.downloadState, this.downloadState,
required this.requiresAuthentication, required this.requiresAuthentication,
required this.reopenByContact, required this.reopenByContact,
required this.storedByContact, required this.stored,
this.reuploadRequestedBy, this.reuploadRequestedBy,
this.displayLimitInMilliseconds, this.displayLimitInMilliseconds,
this.downloadToken, this.downloadToken,
@ -1731,7 +1728,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
} }
map['requires_authentication'] = Variable<bool>(requiresAuthentication); map['requires_authentication'] = Variable<bool>(requiresAuthentication);
map['reopen_by_contact'] = Variable<bool>(reopenByContact); map['reopen_by_contact'] = Variable<bool>(reopenByContact);
map['stored_by_contact'] = Variable<bool>(storedByContact); map['stored'] = Variable<bool>(stored);
if (!nullToAbsent || reuploadRequestedBy != null) { if (!nullToAbsent || reuploadRequestedBy != null) {
map['reupload_requested_by'] = Variable<String>($MediaFilesTable map['reupload_requested_by'] = Variable<String>($MediaFilesTable
.$converterreuploadRequestedByn .$converterreuploadRequestedByn
@ -1769,7 +1766,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
: Value(downloadState), : Value(downloadState),
requiresAuthentication: Value(requiresAuthentication), requiresAuthentication: Value(requiresAuthentication),
reopenByContact: Value(reopenByContact), reopenByContact: Value(reopenByContact),
storedByContact: Value(storedByContact), stored: Value(stored),
reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(reuploadRequestedBy), : Value(reuploadRequestedBy),
@ -1807,7 +1804,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
requiresAuthentication: requiresAuthentication:
serializer.fromJson<bool>(json['requiresAuthentication']), serializer.fromJson<bool>(json['requiresAuthentication']),
reopenByContact: serializer.fromJson<bool>(json['reopenByContact']), reopenByContact: serializer.fromJson<bool>(json['reopenByContact']),
storedByContact: serializer.fromJson<bool>(json['storedByContact']), stored: serializer.fromJson<bool>(json['stored']),
reuploadRequestedBy: reuploadRequestedBy:
serializer.fromJson<List<int>?>(json['reuploadRequestedBy']), serializer.fromJson<List<int>?>(json['reuploadRequestedBy']),
displayLimitInMilliseconds: displayLimitInMilliseconds:
@ -1832,7 +1829,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
$MediaFilesTable.$converterdownloadStaten.toJson(downloadState)), $MediaFilesTable.$converterdownloadStaten.toJson(downloadState)),
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication), 'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
'reopenByContact': serializer.toJson<bool>(reopenByContact), 'reopenByContact': serializer.toJson<bool>(reopenByContact),
'storedByContact': serializer.toJson<bool>(storedByContact), 'stored': serializer.toJson<bool>(stored),
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy), 'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
'displayLimitInMilliseconds': 'displayLimitInMilliseconds':
serializer.toJson<int?>(displayLimitInMilliseconds), serializer.toJson<int?>(displayLimitInMilliseconds),
@ -1851,7 +1848,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
Value<DownloadState?> downloadState = const Value.absent(), Value<DownloadState?> downloadState = const Value.absent(),
bool? requiresAuthentication, bool? requiresAuthentication,
bool? reopenByContact, bool? reopenByContact,
bool? storedByContact, bool? stored,
Value<List<int>?> reuploadRequestedBy = const Value.absent(), Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(), Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(),
@ -1868,7 +1865,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
requiresAuthentication: requiresAuthentication:
requiresAuthentication ?? this.requiresAuthentication, requiresAuthentication ?? this.requiresAuthentication,
reopenByContact: reopenByContact ?? this.reopenByContact, reopenByContact: reopenByContact ?? this.reopenByContact,
storedByContact: storedByContact ?? this.storedByContact, stored: stored ?? this.stored,
reuploadRequestedBy: reuploadRequestedBy.present reuploadRequestedBy: reuploadRequestedBy.present
? reuploadRequestedBy.value ? reuploadRequestedBy.value
: this.reuploadRequestedBy, : this.reuploadRequestedBy,
@ -1901,9 +1898,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
reopenByContact: data.reopenByContact.present reopenByContact: data.reopenByContact.present
? data.reopenByContact.value ? data.reopenByContact.value
: this.reopenByContact, : this.reopenByContact,
storedByContact: data.storedByContact.present stored: data.stored.present ? data.stored.value : this.stored,
? data.storedByContact.value
: this.storedByContact,
reuploadRequestedBy: data.reuploadRequestedBy.present reuploadRequestedBy: data.reuploadRequestedBy.present
? data.reuploadRequestedBy.value ? data.reuploadRequestedBy.value
: this.reuploadRequestedBy, : this.reuploadRequestedBy,
@ -1935,7 +1930,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
..write('downloadState: $downloadState, ') ..write('downloadState: $downloadState, ')
..write('requiresAuthentication: $requiresAuthentication, ') ..write('requiresAuthentication: $requiresAuthentication, ')
..write('reopenByContact: $reopenByContact, ') ..write('reopenByContact: $reopenByContact, ')
..write('storedByContact: $storedByContact, ') ..write('stored: $stored, ')
..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ')
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
..write('downloadToken: $downloadToken, ') ..write('downloadToken: $downloadToken, ')
@ -1955,7 +1950,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
downloadState, downloadState,
requiresAuthentication, requiresAuthentication,
reopenByContact, reopenByContact,
storedByContact, stored,
reuploadRequestedBy, reuploadRequestedBy,
displayLimitInMilliseconds, displayLimitInMilliseconds,
$driftBlobEquality.hash(downloadToken), $driftBlobEquality.hash(downloadToken),
@ -1973,7 +1968,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
other.downloadState == this.downloadState && other.downloadState == this.downloadState &&
other.requiresAuthentication == this.requiresAuthentication && other.requiresAuthentication == this.requiresAuthentication &&
other.reopenByContact == this.reopenByContact && other.reopenByContact == this.reopenByContact &&
other.storedByContact == this.storedByContact && other.stored == this.stored &&
other.reuploadRequestedBy == this.reuploadRequestedBy && other.reuploadRequestedBy == this.reuploadRequestedBy &&
other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds &&
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
@ -1991,7 +1986,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
final Value<DownloadState?> downloadState; final Value<DownloadState?> downloadState;
final Value<bool> requiresAuthentication; final Value<bool> requiresAuthentication;
final Value<bool> reopenByContact; final Value<bool> reopenByContact;
final Value<bool> storedByContact; final Value<bool> stored;
final Value<List<int>?> reuploadRequestedBy; final Value<List<int>?> reuploadRequestedBy;
final Value<int?> displayLimitInMilliseconds; final Value<int?> displayLimitInMilliseconds;
final Value<Uint8List?> downloadToken; final Value<Uint8List?> downloadToken;
@ -2007,7 +2002,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.downloadState = const Value.absent(), this.downloadState = const Value.absent(),
this.requiresAuthentication = const Value.absent(), this.requiresAuthentication = const Value.absent(),
this.reopenByContact = const Value.absent(), this.reopenByContact = const Value.absent(),
this.storedByContact = const Value.absent(), this.stored = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(), this.reuploadRequestedBy = const Value.absent(),
this.displayLimitInMilliseconds = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(),
this.downloadToken = const Value.absent(), this.downloadToken = const Value.absent(),
@ -2024,7 +2019,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.downloadState = const Value.absent(), this.downloadState = const Value.absent(),
required bool requiresAuthentication, required bool requiresAuthentication,
this.reopenByContact = const Value.absent(), this.reopenByContact = const Value.absent(),
this.storedByContact = const Value.absent(), this.stored = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(), this.reuploadRequestedBy = const Value.absent(),
this.displayLimitInMilliseconds = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(),
this.downloadToken = const Value.absent(), this.downloadToken = const Value.absent(),
@ -2042,7 +2037,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Expression<String>? downloadState, Expression<String>? downloadState,
Expression<bool>? requiresAuthentication, Expression<bool>? requiresAuthentication,
Expression<bool>? reopenByContact, Expression<bool>? reopenByContact,
Expression<bool>? storedByContact, Expression<bool>? stored,
Expression<String>? reuploadRequestedBy, Expression<String>? reuploadRequestedBy,
Expression<int>? displayLimitInMilliseconds, Expression<int>? displayLimitInMilliseconds,
Expression<Uint8List>? downloadToken, Expression<Uint8List>? downloadToken,
@ -2060,7 +2055,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (requiresAuthentication != null) if (requiresAuthentication != null)
'requires_authentication': requiresAuthentication, 'requires_authentication': requiresAuthentication,
if (reopenByContact != null) 'reopen_by_contact': reopenByContact, if (reopenByContact != null) 'reopen_by_contact': reopenByContact,
if (storedByContact != null) 'stored_by_contact': storedByContact, if (stored != null) 'stored': stored,
if (reuploadRequestedBy != null) if (reuploadRequestedBy != null)
'reupload_requested_by': reuploadRequestedBy, 'reupload_requested_by': reuploadRequestedBy,
if (displayLimitInMilliseconds != null) if (displayLimitInMilliseconds != null)
@ -2081,7 +2076,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Value<DownloadState?>? downloadState, Value<DownloadState?>? downloadState,
Value<bool>? requiresAuthentication, Value<bool>? requiresAuthentication,
Value<bool>? reopenByContact, Value<bool>? reopenByContact,
Value<bool>? storedByContact, Value<bool>? stored,
Value<List<int>?>? reuploadRequestedBy, Value<List<int>?>? reuploadRequestedBy,
Value<int?>? displayLimitInMilliseconds, Value<int?>? displayLimitInMilliseconds,
Value<Uint8List?>? downloadToken, Value<Uint8List?>? downloadToken,
@ -2098,7 +2093,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
requiresAuthentication: requiresAuthentication:
requiresAuthentication ?? this.requiresAuthentication, requiresAuthentication ?? this.requiresAuthentication,
reopenByContact: reopenByContact ?? this.reopenByContact, reopenByContact: reopenByContact ?? this.reopenByContact,
storedByContact: storedByContact ?? this.storedByContact, stored: stored ?? this.stored,
reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy,
displayLimitInMilliseconds: displayLimitInMilliseconds:
displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, displayLimitInMilliseconds ?? this.displayLimitInMilliseconds,
@ -2136,8 +2131,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (reopenByContact.present) { if (reopenByContact.present) {
map['reopen_by_contact'] = Variable<bool>(reopenByContact.value); map['reopen_by_contact'] = Variable<bool>(reopenByContact.value);
} }
if (storedByContact.present) { if (stored.present) {
map['stored_by_contact'] = Variable<bool>(storedByContact.value); map['stored'] = Variable<bool>(stored.value);
} }
if (reuploadRequestedBy.present) { if (reuploadRequestedBy.present) {
map['reupload_requested_by'] = Variable<String>($MediaFilesTable map['reupload_requested_by'] = Variable<String>($MediaFilesTable
@ -2178,7 +2173,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..write('downloadState: $downloadState, ') ..write('downloadState: $downloadState, ')
..write('requiresAuthentication: $requiresAuthentication, ') ..write('requiresAuthentication: $requiresAuthentication, ')
..write('reopenByContact: $reopenByContact, ') ..write('reopenByContact: $reopenByContact, ')
..write('storedByContact: $storedByContact, ') ..write('stored: $stored, ')
..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ')
..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ')
..write('downloadToken: $downloadToken, ') ..write('downloadToken: $downloadToken, ')
@ -7107,7 +7102,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
Value<DownloadState?> downloadState, Value<DownloadState?> downloadState,
required bool requiresAuthentication, required bool requiresAuthentication,
Value<bool> reopenByContact, Value<bool> reopenByContact,
Value<bool> storedByContact, Value<bool> stored,
Value<List<int>?> reuploadRequestedBy, Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds, Value<int?> displayLimitInMilliseconds,
Value<Uint8List?> downloadToken, Value<Uint8List?> downloadToken,
@ -7124,7 +7119,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
Value<DownloadState?> downloadState, Value<DownloadState?> downloadState,
Value<bool> requiresAuthentication, Value<bool> requiresAuthentication,
Value<bool> reopenByContact, Value<bool> reopenByContact,
Value<bool> storedByContact, Value<bool> stored,
Value<List<int>?> reuploadRequestedBy, Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds, Value<int?> displayLimitInMilliseconds,
Value<Uint8List?> downloadToken, Value<Uint8List?> downloadToken,
@ -7190,9 +7185,8 @@ class $$MediaFilesTableFilterComposer
column: $table.reopenByContact, column: $table.reopenByContact,
builder: (column) => ColumnFilters(column)); builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get storedByContact => $composableBuilder( ColumnFilters<bool> get stored => $composableBuilder(
column: $table.storedByContact, column: $table.stored, builder: (column) => ColumnFilters(column));
builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<List<int>?, List<int>, String> ColumnWithTypeConverterFilters<List<int>?, List<int>, String>
get reuploadRequestedBy => $composableBuilder( get reuploadRequestedBy => $composableBuilder(
@ -7271,9 +7265,8 @@ class $$MediaFilesTableOrderingComposer
column: $table.reopenByContact, column: $table.reopenByContact,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get storedByContact => $composableBuilder( ColumnOrderings<bool> get stored => $composableBuilder(
column: $table.storedByContact, column: $table.stored, builder: (column) => ColumnOrderings(column));
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get reuploadRequestedBy => $composableBuilder( ColumnOrderings<String> get reuploadRequestedBy => $composableBuilder(
column: $table.reuploadRequestedBy, column: $table.reuploadRequestedBy,
@ -7332,8 +7325,8 @@ class $$MediaFilesTableAnnotationComposer
GeneratedColumn<bool> get reopenByContact => $composableBuilder( GeneratedColumn<bool> get reopenByContact => $composableBuilder(
column: $table.reopenByContact, builder: (column) => column); column: $table.reopenByContact, builder: (column) => column);
GeneratedColumn<bool> get storedByContact => $composableBuilder( GeneratedColumn<bool> get stored =>
column: $table.storedByContact, builder: (column) => column); $composableBuilder(column: $table.stored, builder: (column) => column);
GeneratedColumnWithTypeConverter<List<int>?, String> GeneratedColumnWithTypeConverter<List<int>?, String>
get reuploadRequestedBy => $composableBuilder( get reuploadRequestedBy => $composableBuilder(
@ -7408,7 +7401,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<DownloadState?> downloadState = const Value.absent(), Value<DownloadState?> downloadState = const Value.absent(),
Value<bool> requiresAuthentication = const Value.absent(), Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> reopenByContact = 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<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(), Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(),
@ -7425,7 +7418,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
downloadState: downloadState, downloadState: downloadState,
requiresAuthentication: requiresAuthentication, requiresAuthentication: requiresAuthentication,
reopenByContact: reopenByContact, reopenByContact: reopenByContact,
storedByContact: storedByContact, stored: stored,
reuploadRequestedBy: reuploadRequestedBy, reuploadRequestedBy: reuploadRequestedBy,
displayLimitInMilliseconds: displayLimitInMilliseconds, displayLimitInMilliseconds: displayLimitInMilliseconds,
downloadToken: downloadToken, downloadToken: downloadToken,
@ -7442,7 +7435,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<DownloadState?> downloadState = const Value.absent(), Value<DownloadState?> downloadState = const Value.absent(),
required bool requiresAuthentication, required bool requiresAuthentication,
Value<bool> reopenByContact = 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<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(), Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(),
@ -7459,7 +7452,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
downloadState: downloadState, downloadState: downloadState,
requiresAuthentication: requiresAuthentication, requiresAuthentication: requiresAuthentication,
reopenByContact: reopenByContact, reopenByContact: reopenByContact,
storedByContact: storedByContact, stored: stored,
reuploadRequestedBy: reuploadRequestedBy, reuploadRequestedBy: reuploadRequestedBy,
displayLimitInMilliseconds: displayLimitInMilliseconds, displayLimitInMilliseconds: displayLimitInMilliseconds,
downloadToken: downloadToken, downloadToken: downloadToken,

View file

@ -1,7 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_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:http/http.dart' as http;
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.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/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/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/log.dart';
import 'package:twonly/src/utils/storage.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 { Future<void> tryDownloadAllMediaFiles({bool force = false}) async {
// This is called when WebSocket is newly connected, so allow all downloads to be restarted. // This is called when WebSocket is newly connected, so allow all downloads to be restarted.
final messages = final mediaFiles =
await twonlyDB.messagesDao.getAllMessagesPendingDownloading(); await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload();
for (final message in messages) { for (final mediaFile in mediaFiles) {
await startDownloadMedia(message, force); 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 connectivityResult = await Connectivity().checkConnectivity();
final user = await getUser(); final user = await getUser();
@ -76,7 +73,7 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
} }
Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) 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; var failed = false;
if (update.status == TaskStatus.failed || if (update.status == TaskStatus.failed ||
@ -93,83 +90,45 @@ Future<void> handleDownloadStatusUpdate(TaskStatusUpdate update) async {
); );
} }
} else { } else {
Log.info('Got ${update.status} for $messageId'); Log.info('Got ${update.status} for $mediaId');
return; return;
} }
await handleDownloadStatusUpdateInternal(messageId, failed);
}
Future<void> handleDownloadStatusUpdateInternal(
int messageId,
bool failed,
) async {
if (failed) { if (failed) {
Log.error('Download failed for $messageId'); await requestMediaReupload(mediaId);
final message = await twonlyDB.messagesDao
.getMessageByMessageId(messageId)
.getSingleOrNull();
if (message != null && message.downloadState != DownloadState.downloaded) {
await handleMediaError(message);
}
} else { } else {
Log.info('Download was successfully for $messageId'); await handleEncryptedFile(mediaId);
await handleEncryptedFile(messageId);
} }
} }
Mutex protectDownload = Mutex(); Mutex protectDownload = Mutex();
Future<void> startDownloadMedia(MediaFile media, bool force) async { Future<void> startDownloadMedia(MediaFile media, bool force) async {
Log.info( final mediaService = await MediaFileService.fromMedia(media);
'Download blocked for ${message.messageId} because of network state.',
); if (mediaService.encryptedPath.existsSync()) {
if (message.contentJson == null) { await handleEncryptedFile(media.mediaId);
Log.error('Content of ${message.messageId} not found.');
await handleMediaError(message);
return; return;
} }
final content = MessageContent.fromJson( if (!force &&
message.kind, !await isAllowedToDownload(isVideo: media.type == MediaType.video)) {
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)) {
Log.warn( Log.warn(
'Download blocked for ${message.messageId} because of network state.', 'Download blocked for ${media.mediaId} because of network state.',
); );
return; return;
} }
final isBlocked = await protectDownload.protect<bool>(() async { final isBlocked = await protectDownload.protect<bool>(() async {
final msg = await twonlyDB.messagesDao final msg = await twonlyDB.mediaFilesDao.getMediaFileById(media.mediaId);
.getMessageByMessageId(message.messageId)
.getSingleOrNull();
if (msg == null) return true; if (msg == null || msg.downloadState != DownloadState.pending) {
if (msg.downloadState != DownloadState.pending) {
Log.error(
'${message.messageId} is already downloaded or is downloading.',
);
return true; return true;
} }
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.mediaFilesDao.updateMedia(
message.messageId, msg.mediaId,
const MessagesCompanion( const MediaFilesCompanion(
downloadState: Value(DownloadState.downloading), downloadState: Value(DownloadState.downloading),
), ),
); );
@ -178,11 +137,16 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
}); });
if (isBlocked) { if (isBlocked) {
Log.info('Download for ${message.messageId} already started.'); Log.info('Download for ${media.mediaId} already started.');
return; 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 = final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken'; 'http${apiService.apiSecure}://${apiService.apiHost}/api/download/$downloadToken';
@ -190,20 +154,20 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
try { try {
final task = DownloadTask( final task = DownloadTask(
url: apiUrl, url: apiUrl,
taskId: 'download_${message.messageId}', taskId: 'download_${media.mediaId}',
directory: 'media/received/', directory: mediaService.encryptedPath.parent.path,
baseDirectory: BaseDirectory.applicationSupport, baseDirectory: BaseDirectory.root,
filename: '${message.messageId}.encrypted', filename: basename(mediaService.encryptedPath.path),
priority: 0, priority: 0,
retries: 10, retries: 10,
); );
Log.info( Log.info(
'Got media file. Starting download: ${downloadToken.substring(0, 10)}', 'Downloading ${media.mediaId} to ${mediaService.encryptedPath}',
); );
try { try {
await downloadFileFast(message.messageId, apiUrl); await downloadFileFast(media, apiUrl, mediaService.encryptedPath);
} catch (e) { } catch (e) {
Log.error('Fast download failed: $e'); Log.error('Fast download failed: $e');
await FileDownloader().enqueue(task); await FileDownloader().enqueue(task);
@ -214,269 +178,114 @@ Future<void> startDownloadMedia(MediaFile media, bool force) async {
} }
Future<void> downloadFileFast( Future<void> downloadFileFast(
int messageId, MediaFile media,
String apiUrl, String apiUrl,
File filePath,
) async { ) 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 = final response =
await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10)); await http.get(Uri.parse(apiUrl)).timeout(const Duration(seconds: 10));
if (response.statusCode == 200) { if (response.statusCode == 200) {
await File(filePath).writeAsBytes(response.bodyBytes); await filePath.writeAsBytes(response.bodyBytes);
Log.info('Fast Download successful: $filePath'); Log.info('Fast Download successful: $filePath');
await handleDownloadStatusUpdateInternal(messageId, false); await handleEncryptedFile(media.mediaId);
return; return;
} else { } else {
if (response.statusCode == 404 || response.statusCode == 403) { if (response.statusCode == 404 ||
await handleDownloadStatusUpdateInternal(messageId, true); 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; return;
} }
// can be tried again // Will be tried again using the slow method...
throw Exception('Fast download failed with status: ${response.statusCode}'); throw Exception('Fast download failed with status: ${response.statusCode}');
} }
} }
Future<void> handleEncryptedFile(int messageId) async { Future<void> requestMediaReupload(String mediaId) async {
final msg = await twonlyDB.messagesDao final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId);
.getMessageByMessageId(messageId) if (messages.length != 1 || messages.first.senderId == null) {
.getSingleOrNull(); Log.error(
if (msg == null) { 'Media file has none or more than one sender. That is not possible');
Log.error('Not message for downloaded file found: $messageId');
return; 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) { await twonlyDB.mediaFilesDao.updateMedia(
Log.error('encrypted bytes are not found for ${msg.messageId}'); 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; return;
} }
final content = await twonlyDB.mediaFilesDao.updateMedia(
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!) as Map); 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 { try {
final chacha20 = FlutterChacha20.poly1305Aead(); final chacha20 = FlutterChacha20.poly1305Aead();
final secretKeyData = SecretKeyData(content.encryptionKey!); final secretKeyData = SecretKeyData(mediaService.mediaFile.encryptionKey!);
final secretBox = SecretBox( final secretBox = SecretBox(
encryptedBytes, encryptedBytes,
nonce: content.encryptionNonce!, nonce: mediaService.mediaFile.encryptionNonce!,
mac: Mac(content.encryptionMac!), mac: Mac(mediaService.mediaFile.encryptionMac!),
); );
// try {
final plaintextBytes = final plaintextBytes =
await chacha20.decrypt(secretBox, secretKey: secretKeyData); await chacha20.decrypt(secretBox, secretKey: secretKeyData);
var imageBytes = Uint8List.fromList(plaintextBytes);
if (content.isVideo) { final rawMediaBytes = Uint8List.fromList(plaintextBytes);
final extractedBytes = extractUint8Lists(imageBytes);
imageBytes = extractedBytes[0];
await writeMediaFile(msg.messageId, 'mp4', extractedBytes[1]);
}
await writeMediaFile(msg.messageId, 'png', imageBytes); await mediaService.tempPath.writeAsBytes(rawMediaBytes);
// } catch (e) {
// Log.error(
// "could not decrypt the media file in the second try. reporting error to user: $e");
// handleMediaError(msg);
// return;
// }
} 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!),
);
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) { } catch (e) {
Log.error( Log.error(
'could not decrypt the media file in the second try. reporting error to user: $e', 'Could not decrypt the media file. Requesting a new upload.',
); );
await handleMediaError(msg); await requestMediaReupload(mediaId);
return; return;
} }
}
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.mediaFilesDao.updateMedia(
msg.messageId, mediaId,
const MessagesCompanion(downloadState: Value(DownloadState.downloaded)), 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

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.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/media_download.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/mediafile.service.dart'; import 'package:twonly/src/services/mediafile.service.dart';
import 'package:twonly/src/services/thumbnail.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<void> handleMedia( Future<void> handleMedia(
@ -33,7 +31,10 @@ Future<void> handleMedia(
} }
// in case there was already a downloaded file delete it... // 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( await twonlyDB.mediaFilesDao.updateMedia(
message.mediaId!, message.mediaId!,
@ -81,6 +82,7 @@ Future<void> handleMedia(
); );
if (mediaFile == null) { if (mediaFile == null) {
Log.error('Could not insert media file into database');
return; return;
} }
@ -121,7 +123,11 @@ Future<void> handleMediaUpdate(
if (message == null || message.mediaId == null) return; if (message == null || message.mediaId == null) return;
final mediaFile = final mediaFile =
await twonlyDB.mediaFilesDao.getMediaFileById(message.mediaId!); 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) { switch (mediaUpdate.type) {
case EncryptedContent_MediaUpdate_Type.REOPENED: case EncryptedContent_MediaUpdate_Type.REOPENED:
@ -137,11 +143,12 @@ Future<void> handleMediaUpdate(
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId, mediaFile.mediaId,
const MediaFilesCompanion( 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: case EncryptedContent_MediaUpdate_Type.DECRYPTION_ERROR:
Log.info('Got media file decryption error ${mediaFile.mediaId}'); Log.info('Got media file decryption error ${mediaFile.mediaId}');

View file

@ -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'; import 'package:twonly/src/utils/log.dart';
Future<void> removeMediaFile(String mediaId) async { class MediaFileService {
Log.error('TODO removeMediaFile: $mediaId'); 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',
);
} }

View file

@ -1,26 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart'; 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:twonly/src/utils/log.dart';
import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:video_thumbnail/video_thumbnail.dart';
Future<void> createThumbnailsForImage(
Future<void> createThumbnailForMediaFile(MediaFile media) async { File sourceFile,
File destinationFile,
switch (media.type) { ) async {
case MediaType.image: final fileExtension = sourceFile.path.split('.').last.toLowerCase();
TODO
break;
default:
}
}
Future<void> createThumbnailsForImage(File file) async {
final fileExtension = file.path.split('.').last.toLowerCase();
if (fileExtension != 'png') { if (fileExtension != 'png') {
Log.error('Could not create thumbnail for image. $fileExtension != .png'); Log.error('Could not create thumbnail for image. $fileExtension != .png');
return; return;
@ -30,7 +17,7 @@ Future<void> createThumbnailsForImage(File file) async {
final imageBytesCompressed = await FlutterImageCompress.compressWithFile( final imageBytesCompressed = await FlutterImageCompress.compressWithFile(
minHeight: 800, minHeight: 800,
minWidth: 450, minWidth: 450,
file.path, sourceFile.path,
format: CompressFormat.webp, format: CompressFormat.webp,
quality: 50, quality: 50,
); );
@ -39,16 +26,17 @@ Future<void> createThumbnailsForImage(File file) async {
Log.error('Could not compress the image'); Log.error('Could not compress the image');
return; return;
} }
await destinationFile.writeAsBytes(imageBytesCompressed);
final thumbnailFile = getThumbnailPath(file);
await thumbnailFile.writeAsBytes(imageBytesCompressed);
} catch (e) { } catch (e) {
Log.error('Could not compress the image got :$e'); Log.error('Could not compress the image got :$e');
} }
} }
Future<void> createThumbnailsForVideo(File file) async { Future<void> createThumbnailsForVideo(
final fileExtension = file.path.split('.').last.toLowerCase(); File sourceFile,
File destinationFile,
) async {
final fileExtension = sourceFile.path.split('.').last.toLowerCase();
if (fileExtension != 'mp4') { if (fileExtension != 'mp4') {
Log.error('Could not create thumbnail for video. $fileExtension != .mp4'); Log.error('Could not create thumbnail for video. $fileExtension != .mp4');
return; return;
@ -56,8 +44,8 @@ Future<void> createThumbnailsForVideo(File file) async {
try { try {
await VideoThumbnail.thumbnailFile( await VideoThumbnail.thumbnailFile(
video: file.path, video: sourceFile.path,
thumbnailPath: getThumbnailPath(file).path, thumbnailPath: destinationFile.path,
maxWidth: 450, maxWidth: 450,
quality: 75, quality: 75,
); );
@ -65,15 +53,3 @@ Future<void> createThumbnailsForVideo(File file) async {
Log.error('Could not create the video thumbnail: $e'); 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));
}

View file

@ -13,7 +13,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.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/api/media_upload.dart';
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart'; import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/utils/log.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/')); final backupDir = Directory(join(baseDir, 'backup_twonly_safe/'));
await backupDir.create(recursive: true); await backupDir.create(recursive: true);
final backupDatabaseFile = final backupDatabaseFile = File(join(backupDir.path, 'twonly.backup.sqlite'));
File(join(backupDir.path, 'twonly_database.backup.sqlite'));
final backupDatabaseFileCleaned = final backupDatabaseFileCleaned =
File(join(backupDir.path, 'twonly_database.backup.cleaned.sqlite')); File(join(backupDir.path, 'twonly.backup.cleaned.sqlite'));
// copy database // copy database
final originalDatabase = File(join(baseDir, 'twonly_database.sqlite')); final originalDatabase = File(join(baseDir, 'twonly.sqlite'));
await originalDatabase.copy(backupDatabaseFile.path); await originalDatabase.copy(backupDatabaseFile.path);
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
final backupDB = TwonlyDatabase( final backupDB = TwonlyDB(
driftDatabase( driftDatabase(
name: 'twonly_database.backup', name: 'twonly.backup',
native: DriftNativeOptions( native: DriftNativeOptions(
databaseDirectory: () async { databaseDirectory: () async {
return backupDir; return backupDir;

View file

@ -10,10 +10,8 @@ import 'package:http/http.dart' as http;
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/secure_storage_keys.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/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/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -90,44 +88,9 @@ Future<void> handleBackupData(
); );
final baseDir = (await getApplicationSupportDirectory()).path; 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); 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(); const storage = FlutterSecureStorage();
final secureStorage = jsonDecode(backupContent.secureStorageJson); final secureStorage = jsonDecode(backupContent.secureStorageJson);

View file

@ -340,6 +340,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Future<Uint8List?> getMergedImage() async { Future<Uint8List?> getMergedImage() async {
Uint8List? image; Uint8List? image;
TODO: When changed then create a new mediaID!!!!!!
As storedMediaId would overwrite it....
if (layers.length > 1 || widget.videoFilePath != null) { if (layers.length > 1 || widget.videoFilePath != null) {
for (final x in layers) { for (final x in layers) {
x.showCustomButtons = false; x.showCustomButtons = false;