fromV6 method Null safety

Future<int?> fromV6(
  1. {required List<String>? urlTemplates,
  2. Directory? customDirectory,
  3. bool deleteOldStructure = true}
)

Migrates a v6 file structure to a v7 structure

Note that this method can be inefficient on large tilesets, so it's best to offer a choice to your users as to whether they would like to migrate, or just loose all stored tiles.

Checks within getApplicationDocumentsDirectory() and getTemporaryDirectory() for a directory named 'fmtc'. Alternatively, specify a customDirectory to search for 'fmtc' within.

In order to migrate the tiles to the new format, urlTemplates must be used. Pass every URL template used to store any of the tiles that might be in the store. Specifying null will use the preset OSM tile server only.

Set deleteOldStructure to false to keep the old structure.

Only supports placeholders in the normal flutter_map form, those that meet the RegEx: \{ *([\w_-]+) *\}. Only supports tiles that were sanitised with the default sanitiser included in FMTC.

Recovery information and cached statistics will be lost.

Returns null if no structure was found or migration failed, otherwise the number of tiles that could not be matched to any of the urlTemplates. A fully sucessful migration will return 0.

Implementation

Future<int?> fromV6({
  required List<String>? urlTemplates,
  Directory? customDirectory,
  bool deleteOldStructure = true,
}) async {
  final placeholderRegex = RegExp(r'\{ *([\w_-]+) *\}');

  final List<List<String>> matchables = [
    ...[
      'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
      ...urlTemplates ?? [],
    ].map((url) {
      final sanitised = _filesystemSanitiseValidate(
        inputString: url,
        throwIfInvalid: false,
      );
      return [
        sanitised.replaceAll(placeholderRegex, '(.*?)'),
        sanitised,
        url,
      ];
    }),
  ];

  // Search for the previous structure
  final Directory normal =
      (await getApplicationDocumentsDirectory()) >> 'fmtc';
  final Directory temporary = (await getTemporaryDirectory()) >> 'fmtc';
  final Directory? custom =
      customDirectory == null ? null : customDirectory >> 'fmtc';
  final Directory? root = await normal.exists()
      ? normal
      : await temporary.exists()
          ? temporary
          : custom == null
              ? null
              : await custom.exists()
                  ? custom
                  : null;
  if (root == null) return null;

  // Delete recovery files and cached statistics
  if (deleteOldStructure) {
    final oldRecovery = root >> 'recovery';
    if (await oldRecovery.exists()) await oldRecovery.delete(recursive: true);
    final oldStats = root >> 'stats';
    if (await oldStats.exists()) await oldStats.delete(recursive: true);
  }

  // Don't continue migration if there are no stores
  final oldStores = root >> 'stores';
  if (!await oldStores.exists()) return null;

  // Migrate stores
  int failedTiles = 0;
  await for (final storeDirectory
      in oldStores.list().whereType<Directory>()) {
    final store = FMTCRegistry.instance.storeDatabases[await FMTC
        .instance(path.basename(storeDirectory.absolute.path))
        .manage
        ._advancedCreate()]!;

    // Migrate tiles
    await store.writeTxn(
      () async => store.tiles.putAll(
        await (storeDirectory >> 'tiles')
            .list()
            .whereType<File>()
            .asyncMap(
              (f) async {
                final filename = path.basename(f.absolute.path);
                final Map<String, String> placeholderValues = {};

                for (final e in matchables) {
                  if (!RegExp('^${e[0]}\$', multiLine: true)
                      .hasMatch(filename)) {
                    continue;
                  }

                  String filenameChangable = filename;
                  List<String> filenameSplit = filename.split('')..add('');

                  for (final match in placeholderRegex.allMatches(e[1])) {
                    final templateValue =
                        e[1].substring(match.start, match.end);
                    final afterChar = (e[1].split('')..add(''))[match.end];

                    final memory = StringBuffer();
                    int i = match.start;
                    for (; filenameSplit[i] != afterChar; i++) {
                      memory.write(filenameSplit[i]);
                    }
                    filenameChangable = filenameChangable.replaceRange(
                      match.start,
                      i,
                      templateValue,
                    );
                    filenameSplit = filenameChangable.split('')..add('');

                    placeholderValues[templateValue.substring(
                      1,
                      templateValue.length - 1,
                    )] = memory.toString();
                  }

                  return DbTile(
                    url:
                        TileLayer().templateFunction(e[2], placeholderValues),
                    bytes: await f.readAsBytes(),
                  );
                }

                failedTiles++;
                return null;
              },
            )
            .whereNotNull()
            .toList(),
      ),
    );

    // Migrate metadata
    await store.writeTxn(
      () async => store.metadata.putAll(
        await (storeDirectory >> 'metadata')
            .list()
            .whereType<File>()
            .asyncMap(
              (f) async => DbMetadata(
                name: path.basename(f.absolute.path).split('.metadata')[0],
                data: await f.readAsString(),
              ),
            )
            .toList(),
      ),
    );
  }

  // Delete store files
  await oldStores.delete(recursive: true);

  return failedTiles;
}