1 | import archiver from "archiver"
|
2 | import {execFile} from "child_process"
|
3 | import {createReadStream} from "fs"
|
4 | import {tmpdir} from "os"
|
5 | import {extname, join} from "path"
|
6 | import {Readable, Stream} from "stream"
|
7 |
|
8 | type JSONLike = Record<string, unknown>
|
9 | type RunOutput = {stdout: string; stderr: string}
|
10 |
|
11 | type Input = string | JSONLike | Stream
|
12 | interface Result {
|
13 | cmd: string
|
14 | text: string
|
15 | data?: JSONLike
|
16 | stream?: Readable
|
17 | extname?: string
|
18 | details: string
|
19 | }
|
20 | type Callback = (err: Error | null, res?: Result) => void
|
21 | interface 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 |
|
32 | const stdoutRe = /csv|geojson|georss|gml|gmt|gpx|jml|kml|mapml|pdf|vdv/i
|
33 | const vsiStdIn = "/vsistdin/"
|
34 | const vsiStdOut = "/vsistdout/"
|
35 |
|
36 | let uniq = Date.now()
|
37 |
|
38 | class 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 |
|
199 | function ogr2ogr(input: Input, opts?: Options): Ogr2ogr {
|
200 | return new Ogr2ogr(input, opts)
|
201 | }
|
202 |
|
203 | ogr2ogr.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 |
|
213 | export default ogr2ogr
|
214 |
|
\ | No newline at end of file |