refactor: Move file storage to mixin to not import dart:io

Original issue and former solution by
The one with the braid <info@braid.business>
Special thanks for pointing out the problem.

This fixes that dart:io is imported into the
SDK database by moving it into it's own mixin which
makes it reusable and platform independent by
using conditional imports.
This commit is contained in:
Krille 2024-04-11 14:54:20 +02:00
parent 87c0e7fbe8
commit 49e6d55d32
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652
5 changed files with 88 additions and 102 deletions

View File

@ -0,0 +1,55 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:matrix/matrix.dart';
mixin DatabaseFileStorage {
bool get supportsFileStoring => fileStorageLocation != null;
late final Uri? fileStorageLocation;
late final Duration? deleteFilesAfterDuration;
Future<void> storeFile(Uri mxcUri, Uint8List bytes, int time) async {
final fileStorageLocation = this.fileStorageLocation;
if (!supportsFileStoring || fileStorageLocation == null) return;
final dir = Directory.fromUri(fileStorageLocation);
final file = File('${dir.path}/${mxcUri.toString().split('/').last}');
if (await file.exists()) return;
await file.writeAsBytes(bytes);
}
Future<Uint8List?> getFile(Uri mxcUri) async {
final fileStorageLocation = this.fileStorageLocation;
if (!supportsFileStoring || fileStorageLocation == null) return null;
final dir = Directory.fromUri(fileStorageLocation);
final file = File('${dir.path}/${mxcUri.toString().split('/').last}');
if (await file.exists()) return await file.readAsBytes();
return null;
}
Future<void> deleteOldFiles(int savedAt) async {
final dirUri = fileStorageLocation;
final deleteFilesAfterDuration = this.deleteFilesAfterDuration;
if (!supportsFileStoring ||
dirUri == null ||
deleteFilesAfterDuration == null) {
return;
}
final dir = Directory.fromUri(dirUri);
final entities = await dir.list().toList();
for (final file in entities) {
if (file is! File) continue;
final stat = await file.stat();
if (DateTime.now().difference(stat.modified) > deleteFilesAfterDuration) {
Logs().v('Delete old file', file.path);
await file.delete();
}
}
}
}

View File

@ -0,0 +1,20 @@
import 'dart:typed_data';
mixin DatabaseFileStorage {
bool get supportsFileStoring => false;
late final Uri? fileStorageLocation;
late final Duration? deleteFilesAfterDuration;
Future<void> storeFile(Uri mxcUri, Uint8List bytes, int time) async {
return;
}
Future<Uint8List?> getFile(Uri mxcUri) async {
return null;
}
Future<void> deleteOldFiles(int savedAt) async {
return;
}
}

View File

@ -1,37 +0,0 @@
// This is a stub implementation of all filesystem related calls done
// by the matrix SDK database. This fake implementation ensures we can compile
// using dart2js.
import 'dart:typed_data';
class File extends FileSystemEntry {
const File(super.path);
Future<Uint8List> readAsBytes() async => Uint8List(0);
Future<void> writeAsBytes(Uint8List data) async => Future.value();
}
class Directory extends FileSystemEntry {
const Directory(super.path);
Stream<FileSystemEntry> list() async* {
return;
}
}
abstract class FileSystemEntry {
final String path;
const FileSystemEntry(this.path);
Future<void> delete() => Future.value();
Future<FileStat> stat() async => FileStat();
Future<bool> exists() async => false;
}
class FileStat {
final modified = DateTime.fromMillisecondsSinceEpoch(0);
}

View File

@ -1 +0,0 @@
export 'dart:io';

View File

