Source: src/datastore.js

import fetch from 'node-fetch';
import fs from 'fs';
import { getGeoServerResponseText, GeoServerResponseError } from './util/geoserver.js';
import AboutClient from './about.js'

/**
 * Client for GeoServer data stores
 *
 * @module DatastoreClient
 */
export default class DatastoreClient {
  /**
   * Creates a GeoServer REST DatastoreClient instance.
   *
   * @param {String} url The URL of the GeoServer REST API endpoint
   * @param {String} auth The Basic Authentication string
   */
  constructor (url, auth) {
    this.url = url;
    this.auth = auth;
  }

  /**
   * Get all DataStores in a workspace.
   *
   * @param {String} workspace The workspace to get DataStores for
   *
   * @returns {Object} An object containing store details
   */
  async getDataStores (workspace) {
    return this.getStores(workspace, 'datastores');
  }

  /**
   * Get all CoverageStores in a workspace.
   *
   * @param {String} workspace The workspace to get CoverageStores for
   *
   * @returns {Object} An object containing store details
   */
  async getCoverageStores (workspace) {
    return this.getStores(workspace, 'coveragestores');
  }

  /**
   * Get all WmsStores in a workspace.
   *
   * @param {String} workspace The workspace to get WmsStores for
   *
   * @returns {Object} An object containing store details
   */
  async getWmsStores (workspace) {
    return this.getStores(workspace, 'wmsstores');
  }

  /**
   * Get all WmtsStores in a workspace.
   *
   * @param {String} workspace The workspace to get WmtsStores for
   *
   * @returns {Object} An object containing store details
   */
  async getWmtsStores (workspace) {
    return this.getStores(workspace, 'wmtsstores');
  }

  /**
   * Get information about various store types in a workspace.
   *
   * @param {String} workspace The workspace name
   * @param {String} storeType The type of store
   *
   * @throws Error if request fails
   *
   * @returns {Object} An object containing store details or undefined if it cannot be found
   * @private
   */
  async getStores (workspace, storeType) {
    const response = await fetch(this.url + 'workspaces/' + workspace + '/' + storeType + '.json', {
      credentials: 'include',
      method: 'GET',
      headers: {
        Authorization: this.auth
      }
    });
    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
    return response.json();
  }

  /**
   * Get specific DataStore by name in a workspace.
   *
   * @param {String} workspace The workspace to search DataStore in
   * @param {String} dataStore DataStore name
   *
   * @returns {Object} An object containing store details or undefined if it cannot be found
   */
  async getDataStore (workspace, dataStore) {
    return this.getStore(workspace, dataStore, 'datastores');
  }

  /**
   * Get specific CoverageStore by name in a workspace.
   *
   * @param {String} workspace The workspace to search CoverageStore in
   * @param {String} covStore CoverageStore name
   *
   * @returns {Object} An object containing store details or undefined if it cannot be found
   */
  async getCoverageStore (workspace, covStore) {
    return this.getStore(workspace, covStore, 'coveragestores');
  }

  /**
   * Get specific WmsStore by name in a workspace.
   *
   * @param {String} workspace The workspace to search WmsStore in
   * @param {String} wmsStore WmsStore name
   *
   * @returns {Object} An object containing store details or undefined if it cannot be found
   *
   */
  async getWmsStore (workspace, wmsStore) {
    return this.getStore(workspace, wmsStore, 'wmsstores');
  }

  /**
   * Get specific WmtsStore by name in a workspace.
   *
   * @param {String} workspace The workspace to search WmtsStore in
   * @param {String} wmtsStore WmtsStore name
   *
   * @returns {Object} An object containing store details or undefined if it cannot be found
   */
  async getWmtsStore (workspace, wmtsStore) {
    return this.getStore(workspace, wmtsStore, 'wmtsstores');
  }

  /**
   * Get GeoServer store by type
   *
   * @param {String} workspace The name of the workspace
   * @param {String} storeName The name of the store
   * @param {String} storeType The type of the store
   *
   * @throws Error if request fails
   *
   * @returns {Object} An object containing store details or undefined if it cannot be found
   * @private
   */
  async getStore (workspace, storeName, storeType) {
    const url = this.url + 'workspaces/' + workspace + '/' + storeType + '/' + storeName + '.json';
    const response = await fetch(url, {
      credentials: 'include',
      method: 'GET',
      headers: {
        Authorization: this.auth
      }
    });

    if (!response.ok) {
      const grc = new AboutClient(this.url, this.auth);
      if (await grc.exists()) {
        // GeoServer exists, but requested item does not exist,  we return empty
        return;
      } else {
        // There was a general problem with GeoServer
        const geoServerResponse = await getGeoServerResponseText(response);
        throw new GeoServerResponseError(null, geoServerResponse);
      }
    }
    return response.json();
  }

