mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:38:41 +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();
|
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: [
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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)))
|
||||||
|
|
|
||||||
|
|
@ -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()();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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}');
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue