fromV6 method

Future<Map<String, 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 slow on large tilesets, so it's best to offer a choice to your users as to whether they would like to migrate, or just lose 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 an empty list will use the preset OSM tile servers only.

Set deleteOldStructure to false to keep the old structure. If a store exists with the same name, it will not be overwritten, and the deleteOldStructure parameter will be followed regardless.

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 root was found, otherwise a Map of the store names to the number of failed tiles (tiles that could not be matched to any of the urlTemplates), or null if it was skipped because there was an existing store with the same name. A successful migration will have all values 0.

Implementation

Future<Map<String, int?>?> fromV6({
  required List<String> urlTemplates,
  Directory? customDirectory,
  bool deleteOldStructure = true,
}) async {
  // Prepare the migration regular expressions
  final placeholderRegex = RegExp(r'\{ *([\w_-]+) *\}');
  final matchables = [
    ...[
      'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
      ...urlTemplates,
    ].map((url) {
      final sanitised = _defaultFilesystemSanitiser(url).validOutput;

      return [
        sanitised.replaceAll('.', r'\.').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 {};

  // Prepare results map
  final Map<String, int?> results = {};

  // Migrate stores
  await for (final storeDirectory
      in oldStores.list().whereType<Directory>()) {
    final name = path.basename(storeDirectory.absolute.path);
    results[name] = 0;

    // Ignore this store if a counterpart already exists
    if (FMTC.instance(name).manage.ready) {
      results[name] = null;
      continue;
    }
    await FMTC.instance(name).manage.createAsync();
    final store = FMTCRegistry.instance(name);

    // Migrate tiles in transaction batches of 250
    await for (final List<File> tiles
        in (storeDirectory >> 'tiles').list().whereType<File>().slices(250)) {
      await store.writeTxn(
        () async => store.tiles.putAll(
          (await Future.wait(
            tiles.map(
              (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(),
                  );
                }

                results[name] = results[name]! + 1;
                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
  if (deleteOldStructure && await oldStores.exists()) {
    await oldStores.delete(recursive: true);
  }

  return results;
}