  /**
   * Creates a GeoTIFF store from a file by path and publishes it as layer.
   * The GeoTIFF file has to be placed on the server, where your GeoServer
   * is running.
   *
   * @param {String} workspace The workspace to create GeoTIFF store in
   * @param {String} coverageStore The name of the new GeoTIFF store
   * @param {String} layerName The published name of the new layer
   * @param {String} layerTitle The published title of the new layer
   * @param {String} filePath The path to the GeoTIFF file on the server
   *
   * @throws Error if request fails
   *
   * @returns {String} The successful response text
   */
  async createGeotiffFromFile (workspace, coverageStore, layerName, layerTitle, filePath) {
    const stats = fs.statSync(filePath);
    const fileSizeInBytes = stats.size;
    const readStream = fs.createReadStream(filePath);

    return this.createGeotiffFromStream(workspace, coverageStore, layerName, layerTitle, readStream, fileSizeInBytes);
  }

  /**
   * Creates a GeoTIFF store from a file by stream and publishes it as layer.
   * The GeoTIFF file is placed on the server, where your GeoServer
   * is running.
   *
   * @param {String} workspace The workspace to create GeoTIFF store in
   * @param {String} coverageStore The name of the new GeoTIFF store
   * @param {String} layerName The published name of the new layer
   * @param {String} layerTitle The published title of the new layer
   * @param {Stream} readStream The stream of the GeoTIFF file
   * @param {Number} fileSizeInBytes The number of bytes of the stream
   *
   * @throws Error if request fails
   *
   * @returns {String} The successful response text
   */
  async createGeotiffFromStream (workspace, coverageStore, layerName, layerTitle, readStream, fileSizeInBytes) {
    const lyrTitle = layerTitle || layerName;

    let url = this.url + 'workspaces/' + workspace + '/coveragestores/' +
        coverageStore + '/file.geotiff';
    url += '?filename=' + lyrTitle + '&coverageName=' + layerName;
    const response = await fetch(url, {
      credentials: 'include',
      method: 'PUT',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'image/tiff',
        'Content-length': fileSizeInBytes
      },
      body: readStream
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
    // TODO: enforce JSON response or parse XML
    return response.text();
  }

  /**
   * Creates a PostGIS based data store.
   *
   * @param {String} workspace The WS to create the data store in
   * @param {String} namespaceUri The namespace URI of the workspace
   * @param {String} dataStore The data store name to be created
   * @param {String} pgHost The PostGIS DB host
   * @param {Number} pgPort The PostGIS DB port
   * @param {String} pgUser The PostGIS DB user
   * @param {String} pgPassword The PostGIS DB password
   * @param {String} pgSchema The PostGIS DB schema
   * @param {String} pgDb The PostGIS DB name
   * @param {Boolean} [exposePk] expose primary key, defaults to false
   *
   * @throws Error if request fails
   */
  async createPostgisStore (workspace, namespaceUri, dataStore, pgHost, pgPort, pgUser, pgPassword, pgSchema, pgDb, exposePk) {
    const body = {
      dataStore: {
        name: dataStore,
        type: 'PostGIS',
        enabled: true,
        workspace: {
          name: workspace
        },
        connectionParameters: {
          entry: [
            {
              '@key': 'dbtype',
              $: 'postgis'
            },
            {
              '@key': 'schema',
              $: pgSchema
            },
            {
              '@key': 'database',
              $: pgDb
            },
            {
              '@key': 'host',
              $: pgHost
            },
            {
              '@key': 'port',
              $: pgPort
            },
            {
              '@key': 'passwd',
              $: pgPassword
            },
            {
              '@key': 'namespace',
              $: namespaceUri
            },
            {
              '@key': 'user',
              $: pgUser
            },
            {
              '@key': 'Expose primary keys',
              $: exposePk || false
            }
          ]
        }
      }
    };

    const url = this.url + 'workspaces/' + workspace + '/datastores';
    const response = await fetch(url, {
      credentials: 'include',
      method: 'POST',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    });

    // TODO: not tested yet
    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }

  /**
   * Creates an ImageMosaic store from a zip archive with the 3 necessary files
   *   - datastore.properties
   *   - indexer.properties
   *   - timeregex.properties
   *
   * The zip archive has to be given as absolute path, so before it has to be
   * placed on the server, where your GeoServer is running.
   *
   * @param {String} workspace The WS to create the data store in
   * @param {String} dataStore The data store name
   * @param {String} zipArchivePath Absolute path to zip archive with the 3 properties files
   *
   * @throws Error if request fails
   *
   * @returns {String} The response text
   */
  async createImageMosaicStore (workspace, coverageStore, zipArchivePath) {
    const readStream = fs.createReadStream(zipArchivePath);

    const url = this.url + 'workspaces/' + workspace + '/coveragestores/' + coverageStore + '/file.imagemosaic';
    const response = await fetch(url, {
      credentials: 'include',
      method: 'PUT',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/zip'
      },
      body: readStream
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }

    return response.text();
  };

  /**
   * Creates a WMS based data store.
   *
   * @param {String} workspace The WS to create the data store in
   * @param {String} dataStore The data store name
   * @param {String} wmsCapabilitiesUrl Base WMS capabilities URL
   *
   * @throws Error if request fails
   */
  async createWmsStore (workspace, dataStore, wmsCapabilitiesUrl) {
    const body = {
      wmsStore: {
        name: dataStore,
        type: 'WMS',
        capabilitiesURL: wmsCapabilitiesUrl
      }
    };

    const url = this.url + 'workspaces/' + workspace + '/wmsstores';
    const response = await fetch(url, {
      credentials: 'include',
      method: 'POST',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }

  /**
   * Creates a WMTS based data store.
   *
   * @param {String} workspace The WS to create the data store in
   * @param {String} dataStore The data store name
   * @param {String} wmtsCapabilitiesUrl Base WMTS capabilities URL
   *
   * @throws Error if request fails
   */
  async createWmtsStore (workspace, dataStore, wmtsCapabilitiesUrl) {
    const body = {
      wmtsStore: {
        name: dataStore,
        type: 'WMTS',
        capabilitiesURL: wmtsCapabilitiesUrl
      }
    };

    const url = this.url + 'workspaces/' + workspace + '/wmtsstores';
    const response = await fetch(url, {
      credentials: 'include',
      method: 'POST',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }

  /**
   * Creates a WFS based data store.
   *
   * @param {String} workspace The WS to create the data store in
   * @param {String} dataStore The data store name
   * @param {String} wfsCapabilitiesUrl WFS capabilities URL
   * @param {String} namespaceUrl URL of the GeoServer namespace
   * @param {Boolean} [useHttpConnectionPooling=true] use HTTP connection pooling for WFS connection
   *
   * @throws Error if request fails
   */
  async createWfsStore (workspace, dataStore, wfsCapabilitiesUrl, namespaceUrl, useHttpConnectionPooling) {
    const body = {
      dataStore: {
        name: dataStore,
        type: 'Web Feature Server (NG)',
        connectionParameters: {
          entry: [
            {
              '@key': 'WFSDataStoreFactory:GET_CAPABILITIES_URL',
              $: wfsCapabilitiesUrl
            },
            {
              '@key': 'namespace',
              $: namespaceUrl
            },
            {
              '@key': 'WFSDataStoreFactory:USE_HTTP_CONNECTION_POOLING',
              $: useHttpConnectionPooling !== false ? 'true' : 'false'
            }
          ]
        }
      }
    };

    const url = this.url + 'workspaces/' + workspace + '/datastores';
    const response = await fetch(url, {
      credentials: 'include',
      method: 'POST',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }

  /**
   * Deletes a data store.
   *
   * @param {String} workspace The workspace where the data store is in
   * @param {String} coverageStore Name of data store to delete
   * @param {String} recurse Flag to enable recursive deletion
   *
   * @throws Error if request fails
   */
  async deleteDataStore (workspace, dataStore, recurse) {
    let url = this.url + 'workspaces/' + workspace + '/datastores/' + dataStore;
    url += '?recurse=' + recurse;

    const response = await fetch(url, {
      credentials: 'include',
      method: 'DELETE',
      headers: {
        Authorization: this.auth
      }
    });

    if (!response.ok) {
      // TODO: could not find status codes in the docs or via testing
      //       https://docs.geoserver.org/latest/en/api/#1.0.0/datastores.yaml
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }

  /**
   * Deletes a CoverageStore.
   *
   * @param {String} workspace The workspace where the CoverageStore is in
   * @param {String} coverageStore Name of CoverageStore to delete
   * @param {String} recurse Flag to enable recursive deletion
   *
   * @throws Error if request fails
   */
  async deleteCoverageStore (workspace, coverageStore, recurse) {
    let url = this.url + 'workspaces/' + workspace + '/coveragestores/' + coverageStore;
    url += '?recurse=' + recurse;

    const response = await fetch(url, {
      credentials: 'include',
      method: 'DELETE',
      headers: {
        Authorization: this.auth
      }
    });

    // TODO: could not test it
    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      switch (response.status) {
        case 401:
          throw new GeoServerResponseError('Deletion failed. There might be dependant objects to ' +
          'this store. Delete them first or call this with "recurse=false"', geoServerResponse);
        default:
          throw new GeoServerResponseError(null, geoServerResponse);
      }
    }
  }

  /**
   * Creates a GeoPackage store from a file placed in the geoserver_data dir.
   *
   * @param {String} workspace The WS to create the data store in
   * @param {String} dataStore The data store name
   * @param {String} gpkgPath Relative path to GeoPackage file within geoserver_data dir
   *
   * @throws Error if request fails
   */
  async createGpkgStore (workspace, dataStore, gpkgPath) {
    const body = {
      dataStore: {
        name: dataStore,
        type: 'GeoPackage',
        connectionParameters: {
          entry: [
            {
              '@key': 'database',
              $: `file:${gpkgPath}`
            },
            {
              '@key': 'dbtype',
              $: 'geopkg'
            }
          ]
        }
      }
    };

    const url = this.url + 'workspaces/' + workspace + '/datastores';
    const response = await fetch(url, {
      credentials: 'include',
      method: 'POST',
      headers: {
        Authorization: this.auth,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(body)
    });

    if (!response.ok) {
      const geoServerResponse = await getGeoServerResponseText(response);
      throw new GeoServerResponseError(null, geoServerResponse);
    }
  }
}