Skip to content

Commit

Permalink
feat: improve access to the GDAL cli (#882)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: this changes how to get access to a new gdal instance to Gdal.create()

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
blacha and kodiakhq[bot] authored Jul 9, 2020
1 parent b380757 commit 5eaef38
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 157 deletions.
4 changes: 2 additions & 2 deletions packages/cli/src/cli/cogify/action.cog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { buildCogForName } from '../../cog/cog';
import { CogVrt } from '../../cog/cog.vrt';
import { Cutline } from '../../cog/cutline';
import { CogJob } from '../../cog/types';
import { GdalCogBuilder } from '../../gdal/gdal';
import { Gdal } from '../../gdal/gdal';
import { CliId, CliInfo } from '../base.cli';
import { getJobPath, makeTempFolder } from '../folder';
import { SemVer } from './semver.util';
Expand Down Expand Up @@ -80,7 +80,7 @@ export class ActionCogCreate extends CommandLineAction {
const logger = LogConfig.get().child({ correlationId: job.id, imageryName: job.name });
LogConfig.set(logger);

const gdalVersion = await GdalCogBuilder.getVersion(logger);
const gdalVersion = await Gdal.version(logger);
logger.info({ version: gdalVersion }, 'GdalVersion');

const name = this.getName(job);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cog/__test__/cog.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Epsg } from '@basemaps/geo';
import { LogConfig } from '@basemaps/shared';
import o from 'ospec';
import { GdalCogBuilder } from '../../gdal/gdal';
import { GdalCogBuilder } from '../../gdal/gdal.cog';
import { buildCogForName } from '../cog';
import { SourceTiffTestHelper } from './source.tiff.testhelper';
import { TilingScheme } from '../../gdal/gdal.config';
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/cog/__test__/cog.vrt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FileOperatorSimple, LogConfig, ProjectionTileMatrixSet } from '@basemap
import { qkToName } from '@basemaps/shared/build/proj/__test__/test.util';
import { round } from '@basemaps/test/build/rounding';
import o from 'ospec';
import { GdalCogBuilder } from '../../gdal/gdal';
import { Gdal } from '../../gdal/gdal';
import { CogVrt } from '../cog.vrt';
import { Cutline } from '../cutline';
import { SourceTiffTestHelper } from './source.tiff.testhelper';
Expand All @@ -29,13 +29,13 @@ o.spec('cog.vrt', () => {
let runSpy = o.spy();

const origFileOperatorWriteJson = FileOperatorSimple.writeJson;
const { getGdal } = GdalCogBuilder;
const { create } = Gdal;

let gdal: any;

o.after(() => {
FileOperatorSimple.writeJson = origFileOperatorWriteJson;
GdalCogBuilder.getGdal = getGdal;
Gdal.create = create;
});

o.beforeEach(() => {
Expand All @@ -44,7 +44,7 @@ o.spec('cog.vrt', () => {
job.source.projection = EpsgCode.Nztm2000;
job.source.resZoom = 13;
gdal = { run: runSpy };
(GdalCogBuilder as any).getGdal = (): any => gdal;
(Gdal as any).create = (): any => gdal;
job.source.files = [tif1, tif2];

cutTiffArgs = [];
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cog/cog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EpsgCode, TileMatrixSet } from '@basemaps/geo';
import { Aws, isConfigS3Role, LogType, ProjectionTileMatrixSet } from '@basemaps/shared';
import { GdalCogBuilder } from '../gdal/gdal';
import { GdalCogBuilder } from '../gdal/gdal.cog';
import { GdalCommand } from '../gdal/gdal.command';
import { TilingScheme } from '../gdal/gdal.config';
import { GdalProgressParser } from '../gdal/gdal.progress';
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/cog/cog.vrt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Epsg } from '@basemaps/geo';
import { Aws, FileOperator, isConfigS3Role, LogType } from '@basemaps/shared';
import { GdalCogBuilder } from '../gdal/gdal';
import { Gdal } from '../gdal/gdal';
import { GdalCommand } from '../gdal/gdal.command';
import { onProgress } from './cog';
import { Cutline } from './cutline';
Expand Down Expand Up @@ -124,7 +124,7 @@ export const CogVrt = {
'Tiff count',
);

const gdalCommand = GdalCogBuilder.getGdal();
const gdalCommand = Gdal.create();
if (gdalCommand.mount) {
gdalCommand.mount(tmpFolder);
}
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/cog/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ import {
LogConfig,
ProjectionTileMatrixSet,
} from '@basemaps/shared';
import { Projection } from '@basemaps/shared/build/proj/projection';
import { CogSource } from '@cogeotiff/core';
import { CogSourceAwsS3 } from '@cogeotiff/source-aws';
import { CogSourceFile } from '@cogeotiff/source-file';
import { promises as fs } from 'fs';
import { basename } from 'path';
import * as ulid from 'ulid';
import { CogBuilder, GdalCogBuilder } from '..';
import { CogBuilder } from '..';
import { CliInfo } from '../cli/base.cli';
import { ActionBatchJob } from '../cli/cogify/action.batch';
import { getJobPath, makeTempFolder } from '../cli/folder';
import { Gdal } from '../gdal/gdal';
import { GdalCogBuilderDefaults, GdalCogBuilderOptionsResampling } from '../gdal/gdal.config';
import { Cutline } from './cutline';
import { CogJob } from './types';
import { Projection } from '@basemaps/shared/build/proj/projection';

export const MaxConcurrencyDefault = 50;

Expand Down Expand Up @@ -91,7 +92,7 @@ export const CogJobFactory = {
const imageryName = basename(ctx.source.path).replace(/\./g, '-'); // batch does not allow '.' in names
const logger = LogConfig.get().child({ id, imageryName });

const gdalVersion = await GdalCogBuilder.getVersion(logger);
const gdalVersion = await Gdal.version(logger);
logger.info({ version: gdalVersion }, 'GdalVersion');

const { source, output } = ctx;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/gdal/__test__/gdal.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import o from 'ospec';
import { GdalCogBuilder } from '../gdal';
import { GdalCogBuilder } from '../gdal.cog';
import { normalizeAwsEnv } from '../gdal.command';
import { GdalCogBuilderDefaults } from '../gdal.config';

Expand Down
125 changes: 125 additions & 0 deletions packages/cli/src/gdal/gdal.cog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { LogType } from '@basemaps/shared';
import { ChildProcessWithoutNullStreams } from 'child_process';
import { Gdal } from './gdal';
import { GdalCommand } from './gdal.command';
import { GdalCogBuilderDefaults, GdalCogBuilderOptions } from './gdal.config';

/** 1% Buffer to the tiff to help prevent gaps between tiles */
// const TiffBuffer = 1.01;

/**
* A docker based GDAL Cog Builder
*
* This uses the new 3.1 COG Driver https://gdal.org/drivers/raster/cog.html
*
* When GDAL 3.1 is released docker could be removed from this process.
*/
export class GdalCogBuilder {
config: GdalCogBuilderOptions;

/**
* Source file generally a .vrt
*/
source: string;
/**
* Output file
*/
target: string;

/**
* Current running child process
*/
child: ChildProcessWithoutNullStreams | null;
/**
* Promise waiting for child process to finish
*/
promise: Promise<void> | null;
/** When the process started */
startTime: number;
/** Gdal process */
gdal: GdalCommand;

constructor(source: string, target: string, config: Partial<GdalCogBuilderOptions> = {}) {
this.source = source;
this.target = target;

this.config = {
bbox: config.bbox,
projection: config.projection ?? GdalCogBuilderDefaults.projection,
alignmentLevels: config.alignmentLevels ?? GdalCogBuilderDefaults.alignmentLevels,
compression: config.compression ?? GdalCogBuilderDefaults.compression,
tilingScheme: config.tilingScheme ?? GdalCogBuilderDefaults.tilingScheme,
resampling: config.resampling ?? GdalCogBuilderDefaults.resampling,
blockSize: config.blockSize ?? GdalCogBuilderDefaults.blockSize,
targetRes: config.targetRes ?? GdalCogBuilderDefaults.targetRes,
quality: config.quality ?? GdalCogBuilderDefaults.quality,
};
this.gdal = Gdal.create();

this.gdal.mount?.(source);
this.gdal.mount?.(target);
}

getBounds(): string[] {
if (this.config.bbox == null) {
return [];
}

// TODO in theory this should be clamped to the lower right of the imagery, as there is no point generating large empty tiffs
const [ulX, ulY, lrX, lrY] = this.config.bbox;
return ['-projwin', ulX, ulY, lrX, lrY, '-projwin_srs', this.config.projection.toEpsgString()].map(String);
}

get args(): string[] {
const tr = this.config.targetRes.toString();
return [
// Force output using COG Driver
'-of',
'COG',
// Force GoogleMaps tiling
'-co',
`TILING_SCHEME=${this.config.tilingScheme}`,
// Max CPU POWER
'-co',
'NUM_THREADS=ALL_CPUS',
// Force big tiff the extra few bytes savings of using little tiffs does not affect us
'-co',
'BIGTIFF=YES',
// Force a alpha layer
'-co',
'ADD_ALPHA=YES',
// User configured output block size
'-co',
`BLOCKSIZE=${this.config.blockSize}`,
// Configured resampling methods
'-co',
`WARP_RESAMPLING=${this.config.resampling.warp}`,
'-co',
`OVERVIEW_RESAMPLING=${this.config.resampling.overview}`,
// User configured compression
'-co',
`COMPRESS=${this.config.compression}`,
// Number of levels to align to web mercator
'-co',
`ALIGNED_LEVELS=${this.config.alignmentLevels}`,
// Default quality of 75 is too low for our needs
'-co',
`QUALITY=${this.config.quality}`,
// most of the imagery contains a lot of empty tiles, no need to output them
'-co',
`SPARSE_OK=YES`,
// Force a target resolution to be better than the imagery not worse
'-tr',
tr,
tr,
...this.getBounds(),

this.source,
this.target,
];
}

async convert(log: LogType): Promise<void> {
await this.gdal.run('gdal_translate', this.args, log);
}
}
23 changes: 15 additions & 8 deletions packages/cli/src/gdal/gdal.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ export abstract class GdalCommand {
protected child: ChildProcessWithoutNullStreams;
protected promise?: Promise<{ stdout: string; stderr: string }>;
protected startTime: number;
/** Should log all of stdout/stderr */
verbose = true;

/** AWS Access */
protected credentials?: AWS.Credentials;
Expand All @@ -43,10 +41,14 @@ export abstract class GdalCommand {
this.credentials = credentials;
}

/**
* Run a GDAL command
* @param cmd command to run eg "gdal_translate"
* @param args command arguments
* @param log logger to use
*/
async run(cmd: string, args: string[], log: LogType): Promise<{ stdout: string; stderr: string }> {
if (this.promise != null) {
return this.promise;
}
if (this.promise != null) throw new Error('Cannot create multiple gdal processes, create a new GdalCommand');
this.parser?.reset();
this.startTime = Date.now();

Expand All @@ -70,6 +72,7 @@ export abstract class GdalCommand {
}
errBuff.push(data);
});

child.stdout.on('data', (data: Buffer) => {
outputBuff.push(data);
this.parser?.data(data);
Expand All @@ -79,20 +82,24 @@ export abstract class GdalCommand {
child.on('exit', (code: number) => {
const stdout = outputBuff.join('').trim();
const stderr = errBuff.join('').trim();
const duration = Date.now() - this.startTime;

if (code != 0) {
log.error({ code, stdout, stderr }, 'GdalFailed');
log.error({ code, stdout, stderr, duration }, 'GdalFailed');
return reject(new Error('Failed to execute GDAL command'));
}
if (this.verbose) log.warn({ stdout, stderr }, 'GdalOutput');
log.trace({ stdout, stderr, duration }, 'GdalDone');

this.promise = undefined;
return resolve({ stdout, stderr });
});

child.on('error', (error: Error) => {
const stdout = outputBuff.join('').trim();
const stderr = errBuff.join('').trim();
log.error({ stdout, stderr }, 'GdalFailed');
const duration = Date.now() - this.startTime;

log.error({ stdout, stderr, duration }, 'GdalFailed');
this.promise = undefined;
reject(error);
});
Expand Down
24 changes: 14 additions & 10 deletions packages/cli/src/gdal/gdal.docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,24 @@ export class GdalDocker extends GdalCommand {
}

/** Provide redacted argument string for logging which removes sensitive information */
maskArgs(args: string[]): string {
const argsStr = args.join(' ');
if (this.credentials) {
return argsStr
.replace(this.credentials.secretAccessKey, '****')
.replace(this.credentials.sessionToken, '****');
}
return argsStr;
maskArgs(args: string[]): string[] {
const cred = this.credentials;
if (cred == null) return args;

return args.map((c) => c.replace(cred.secretAccessKey, '****').replace(cred.sessionToken, '****'));
}

async run(cmd: string, args: string[], log: LogType): Promise<{ stdout: string; stderr: string }> {
const dockerArgs = await this.getDockerArgs();
log.info({ mounts: this.mounts, docker: this.maskArgs(dockerArgs) }, 'SpawnDocker');

log.debug(
{
mounts: this.mounts,
cmd,
docker: this.maskArgs(dockerArgs).join(' '),
gdalArgs: args.slice(0, 50).join(' '),
},
'StartGdal:Docker',
);
return super.run('docker', [...dockerArgs, cmd, ...args], log);
}
}
6 changes: 6 additions & 0 deletions packages/cli/src/gdal/gdal.local.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GdalCommand } from './gdal.command';
import { LogType } from '@basemaps/shared';

export class GdalLocal extends GdalCommand {
async env(): Promise<Record<string, string | undefined>> {
Expand All @@ -15,4 +16,9 @@ export class GdalLocal extends GdalCommand {
AWS_SESSION_TOKEN: this.credentials.sessionToken,
};
}

async run(cmd: string, args: string[], log: LogType): Promise<{ stdout: string; stderr: string }> {
log.debug({ cmd, gdalArgs: args.slice(0, 50).join(' ') }, 'StartGdal:Local');
return super.run(cmd, args, log);
}
}
Loading

0 comments on commit 5eaef38

Please sign in to comment.