refactor(server): move the KML importer and place-image helpers into places/ (#1832)

Two of the three files in be-places-helpers-fold. placePhotoCache is left for
its own PR — it holds three module-level caches and its sweep hangs off the
cron, so it needs both a provider with real state and a second entry in the
scheduler contract.

kmlImport.ts is 212 lines with no state, no database and no I/O — parsing,
sanitising and coordinate arithmetic. It is nest/places/kml-import.helpers.ts
now, unchanged, next to the places.helpers.ts that already consumes it.

placeImage.ts moves to nest/places/place-image.ts as free functions. Making
reclaimPlaceImage a provider would have split it from PLACE_IMAGES_DIR, which
cannot be injected: nest/common/place-image-upload.ts reads it at module scope
to configure multer's storage, before any container exists.

That constant is the one thing here that could have broken quietly:

  path.resolve(__dirname, '../../uploads/places')

__dirname moves with the file. From src/services that resolved to
<server>/uploads/places; from src/nest/places the same string would have
resolved to <server>/src/uploads/places — a directory multer would have happily
created, leaving every uploaded place image in the wrong place and every
existing one unreachable. The path is now '../../../uploads/places', verified to
resolve to the identical absolute directory, and uploads-dirs.test.ts covers it.
This commit is contained in:
Maurice
2026-08-07 19:58:12 +02:00
committed by GitHub
parent 90e35e6970
commit 4e6b45016a
11 changed files with 12 additions and 12 deletions
@@ -31,7 +31,7 @@ import { ADDON_IDS } from '../../addons';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { PLACE_IMAGE_UPLOAD } from '../common/place-image-upload';
import { placeImageUrl } from '../../services/placeImage';
import { placeImageUrl } from '../places/place-image';
import {
CollectionCreateDto,
CollectionUpdateDto,
@@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import { PermissionsService } from '../permissions/permissions.service';
import { RealtimeService } from '../realtime/realtime.service';
import { reclaimPlaceImage } from '../../services/placeImage';
import { reclaimPlaceImage } from '../places/place-image';
import type {
Collection,
CollectionDetailResponse,
+1 -1
View File
@@ -3,7 +3,7 @@ import path from 'path';
import { diskStorage } from 'multer';
import { v4 as uuidv4 } from 'uuid';
import type { Request } from 'express';
import { PLACE_IMAGES_DIR } from '../../services/placeImage';
import { PLACE_IMAGES_DIR } from '../places/place-image';
const MAX_PLACE_IMAGE_SIZE = 20 * 1024 * 1024; // 20 MB — same cap as covers.
@@ -1,11 +1,11 @@
import fs from 'fs';
import path from 'path';
import { db } from '../db/database';
import { db } from '../../db/database';
// Custom place images (the user-uploaded thumbnails from #1136) live in their own
// uploads subdir, served statically like covers. Storing the full /uploads/places/…
// path in image_url lets every existing thumbnail renderer show it unchanged.
export const PLACE_IMAGES_DIR = path.resolve(__dirname, '../../uploads/places');
export const PLACE_IMAGES_DIR = path.resolve(__dirname, '../../../uploads/places');
const URL_PREFIX = '/uploads/places/';
export function placeImageUrl(filename: string): string {
+1 -1
View File
@@ -25,7 +25,7 @@ import { isUpdateConflict } from '../common/conflictResult';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';
import { PLACE_IMAGE_UPLOAD } from '../common/place-image-upload';
import { placeImageUrl } from '../../services/placeImage';
import { placeImageUrl } from './place-image';
import {
PlaceBulkDeleteDto,
PlaceBulkUpdateDto,
+1 -1
View File
@@ -2,7 +2,7 @@ import { XMLParser } from 'fast-xml-parser';
import unzipper from 'unzipper';
import type { Place } from '../../types';
import type { PlaceWithTags } from '../database/database.service';
import type { KmlImportSummary } from '../../services/kmlImport';
import type { KmlImportSummary } from './kml-import.helpers';
import * as placePhotoCache from '../../services/placePhotoCache';
/**
+2 -2
View File
@@ -18,10 +18,10 @@ import {
extractKmlPlacemarkNodes,
parsePlacemarkNode,
resolveCategoryIdForFolder,
} from '../../services/kmlImport';
} from './kml-import.helpers';
import { UnsplashService } from '../unsplash/unsplash.service';
import { type UpdateConflict, isUpdateConflict } from '../common/conflictResult';
import { reclaimPlaceImage } from '../../services/placeImage';
import { reclaimPlaceImage } from './place-image';
import { onPlaceCreated, onPlaceUpdated, onPlaceDeleted } from '../../services/journeyService';
import {
COORD_DEDUP_TOLERANCE,
@@ -54,7 +54,7 @@ import { PermissionsService } from '../../../src/nest/permissions/permissions.se
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
import { CollectionsService } from '../../../src/nest/collections/collections.service';
import { removeIfUnreferenced } from '../../../src/services/placePhotoCache';
import { PLACE_IMAGES_DIR } from '../../../src/services/placeImage';
import { PLACE_IMAGES_DIR } from '../../../src/nest/places/place-image';
const svc = new CollectionsService(new DatabaseService(testDb), new PermissionsService(new DatabaseService(testDb)), new RealtimeService());
@@ -70,7 +70,7 @@ import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
import { PlacesService } from '../../../src/nest/places/places.service';
import { MapsService } from '../../../src/nest/maps/maps.service';
import { QueryHelpersService } from '../../../src/nest/query-helpers/query-helpers.service';
import { PLACE_IMAGES_DIR } from '../../../src/services/placeImage';
import { PLACE_IMAGES_DIR } from '../../../src/nest/places/place-image';
const GPX_FIXTURE = path.join(__dirname, '../../fixtures/test.gpx');
const KML_FIXTURE = path.join(__dirname, '../../fixtures/test.kml');
@@ -7,7 +7,7 @@ import {
parsePlacemarkNode,
resolveCategoryIdForFolder,
sanitizeKmlDescription,
} from '../../../src/services/kmlImport';
} from '../../../../src/nest/places/kml-import.helpers';
describe('kmlImportUtils', () => {
it('sanitizes HTML descriptions with br to newline', () => {