Skip to content

Commit 199cd21

Browse files
RangerMauveMauve Signweaverachou11
authored
Fix/export extensions (#1039)
* fix: Exported attachments should have file extensions * fix: Only use observation position if lat and lon are null * fix: catch error constructing BlobId for unknown attachment type * chore: Account for blob metadata better * chore: Mark project.export*Stream APIs as private * Update src/mapeo-project.js Co-authored-by: Andrew Chou <[email protected]> * Update src/mapeo-project.js Co-authored-by: Andrew Chou <[email protected]> * fix: skip exporting attachments with unknown mime types * chore: tidy feature coordinate creation, thanks @achou11 * chore: add geojson types to devdeps * Update src/mapeo-project.js Co-authored-by: Andrew Chou <[email protected]> * chore: allow file exports without extensions, fix ts errors * chore: remove unused import from merge --------- Co-authored-by: Mauve Signweaver <[email protected]> Co-authored-by: Andrew Chou <[email protected]>
1 parent 0d236e9 commit 199cd21

File tree

4 files changed

+191
-151
lines changed

4 files changed

+191
-151
lines changed

package-lock.json

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"@types/bogon": "^1.0.2",
119119
"@types/compact-encoding": "^2.15.0",
120120
"@types/debug": "^4.1.8",
121+
"@types/geojson": "^7946.0.16",
121122
"@types/json-schema": "^7.0.11",
122123
"@types/json-stable-stringify": "^1.0.36",
123124
"@types/nanobench": "^3.0.0",
@@ -190,7 +191,7 @@
190191
"json-stable-stringify": "^1.1.1",
191192
"magic-bytes.js": "^1.10.0",
192193
"map-obj": "^5.0.2",
193-
"mime": "^4.0.3",
194+
"mime": "^4.0.7",
194195
"multi-core-indexer": "^1.0.0",
195196
"p-defer": "^4.0.0",
196197
"p-event": "^6.0.1",

src/mapeo-project.js

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { discoveryKey } from 'hypercore-crypto'
66
import { TypedEmitter } from 'tiny-typed-emitter'
77
import ZipArchive from 'zip-stream-promise'
88
import * as b4a from 'b4a'
9+
import mime from 'mime/lite'
910
// @ts-expect-error
1011
import { Readable, pipelinePromise } from 'streamx'
1112

@@ -69,6 +70,10 @@ import { createWriteStream } from 'fs'
6970
/** @typedef {Omit<ProjectSettingsValue, 'schemaName'>} EditableProjectSettings */
7071
/** @typedef {ProjectSettingsValue['configMetadata']} ConfigMetadata */
7172
/** @typedef {Map<string,Attachment>} SeenAttachments*/
73+
/** @typedef {object} BlobRef
74+
* @prop {string|undefined} mimeType
75+
* @prop {BlobId} blobId
76+
*/
7277

7378
const CORESTORE_STORAGE_FOLDER_NAME = 'corestore'
7479
const INDEXER_STORAGE_FOLDER_NAME = 'indexer'
@@ -83,8 +88,6 @@ export const kClearDataIfLeft = Symbol('clear data if left project')
8388
export const kSetIsArchiveDevice = Symbol('set isArchiveDevice')
8489
export const kIsArchiveDevice = Symbol('isArchiveDevice (temp - test only)')
8590
export const kGeoJSONFileName = Symbol('geoJSONFileName')
86-
export const kExportGeoJSONStream = Symbol('exportGeoJSONStream')
87-
export const kExportZipStream = Symbol('exportZipStream')
8891

8992
const EMPTY_PROJECT_SETTINGS = Object.freeze({})
9093

@@ -786,40 +789,43 @@ export class MapeoProject extends TypedEmitter {
786789
}
787790
}
788791

