From c560cafb74509f74a3a06c495dcb5c9a5ca94b5a Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 21 Dec 2024 17:47:41 +0100 Subject: [PATCH] Add password protected notes, closes #771 --- api/lib/src/converter/note.dart | 4 +-- api/lib/src/models/asset.dart | 8 ------ api/lib/src/models/data.dart | 35 +++++++++++++++++++++---- api/pubspec.lock | 4 +-- api/pubspec.yaml | 2 +- app/lib/api/changes.dart | 7 +---- app/lib/api/file_system.dart | 11 +++++--- app/lib/cubits/current_index.dart | 4 +-- app/lib/dialogs/file_system/create.dart | 4 +-- app/lib/services/import.dart | 21 ++++++++++++--- app/lib/views/files/entity.dart | 11 ++++---- app/lib/views/files/grid.dart | 2 +- app/lib/views/files/list.dart | 2 +- app/lib/views/files/view.dart | 28 +++++++++++--------- app/lib/views/main.dart | 13 +++++++-- app/pubspec.lock | 8 +++--- app/pubspec.yaml | 2 +- 17 files changed, 104 insertions(+), 62 deletions(-) diff --git a/api/lib/src/converter/note.dart b/api/lib/src/converter/note.dart index 952c31a68283..a36de4819a73 100644 --- a/api/lib/src/converter/note.dart +++ b/api/lib/src/converter/note.dart @@ -7,13 +7,13 @@ import '../../butterfly_api.dart'; const kArchiveSignature = 0x50; -NoteData noteDataMigrator(Uint8List data) { +NoteData noteDataMigrator(Uint8List data, {String? password}) { Archive archive; if (data.isNotEmpty && data[0] != kArchiveSignature) { final map = json.decode(utf8.decode(data)) as Map; archive = convertLegacyDataToArchive(map); } else { - archive = ZipDecoder().decodeBytes(data); + archive = ZipDecoder().decodeBytes(data, password: password); } return archiveNoteDataMigrator(archive); } diff --git a/api/lib/src/models/asset.dart b/api/lib/src/models/asset.dart index 30dd4690bd73..a9eea32b22fd 100644 --- a/api/lib/src/models/asset.dart +++ b/api/lib/src/models/asset.dart @@ -1,16 +1,8 @@ -import 'dart:typed_data'; - import 'package:butterfly_api/butterfly_api.dart'; import 'package:lw_file_system_api/lw_file_system_api.dart'; enum AssetFileType { note, page, image, markdown, pdf, svg, xopp, archive } -extension AppDocumentLoadExtension on FileSystemFile { - NoteData load({bool disableMigrations = false}) => - NoteData.fromData(Uint8List.fromList(data), - disableMigrations: disableMigrations); -} - extension AssetLocationFileTypeExtension on AssetLocation { AssetFileType? get fileType => AssetFileTypeHelper.fromFileExtension(fileExtension); diff --git a/api/lib/src/models/data.dart b/api/lib/src/models/data.dart index 4509dd10c6fc..750a8262a7fa 100644 --- a/api/lib/src/models/data.dart +++ b/api/lib/src/models/data.dart @@ -18,16 +18,39 @@ import 'palette.dart'; final Set validAssetPaths = {kImagesArchiveDirectory}; -@immutable +final class NoteFile { + final Uint8List data; + + NoteFile(this.data); + + bool isEncrypted() => isZipEncrypted(data); + + (String?, NoteData)? _data; + + NoteData? load({String? password}) { + if (_data != null && _data?.$1 == password) { + return _data?.$2; + } + try { + final data = NoteData.fromData(this.data, password: password); + _data = (password, data); + return data; + } catch (_) { + return null; + } + } +} + final class NoteData extends ArchiveData { - NoteData(super.archive, {super.state}); + NoteData(super.archive, {super.state, super.password}); - factory NoteData.fromData(Uint8List data, {bool disableMigrations = false}) { + factory NoteData.fromData(Uint8List data, + {bool disableMigrations = false, String? password}) { if (disableMigrations) { - final archive = ZipDecoder().decodeBytes(data); + final archive = ZipDecoder().decodeBytes(data, password: password); return NoteData(archive); } - return noteDataMigrator(data); + return noteDataMigrator(data, password: password); } factory NoteData.fromArchive(Archive archive, @@ -425,4 +448,6 @@ final class NoteData extends ArchiveData { final removed = Set.from(state.removed)..remove(path); return updateState(state.copyWith(removed: removed)); } + + NoteFile toFile() => NoteFile(exportAsBytes()); } diff --git a/api/pubspec.lock b/api/pubspec.lock index 15e17c050075..72c096943363 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -322,8 +322,8 @@ packages: dependency: "direct main" description: path: "packages/lw_file_system_api" - ref: b9d6c6173bf75247ce5a4d47fab0e48b730a9696 - resolved-ref: b9d6c6173bf75247ce5a4d47fab0e48b730a9696 + ref: "2036c923bb93afcefaadd8108c7e789513ae7082" + resolved-ref: "2036c923bb93afcefaadd8108c7e789513ae7082" url: "https://github.com/LinwoodDev/dart_pkgs" source: git version: "1.0.0" diff --git a/api/pubspec.yaml b/api/pubspec.yaml index bd98f60bc845..7d91b1886d5d 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: git: url: https://github.com/LinwoodDev/dart_pkgs path: packages/lw_file_system_api - ref: b9d6c6173bf75247ce5a4d47fab0e48b730a9696 + ref: 2036c923bb93afcefaadd8108c7e789513ae7082 dev_dependencies: test: ^1.25.3 diff --git a/app/lib/api/changes.dart b/app/lib/api/changes.dart index b32b2cc8c613..e2187aae059a 100644 --- a/app/lib/api/changes.dart +++ b/app/lib/api/changes.dart @@ -4,14 +4,9 @@ import 'package:butterfly/api/open.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:lw_file_system/lw_file_system.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; -Future checkFileChanges( - BuildContext context, FileSystemEntity? entity) async { - if (entity is! FileSystemFile) return null; - final data = entity.data; - if (data == null) return null; +Future checkFileChanges(BuildContext context, NoteData data) async { final metadata = data.getMetadata(); if (metadata == null) return null; final version = metadata.fileVersion; diff --git a/app/lib/api/file_system.dart b/app/lib/api/file_system.dart index 0fc437fea52c..a48b4ac1ef04 100644 --- a/app/lib/api/file_system.dart +++ b/app/lib/api/file_system.dart @@ -15,6 +15,9 @@ import 'package:shared_preferences/shared_preferences.dart'; Uint8List _encode(NoteData data) => Uint8List.fromList(data.exportAsBytes()); NoteData _decode(Uint8List data) => NoteData.fromData(data); +Uint8List _encodeFile(NoteFile file) => file.data; +NoteFile _decodeFile(Uint8List data) => NoteFile(data); + const butterflySubDirectory = '/Linwood/Butterfly'; String? overrideButterflyDirectory; @@ -61,7 +64,7 @@ Future Function(ExternalStorage? storage) _getRemoteDirectory( Future getButterflyDocumentsDirectory([ExternalStorage? storage]) => _getRemoteDirectory('Documents')(storage); -typedef DocumentFileSystem = TypedDirectoryFileSystem; +typedef DocumentFileSystem = TypedDirectoryFileSystem; typedef TemplateFileSystem = TypedKeyFileSystem; typedef PackFileSystem = TypedKeyFileSystem; @@ -170,12 +173,12 @@ class ButterflyFileSystem { await fs.createFile('${pack.name}.bfly', pack); } - TypedDirectoryFileSystem buildDocumentSystem( + TypedDirectoryFileSystem buildDocumentSystem( [ExternalStorage? storage]) => TypedDirectoryFileSystem.build( _documentConfig, - onEncode: _encode, - onDecode: _decode, + onEncode: _encodeFile, + onDecode: _decodeFile, storage: storage, ); TypedKeyFileSystem buildTemplateSystem( diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index a5b198f13c29..f263f0fc0ead 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -976,10 +976,10 @@ class CurrentIndexCubit extends Cubit { state.absolute || location.fileType != AssetFileType.note) { final document = await fileSystem.createFileWithName( - name: currentData.name, suffix: '.bfly', currentData); + name: currentData.name, suffix: '.bfly', currentData.toFile()); location = document.location; } else { - await fileSystem.updateFile(location.path, currentData); + await fileSystem.updateFile(location.path, currentData.toFile()); } state.settingsCubit.addRecentHistory(location); emit(state.copyWith(location: location, saved: SaveState.saved)); diff --git a/app/lib/dialogs/file_system/create.dart b/app/lib/dialogs/file_system/create.dart index bd75d5fd1e01..03fabadfbed9 100644 --- a/app/lib/dialogs/file_system/create.dart +++ b/app/lib/dialogs/file_system/create.dart @@ -62,8 +62,8 @@ class _FileSystemAssetCreateDialogState if (_formKey.currentState?.validate() ?? false) { final newPath = '${widget.path}/${_nameController.text}'; if (!widget.isFolder) { - await widget.fileSystem - .createFile(newPath, DocumentDefaults.createDocument()); + await widget.fileSystem.createFile( + newPath, DocumentDefaults.createDocument().toFile()); } else { await widget.fileSystem.createDirectory(newPath); } diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index e800a3d79762..c8834e869215 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -6,6 +6,7 @@ import 'dart:ui' as ui; import 'package:archive/archive.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/image.dart'; +import 'package:butterfly/dialogs/name.dart'; import 'package:butterfly/helpers/asset.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; @@ -88,6 +89,8 @@ class ImportService { bytes = Uint8List.fromList(List.from(data)); } else if (data is NoteData) { return data; + } else if (data is NoteFile) { + bytes = data.data; } if (type.isEmpty) type = 'note'; final fileType = AssetFileType.values.firstWhereOrNull((element) => @@ -221,7 +224,19 @@ class ImportService { try { final documentOpened = document != null; final realDocument = document ?? DocumentDefaults.createDocument(); - final data = NoteData.fromData(bytes); + final file = NoteFile(bytes); + String? password; + if (file.isEncrypted()) { + password = await showDialog( + context: context, + builder: (context) => NameDialog( + title: AppLocalizations.of(context).password, + ), + ); + if (password == null) return null; + } + final data = file.load(password: password); + if (data == null) return null; if (!data.isValid) { await importArchive(bytes); return null; @@ -758,7 +773,7 @@ class ImportService { if (data.isValid) { final document = await importBfly(bytes); if (document != null) { - fileSystem.createFile(document.name ?? '', document); + fileSystem.createFile(document.name ?? '', document.toFile()); } return document != null; } @@ -767,7 +782,7 @@ class ImportService { if (!file.name.endsWith(fileExtension)) continue; final document = await importBfly(file.content, advanced: false); if (document != null) { - fileSystem.createFile(file.name, document); + fileSystem.createFile(file.name, document.toFile()); } } return true; diff --git a/app/lib/views/files/entity.dart b/app/lib/views/files/entity.dart index de4845ad6a3a..c5fc62bb932d 100644 --- a/app/lib/views/files/entity.dart +++ b/app/lib/views/files/entity.dart @@ -21,7 +21,7 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:popover/popover.dart'; class FileEntityItem extends StatefulWidget { - final FileSystemEntity entity; + final FileSystemEntity entity; final bool active, collapsed, gridView; final bool? selected; final VoidCallback onTap, onReload; @@ -125,11 +125,12 @@ class _FileEntityItemState extends State { PhosphorIconData icon = PhosphorIconsLight.folder; final entity = widget.entity; try { - if (entity is FileSystemFile) { + if (entity is FileSystemFile) { + final data = entity.data?.load(); icon = entity.location.fileType.icon(PhosphorIconsStyle.light); - thumbnail = entity.data?.getThumbnail(); + thumbnail = data?.getThumbnail(); if (thumbnail?.isEmpty ?? false) thumbnail = null; - metadata = entity.data?.getMetadata(); + metadata = data?.getMetadata(); final locale = Localizations.localeOf(context).languageCode; final dateFormatter = DateFormat.yMd(locale); final timeFormatter = DateFormat.Hm(locale); @@ -238,7 +239,7 @@ class _FileEntityItemState extends State { class ContextFileRegion extends StatelessWidget { final ExternalStorage? remote; final DocumentFileSystem documentSystem; - final FileSystemEntity entity; + final FileSystemEntity entity; final SettingsCubit settingsCubit; final bool editable; final ValueChanged onEdit; diff --git a/app/lib/views/files/grid.dart b/app/lib/views/files/grid.dart index 05540618dbfe..ff76ef0a457e 100644 --- a/app/lib/views/files/grid.dart +++ b/app/lib/views/files/grid.dart @@ -17,7 +17,7 @@ class FileEntityGridItem extends StatelessWidget { final VoidCallback onTap, onDelete, onReload; final ValueChanged onEdit, onSelectedChanged; final Uint8List? thumbnail; - final FileSystemEntity entity; + final FileSystemEntity entity; final TextEditingController nameController; const FileEntityGridItem({ diff --git a/app/lib/views/files/list.dart b/app/lib/views/files/list.dart index b3fe58ab54ed..5cf252229a7e 100644 --- a/app/lib/views/files/list.dart +++ b/app/lib/views/files/list.dart @@ -23,7 +23,7 @@ class FileEntityListTile extends StatelessWidget { final VoidCallback onTap, onDelete, onReload; final ValueChanged onEdit, onSelectedChanged; final Uint8List? thumbnail; - final FileSystemEntity entity; + final FileSystemEntity entity; final TextEditingController nameController; final Widget actionButton; diff --git a/app/lib/views/files/view.dart b/app/lib/views/files/view.dart index 8011a505e6b0..f4cf53bbd855 100644 --- a/app/lib/views/files/view.dart +++ b/app/lib/views/files/view.dart @@ -52,7 +52,7 @@ class FilesViewState extends State { ExternalStorage? _remote; String _search = ''; late final SettingsCubit _settingsCubit; - Stream?>? _filesStream; + Stream?>? _filesStream; final Set _selectedFiles = {}; @override @@ -119,9 +119,11 @@ class FilesViewState extends State { directory: path, name: name, suffix: '.bfly', - template.createDocument( - name: name, - )); + template + .createDocument( + name: name, + ) + .toFile()); reloadFileSystem(); } @@ -523,7 +525,7 @@ class FilesViewState extends State { .getAsset(_locationController.text, readData: false); if (directory - is! FileSystemDirectory) { + is! FileSystemDirectory) { return; } setState(() { @@ -608,7 +610,7 @@ class FilesViewState extends State { BlocBuilder( buildWhen: (previous, current) => previous.starred != current.starred, builder: (context, settings) => - StreamBuilder?>( + StreamBuilder?>( stream: _filesStream, builder: (context, snapshot) { if (snapshot.hasError) { @@ -622,7 +624,7 @@ class FilesViewState extends State { child: Text(AppLocalizations.of(context).noElements)); } final entity = snapshot.data; - if (entity is! FileSystemDirectory) { + if (entity is! FileSystemDirectory) { return Container(); } final assets = entity.assets.where((e) { @@ -711,7 +713,7 @@ class FilesViewState extends State { } } - int _sortAssets(FileSystemEntity a, FileSystemEntity b) { + int _sortAssets(FileSystemEntity a, FileSystemEntity b) { try { final settings = _settingsCubit.state; // Test if starred @@ -729,14 +731,14 @@ class FilesViewState extends State { if (b is FileSystemDirectory) { return 1; } - final aFile = a as FileSystemFile; - final bFile = b as FileSystemFile; + final aFile = a as FileSystemFile; + final bFile = b as FileSystemFile; FileMetadata? aInfo, bInfo; try { - aInfo = aFile.data?.getMetadata(); + aInfo = aFile.data?.load()?.getMetadata(); } catch (_) {} try { - bInfo = bFile.data?.getMetadata(); + bInfo = bFile.data?.load()?.getMetadata(); } catch (_) {} if (aInfo == null) { if (bInfo == null) { @@ -791,7 +793,7 @@ class _RecentFilesView extends StatefulWidget { } class _RecentFilesViewState extends State<_RecentFilesView> { - late Stream>> _stream; + late Stream>> _stream; late final ButterflyFileSystem _fileSystem; final ScrollController _recentScrollController = ScrollController(); diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index b13fb9b7ca43..44286422cf9b 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -209,8 +209,17 @@ class _ProjectPageState extends State { if (!absolute) { final asset = await documentSystem.getAsset(location.path); if (!mounted) return; - if (location.fileType == AssetFileType.note) { - document = await checkFileChanges(context, asset); + if (asset is FileSystemFile && + location.fileType == AssetFileType.note) { + final noteData = await globalImportService.load( + document: defaultDocument, + type: widget.type.isEmpty + ? (fileType ?? widget.type) + : widget.type, + data: asset.data); + if (noteData != null) { + document = await checkFileChanges(context, noteData); + } } } else { final data = await documentSystem.loadAbsolute(location.path); diff --git a/app/pubspec.lock b/app/pubspec.lock index 9d84eb69737e..4b819eec0682 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -782,8 +782,8 @@ packages: dependency: "direct main" description: path: "packages/lw_file_system" - ref: f29b1ae0e338ec155e1c5b0a204c399232904540 - resolved-ref: f29b1ae0e338ec155e1c5b0a204c399232904540 + ref: f2e717b77237fb7cdfce6ec22f1b3c296a79d348 + resolved-ref: f2e717b77237fb7cdfce6ec22f1b3c296a79d348 url: "https://github.com/LinwoodDev/dart_pkgs" source: git version: "1.0.0" @@ -791,8 +791,8 @@ packages: dependency: transitive description: path: "packages/lw_file_system_api" - ref: b9d6c6173bf75247ce5a4d47fab0e48b730a9696 - resolved-ref: b9d6c6173bf75247ce5a4d47fab0e48b730a9696 + ref: "2036c923bb93afcefaadd8108c7e789513ae7082" + resolved-ref: "2036c923bb93afcefaadd8108c7e789513ae7082" url: "https://github.com/LinwoodDev/dart_pkgs" source: git version: "1.0.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index ee2fb3e172c1..55b63025b79d 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -99,7 +99,7 @@ dependencies: lw_file_system: git: url: https://github.com/LinwoodDev/dart_pkgs - ref: f29b1ae0e338ec155e1c5b0a204c399232904540 + ref: f2e717b77237fb7cdfce6ec22f1b3c296a79d348 path: packages/lw_file_system flutter_localized_locales: ^2.0.5 dynamic_color: ^1.7.0