@ -19,7 +19,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data';
import 'package:sqflite_common/sqflite.dart'; import 'package:sqflite_common/sqflite.dart';
@ -35,6 +34,9 @@ import 'package:matrix/src/utils/run_benchmarked.dart';
import 'package:matrix/src/database/indexeddb_box.dart' import 'package:matrix/src/database/indexeddb_box.dart'
if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart'; if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';
import 'package:matrix/src/database/database_file_storage_stub.dart'
if (dart.library.io) 'package:matrix/src/database/database_file_storage_io.dart';
/// Database based on SQlite3 on native and IndexedDB on web. For native you /// Database based on SQlite3 on native and IndexedDB on web. For native you
/// have to pass a `Database` object, which can be created with the sqflite /// have to pass a `Database` object, which can be created with the sqflite
/// package like this: /// package like this:
@ -49,7 +51,7 @@ import 'package:matrix/src/database/indexeddb_box.dart'
/// [sqflite_common_ffi](https://pub.dev/packages/sqflite_common_ffi). /// [sqflite_common_ffi](https://pub.dev/packages/sqflite_common_ffi).
/// Learn more at: /// Learn more at:
/// https://github.com/famedly/matrix-dart-sdk/issues/1642#issuecomment-1865827227 /// https://github.com/famedly/matrix-dart-sdk/issues/1642#issuecomment-1865827227
class MatrixSdkDatabase extends DatabaseApi { class MatrixSdkDatabase extends DatabaseApi with DatabaseFileStorage {
static const int version = 7; static const int version = 7;
final String name; final String name;
late BoxCollection _collection; late BoxCollection _collection;
@ -100,8 +102,6 @@ class MatrixSdkDatabase extends DatabaseApi {
late Box<String> _seenDeviceKeysBox; late Box<String> _seenDeviceKeysBox;
@override
bool get supportsFileStoring => fileStorageLocation != null;
@override @override
final int maxFileSize; final int maxFileSize;
@ -111,18 +111,6 @@ class MatrixSdkDatabase extends DatabaseApi {
'Breaks support for web standalone. Use [fileStorageLocation] instead.') 'Breaks support for web standalone. Use [fileStorageLocation] instead.')
Object? get fileStoragePath => fileStorageLocation?.toFilePath(); Object? get fileStoragePath => fileStorageLocation?.toFilePath();
/// A [Uri] defining the file storage location.
///
/// Unless you support custom Uri schemes, this should usually be a
/// [Uri.directory] identifier.
///
/// Using a [Uri] as type here enables users to technically extend the API to
/// support file storage on non-io platforms as well - or to use non-File
/// based storage mechanisms on io.
final Uri? fileStorageLocation;
final Duration? deleteFilesAfterDuration;
static const String _clientBoxName = 'box_client'; static const String _clientBoxName = 'box_client';
static const String _accountDataBoxName = 'box_account_data'; static const String _accountDataBoxName = 'box_account_data';
@ -185,10 +173,17 @@ class MatrixSdkDatabase extends DatabaseApi {
this.sqfliteFactory, this.sqfliteFactory,
this.maxFileSize = 0, this.maxFileSize = 0,
// TODO : remove deprecated member migration on next major release // TODO : remove deprecated member migration on next major release
@Deprecated(
'Breaks support for web standalone. Use [fileStorageLocation] instead.')
dynamic fileStoragePath, dynamic fileStoragePath,
Uri? fileStorageLocation, Uri? fileStorageLocation,
this.deleteFilesAfterDuration, Duration? deleteFilesAfterDuration,
}) : fileStorageLocation = fileStorageLocation ?? fileStoragePath?.path; }) {
final legacyPath = fileStoragePath?.path;
this.fileStorageLocation = fileStorageLocation ??
(legacyPath is String ? Uri.tryParse(legacyPath) : null);
this.deleteFilesAfterDuration = deleteFilesAfterDuration;
}
Future<void> open() async { Future<void> open() async {
_collection = await BoxCollection.open( _collection = await BoxCollection.open(
@ -327,28 +322,6 @@ class MatrixSdkDatabase extends DatabaseApi {
return; return;
} }
@override
Future<void> deleteOldFiles(int savedAt) async {
if (_kIsWeb) return;
final path = fileStorageLocation?.toFilePath();
final deleteFilesAfterDuration = this.deleteFilesAfterDuration;
if (!supportsFileStoring ||
path == null ||
deleteFilesAfterDuration == null) {
return;
}
final dir = Directory(path);
final entities = await dir.list().toList();
for (final file in entities) {
if (file is! File) continue;
final stat = await file.stat();
if (DateTime.now().difference(stat.modified) > deleteFilesAfterDuration) {
Logs().v('Delete old file', file.path);
await file.delete();
}
}
}
@override @override
Future<void> forgetRoom(String roomId) async { Future<void> forgetRoom(String roomId) async {
await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString()); await _timelineFragmentsBox.delete(TupleKey(roomId, '').toString());
@ -471,18 +444,6 @@ class MatrixSdkDatabase extends DatabaseApi {
return await _getEventsByIds(eventIds.cast<String>(), room); return await _getEventsByIds(eventIds.cast<String>(), room);
}); });
@override
Future<Uint8List?> getFile(Uri mxcUri) async {
if (_kIsWeb) return null;
final path = fileStorageLocation?.toFilePath();
if (!supportsFileStoring || path == null) return null;
final file = File('$path/${mxcUri.toString().split('/').last}');
if (await file.exists()) return await file.readAsBytes();
return null;
}
@override @override
Future<StoredInboundGroupSession?> getInboundGroupSession( Future<StoredInboundGroupSession?> getInboundGroupSession(
String roomId, String roomId,
@ -1156,18 +1117,6 @@ class MatrixSdkDatabase extends DatabaseApi {
} }
} }
@override
Future<void> storeFile(Uri mxcUri, Uint8List bytes, int time) async {
if (_kIsWeb) return;
final path = fileStorageLocation?.toFilePath();
if (!supportsFileStoring || path == null) return;
final file = File('$path/${mxcUri.toString().split('/').last}');
if (await file.exists()) return;
await file.writeAsBytes(bytes);
}
@override @override
Future<void> storeInboundGroupSession( Future<void> storeInboundGroupSession(
String roomId, String roomId,