feat(Maps): automatically download base assets if missing

This commit is contained in:
Jake Turner 2026-01-28 04:49:01 +00:00 committed by Jake Turner
parent e7336f2a8e
commit c8de767052
4 changed files with 54 additions and 23 deletions

View File

@ -13,7 +13,7 @@ export default class MapsController {
constructor(private mapService: MapService) {}
async index({ inertia }: HttpContext) {
const baseAssetsCheck = await this.mapService.checkBaseAssetsExist()
const baseAssetsCheck = await this.mapService.ensureBaseAssets()
const regionFiles = await this.mapService.listRegions()
return inertia.render('maps', {
maps: {
@ -23,11 +23,6 @@ export default class MapsController {
})
}
async checkBaseAssets({}: HttpContext) {
const exists = await this.mapService.checkBaseAssetsExist()
return { exists }
}
async downloadBaseAssets({ request }: HttpContext) {
const payload = await request.validateUsing(remoteDownloadValidatorOptional)
await this.mapService.downloadBaseAssets(payload.url)
@ -75,6 +70,15 @@ export default class MapsController {
}
async styles({ response }: HttpContext) {
// Automatically ensure base assets are present before generating styles
const baseAssetsExist = await this.mapService.ensureBaseAssets()
if (!baseAssetsExist) {
return response.status(500).send({
message:
'Base map assets are missing and could not be downloaded. Please check your connection and try again.',
})
}
const styles = await this.mapService.generateStylesJSON()
return response.json(styles)
}

View File

@ -37,7 +37,7 @@ export default class SettingsController {
}
async maps({ inertia }: HttpContext) {
const baseAssetsCheck = await this.mapService.checkBaseAssetsExist();
const baseAssetsCheck = await this.mapService.ensureBaseAssets();
const regionFiles = await this.mapService.listRegions();
return inertia.render('settings/maps', {
maps: {

View File

@ -9,7 +9,6 @@ import { extract } from 'tar'
import env from '#start/env'
import {
listDirectoryContentsRecursive,
listDirectoryContents,
getFileStatsIfExists,
deleteFileIfExists,
getFile,
@ -50,6 +49,7 @@ export class MapService implements IMapService {
private readonly basemapsAssetsDir = 'basemaps-assets'
private readonly baseAssetsTarFile = 'base-assets.tar.gz'
private readonly baseDirPath = join(process.cwd(), this.mapStoragePath)
private baseAssetsExistCache: boolean | null = null
async listRegions() {
const files = (await this.listAllMapStorageItems()).filter(
@ -93,6 +93,9 @@ export class MapService implements IMapService {
await deleteFileIfExists(tempTarPath)
// Invalidate cache since we just downloaded new assets
this.baseAssetsExistCache = true
return true
}
@ -170,6 +173,15 @@ export class MapService implements IMapService {
const filepath = join(process.cwd(), this.mapStoragePath, 'pmtiles', filename)
// First, ensure base assets are present - regions depend on them
const baseAssetsExist = await this.ensureBaseAssets()
if (!baseAssetsExist) {
throw new Error(
'Base map assets are missing and could not be downloaded. Please check your connection and try again.'
)
}
// Dispatch background job
const result = await RunDownloadJob.dispatch({
url,
@ -250,18 +262,6 @@ export class MapService implements IMapService {
return styles
}
async checkBaseAssetsExist() {
const storageContents = await this.listMapStorageItems()
const baseStyleItem = storageContents.find(
(item) => item.type === 'file' && item.name === this.baseStylesFile
)
const basemapsAssetsItem = storageContents.find(
(item) => item.type === 'directory' && item.name === this.basemapsAssetsDir
)
return !!baseStyleItem && !!basemapsAssetsItem
}
async listCuratedCollections(): Promise<CuratedCollectionWithStatus[]> {
const collections = await CuratedCollection.query().where('type', 'map').preload('resources')
return collections.map((collection) => ({
@ -303,9 +303,37 @@ export class MapService implements IMapService {
}
}
private async listMapStorageItems(): Promise<FileEntry[]> {
async ensureBaseAssets(): Promise<boolean> {
const exists = await this.checkBaseAssetsExist()
if (exists) {
return true
}
return await this.downloadBaseAssets()
}
private async checkBaseAssetsExist(useCache: boolean = true): Promise<boolean> {
// Return cached result if available and caching is enabled
if (useCache && this.baseAssetsExistCache !== null) {
return this.baseAssetsExistCache
}
await ensureDirectoryExists(this.baseDirPath)
return await listDirectoryContents(this.baseDirPath)
const baseStylePath = join(this.baseDirPath, this.baseStylesFile)
const basemapsAssetsPath = join(this.baseDirPath, this.basemapsAssetsDir)
const [baseStyleExists, basemapsAssetsExists] = await Promise.all([
getFileStatsIfExists(baseStylePath),
getFileStatsIfExists(basemapsAssetsPath),
])
const exists = !!baseStyleExists && !!basemapsAssetsExists
// update cache
this.baseAssetsExistCache = exists
return exists
}
private async listAllMapStorageItems(): Promise<FileEntry[]> {

View File

@ -60,7 +60,6 @@ router
.group(() => {
router.get('/regions', [MapsController, 'listRegions'])
router.get('/styles', [MapsController, 'styles'])
router.get('/preflight', [MapsController, 'checkBaseAssets'])
router.get('/curated-collections', [MapsController, 'listCuratedCollections'])
router.post('/fetch-latest-collections', [MapsController, 'fetchLatestCollections'])
router.post('/download-base-assets', [MapsController, 'downloadBaseAssets'])