YehudaKremer / msix

Create Msix installer for flutter windows-build files.
https://pub.dev/packages/msix
MIT License
280 stars 70 forks source link

[FEATURE REQUEST] Specify the path to be removed on uninstall #78

Closed arielt closed 2 years ago

arielt commented 2 years ago

:speech_balloon: Description

Application may create housekeeping files in folders like Documents. It can be useful to have a configuration (path) that will be cleared up by Windows when uninstalling an application.

:question: Platform

Windows

YehudaKremer commented 2 years ago

Hello @arielt

One of the strongest points of MSIX is total isolation by virtualize the file system.

After the app is installed, windows creates a virtual folder and files tree (copy of the real folders and files), and the installed app can work (read/write) only with this virtual tree.

After the app is uninstalled, the all virtual tree is deleted.

I created a small app to illustrate those points, its create new folder 'MyFolder', and create random files in it. image

  1. create msix installer for this app, and install it -> the app show that "MyFolder" not exist yet
  2. click on "Create My Folder" button, then spam-click the "create random files" button -> the app show the location of the created folder and its content (remember that the folder location is not 'real', you cant manually open the path, only the app can use this virtual location)
  3. close and reopen the app -> the app show the last created folder and files
  4. uninstall and reinstall the app - the app show that the folder and its content are deleted (uninstalling deletes the virtual file system)

The app code:

import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';

String myFolderPath = '${Platform.environment['AppData']}\\MyFolder';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool isMyFolderExists = false;
  List<String> files = [];

  _MyHomePageState() {
    checkMyFolder();
  }

  Future<void> createMyFolder() async {
    await Directory(myFolderPath).create();
    await checkMyFolder();
  }

  Future<void> deleteMyFolder() async {
    await Directory(myFolderPath).delete(recursive: true);
    await checkMyFolder();
  }

  Future<void> createRandomFileInMyFolder() async {
    final String fileName = '${Random().nextInt(100)}';
    final File file = File('$myFolderPath\\$fileName.txt');
    await file.create();
    await checkMyFolder();
  }

  Future<void> checkMyFolder() async {
    bool myFolderExists = await Directory(myFolderPath).exists();
    List<String> myFolderFileNames = [];

    if (myFolderExists) {
      var myFolderEntities = await Directory(myFolderPath).list().toList();
      myFolderFileNames = myFolderEntities.map((e) => e.path).toList();
    }

    setState(() {
      isMyFolderExists = myFolderExists;
      files = myFolderFileNames;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: createMyFolder,
                  child: const Text(
                    'Create My Folder',
                  ),
                ),
                Container(width: 10),
                isMyFolderExists
                    ? ElevatedButton(
                        onPressed: deleteMyFolder,
                        child: const Text(
                          'Delete My Folder',
                        ),
                      )
                    : const Text(''),
                Container(width: 10),
                isMyFolderExists
                    ? ElevatedButton(
                        onPressed: createRandomFileInMyFolder,
                        child: const Text(
                          'create random file in my folder',
                        ),
                      )
                    : const Text(''),
              ],
            ),
            Text(
              isMyFolderExists
                  ? 'is my folder exists in: $myFolderPath'
                  : 'my folder not exists in: $myFolderPath',
              style: TextStyle(
                color: isMyFolderExists ? Colors.green : Colors.red,
              ),
            ),
            Column(
              children: files.map((e) => Text(e)).toList(),
            ),
          ],
        ),
      ),
    );
  }
}
arielt commented 2 years ago

@YehudaKremer thank you for the quick reply.

I was referring to the Documents folder because that's what Flutter documentation mentions: https://docs.flutter.dev/cookbook/persistence/reading-writing-files -getApplicationDocumentsDirectory()

In your example, you are using Windows' %APPDATA% which would be mapped to something like C:\Users\xxx\AppData\Roaming in Debug or C:\Users\xxx\AppData\Local\Packages\Company.Suite.App\LocalCache\Roaming in Release.

Theoretically, those folders are subject to synchronization between computers.

Do you know if using something like %LOCALAPPDATA% is fine from MSIX perspective, so it would be cleaned up properly?

YehudaKremer commented 2 years ago

You welcome @arielt

On my pc, the "AppData" map to the same path in both debug and release 👍:

String myFolderPath = '${Platform.environment['AppData']}\\MyFolder';

image

i checked the "LOCALAPPDATA" environment variable, its the same in debug and release, and its cleanup after uninstalling just like "APPDATA" 👍 :

String myFolderPath = '${Platform.environment['LOCALAPPDATA']}\\MyFolder';

image

About the "path_provider" package: getApplicationDocumentsDirectory working with the "Documents" folder (the real one, not a virtual) so no cleanup here after uninstalling 👎, even with getApplicationSupportDirectory; image for some reason there is NO CLEANUP after uninstalling👎

arielt commented 2 years ago

Thanks @YehudaKremer %LOCALAPPDATA% seems to work for me, deleted properly on uninstall.

The location is still different in debug and release modes: C:\Users\xxx\AppData\Local vs. C:\Users\xxx\AppData\Local\Packages\Company.Suite.App\LocalCache\LocalCache\Local in Release. Not a problem for me though.