fromV6 method Null safety
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;
}