UNPKG

5.23 kBPlain TextView Raw
1import archiver from "archiver"
2import {execFile} from "child_process"
3import {createReadStream} from "fs"
4import {tmpdir} from "os"
5import {extname, join} from "path"
6import {Readable, Stream} from "stream"
7
8type JSONLike = Record<string, unknown>
9type RunOutput = {stdout: string; stderr: string}
10
11type Input = string | JSONLike | Stream
12interface Result {
13 cmd: string
14 text: string
15 data?: JSONLike
16 stream?: Readable
17 extname?: string
18 details: string
19}
20type Callback = (err: Error | null, res?: Result) => void
21interface Options {
22 command?: string
23 format?: string
24 options?: string[]
25 destination?: string
26 env?: Record<string, string>
27 timeout?: number
28 maxBuffer?: number
29}
30
31// Known /vsistdout/ support.
32const stdoutRe = /csv|geojson|georss|gml|gmt|gpx|jml|kml|mapml|pdf|vdv/i
33const vsiStdIn = "/vsistdin/"
34const vsiStdOut = "/vsistdout/"
35
36let uniq = Date.now()
37
38class Ogr2ogr implements PromiseLike<Result> {
39 private inputStream?: Readable
40 private inputPath: string
41 private outputPath: string
42 private outputFormat: string
43 private outputExt: string
44 private customCommand?: string
45 private customOptions?: string[]
46 private customDestination?: string
47 private customEnv?: Record<string, string>
48 private timeout: number
49 private maxBuffer: number
50
51 constructor(input: Input, opts: Options = {}) {
52 this.inputPath = vsiStdIn
53 this.outputFormat = opts.format ?? "GeoJSON"
54 this.customCommand = opts.command
55 this.customOptions = opts.options
56 this.customDestination = opts.destination
57 this.customEnv = opts.env
58 this.timeout = opts.timeout ?? 0
59 this.maxBuffer = opts.maxBuffer ?? 1024 * 1024 * 50
60
61 let {path, ext} = this.newOutputPath(this.outputFormat)
62 this.outputPath = path
63 this.outputExt = ext
64
65 if (input instanceof Readable) {
66 this.inputStream = input
67 } else if (typeof input === "string") {
68 this.inputPath = this.newInputPath(input)
69 } else {
70 this.inputStream = Readable.from([JSON.stringify(input)])
71 }
72 }
73
74 exec(cb: Callback) {
75 this.run()
76 .then((res) => cb(null, res))
77 .catch((err) => cb(err))
78 }
79
80 then<TResult1 = Result, TResult2 = never>(
81 onfulfilled?: (value: Result) => TResult1 | PromiseLike<TResult1>,
82 onrejected?: (reason: string) => TResult2 | PromiseLike<TResult2>,
83 ): PromiseLike<TResult1 | TResult2> {
84 return this.run().then(onfulfilled, onrejected)
85 }
86
87 private newInputPath(p: string): string {
88 let path = ""
89 let ext = extname(p)
90
91 switch (ext) {
92 case ".zip":
93 case ".kmz":
94 case ".shz":
95 path = "/vsizip/"
96 break
97 case ".gz":
98 path = "/vsigzip/"
99 break
100 case ".tar":
101 path = "/vsitar/"
102 break
103 }
104
105 if (/^(http|ftp)/.test(p)) {
106 path += "/vsicurl/" + p
107 return path
108 }
109
110 path += p
111 return path
112 }
113
114 private newOutputPath(f: string) {
115 let ext = "." + f.toLowerCase()
116
117 if (stdoutRe.test(this.outputFormat)) {
118 return {path: vsiStdOut, ext}
119 }
120
121 let path = join(tmpdir(), "/ogr_" + uniq++)
122
123 switch (f.toLowerCase()) {
124 case "esri shapefile":
125 path += ".shz"
126 ext = ".shz"
127 break
128 case "mapinfo file":
129 case "flatgeobuf":
130 ext = ".zip"
131 break
132 default:
133 path += ext
134 }
135
136 return {path, ext}
137 }
138
139 private createZipStream(p: string) {
140 let archive = archiver("zip")
141 archive.directory(p, false)
142 archive.on("error", console.error)
143 archive.finalize()
144 return archive
145 }
146
147 private async run() {
148 let command = this.customCommand ?? "ogr2ogr"
149 let args = [
150 "-f",
151 this.outputFormat,
152 "-skipfailures",
153 this.customDestination || this.outputPath,
154 this.inputPath,
155 ]
156 if (this.customOptions) args.push(...this.customOptions)
157 let env = this.customEnv ? {...process.env, ...this.customEnv} : undefined
158
159 let {stdout, stderr} = await new Promise<RunOutput>((res, rej) => {
160 let proc = execFile(
161 command,
162 args,
163 {env, timeout: this.timeout, maxBuffer: this.maxBuffer},
164 (err, stdout, stderr) => {
165 if (err) rej(err)
166 res({stdout, stderr})
167 },
168 )
169 if (this.inputStream && proc.stdin) this.inputStream.pipe(proc.stdin)
170 })
171
172 let res: Result = {
173 cmd: [command, ...args].join(" "),
174 text: stdout,
175 details: stderr,
176 extname: this.outputExt,
177 }
178
179 if (/^geojson$/i.test(this.outputFormat)) {
180 try {
181 res.data = JSON.parse(stdout)
182 } catch (err) {
183 // ignore error
184 }
185 }
186
187 if (!this.customDestination && this.outputPath !== vsiStdOut) {
188 if (this.outputExt === ".zip") {
189 res.stream = this.createZipStream(this.outputPath)
190 } else {
191 res.stream = createReadStream(this.outputPath)
192 }
193 }
194
195 return res
196 }
197}
198
199function ogr2ogr(input: Input, opts?: Options): Ogr2ogr {
200 return new Ogr2ogr(input, opts)
201}
202
203ogr2ogr.version = async () => {
204 let vers = await new Promise<string>((res, rej) => {
205 execFile("ogr2ogr", ["--version"], {}, (err, stdout) => {
206 if (err) rej(err)
207 res(stdout)
208 })
209 })
210 return vers.trim()
211}
212
213export default ogr2ogr
214
\No newline at end of file