From f779c631425225d72e00dc0e611974b53cab2dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20B=C3=A4hr?= Date: Fri, 22 Mar 2024 11:21:57 +0100 Subject: [PATCH] Improvement: There should be a simple function to check if a file exists in the cloud. Fix: Function getAllFiles for Google Drive should return more than 100 files. --- CHANGELOG.md | 8 +- example/lib/cloud_storage_demo.dart | 181 +++++++++++------- example/lib/main.dart | 27 +-- .../Flutter/GeneratedPluginRegistrant.swift | 2 + example/pubspec.lock | 74 ++++--- .../cloud_storage_service.dart | 16 +- .../interface/cloud_file.dart | 3 + .../interface/cloud_service.dart | 7 +- .../google_drive/google_drive_file.dart | 5 +- .../google_drive/google_drive_service.dart | 84 +++++--- lib/fl_cloud_storage/vendor/storage_type.dart | 2 + pubspec.yaml | 6 +- 12 files changed, 251 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f04a49e..1d7f53f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ +## 0.0.16 + +* Improvement: There should be a simple function to check if a file exists in the cloud. +* Fix: Function getAllFiles for Google Drive should return more than 100 files +* Refactor: Update libraries google_sign_in and googleapis + ## 0.0.15 -* Login at web should work +* Fix: Login at web should work ## 0.0.13 diff --git a/example/lib/cloud_storage_demo.dart b/example/lib/cloud_storage_demo.dart index 11338e6..bd57e9b 100644 --- a/example/lib/cloud_storage_demo.dart +++ b/example/lib/cloud_storage_demo.dart @@ -41,7 +41,6 @@ class _GoogleDriveDemoState extends State { body: FutureBuilder( future: service, builder: (context, AsyncSnapshot snapshot) { - // FIXME google_sign_in_web switch (snapshot.connectionState) { case ConnectionState.none: case ConnectionState.waiting: @@ -70,7 +69,6 @@ class _GoogleDriveDemoState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, @@ -95,64 +93,40 @@ class _GoogleDriveDemoState extends State { ], ), FutureBuilder( - future: Future.value(svc.getAllFiles()), + future: Future.value(svc.getAllFiles( + ignoreTrashedFiles: false, + )), builder: (context, snapshot) { if (snapshot.hasData) { List files = snapshot.data!; return Column( children: [ Text('Amount of files: ${files.length}'), - Container( - height: 500, - padding: - const EdgeInsets.symmetric(horizontal: 15), - child: GridView.count( - crossAxisCount: 2, - crossAxisSpacing: 15, - mainAxisSpacing: 15, - children: files - .map((e) => Card( - downloadFn: ({required file}) async { - final newfile = await svc - .downloadFile(file: file); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text( - 'Downloaded content'), - content: Text(newfile - ?.content ?? - 'Unable to download file'), - actions: [ - TextButton( - style: - TextButton.styleFrom( - textStyle: - Theme.of(context) - .textTheme - .labelLarge, - ), - child: const Text('Ok'), - onPressed: () { - Navigator.of(context) - .pop(); - }, - ), - ], - ); - }, - ); - }, - deleteFn: ({required file}) async { - await svc.deleteFile(file: file); - // fixme refresh view! - setState(() {}); - }, - file: e, - )) - .toList(), - ), + Wrap( + runSpacing: 16, + spacing: 16, + children: files + .map((e) => Card( + downloadFn: ({required file}) => + onDownloadFile( + cloudStorageService: svc, + file: file, + ), + deleteFn: ( + {required CloudFile + file}) => + onDelete( + cloudStorageService: svc, + file: file, + ), + checkIfExistsFn: ({required file}) => + checkIfExists( + cloudStorageService: svc, + file: file, + ), + file: e, + )) + .toList(), ), ], ); @@ -160,21 +134,25 @@ class _GoogleDriveDemoState extends State { return const CircularProgressIndicator(); }, ), - OutlinedButton( - onPressed: () async { - final List bytes = - utf8.encode('Das Wandern ist des Müllers Lust.'); - final file = GoogleDriveFile( - fileId: null, - fileName: 'wandern.txt', - description: 'Über das Wandern', - parents: [], - bytes: bytes, - ); - await svc.uploadFile(file: file); - setState(() {}); // refresh view - }, - child: const Text('Upload a random.txt file'), + const Expanded(child: SizedBox.shrink()), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: OutlinedButton( + onPressed: () async { + final List bytes = + utf8.encode('Das Wandern ist des Müllers Lust.'); + final file = GoogleDriveFile( + fileId: null, + fileName: 'wandern.txt', + description: 'Über das Wandern', + parents: [], + bytes: bytes, + ); + await svc.uploadFile(file: file); + setState(() {}); // refresh view + }, + child: const Text('Upload a random.txt file'), + ), ), ], ); @@ -183,6 +161,55 @@ class _GoogleDriveDemoState extends State { ), ); } + + Future onDownloadFile({ + required CloudStorageService cloudStorageService, + required CloudFile file, + }) async { + final newFile = await cloudStorageService.downloadFile(file: file); + if (mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Downloaded content'), + content: Text(newFile?.content ?? 'Unable to download file'), + actions: [ + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + child: const Text('Ok'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + } + + Future checkIfExists({ + required CloudStorageService cloudStorageService, + required CloudFile file, + }) async { + final scaffoldMessenger = ScaffoldMessenger.of(context); + final doesFileExist = await cloudStorageService.doesFileExist(file: file); + scaffoldMessenger.showSnackBar(SnackBar( + content: Text('doesFileExist: $doesFileExist'), + )); + } + + Future onDelete({ + required CloudStorageService cloudStorageService, + required CloudFile file, + }) async { + await cloudStorageService.deleteFile(file: file); + // fixme refresh view! + setState(() {}); + } } class Card extends StatelessWidget { @@ -196,26 +223,32 @@ class Card extends StatelessWidget { required CloudFile file, }) deleteFn; + final Future Function({ + required CloudFile file, + }) checkIfExistsFn; + const Card({ Key? key, required this.file, required this.downloadFn, required this.deleteFn, + required this.checkIfExistsFn, }) : super(key: key); @override Widget build(BuildContext context) { return Container( - width: 75, - height: 75, padding: const EdgeInsets.all(10), decoration: BoxDecoration( border: Border.all(), borderRadius: BorderRadius.circular(10), ), child: Column( + mainAxisSize: MainAxisSize.min, children: [ Text(file.file.name), + if (file.trashed) + Text('Trashed', style: Theme.of(context).textTheme.bodySmall), OutlinedButton( onPressed: () async { await downloadFn(file: file); @@ -231,6 +264,12 @@ class Card extends StatelessWidget { }, child: const Text('Delete'), ), + OutlinedButton( + onPressed: () async { + await checkIfExistsFn(file: file); + }, + child: const Text('Check if a file exists in the cloud'), + ), ], ), ); diff --git a/example/lib/main.dart b/example/lib/main.dart index 09cd586..0644751 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -41,34 +41,37 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { + final storageTypeOptions = StorageType.values + .map>( + (StorageType storageType) => DropdownMenuItem( + key: Key(storageType.name), + value: storageType, + child: Text(storageType.name), + ), + ) + .toList(); return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: Text('${widget.title} - Login'), ), body: Column( children: [ const SizedBox(height: 50), const Center( - child: Text('fl_cloud_storage'), + child: Text('1. Select your vendor'), ), - const SizedBox(height: 25), DropdownButton( value: selection, - items: StorageType.values - .map>( - (StorageType e) => DropdownMenuItem( - key: Key(e.name), - value: e, - child: Text(e.name), - ), - ) - .toList(), + items: storageTypeOptions, onChanged: (StorageType? value) { setState(() { selection = value; }); }, ), + const Center( + child: Text('2. Select the scope'), + ), if (selection == StorageType.GOOGLE_DRIVE) DropdownButton( value: driveScope, diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..82f2a45 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import google_sign_in_ios func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) } diff --git a/example/pubspec.lock b/example/pubspec.lock index f3fc9f0..d7edc5b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -63,7 +63,7 @@ packages: path: ".." relative: true source: path - version: "0.0.14" + version: "0.0.16" flutter: dependency: "direct main" description: flutter @@ -73,10 +73,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -91,66 +91,66 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "7940fdc3b1035db4d65d387c1bdd6f9574deaa6777411569c05ecc25672efacd" + sha256: "9482364c9f8b7bd36902572ebc3a7c2b5c8ee57a9c93e6eb5099c1a9ec5265d8" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.3.1+1" google_sign_in: dependency: transitive description: name: google_sign_in - sha256: "776a4c988dc179c3b8e9201de0ad61bf350a4e75d378ff9d94c76880378c7bca" + sha256: "0b8787cb9c1a68ad398e8010e8c8766bfa33556d2ab97c439fb4137756d7308f" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.1" google_sign_in_android: dependency: transitive description: name: google_sign_in_android - sha256: e89179df2bbcc5988c0e2e98d1c95d471fe7910d323a15b5569ac6eddf085b95 + sha256: "38cef11ed22fc0c9bfa7b00bf7f2d91012138f5613089522917fd26c853be93e" url: "https://pub.dev" source: hosted - version: "6.1.15" + version: "6.1.22" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: "6ec0e13a4c5c646471b9f6a25ceb3ae76d339889d4c0f79b729bf0714215a63e" + sha256: a7d653803468d30b82ceb47ea00fe86d23c56e63eb2e5c2248bb68e9df203217 url: "https://pub.dev" source: hosted - version: "5.6.2" + version: "5.7.4" google_sign_in_platform_interface: dependency: transitive description: name: google_sign_in_platform_interface - sha256: e69553c0fc6a76216e9d06a8c3767e291ad9be42171f879aab7ab708569d4393 + sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.5" google_sign_in_web: dependency: transitive description: name: google_sign_in_web - sha256: "69b9ce0e760945ff52337921a8b5871592b74c92f85e7632293310701eea68cc" + sha256: fc0f14ed45ea616a6cfb4d1c7534c2221b7092cc4f29a709f0c3053cc3e821bd url: "https://pub.dev" source: hosted - version: "0.12.0+2" + version: "0.12.4" googleapis: dependency: transitive description: name: googleapis - sha256: d02ede69d06f408ed929c615cafeb96fabb2a836432e0a576f96157aafa96278 + sha256: "4eefba93b5f714d6c2bcc1695ff6fb09d36684d2a06912285d3981069796da10" url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "13.1.0" http: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: "4c3f04bfb64d3efd508d06b41b825542f08122d30bda4933fb95c069d22a4fa3" url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.0.0" http_parser: dependency: transitive description: @@ -159,14 +159,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" json_annotation: dependency: "direct dev" description: @@ -211,10 +203,10 @@ packages: dependency: transitive description: name: logger - sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7 + sha256: b3ff55aeb08d9d8901b767650285872cb1bb8f508373b3e348d60268b0c7f770 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "2.1.0" matcher: dependency: transitive description: @@ -251,18 +243,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.4" - quiver: - dependency: transitive - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.dev" - source: hosted - version: "3.2.1" + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -340,6 +324,14 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/lib/fl_cloud_storage/cloud_storage_service.dart b/lib/fl_cloud_storage/cloud_storage_service.dart index eaac694..1dcf853 100644 --- a/lib/fl_cloud_storage/cloud_storage_service.dart +++ b/lib/fl_cloud_storage/cloud_storage_service.dart @@ -8,7 +8,7 @@ import 'package:logger/logger.dart'; /// Root logger. final Logger log = Logger( printer: MyPrinter('FL_CLOUD_STORAGE'), - level: Level.verbose, + level: Level.info, ); /// This class is the entrypoint for the fl_cloud_storage package. It is a @@ -76,6 +76,14 @@ class CloudStorageService { /// Invokes the [authorize] method of the delegate instance. FutureOr authorize() => _delegate.authorize(); + Future doesFileExist({ + required CloudFile file, + bool ignoreTrashedFiles = true, + }) async { + return _delegate.doesFileExist( + file: file, ignoreTrashedFiles: ignoreTrashedFiles); + } + /// Invokes the [deleteFile] method of the delegate instance. FutureOr deleteFile({ required CloudFile file, @@ -141,9 +149,13 @@ class CloudStorageService { /// Invokes the [getAllFiles] method of the delegate instance. FutureOr>> getAllFiles({ CloudFolder? folder, + bool ignoreTrashedFiles = true, }) { try { - return _delegate.getAllFiles(folder: folder); + return _delegate.getAllFiles( + folder: folder, + ignoreTrashedFiles: ignoreTrashedFiles, + ); } catch (ex) { log.e(ex); } diff --git a/lib/fl_cloud_storage/interface/cloud_file.dart b/lib/fl_cloud_storage/interface/cloud_file.dart index ba830a0..1c9f36c 100644 --- a/lib/fl_cloud_storage/interface/cloud_file.dart +++ b/lib/fl_cloud_storage/interface/cloud_file.dart @@ -18,4 +18,7 @@ abstract class CloudFile { /// Unique id of this file. Can be null if the file is not uploaded yet. /// Be aware, that a file without [fileId] cannot be used to download it from cloud. String? get fileId; + + /// Returns true, if this file was trashed and is inside the cloud bin. + bool get trashed; } diff --git a/lib/fl_cloud_storage/interface/cloud_service.dart b/lib/fl_cloud_storage/interface/cloud_service.dart index 5ac2e99..b2f168d 100644 --- a/lib/fl_cloud_storage/interface/cloud_service.dart +++ b/lib/fl_cloud_storage/interface/cloud_service.dart @@ -35,8 +35,13 @@ abstract class ICloudService, // FILES + /// Check if file exists in cloud. + Future doesFileExist( + {required FILE file, bool ignoreTrashedFiles = true}); + /// List all files or those of a folder if not null - Future> getAllFiles({FOLDER? folder}); + Future> getAllFiles( + {FOLDER? folder, bool ignoreTrashedFiles = true}); /// Create or update a file /// This method is meant to be idempotent but must not lead to data loss when diff --git a/lib/fl_cloud_storage/vendor/google_drive/google_drive_file.dart b/lib/fl_cloud_storage/vendor/google_drive/google_drive_file.dart index 521ca71..0f73c6b 100644 --- a/lib/fl_cloud_storage/vendor/google_drive/google_drive_file.dart +++ b/lib/fl_cloud_storage/vendor/google_drive/google_drive_file.dart @@ -11,7 +11,7 @@ class GoogleDriveFile implements CloudFile { drive.Media? media, this.fileContent, this.mimeType, - this.trashed, + this.trashed = false, }) : _media = media, super(); @@ -35,7 +35,8 @@ class GoogleDriveFile implements CloudFile { /// Optional value. Some text that is stored in the metadata of the file. final String? description; - final bool? trashed; + @override + final bool trashed; @override drive.File get file { diff --git a/lib/fl_cloud_storage/vendor/google_drive/google_drive_service.dart b/lib/fl_cloud_storage/vendor/google_drive/google_drive_service.dart index f4bc526..a5d6c7e 100644 --- a/lib/fl_cloud_storage/vendor/google_drive/google_drive_service.dart +++ b/lib/fl_cloud_storage/vendor/google_drive/google_drive_service.dart @@ -157,6 +157,23 @@ class GoogleDriveService // FILES + @override + Future doesFileExist( + {required GoogleDriveFile file, bool ignoreTrashedFiles = true}) async { + if (_driveApi == null) { + return false; + } + + try { + final v3.File? cloudFile = + await _driveApi!.files.get(file.file.id!) as v3.File?; + return cloudFile != null && + (!ignoreTrashedFiles || !(cloudFile.trashed == true)); + } catch (e) { + return false; + } + } + @override Future deleteFile({required GoogleDriveFile file}) async { if (_driveApi == null) { @@ -276,44 +293,49 @@ class GoogleDriveService } @override - Future> getAllFiles({GoogleDriveFolder? folder}) async { + Future> getAllFiles( + {GoogleDriveFolder? folder, bool ignoreTrashedFiles = true}) async { if (_driveApi == null) { throw Exception('DriveApi is null, unable to get all files.'); } + final List result = []; + // Completes with a commons.ApiRequestError if the API endpoint returned an error - final v3.FileList res; - if (folder == null) { - res = await _driveApi!.files.list( - $fields: 'files/*', - q: 'trashed=false', - ); - } else { - res = await _driveApi!.files.list( - $fields: 'files/*', - q: "'${folder.folder.id}' in parents and trashed=false", - ); - } - if (res.nextPageToken != null) { - // TODO complete the files list - } + v3.FileList? res; + do { + // Complete the files list, otherwise maximum 100 files are returned + if (folder == null) { + res = await _driveApi!.files.list( + $fields: 'files/*', + q: 'trashed=${!ignoreTrashedFiles}', + ); + } else { + res = await _driveApi!.files.list( + $fields: 'files/*', + q: "'${folder.folder.id}' in parents and trashed=${!ignoreTrashedFiles}", + ); + } + + for (final v3.File file in res.files!) { + final driveFile = GoogleDriveFile( + fileId: file.id, + fileName: file.name!, + parents: file.parents, + description: file.description, + mimeType: file.mimeType, + trashed: (file.trashed ?? false) || (file.explicitlyTrashed ?? false), + bytes: null, + ); + result.add(driveFile); + } + } while (res.nextPageToken != null); + if (res.files == null) { throw Exception('Unable to list all files!'); } - return res.files! - .map( - (file) => GoogleDriveFile( - fileId: file.id, - fileName: file.name!, - parents: file.parents, - description: file.description, - mimeType: file.mimeType, - trashed: - (file.trashed ?? false) || (file.explicitlyTrashed ?? false), - bytes: null, - ), - ) - .toList(); + + return result; } // FOLDERS @@ -413,4 +435,4 @@ class GoogleDriveService ? GoogleDriveFolder(folder: res.files![0]) : null; } -} \ No newline at end of file +} diff --git a/lib/fl_cloud_storage/vendor/storage_type.dart b/lib/fl_cloud_storage/vendor/storage_type.dart index a5c1175..aa63d82 100644 --- a/lib/fl_cloud_storage/vendor/storage_type.dart +++ b/lib/fl_cloud_storage/vendor/storage_type.dart @@ -12,6 +12,8 @@ enum StorageType { // add your cloud provider enum here, e.g. Dropbox const StorageType({required this.name, required this.supportedPlatforms}); + final String name; + final Set supportedPlatforms; } diff --git a/pubspec.yaml b/pubspec.yaml index 1765a7c..37ac2ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fl_cloud_storage description: Store files in the cloud from Flutter apps. In the first step we only support Google Drive but adding other clouds is much appreciated. -version: 0.0.15 +version: 0.0.16 homepage: https://github.com/ehwplus/fl_cloud_storage environment: @@ -11,8 +11,8 @@ dependencies: flutter: sdk: flutter google_sign_in: '>=5.4.3 <=6.2.1' - googleapis: '>=9.2.0 <=11.2.0' - http: '>=0.11.3 <=0.13.6' + googleapis: '>=9.2.0 <=13.1.0' + http: '>=0.11.3 <=1.0.0' json_annotation: '>=4.7.0 <=4.8.1' logger: '>=1.1.0 <=2.1.0'