diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 07778d5ccef3..31f7fa005adb 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.6.15 + +* Endorse Windows implementation. +* Remove the need to call disablePathProviderPlatformOverride in tests + ## 1.6.14 * Update package:e2e -> package:integration_test diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 982a9f49cae5..e8d97e0106a3 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -23,14 +23,7 @@ Please see the example app of this plugin for a full example. ### Usage in tests -`path_provider` now uses a `PlatformInterface`, meaning that not all platforms share the a single `PlatformChannel`-based implementation. +`path_provider` now uses a `PlatformInterface`, meaning that not all platforms share the a single `PlatformChannel`-based implementation. With that change, tests should be updated to mock `PathProviderPlatform` rather than `PlatformChannel`. See this `path_provider` [test](https://github.com/flutter/plugins/blob/master/packages/path_provider/path_provider/test/path_provider_test.dart) for an example. - -You will also have to temporarily add the following line to the setup of your test. -```dart -disablePathProviderPlatformOverride = true; -``` - -See this [issue](https://github.com/flutter/flutter/issues/52267), for more details on why this is needed. \ No newline at end of file diff --git a/packages/path_provider/path_provider/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index ae959b133094..0fbab57700be 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -7,42 +7,37 @@ import 'dart:io' show Directory, Platform; import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:path_provider_platform_interface/src/method_channel_path_provider.dart'; export 'package:path_provider_platform_interface/path_provider_platform_interface.dart' show StorageDirectory; -/// Disables platform override in order to use a manually registered [PathProviderPlatform], only for testing right now -/// -/// Make sure to disable the override before using any of the `path_provider` methods -/// To use your own [PathProviderPlatform], make sure to include the following lines -/// ``` -/// PathProviderPlatform.instance = YourPathProviderPlatform(); -/// disablePathProviderPlatformOverride = true; -/// // Use the `path_provider` methods: -/// final dir = await getTemporaryDirectory(); -/// ``` -/// See this issue https://github.com/flutter/flutter/issues/52267 for why this is required @visibleForTesting -set disablePathProviderPlatformOverride(bool override) { - _disablePlatformOverride = override; -} +@Deprecated('This is no longer necessary, and is now a no-op') +set disablePathProviderPlatformOverride(bool override) {} -bool _disablePlatformOverride = false; -PathProviderPlatform __platform; +bool _manualDartRegistrationNeeded = true; -// This is to manually endorse the linux path provider until automatic registration of dart plugins is implemented. -// See this issue https://github.com/flutter/flutter/issues/52267 for details PathProviderPlatform get _platform { - if (__platform != null) { - return __platform; - } - if (!kIsWeb && Platform.isLinux && !_disablePlatformOverride) { - __platform = PathProviderLinux(); - } else { - __platform = PathProviderPlatform.instance; + // This is to manually endorse Dart implementations until automatic + // registration of Dart plugins is implemented. For details see + // https://github.com/flutter/flutter/issues/52267. + if (_manualDartRegistrationNeeded) { + // Only do the initial registration if it hasn't already been overridden + // with a non-default instance. + if (!kIsWeb && PathProviderPlatform.instance is MethodChannelPathProvider) { + if (Platform.isLinux) { + PathProviderPlatform.instance = PathProviderLinux(); + } else if (Platform.isWindows) { + PathProviderPlatform.instance = PathProviderWindows(); + } + } + _manualDartRegistrationNeeded = false; } - return __platform; + + return PathProviderPlatform.instance; } /// Path to the temporary directory on the device that is not backed up and is diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 7781c76331d4..982186a5354c 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -1,8 +1,7 @@ name: path_provider -description: Flutter plugin for getting commonly used locations on the Android & - iOS file systems, such as the temp and app data directories. +description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider -version: 1.6.14 +version: 1.6.15 flutter: plugin: @@ -16,6 +15,8 @@ flutter: default_package: path_provider_macos linux: default_package: path_provider_linux + windows: + default_package: path_provider_windows dependencies: flutter: @@ -23,6 +24,7 @@ dependencies: path_provider_platform_interface: ^1.0.1 path_provider_macos: ^0.0.4 path_provider_linux: ^0.0.1 + path_provider_windows: ^0.0.1 dev_dependencies: integration_test: diff --git a/packages/path_provider/path_provider/test/path_provider_test.dart b/packages/path_provider/path_provider/test/path_provider_test.dart index 90a996fd68c8..eb17178b9975 100644 --- a/packages/path_provider/path_provider/test/path_provider_test.dart +++ b/packages/path_provider/path_provider/test/path_provider_test.dart @@ -25,10 +25,6 @@ void main() { setUp(() async { PathProviderPlatform.instance = MockPathProviderPlatform(); - // This is required because we manually register the Linux path provider when on the Linux platform. - // Will be removed when automatic registration of dart plugins is implemented. - // See this issue https://github.com/flutter/flutter/issues/52267 for details - disablePathProviderPlatformOverride = true; }); test('getTemporaryDirectory', () async { diff --git a/packages/path_provider/path_provider_windows/.gitignore b/packages/path_provider/path_provider_windows/.gitignore new file mode 100644 index 000000000000..53e92cc4181f --- /dev/null +++ b/packages/path_provider/path_provider_windows/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md new file mode 100644 index 000000000000..982602c3ae1a --- /dev/null +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -0,0 +1,11 @@ +## 0.0.2 + +* README update for endorsement. +* Changed getApplicationSupportPath location. +* Removed getLibraryPath. + +## 0.0.1+2 + +* The initial implementation of path_provider for Windows + * Implements getTemporaryPath, getApplicationSupportPath, getLibraryPath, + getApplicationDocumentsPath and getDownloadsPath. diff --git a/packages/path_provider/path_provider_windows/LICENSE b/packages/path_provider/path_provider_windows/LICENSE new file mode 100644 index 000000000000..a6d6c0749818 --- /dev/null +++ b/packages/path_provider/path_provider_windows/LICENSE @@ -0,0 +1,25 @@ +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/path_provider/path_provider_windows/README.md b/packages/path_provider/path_provider_windows/README.md new file mode 100644 index 000000000000..66a05f9e7347 --- /dev/null +++ b/packages/path_provider/path_provider_windows/README.md @@ -0,0 +1,31 @@ +# path_provider_windows + +The Windows implementation of [`path_provider`][1]. + +**Please set your constraint to `path_provider_windows: '>=0.0.y+x <2.0.0'`** + +## Backward compatible 1.0.0 version is coming + +The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. +Please use `path_provider_windows: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. +For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 + +## Usage + +### Import the package + +This package has been endorsed, meaning that you only need to add `path_provider` +as a dependency in your `pubspec.yaml`. It will be automatically included in your app +when you depend on `package:path_provider`. + +This is what the above means to your `pubspec.yaml`: + +```yaml +... +dependencies: + ... + path_provider: ^1.6.15 + ... +``` + +[1]:../ diff --git a/packages/path_provider/path_provider_windows/example/.gitignore b/packages/path_provider/path_provider_windows/example/.gitignore new file mode 100644 index 000000000000..f3c205341e7d --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/path_provider/path_provider_windows/example/.metadata b/packages/path_provider/path_provider_windows/example/.metadata new file mode 100644 index 000000000000..bc654e753a99 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f2320c3b7a42bc27e7f038212eed1b01f4269641 + channel: master + +project_type: app diff --git a/packages/path_provider/path_provider_windows/example/README.md b/packages/path_provider/path_provider_windows/example/README.md new file mode 100644 index 000000000000..f3ca03ff37c3 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/README.md @@ -0,0 +1,8 @@ +# path_provider_windows_example + +Demonstrates how to use the path_provider_windows plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). diff --git a/packages/path_provider/path_provider_windows/example/lib/main.dart b/packages/path_provider/path_provider_windows/example/lib/main.dart new file mode 100644 index 000000000000..4fbb1decf2f4 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/lib/main.dart @@ -0,0 +1,93 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; + +void main() async { + runApp(MyApp()); +} + +/// Sample app +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _tempDirectory = 'Unknown'; + String _downloadsDirectory = 'Unknown'; + String _appSupportDirectory = 'Unknown'; + String _documentsDirectory = 'Unknown'; + + @override + void initState() { + super.initState(); + initDirectories(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initDirectories() async { + String tempDirectory; + String downloadsDirectory; + String appSupportDirectory; + String documentsDirectory; + final PathProviderWindows provider = PathProviderWindows(); + + try { + tempDirectory = await provider.getTemporaryPath(); + } catch (exception) { + tempDirectory = 'Failed to get temp directory: $exception'; + } + try { + downloadsDirectory = await provider.getDownloadsPath(); + } catch (exception) { + downloadsDirectory = 'Failed to get downloads directory: $exception'; + } + + try { + documentsDirectory = await provider.getApplicationDocumentsPath(); + } catch (exception) { + documentsDirectory = 'Failed to get documents directory: $exception'; + } + + try { + appSupportDirectory = await provider.getApplicationSupportPath(); + } catch (exception) { + appSupportDirectory = 'Failed to get app support directory: $exception'; + } + + setState(() { + _tempDirectory = tempDirectory; + _downloadsDirectory = downloadsDirectory; + _appSupportDirectory = appSupportDirectory; + _documentsDirectory = documentsDirectory; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Path Provider example app'), + ), + body: Center( + child: Column( + children: [ + Text('Temp Directory: $_tempDirectory\n'), + Text('Documents Directory: $_documentsDirectory\n'), + Text('Downloads Directory: $_downloadsDirectory\n'), + Text('Application Support Directory: $_appSupportDirectory\n'), + ], + ), + ), + ), + ); + } +} diff --git a/packages/path_provider/path_provider_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml new file mode 100644 index 000000000000..8dbe6e020906 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: path_provider_example +description: Demonstrates how to use the path_provider plugin. + +dependencies: + flutter: + sdk: flutter + path_provider_windows: any + +dependency_overrides: + path_provider_windows: + path: ../ + +dev_dependencies: + e2e: ^0.2.1 + flutter_driver: + sdk: flutter + test: any + pedantic: ^1.8.0 + +flutter: + uses-material-design: true diff --git a/packages/path_provider/path_provider_windows/example/test_driver/path_provider_e2e.dart b/packages/path_provider/path_provider_windows/example/test_driver/path_provider_e2e.dart new file mode 100644 index 000000000000..ee9427686026 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/test_driver/path_provider_e2e.dart @@ -0,0 +1,53 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:e2e/e2e.dart'; + +void main() { + E2EWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + final String result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + final String result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + final String result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getDownloadsDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + final String result = await provider.getDownloadsPath(); + _verifySampleFile(result, 'downloads'); + }); +} + +/// Verify a file called [name] in [directoryPath] by recreating it with test +/// contents when necessary. +void _verifySampleFile(String directoryPath, String name) { + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_windows/example/test_driver/path_provider_e2e_test.dart b/packages/path_provider/path_provider_windows/example/test_driver/path_provider_e2e_test.dart new file mode 100644 index 000000000000..f3aa9e218d82 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/test_driver/path_provider_e2e_test.dart @@ -0,0 +1,15 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String result = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + exit(result == 'pass' ? 0 : 1); +} diff --git a/packages/path_provider/path_provider_windows/lib/folders.dart b/packages/path_provider/path_provider_windows/lib/folders.dart new file mode 100644 index 000000000000..fc2ea8351476 --- /dev/null +++ b/packages/path_provider/path_provider_windows/lib/folders.dart @@ -0,0 +1,240 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:win32/win32.dart'; + +/// A class containing the GUID references for each of the documented Windows +/// known folders. A property of this class may be passed to the `getPath` +/// method in the [PathProvidersWindows] class to retrieve a known folder from +/// Windows. +class WindowsKnownFolder { + /// The file system directory that is used to store administrative tools for + /// an individual user. The MMC will save customized consoles to this + /// directory, and it will roam with the user. + static String get AdminTools => FOLDERID_AdminTools; + + /// The file system directory that acts as a staging area for files waiting to + /// be written to a CD. A typical path is C:\Documents and + /// Settings\username\Local Settings\Application Data\Microsoft\CD Burning. + static String get CDBurning => FOLDERID_CDBurning; + + /// The file system directory that contains administrative tools for all users + /// of the computer. + static String get CommonAdminTools => FOLDERID_CommonAdminTools; + + /// The file system directory that contains the directories for the common + /// program groups that appear on the Start menu for all users. A typical path + /// is C:\Documents and Settings\All Users\Start Menu\Programs. + static String get CommonPrograms => FOLDERID_CommonPrograms; + + /// The file system directory that contains the programs and folders that + /// appear on the Start menu for all users. A typical path is C:\Documents and + /// Settings\All Users\Start Menu. + static String get CommonStartMenu => FOLDERID_CommonStartMenu; + + /// The file system directory that contains the programs that appear in the + /// Startup folder for all users. A typical path is C:\Documents and + /// Settings\All Users\Start Menu\Programs\Startup. + static String get CommonStartup => FOLDERID_CommonStartup; + + /// The file system directory that contains the templates that are available + /// to all users. A typical path is C:\Documents and Settings\All + /// Users\Templates. + static String get CommonTemplates => FOLDERID_CommonTemplates; + + /// The virtual folder that represents My Computer, containing everything on + /// the local computer: storage devices, printers, and Control Panel. The + /// folder can also contain mapped network drives. + static String get ComputerFolder => FOLDERID_ComputerFolder; + + /// The virtual folder that represents Network Connections, that contains + /// network and dial-up connections. + static String get ConnectionsFolder => FOLDERID_ConnectionsFolder; + + /// The virtual folder that contains icons for the Control Panel applications. + static String get ControlPanelFolder => FOLDERID_ControlPanelFolder; + + /// The file system directory that serves as a common repository for Internet + /// cookies. A typical path is C:\Documents and Settings\username\Cookies. + static String get Cookies => FOLDERID_Cookies; + + /// The virtual folder that represents the Windows desktop, the root of the + /// namespace. + static String get Desktop => FOLDERID_Desktop; + + /// The virtual folder that represents the My Documents desktop item. + static String get Documents => FOLDERID_Documents; + + /// The file system directory that serves as a repository for Internet + /// downloads. + static String get Downloads => FOLDERID_Downloads; + + /// The file system directory that serves as a common repository for the + /// user's favorite items. A typical path is C:\Documents and + /// Settings\username\Favorites. + static String get Favorites => FOLDERID_Favorites; + + /// A virtual folder that contains fonts. A typical path is C:\Windows\Fonts. + static String get Fonts => FOLDERID_Fonts; + + /// The file system directory that serves as a common repository for Internet + /// history items. + static String get History => FOLDERID_History; + + /// The file system directory that serves as a common repository for temporary + /// Internet files. A typical path is C:\Documents and Settings\username\Local + /// Settings\Temporary Internet Files. + static String get InternetCache => FOLDERID_InternetCache; + + /// A virtual folder for Internet Explorer. + static String get InternetFolder => FOLDERID_InternetFolder; + + /// The file system directory that serves as a data repository for local + /// (nonroaming) applications. A typical path is C:\Documents and + /// Settings\username\Local Settings\Application Data. + static String get LocalAppData => FOLDERID_LocalAppData; + + /// The file system directory that serves as a common repository for music + /// files. A typical path is C:\Documents and Settings\User\My Documents\My + /// Music. + static String get Music => FOLDERID_Music; + + /// A file system directory that contains the link objects that may exist in + /// the My Network Places virtual folder. A typical path is C:\Documents and + /// Settings\username\NetHood. + static String get NetHood => FOLDERID_NetHood; + + /// The folder that represents other computers in your workgroup. + static String get NetworkFolder => FOLDERID_NetworkFolder; + + /// The file system directory that serves as a common repository for image + /// files. A typical path is C:\Documents and Settings\username\My + /// Documents\My Pictures. + static String get Pictures => FOLDERID_Pictures; + + /// The file system directory that contains the link objects that can exist in + /// the Printers virtual folder. A typical path is C:\Documents and + /// Settings\username\PrintHood. + static String get PrintHood => FOLDERID_PrintHood; + + /// The virtual folder that contains installed printers. + static String get PrintersFolder => FOLDERID_PrintersFolder; + + /// The user's profile folder. A typical path is C:\Users\username. + /// Applications should not create files or folders at this level. + static String get Profile => FOLDERID_Profile; + + /// The file system directory that contains application data for all users. A + /// typical path is C:\Documents and Settings\All Users\Application Data. This + /// folder is used for application data that is not user specific. For + /// example, an application can store a spell-check dictionary, a database of + /// clip art, or a log file in the CSIDL_COMMON_APPDATA folder. This + /// information will not roam and is available to anyone using the computer. + static String get ProgramData => FOLDERID_ProgramData; + + /// The Program Files folder. A typical path is C:\Program Files. + static String get ProgramFiles => FOLDERID_ProgramFiles; + + /// The common Program Files folder. A typical path is C:\Program + /// Files\Common. + static String get ProgramFilesCommon => FOLDERID_ProgramFilesCommon; + + /// On 64-bit systems, a link to the common Program Files folder. A typical path is + /// C:\Program Files\Common Files. + static String get ProgramFilesCommonX64 => FOLDERID_ProgramFilesCommonX64; + + /// On 64-bit systems, a link to the 32-bit common Program Files folder. A + /// typical path is C:\Program Files (x86)\Common Files. On 32-bit systems, a + /// link to the Common Program Files folder. + static String get ProgramFilesCommonX86 => FOLDERID_ProgramFilesCommonX86; + + /// On 64-bit systems, a link to the Program Files folder. A typical path is + /// C:\Program Files. + static String get ProgramFilesX64 => FOLDERID_ProgramFilesX64; + + /// On 64-bit systems, a link to the 32-bit Program Files folder. A typical + /// path is C:\Program Files (x86). On 32-bit systems, a link to the Common + /// Program Files folder. + static String get ProgramFilesX86 => FOLDERID_ProgramFilesX86; + + /// The file system directory that contains the user's program groups (which + /// are themselves file system directories). + static String get Programs => FOLDERID_Programs; + + /// The file system directory that contains files and folders that appear on + /// the desktop for all users. A typical path is C:\Documents and Settings\All + /// Users\Desktop. + static String get PublicDesktop => FOLDERID_PublicDesktop; + + /// The file system directory that contains documents that are common to all + /// users. A typical path is C:\Documents and Settings\All Users\Documents. + static String get PublicDocuments => FOLDERID_PublicDocuments; + + /// The file system directory that serves as a repository for music files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Music. + static String get PublicMusic => FOLDERID_PublicMusic; + + /// The file system directory that serves as a repository for image files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Pictures. + static String get PublicPictures => FOLDERID_PublicPictures; + + /// The file system directory that serves as a repository for video files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Videos. + static String get PublicVideos => FOLDERID_PublicVideos; + + /// The file system directory that contains shortcuts to the user's most + /// recently used documents. A typical path is C:\Documents and + /// Settings\username\My Recent Documents. + static String get Recent => FOLDERID_Recent; + + /// The virtual folder that contains the objects in the user's Recycle Bin. + static String get RecycleBinFolder => FOLDERID_RecycleBinFolder; + + /// The file system directory that contains resource data. A typical path is + /// C:\Windows\Resources. + static String get ResourceDir => FOLDERID_ResourceDir; + + /// The file system directory that serves as a common repository for + /// application-specific data. A typical path is C:\Documents and + /// Settings\username\Application Data. + static String get RoamingAppData => FOLDERID_RoamingAppData; + + /// The file system directory that contains Send To menu items. A typical path + /// is C:\Documents and Settings\username\SendTo. + static String get SendTo => FOLDERID_SendTo; + + /// The file system directory that contains Start menu items. A typical path + /// is C:\Documents and Settings\username\Start Menu. + static String get StartMenu => FOLDERID_StartMenu; + + /// The file system directory that corresponds to the user's Startup program + /// group. The system starts these programs whenever the associated user logs + /// on. A typical path is C:\Documents and Settings\username\Start + /// Menu\Programs\Startup. + static String get Startup => FOLDERID_Startup; + + /// The Windows System folder. A typical path is C:\Windows\System32. + static String get System => FOLDERID_System; + + /// The 32-bit Windows System folder. On 32-bit systems, this is typically + /// C:\Windows\system32. On 64-bit systems, this is typically + /// C:\Windows\syswow64. + static String get SystemX86 => FOLDERID_SystemX86; + + /// The file system directory that serves as a common repository for document + /// templates. A typical path is C:\Documents and Settings\username\Templates. + static String get Templates => FOLDERID_Templates; + + /// The file system directory that serves as a common repository for video + /// files. A typical path is C:\Documents and Settings\username\My + /// Documents\My Videos. + static String get Videos => FOLDERID_Videos; + + /// The Windows directory or SYSROOT. This corresponds to the %windir% or + /// %SYSTEMROOT% environment variables. A typical path is C:\Windows. + static String get Windows => FOLDERID_Windows; +} diff --git a/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart b/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart new file mode 100644 index 000000000000..e29b70a714d9 --- /dev/null +++ b/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart @@ -0,0 +1,222 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:path_provider_windows/folders.dart'; +import 'package:win32/win32.dart'; + +/// Wraps the Win32 VerQueryValue API call. +/// +/// This class exists to allow injecting alternate metadata in tests without +/// building multiple custom test binaries. +@visibleForTesting +class VersionInfoQuerier { + /// Returns the value for [key] in [versionInfo]s English strings section, or + /// null if there is no such entry, or if versionInfo is null. + getStringValue(Pointer versionInfo, key) { + if (versionInfo == null) { + return null; + } + const kEnUsLanguageCode = '040904e4'; + final keyPath = TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key'); + final length = allocate(); + final valueAddress = allocate(); + try { + if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) { + return null; + } + return Pointer.fromAddress(valueAddress.value) + .unpackString(length.value); + } finally { + free(keyPath); + free(length); + free(valueAddress); + } + } +} + +/// The Windows implementation of [PathProviderPlatform] +/// +/// This class implements the `package:path_provider` functionality for Windows. +class PathProviderWindows extends PathProviderPlatform { + /// The object to use for performing VerQueryValue calls. + @visibleForTesting + VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier(); + + /// This is typically the same as the TMP environment variable. + @override + Future getTemporaryPath() async { + final buffer = allocate(count: MAX_PATH + 1).cast(); + String path; + + try { + final length = GetTempPath(MAX_PATH, buffer); + + if (length == 0) { + final error = GetLastError(); + throw WindowsException(error); + } else { + path = buffer.unpackString(length); + + // GetTempPath adds a trailing backslash, but SHGetKnownFolderPath does + // not. Strip off trailing backslash for consistency with other methods + // here. + if (path.endsWith('\\')) { + path = path.substring(0, path.length - 1); + } + } + + // Ensure that the directory exists, since GetTempPath doesn't. + final directory = Directory(path); + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + + return Future.value(path); + } finally { + free(buffer); + } + } + + @override + Future getApplicationSupportPath() async { + final appDataRoot = await getPath(WindowsKnownFolder.RoamingAppData); + final directory = Directory( + path.join(appDataRoot, _getApplicationSpecificSubdirectory())); + // Ensure that the directory exists if possible, since it will on other + // platforms. If the name is longer than MAXPATH, creating will fail, so + // skip that step; it's up to the client to decide what to do with the path + // in that case (e.g., using a short path). + if (directory.path.length <= MAX_PATH) { + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + } + return directory.path; + } + + @override + Future getApplicationDocumentsPath() => + getPath(WindowsKnownFolder.Documents); + + @override + Future getDownloadsPath() => getPath(WindowsKnownFolder.Downloads); + + /// Retrieve any known folder from Windows. + /// + /// folderID is a GUID that represents a specific known folder ID, drawn from + /// [WindowsKnownFolder]. + Future getPath(String folderID) { + final pathPtrPtr = allocate(); + Pointer pathPtr; + + try { + GUID knownFolderID = GUID.fromString(folderID); + + final hr = SHGetKnownFolderPath( + knownFolderID.addressOf, KF_FLAG_DEFAULT, NULL, pathPtrPtr); + + if (FAILED(hr)) { + if (hr == E_INVALIDARG || hr == E_FAIL) { + throw WindowsException(hr); + } + } + + pathPtr = Pointer.fromAddress(pathPtrPtr.value); + final path = pathPtr.unpackString(MAX_PATH); + return Future.value(path); + } finally { + CoTaskMemFree(pathPtr.cast()); + free(pathPtrPtr); + } + } + + /// Returns the relative path string to append to the root directory returned + /// by Win32 APIs for application storage (such as RoamingAppDir) to get a + /// directory that is unique to the application. + /// + /// The convention is to use company-name\product-name\. This will use that if + /// possible, using the data in the VERSIONINFO resource, with the following + /// fallbacks: + /// - If the company name isn't there, that component will be dropped. + /// - If the product name isn't there, it will use the exe's filename (without + /// extension). + String _getApplicationSpecificSubdirectory() { + String companyName; + String productName; + + final Pointer moduleNameBuffer = + allocate(count: MAX_PATH + 1).cast(); + final Pointer unused = allocate(); + Pointer infoBuffer; + try { + // Get the module name. + final moduleNameLength = GetModuleFileName(0, moduleNameBuffer, MAX_PATH); + if (moduleNameLength == 0) { + final error = GetLastError(); + throw WindowsException(error); + } + + // From that, load the VERSIONINFO resource + int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused); + if (infoSize != 0) { + infoBuffer = allocate(count: infoSize); + if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) == + 0) { + free(infoBuffer); + infoBuffer = null; + } + } + companyName = _sanitizedDirectoryName( + versionInfoQuerier.getStringValue(infoBuffer, 'CompanyName')); + productName = _sanitizedDirectoryName( + versionInfoQuerier.getStringValue(infoBuffer, 'ProductName')); + + // If there was no product name, use the executable name. + if (productName == null) { + productName = path.basenameWithoutExtension( + moduleNameBuffer.unpackString(moduleNameLength)); + } + + return companyName != null + ? path.join(companyName, productName) + : productName; + } finally { + free(moduleNameBuffer); + free(unused); + if (infoBuffer != null) { + free(infoBuffer); + } + } + } + + /// Makes [rawString] safe as a directory component. See + /// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions + /// + /// If after sanitizing the string is empty, returns null. + String _sanitizedDirectoryName(String rawString) { + if (rawString == null) { + return null; + } + String sanitized = rawString + // Replace banned characters. + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') + // Remove trailing whitespace. + .trimRight() + // Ensure that it does not end with a '.'. + .replaceAll(RegExp(r'[.]+$'), ''); + const kMaxComponentLength = 255; + if (sanitized.length > kMaxComponentLength) { + sanitized = sanitized.substring(0, kMaxComponentLength); + } + return sanitized.isEmpty ? null : sanitized; + } +} diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml new file mode 100644 index 000000000000..54d5a5190ddf --- /dev/null +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -0,0 +1,29 @@ +name: path_provider_windows +description: Windows implementation of the path_provider plugin +homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows +version: 0.0.2 + +flutter: + plugin: + platforms: + windows: + dartPluginClass: PathProviderWindows + + +dependencies: + path_provider_platform_interface: ^1.0.3 + meta: ^1.0.5 + path: ^1.6.4 + flutter: + sdk: flutter + ffi: ^0.1.3 + win32: ^1.7.1 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.8.0 + +environment: + sdk: ">=2.1.0 <3.0.0" + flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart new file mode 100644 index 000000000000..83ceea9cdf0c --- /dev/null +++ b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart @@ -0,0 +1,109 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:ffi'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; + +// A fake VersionInfoQuerier that just returns preset responses. +class FakeVersionInfoQuerier implements VersionInfoQuerier { + FakeVersionInfoQuerier(this.responses); + + final Map responses; + + getStringValue(Pointer versionInfo, key) => responses[key]; +} + +void main() { + test('getTemporaryPath', () async { + final pathProvider = PathProviderWindows(); + expect(await pathProvider.getTemporaryPath(), contains(r'C:\')); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with no version info', () async { + final pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = + FakeVersionInfoQuerier({}); + final path = await pathProvider.getApplicationSupportPath(); + expect(path, contains(r'C:\')); + expect(path, contains(r'AppData')); + // The last path component should be the executable name. + expect(path, endsWith(r'flutter_tester')); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with full version info', () async { + final pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': 'Amazing App', + }); + final path = await pathProvider.getApplicationSupportPath(); + expect(path, endsWith(r'AppData\Roaming\A Company\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with missing company', () async { + final pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'ProductName': 'Amazing App', + }); + final path = await pathProvider.getApplicationSupportPath(); + expect(path, endsWith(r'AppData\Roaming\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with problematic values', () async { + final pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': r'A Company: Name.', + 'ProductName': r'A"/Terrible\|App?*Name', + }); + final path = await pathProvider.getApplicationSupportPath(); + expect( + path, + endsWith(r'AppData\Roaming\' + r'A _Bad_ Company_ Name\' + r'A__Terrible__App__Name')); + expect(Directory(path).existsSync(), isTrue); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with a completely invalid company', () async { + final pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': r'..', + 'ProductName': r'Amazing App', + }); + final path = await pathProvider.getApplicationSupportPath(); + expect(path, endsWith(r'AppData\Roaming\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with very long app name', () async { + final pathProvider = PathProviderWindows(); + final truncatedName = 'A' * 255; + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': truncatedName * 2, + }); + final path = await pathProvider.getApplicationSupportPath(); + expect(path, endsWith('\\$truncatedName')); + // The directory won't exist, since it's longer than MAXPATH, so don't check + // that here. + }, skip: !Platform.isWindows); + + test('getApplicationDocumentsPath', () async { + final pathProvider = PathProviderWindows(); + final path = await pathProvider.getApplicationDocumentsPath(); + expect(path, contains(r'C:\')); + expect(path, contains(r'Documents')); + }, skip: !Platform.isWindows); + + test('getDownloadsPath', () async { + final pathProvider = PathProviderWindows(); + final path = await pathProvider.getDownloadsPath(); + expect(path, contains(r'C:\')); + expect(path, contains(r'Downloads')); + }, skip: !Platform.isWindows); +}