789-
let latitude = lat
790-
let longitude = lon
791-
let altitude = null
792-
const position = observation?.metadata?.position?.coords
793-
if (position) {
794-
latitude = position.latitude
795-
longitude = position.longitude
796-
if (position.altitude !== undefined) {
797-
altitude = position.altitude
792+
const metadataCoords = observation.metadata?.position?.coords
793+
const altitude = metadataCoords?.altitude
794+
795+
/** @type {[number, number] | [number, number, number] | null} */
796+
let coordinates = null
797+
798+
// Prioritize using the observation's `lat` and `lon` fields
799+
if (typeof lat === 'number' && typeof lon === 'number') {
800+
coordinates =
801+
typeof altitude === 'number' ? [lon, lat, altitude] : [lon, lat]
802+
} else {
803+
// Fall back to using the observation metadata's position if possible
804+
if (
805+
typeof metadataCoords?.latitude === 'number' &&
806+
typeof metadataCoords?.longitude === 'number'
807+
) {
808+
coordinates =
809+
typeof altitude === 'number'
810+
? [metadataCoords.longitude, metadataCoords.latitude, altitude]
811+
: [metadataCoords.longitude, metadataCoords.latitude]
798812
}
799813
}
800814

801-
const coordinates = [longitude, latitude]
802-
if (typeof altitude === 'number') {
803-
coordinates.push(altitude)
815+
/** @type {import('geojson').Feature<import('geojson').Point | null>} */
816+
const feature = {
817+
type: 'Feature',
818+
properties: observation,
819+
geometry: coordinates
820+
? {
821+
type: 'Point',
822+
coordinates,
823+
}
824+
: null,
804825
}
805-
const hasLatLon =
806-
typeof longitude === 'number' && typeof latitude === 'number'
807-
const geometry = hasLatLon
808-
? {
809-
type: 'Point',
810-
coordinates,
811-
}
812-
: null
813826
const comma = first ? '' : ','
814827
first = false
815-
yield b4a.from(
816-
`${comma}\n ` +
817-
JSON.stringify({
818-
type: 'Feature',
819-
properties: observation,
820-
geometry,
821-
})
822-
)
828+
yield b4a.from(`${comma}\n ` + JSON.stringify(feature))
823829
}
824830
}
825831

@@ -941,7 +947,7 @@ export class MapeoProject extends TypedEmitter {
941947
* @param {string} [options.lang]
942948
* @returns {Readable<Buffer | Uint8Array>}
943949
*/
944-
[kExportGeoJSONStream]({
950+
#exportGeoJSONStream({
945951
observations = true,
946952
tracks = true,
947953
lang,
@@ -1027,7 +1033,7 @@ export class MapeoProject extends TypedEmitter {
10271033
) {
10281034
const fileName = await this[kGeoJSONFileName](observations, tracks)
10291035
const filePath = path.join(exportFolder, fileName)
1030-
const source = this[kExportGeoJSONStream]({ observations, tracks, lang })
1036+
const source = this.#exportGeoJSONStream({ observations, tracks, lang })
10311037
const sink = createWriteStream(filePath)
10321038
await pipelinePromise(source, sink)
10331039

@@ -1036,15 +1042,37 @@ export class MapeoProject extends TypedEmitter {
10361042

10371043
/**
10381044
* @param {Attachment} attachment
1039-
* @returns {Promise<null | BlobId>}
1045+
* @returns {Promise<null | BlobRef>}
10401046
*/
1041-
async #tryGetBlobId(attachment) {
1047+
async #tryGetAttachmentBlob(attachment) {
10421048
// Audio must not have variants
10431049
for (const variant of VARIANT_EXPORT_ORDER) {
1044-
const blobId = buildBlobId(attachment, variant)
1045-
const entry = await this.#blobStore.entry(blobId)
1046-
if (!entry) continue
1047-
return blobId
1050+
try {
1051+
const blobId = buildBlobId(attachment, variant)
1052+
const entry = await this.#blobStore.entry(blobId)
1053+
if (!entry) continue
1054+
const metadata = entry.value.metadata
1055+
if (!metadata || typeof metadata !== 'object') continue
1056+
let mimeType = undefined
1057+
if ('mimeType' in metadata) {
1058+
if (typeof metadata.mimeType === 'string') {
1059+
mimeType = metadata.mimeType
1060+
} else {
1061+
this.#l.log('Invalid type for mimeType in blob', blobId, entry)
1062+
continue
1063+
}
1064+
}
1065+
return { blobId, mimeType }
1066+
} catch (e) {
1067+
if (!(e instanceof Error)) throw e
1068+
this.#l.log(
1069+
'Error loading blob id for attachment',
1070+
attachment,
1071+
variant,
1072+
e.message
1073+
)
1074+
continue
1075+
}
10481076
}
10491077

10501078
return null
@@ -1066,7 +1094,7 @@ export class MapeoProject extends TypedEmitter {
10661094
// GeoJSON
10671095
const geoJSONFileName = await this[kGeoJSONFileName](observations, tracks)
10681096
const seenAttachments = new Map()
1069-
const geoJSONStream = this[kExportGeoJSONStream]({
1097+
const geoJSONStream = this.#exportGeoJSONStream({
10701098
observations,
10711099
tracks,
10721100
lang,
@@ -1079,16 +1107,31 @@ export class MapeoProject extends TypedEmitter {
10791107
const missingAttachments = []
10801108
// Attachments
10811109
if (attachments) {
1082-
const mediaFolder = this.#exportPrefix('Media') + '/'
1110+
const mediaFolder = (await this.#exportPrefix('Media')) + '/'
10831111
for (const attachment of seenAttachments.values()) {
1084-
const blobId = await this.#tryGetBlobId(attachment)
1085-
if (blobId === null) {
1112+
const ref = await this.#tryGetAttachmentBlob(attachment)
1113+
if (ref === null) {
10861114
missingAttachments.push(attachment)
10871115
continue
10881116
}
10891117

1118+
const { blobId, mimeType } = ref
1119+
let extensionString = ''
1120+
if (mimeType) {
1121+
const extension = mime.getExtension(mimeType)
1122+
if (extension) {
1123+
extensionString = '.' + extension
1124+
} else {
1125+
this.#l.log('Got unknown mime type in attachment blob', attachment)
1126+
}
1127+
}
1128+
10901129
const stream = this.#blobStore.createReadStream(blobId)
1091-
const name = mediaFolder + blobId.variant + '/' + attachment.name
1130+
const name = path.posix.join(
1131+
mediaFolder,
1132+
blobId.variant,
1133+
`${attachment.name}${extensionString}`
1134+
)
10921135

10931136
// @ts-expect-error
10941137
await archive.entry(stream, { name })
@@ -1118,7 +1161,7 @@ export class MapeoProject extends TypedEmitter {
11181161
* @param {string} [options.lang]
11191162
* @returns {Readable<Buffer | Uint8Array>}
11201163
*/
1121-
[kExportZipStream]({
1164+
#exportZipStream({
11221165
observations = true,
11231166
tracks = true,
11241167
attachments = true,
@@ -1153,7 +1196,7 @@ export class MapeoProject extends TypedEmitter {
11531196
) {
11541197
const fileName = await this.#zipFileName(observations, tracks)
11551198
const filePath = path.join(exportFolder, fileName)
1156-
const source = this[kExportZipStream]({
1199+
const source = this.#exportZipStream({
11571200
observations,
11581201
tracks,
11591202
attachments,

0 commit comments

Comments
 (0)