Merge branch '161-transfer-files-to-asaqc' into 'devel'

Resolve "Transfer files to ASAQC"

Closes #161

See merge request wgp/dougal/software!16
This commit is contained in:
D. Berge
2021-10-09 09:23:54 +00:00
45 changed files with 1699 additions and 78 deletions

View File

@@ -1,5 +1,8 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
'@babel/plugin-proposal-logical-assignment-operators'
]
}

View File

@@ -28,6 +28,7 @@
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/plugin-proposal-logical-assignment-operators": "^7.14.5",
"@vue/cli-plugin-babel": "~4.4.0",
"@vue/cli-plugin-router": "~4.4.0",
"@vue/cli-plugin-vuex": "~4.4.0",
@@ -250,10 +251,13 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"dev": true
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz",
"integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-regex": {
"version": "7.10.4",
@@ -412,6 +416,22 @@
"@babel/plugin-syntax-json-strings": "^7.8.0"
}
},
"node_modules/@babel/plugin-proposal-logical-assignment-operators": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz",
"integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.14.5",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz",
@@ -540,6 +560,18 @@
"@babel/helper-plugin-utils": "^7.10.4"
}
},
"node_modules/@babel/plugin-syntax-logical-assignment-operators": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
"integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
"dev": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.10.4"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
@@ -13524,9 +13556,9 @@
}
},
"@babel/helper-plugin-utils": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz",
"integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==",
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz",
"integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==",
"dev": true
},
"@babel/helper-regex": {
@@ -13680,6 +13712,16 @@
"@babel/plugin-syntax-json-strings": "^7.8.0"
}
},
"@babel/plugin-proposal-logical-assignment-operators": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz",
"integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.14.5",
"@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
}
},
"@babel/plugin-proposal-nullish-coalescing-operator": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz",
@@ -13805,6 +13847,15 @@
"@babel/helper-plugin-utils": "^7.10.4"
}
},
"@babel/plugin-syntax-logical-assignment-operators": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
"integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.10.4"
}
},
"@babel/plugin-syntax-nullish-coalescing-operator": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",

View File

@@ -26,6 +26,7 @@
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/plugin-proposal-logical-assignment-operators": "^7.14.5",
"@vue/cli-plugin-babel": "~4.4.0",
"@vue/cli-plugin-router": "~4.4.0",
"@vue/cli-plugin-vuex": "~4.4.0",

View File

@@ -0,0 +1,33 @@
/**
* Throttle a function call.
*
* It delays `callback` by `delay` ms and ignores any
* repeated calls from `caller` within at most `maxWait`
* milliseconds.
*
* Used to react to server events in cases where we get
* a separate notification for each row of a bulk update.
*/
function throttle (callback, caller, delay = 100, maxWait = 500) {
const schedule = async () => {
caller.triggeredAt = Date.now();
caller.timer = setTimeout(async () => {
await callback();
caller.timer = null;
}, delay);
}
if (!caller.timer) {
schedule();
} else {
const elapsed = Date.now() - caller.triggeredAt;
if (elapsed > maxWait) {
cancelTimeout(caller.timer);
schedule();
}
}
}
export default throttle;

View File

@@ -80,6 +80,67 @@
<v-list-item-title>PDF</v-list-item-title>
</v-list-item>
</v-list-group>
<!-- ASAQC transfer queue actions -->
<!-- Item is not in queue -->
<v-list-item
v-if="writeaccess && !contextMenuItemInTransferQueue"
@click="addToTransferQueue(); contextMenuShow=false"
>
<v-list-item-content>
<v-list-item-title>Send to ASAQC</v-list-item-title>
</v-list-item-content>
<v-list-item-icon>
<v-icon small>mdi-tray-plus</v-icon>
</v-list-item-icon>
</v-list-item>
<!-- Item queued, not yet sent -->
<v-list-item two-line
v-else-if="writeaccess && contextMenuItemInTransferQueue.status == 'queued'"
@click="removeFromTransferQueue(); contextMenuShow=false"
>
<v-list-item-content>
<v-list-item-title class="red--text">Cancel sending to ASAQC</v-list-item-title>
<v-list-item-subtitle class="info--text">
Queued since: {{contextMenuItemInTransferQueue.created_on}}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-icon>
<v-icon small color="red">mdi-tray-remove</v-icon>
</v-list-item-icon>
</v-list-item>
<!-- Item already sent -->
<v-list-item two-line
v-else-if="writeaccess && contextMenuItemInTransferQueue.status == 'sent'"
@click="addToTransferQueue(); contextMenuShow=false"
>
<v-list-item-content>
<v-list-item-title>Resend to ASAQC</v-list-item-title>
<v-list-item-subtitle class="success--text">
Last sent on: {{ contextMenuItemInTransferQueue.created_on }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-icon>
<v-icon small>mdi-tray-plus</v-icon>
</v-list-item-icon>
</v-list-item>
<!-- Item sending was cancelled -->
<v-list-item two-line
v-else-if="writeaccess && contextMenuItemInTransferQueue.status == 'cancelled'"
@click="addToTransferQueue(); contextMenuShow=false"
>
<v-list-item-content>
<v-list-item-title>Send to ASAQC</v-list-item-title>
<v-list-item-subtitle class="info--text">
Last send cancelled on: {{contextMenuItemInTransferQueue.updated_on}}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-icon>
<v-icon small>mdi-tray-plus</v-icon>
</v-list-item-icon>
</v-list-item>
</v-list>
</v-menu>
@@ -271,6 +332,23 @@
<template v-slot:item.status="{value, item}">
<span :class="{'success--text': value=='final', 'warning--text': value=='raw', 'error--text': value=='ntbp'}">
{{ value == "final" ? "Processed" : value == "raw" ? item.raw_files ? "Acquired" : "In acquisition" : value == "ntbp" ? "NTBP" : `Unknown (${status})` }}
<v-icon small :title="`Sent to ASAQC on ${queuedItem(item.sequence).updated_on}`"
color="success"
v-if="queuedItem(item.sequence).status == 'sent'"
>mdi-upload</v-icon>
<v-icon small
title="Queued for sending to ASAQC"
v-else-if="queuedItem(item.sequence).status == 'queued'"
>mdi-upload-outline</v-icon>
<v-icon small
:title="`ASAQC transfer cancelled at ${queuedItem(item.sequence).updated_on}`"
v-else-if="queuedItem(item.sequence).status == 'cancelled'"
>mdi-upload-off-outline</v-icon>
<v-icon small
color="warning"
:title="`ASAQC transfer failed at ${queuedItem(item.sequence).updated_on}`"
v-else-if="queuedItem(item.sequence).status == 'failed'"
>mdi-upload-off</v-icon>
</span>
</template>
@@ -371,6 +449,7 @@ tr :nth-child(5), tr :nth-child(8), tr :nth-child(11), tr :nth-child(14) {
<script>
import { mapActions, mapGetters } from 'vuex';
import { basename } from 'path';
import throttle from '@/lib/throttle';
export default {
name: "SequenceList",
@@ -482,11 +561,19 @@ export default {
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null
contextMenuItem: null,
// ASAQC transfer queue
queuedItems: []
}
},
computed: {
contextMenuItemInTransferQueue () {
return this.queuedItems.find(i => i.payload.sequence == this.contextMenuItem.sequence);
},
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
},
@@ -497,7 +584,7 @@ export default {
},
deep: true
},
async edit (newVal, oldVal) {
if (newVal === null && oldVal !== null) {
const item = this.items.find(i => i.sequence == oldVal.sequence);
@@ -523,25 +610,34 @@ export default {
} else {
this.queuedReload = true;
}
} else if (event.channel == "queue_items") {
const project =
event.payload?.project ??
event.payload?.new?.payload?.project ??
event.payload?.old?.payload?.project;
if (project == this.$route.params.project) {
this.getQueuedItems();
}
}
},
queuedReload (newVal, oldVal) {
if (newVal && !oldVal && !this.loading) {
this.getSequences();
}
},
loading (newVal, oldVal) {
if (!newVal && oldVal && this.queuedReload) {
this.getSequences();
}
},
itemsPerPage (newVal, oldVal) {
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`, newVal);
},
user (newVal, oldVal) {
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25;
}
@@ -611,6 +707,59 @@ export default {
await this.api([url, init]);
},
async addToTransferQueue () {
const payload = [
{
project: this.$route.params.project,
sequence: this.contextMenuItem.sequence
}
];
const url = `/queue/outgoing/asaqc`;
const init = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: payload
}
const callback = (err, res) => {
if (res && res.ok) {
const text = `Sequence ${this.contextMenuItem.sequence} queued for sending to ASAQC`;
this.showSnack([text, "info"]);
}
};
await this.api([url, init, callback]);
},
async removeFromTransferQueue () {
const item_id = this.contextMenuItemInTransferQueue.item_id;
if (item_id) {
const url = `/queue/outgoing/asaqc/${item_id}`;
const init = {
method: "DELETE",
headers: { "Content-Type": "application/json" }
}
const callback = (err, res) => {
if (res && res.ok) {
const text = `Cancelled sending of sequence ${this.contextMenuItem.sequence} to ASAQC`;
this.showSnack([text, "primary"]);
}
};
this.api([url, init, callback]);
} else {
this.showSnack(["No item ID in transfer queue", "error"]);
}
},
queuedItem (sequence) {
return this.queuedItems.find(i => i.payload.sequence == sequence) || {};
},
editItem (item, key) {
this.edit = {
sequence: item.sequence,
@@ -618,10 +767,10 @@ export default {
value: item[key]
}
},
async saveItem (edit) {
if (!edit) return;
try {
const url = `/project/${this.$route.params.project}/sequence/${edit.sequence}`;
const init = {
@@ -630,7 +779,7 @@ export default {
[edit.key]: edit.value
}
};
let res;
await this.api([url, init, (e, r) => res = r]);
return res && res.ok;
@@ -665,6 +814,14 @@ export default {
},
async getQueuedItems () {
const callback = async () => {
const url = `/queue/outgoing/asaqc/project/${this.$route.params.project}`;
this.queuedItems = Object.freeze(await this.api([url]) || []);
}
throttle(callback, this.getQueuedItems, 100, 500);
},
basename (path, ext) {
return basename(path, ext);
},
@@ -702,12 +859,13 @@ export default {
return false;
},
...mapActions(["api"])
...mapActions(["api", "showSnack"])
},
mounted () {
this.getSequences();
this.getNumLines();
this.getQueuedItems();
}
}

View File

@@ -196,6 +196,21 @@ app.map({
delete: [ mw.auth.access.write, mw.info.delete ]
}
},
'/queue/outgoing/': {
'asaqc': {
get: [ mw.queue.asaqc.get ],
post: [ mw.auth.access.write, mw.queue.asaqc.post ],
'/project/:project': {
get: [ mw.queue.asaqc.get ],
'/sequence/:sequence': {
get: [ mw.queue.asaqc.get ],
}
},
'/:id': {
delete: [ mw.auth.access.write, mw.queue.asaqc.delete ]
}
}
},
'/rss/': {
get: [ mw.rss.get ]
}

View File

@@ -9,6 +9,7 @@ module.exports = {
gis: require('./gis'),
label: require('./label'),
navdata: require('./navdata'),
queue: require('./queue'),
configuration: require('./configuration'),
info: require('./info'),
meta: require('./meta'),

View File

@@ -0,0 +1,14 @@
const { queue } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
await queue.delete('asaqc', req.params.id);
res.status(204).send();
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,13 @@
const { queue } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
res.status(200).send(await queue.get('asaqc', {...req.query, ...req.params}));
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,5 @@
module.exports = {
get: require('./get'),
post: require('./post'),
delete: require('./delete')
};

View File

@@ -0,0 +1,16 @@
const { queue } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
const payload = req.body;
await queue.post('asaqc', payload);
res.status(202).send();
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,3 @@
module.exports = {
asaqc: require('./asaqc')
};

View File

@@ -92,6 +92,7 @@ if (process.env.PGDATABASE) {
delete config.db;
}
config.DOUGAL_ROOT = cfgPrefix;
config._ = (k) => k.split(".").reduce((a, b) => typeof a != "undefined" ? a[b] : a, config);
module.exports = Object.freeze(config);

View File

@@ -9,5 +9,6 @@ module.exports = [
"preplot_lines", "preplot_points",
"planned_lines",
"raw_lines", "raw_shots",
"final_lines", "final_shots", "info"
"final_lines", "final_shots", "info",
"queue_items"
];

View File

@@ -10,5 +10,6 @@ module.exports = {
configuration: require('./configuration'),
info: require('./info'),
meta: require('./meta'),
navdata: require('./navdata')
navdata: require('./navdata'),
queue: require('./queue')
};

View File

@@ -0,0 +1,24 @@
const { transaction, pool } = require('../connection');
async function post(queueId/*unused*/, item_id) {
const client = await pool.connect();
const text = `
UPDATE queue_items
SET status = 'cancelled'
WHERE status = 'queued'
AND (item_id = $1
OR parent_id = $1);
`;
const res = await client.query(text, [item_id]);
client.release();
if (!res.rowCount) {
throw { status: 404, message: "No cancellable requests" };
}
}
module.exports = post;

View File

@@ -0,0 +1,72 @@
const { pool } = require('../connection');
async function get (queueId/*unused*/, opts = {}) {
const client = await pool.connect();
const validStatuses = [
'queued',
'cancelled',
'failed',
'sent'
];
const validOrders = {
created_on: "created_on",
updated_on: "updated_on",
not_before: "not_before",
item_id: "item_id",
status: "status",
parent_id: "parent_id",
project: "payload->'project'",
sequence: "payload->'sequence'"
};
const limit = Math.min(Math.abs(opts.limit) || 100, 1000);
const offset = Math.abs(opts.offset) || 0;
const order = validOrders[opts.order] || "updated_on";
const dir = (!opts.dir || opts.dir == "-")
? "DESC"
: "ASC";
const validStatus = validStatuses.includes(opts.status);
const status = validStatus ? opts.status : 1;
const hasProject = "project" in opts;
const hasSequence = "sequence" in opts;
const project = hasProject
? opts.project
: 1;
const sequence = hasSequence
? opts.sequence
: 1;
const restrict1 = validStatus
? "status = $3"
: "$3 = $3";
const restrict2 = hasProject
? "AND payload->>'project' = $4"
: "AND $4 = $4";
// Yes, technically a user could restrict by sequence
// only, without specifying a project. Let's call that
// a feature.
const restrict3 = hasSequence
? "AND payload->>'sequence' = $5"
: "AND $5 = $5";
const text = `
SELECT *
FROM queue_items
WHERE ${restrict1} ${restrict2} ${restrict3}
ORDER BY ${order} ${dir}
LIMIT $1
OFFSET $2;
`;
const res = await client.query(text, [limit, offset, status, project, sequence]);
client.release();
return res.rows;
}
module.exports = get;

View File

@@ -0,0 +1,6 @@
module.exports = {
get: require('./get'),
post: require('./post'),
put: require('./put'),
delete: require('./delete')
};

View File

@@ -0,0 +1,38 @@
const { transaction, pool } = require('../connection');
async function post(queueId/*unused*/, payload, parent_id) {
// Technically the API only supports an array payload,
// but we'll be permissive and accept a single request
// object.
if (!Array.isArray(payload)) {
payload = [payload];
}
const client = await pool.connect();
await transaction.begin(client);
for (const {project, sequence} of payload) {
if ([project, sequence].some(v => typeof v === "undefined")) {
throw { status: 400, message: "Malformed request" };
}
// If we got here, the request is probably OK.
// Most fields just take default values.
const text = `
INSERT INTO queue_items (payload, parent_id)
VALUES ($1, $2);
`;
await client.query(text, [{project, sequence}, parent_id]);
}
await transaction.commit(client);
client.release();
}
module.exports = post;

View File

@@ -0,0 +1,49 @@
const { pool } = require('../connection');
/**
* Stringify arrays.
*
* node-postgres has an issue in that it transforms
* JSON array objects into PostgreSQL arrays instead
* of JSON arrays, which causes a problem for json/jsonb
* fields.
*
* Note: this is a feature not a bug. See:
* https://github.com/brianc/node-postgres/issues/442
*/
function formatValue(value) {
if (Array.isArray(value)) {
return JSON.stringify(value);
} else {
return value;
}
}
async function put (item_id, values) {
const client = await pool.connect();
const updateable = [ "status", "results", "parent_id" ];
const fields = [];
const params = [item_id];
for (const field of updateable) {
if (field in values) {
fields.push(`${field} = $${params.length+1}`);
params.push(formatValue(values[field]));
}
}
const text = `
UPDATE queue_items
SET
${fields.join(",\n")}
WHERE item_id = $1
RETURNING *;
`;
const res = await client.query(text, params);
client.release();
return res.rows[0];
}
module.exports = put;

View File

@@ -1,29 +1,26 @@
const { setSurvey } = require('../connection');
function thinout (key, obj) {
const path = key.split(".");
// console.log("path", path);
const value = path.reduce( (a, b, i) => {
// console.log("index", i);
if (a !== null && typeof a != "undefined") {
if (b == "*" && Array.isArray(a)) {
const subkey = path.splice(i+1).join(".");
console.log("subkey", subkey);
return a.map(e => thinout(subkey, e));
} else {
// console.log("key", b, "value", a);
return a[b];
}
} else {
// console.log("null or undef");
return a;
}
}, obj);
return value;
async function getSummary (projectId, sequence, opts = {}) {
const client = await setSurvey(projectId);
const text = `
SELECT *
FROM sequences_summary
WHERE sequence = $1;
`;
const res = await client.query(text, [sequence]);
client.release();
return res.rows[0];
}
async function get (projectId, sequence, opts = {}) {
if (opts.summary) {
return await getSummary(projectId, sequence, opts);
}
const client = await setSurvey(projectId);
const sortFields = [
@@ -33,11 +30,11 @@ async function get (projectId, sequence, opts = {}) {
const sortDir = opts.sortDesc == "false" ? "ASC" : "DESC";
const offset = Math.abs((opts.page-1)*opts.itemsPerPage) || 0;
const limit = Math.abs(Number(opts.itemsPerPage)) || null;
const restriction = sequence != 0
? "sequence = $3"
: "TRUE OR $3";
const text = `
SELECT
sequence, sailline, line, point, tstamp,
@@ -58,32 +55,13 @@ async function get (projectId, sequence, opts = {}) {
const values = [offset, limit, sequence, opts.path || "$"];
const res = await client.query(text, values);
client.release();
if (opts.project) {
const tokens = opts.project.split(/\s*[,;:\s]\s*/).filter(e => e.length);
const project = tokens.map(i => i.replace(/^([^.]+)\..*$/, "$1"));
return res.rows.map( r =>
Object.fromEntries(Object.entries(r).filter(entry => project.includes(entry[0])))
);
// const deep = tokens.filter(i => i.includes("."));
// console.log("tokens", tokens, "project", project, "deep", deep);
// if (deep.length) {
// return res.rows.map( r => {
// const o = Object.fromEntries(Object.entries(r).filter(entry => project.includes(entry[0])))
// deep.forEach(path => {
// console.log("path", path, path.split(".")[0]);
// console.log("object", o);
// console.log("result", thinout(path, o));
// return o[path.split(".")[0]] = thinout(path, o)
// })
// console.log("Object", o);
// return o;
// });
// } else {
// return res.rows.map( r =>
// Object.fromEntries(Object.entries(r).filter(entry => project.includes(entry[0])))
// );
// }
} else {
return res.rows;
}

View File

@@ -14,8 +14,12 @@ async function list (projectId, opts = {}) {
const filter = opts.filter;
const noFilter = !("filter" in opts) || opts.filter === null || !String(opts.filter).trim().length;
const sequenceText = opts.sequence
? "sequence = $4"
: "$4 = $4";
const filterText = noFilter
? "TRUE"
? "$3 = $3"
: `
$3 = sequence::text
OR line::text ~ $3
@@ -26,7 +30,7 @@ async function list (projectId, opts = {}) {
OR (($3 ILIKE 'process%' OR $3 ILIKE 'final%') AND duration_final IS NOT NULL)
OR ($3 ILIKE 'raw' AND duration_final IS NULL)
`;
const missing_shots = opts.missing
? `,
COALESCE((SELECT jsonb_agg(msrp) FROM missing_sequence_raw_points msrp WHERE msrp.sequence = ss.sequence), '[]'::jsonb) missing_raw,
@@ -62,14 +66,14 @@ async function list (projectId, opts = {}) {
`)
+ `
WHERE
${sequenceText} AND
${filterText}
ORDER BY ${sortKey} ${sortDir}
OFFSET $1
LIMIT $2;
`;
const values = [offset, limit];
if (!noFilter) values.push(filter);
const values = [offset, limit, filter||0, opts.sequence||0];
const res = await client.query(text, values);
client.release();
return res.rows;

View File

View File

@@ -0,0 +1,19 @@
const path = require('path');
function filename(sequenceOrFilename, extension) {
if (typeof sequenceOrFilename === "string") {
const name = sequenceOrFilename;
return path.basename(name, path.extname(name))+extension;
} else if (typeof sequenceOrFilename === "object") {
const sequence = sequenceOrFilename;
const lineName = sequence?.meta?.lineName;
const fileName = lineName
? `${lineName}-NavLog${extension}`
: `sequence-${sequence?.sequence}${extension}`;
return fileName;
} else {
return "SequenceData"+extension;
}
}
module.exports = filename;

View File

@@ -0,0 +1,29 @@
const transform = require('../transform');
const prepare = require('../prepare');
const filename = require('./filename');
function makeGeoJSON (events) {
return {
type: "FeatureCollection",
features: events.filter(event => event.geometry).map(event => {
const feature = {
type: "Feature",
geometry: event.geometry,
properties: event
};
delete feature.properties.geometry;
return feature;
})
};
}
async function geojson ({project, sequence}) {
const {events, sequences} = await prepare(project, {sequence});
const data = makeGeoJSON(events);
const name = filename(sequences.find(i => i.sequence == sequence), ".geojson");
return { name, data, mimetype: "application/geo+json" };
}
module.exports = geojson;

View File

@@ -0,0 +1,21 @@
const db = require('../../db');
const render = require('../../render');
const seisjson = require('./seisjson');
const filename = require('./filename');
// FIXME Refactor when able
const defaultTemplatePath = require('path').resolve(__dirname, "../../../../../../etc/default/templates/sequence.html.njk");
async function html ({project, sequence}, seisdata) {
const sequenceData = async () => db.sequence.get(project, sequence, {summary: true});
const template = (await db.configuration.get(project, "sse/templates/0/template")) || defaultTemplatePath;
const json = seisdata?.data ?? await seisjson({project, sequence});
const data = await render(json, template);
const name = filename(seisdata?.name || await sequenceData(), ".html");
return { name, data, mimetype: "text/html" };
}
module.exports = html;

View File

@@ -0,0 +1,8 @@
module.exports = {
// csv: require('./csv'),
geojson: require('./geojson'),
html: require('./html'),
pdf: require('./pdf'),
seisjson: require('./seisjson'),
};

View File

@@ -0,0 +1,40 @@
const fs = require('fs/promises');
const Path = require('path');
const crypto = require('crypto');
const { url2pdf } = require('../../selenium');
const html = require('./html');
const filename = require('./filename');
function tmpname (tmpdir="/dev/shm") {
return Path.join(tmpdir, crypto.randomBytes(16).toString('hex')+".tmp");
}
async function makePDF (text) {
const fname = tmpname();
try {
await fs.writeFile(fname, text);
const pdf = Buffer.from(await url2pdf("file://"+fname), "base64");
return pdf;
} catch (err) {
if (err.message.startsWith("template")) {
throw {message: err.message};
} else {
throw err;
}
} finally {
await fs.unlink(fname);
}
}
async function pdf ({project, sequence}, seisdata) {
const htmlData = await html({project, sequence}, seisdata);
const text = htmlData.data;
const data = await makePDF(text);
const name = filename(seisdata?.name, ".pdf");
return { name, data, mimetype: "application/pdf" };
}
module.exports = pdf;

View File

@@ -0,0 +1,14 @@
const transform = require('../transform');
const prepare = require('../prepare');
const filename = require('./filename');
async function seisjson ({project, sequence}) {
const {events, sequences} = await prepare(project, {sequence});
const data = transform(events, sequences, {projectId: project});
const name = filename(sequences.find(i => i.sequence == sequence), ".json");
return { name, data, mimetype: "application/vnd.seis+json" };
}
module.exports = seisjson;

View File

@@ -0,0 +1,51 @@
const { queue } = require('../../lib/db');
/**
* Fetch up to limit items from the queue having
* status = status. Take oldest first.
*/
async function fetchItems ({status, limit}) {
status = status || "queued";
limit = limit || 10;
return await queue.get(null, {status, limit, order: "created_on", dir: "+"})
}
/**
* Set items to status=failed in the database, save
* the results *and* modify the item itself, so that
* it can be picked up by rescheduleFailed().
*/
async function markFailed (item, results) {
item.status = "failed";
item.results = results;
return await queue.put(item.item_id, {status: 'failed', results});
}
/**
* Set items to status=sent in the database and save
* the results.
*/
async function markSent (item, results) {
return await queue.put(item.item_id, {status: 'sent', results});
}
/**
* Reschedule any item in `items` that has been marked
* as failed. The newly created item will take its parent's
* parent_id if present, or else its item_id.
*/
async function rescheduleFailed (items) {
const failed = items.filter(i => i.status == 'failed');
if (failed.length)
console.warn(failed.length, "failed items");
for (const item of failed) {
await queue.post('asaqc', item.payload, item.parent_id || item.item_id);
}
}
module.exports = {fetchItems, markFailed, markSent, rescheduleFailed};

View File

@@ -0,0 +1,56 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const fetch = require('node-fetch');
const DOUGAL_ROOT = require('../../lib/config').DOUGAL_ROOT;
const cfg = require('../../lib/config').global.queues.asaqc.request;
/**
* Return a suitably configured httpsAgent with the client's TLS
* credentials if given.
*/
function httpsAgent () {
// References:
// https://github.com/node-fetch/node-fetch/issues/904
// https://nodejs.org/api/https.html#https_https_request_options_callback
if (!cfg.httpsAgent) {
return;
}
const options = {
key: fs.readFileSync(path.resolve(DOUGAL_ROOT, cfg.httpsAgent.key)),
cert: fs.readFileSync(path.resolve(DOUGAL_ROOT, cfg.httpsAgent.cert))
}
return https.Agent(options);
}
/**
* Send a payload to the ASAQC `upload-file-encoded` endpoint.
* https://api.equinor.com/docs/services/vessel-track/operations/FileUploadEncoded
*/
async function despatchPayload(payload) {
cfg.args.headers["Ocp-Apim-Subscription-Key"] = process.env.DOUGAL_ASAQC_SUBSCRIPTION_KEY;
try {
const res = await fetch(cfg.url, {
...cfg.args,
body: JSON.stringify(payload),
agent: httpsAgent()
});
if (res) {
return await res.json();
} else {
console.error("NO RESPONSE FROM ASAQC ENDPOINT");
}
} catch (err) {
console.error(err);
return {error: err};
}
}
module.exports = despatchPayload;

View File

@@ -0,0 +1,156 @@
/**
* A minimalist mock-up of the ASAQC API, used
* for testing.
*
* Use the following environment variables to
* configure its behaviour:
*
* HTTP_PORT Port to listen to. Defaults to 3077.
*
* HTTPS_PEM Combined public and private parts of
* a TLS certificate to use. If provided, an HTTPS
* server will be started. Alternatively, the user
* may provide separate files (see below).
*
* HTTPS_CERT Public certificate to use in HTTPS
* mode.
*
* HTTPS_KEY Private key to use along `HTTPS_CERT`
* in HTTPS mode. Note that both must be provided.
*
* HTTPS_CA Public certificate of a certificate
* authority or the public part of a client certificate.
* If provided, the client needs to authenticate with
* a certificate signed by this authority, or with this
* certifcate itself, if self-signed.
*
* See also ../index.js for other environment
* variable options.
*
*/
const http = require('http');
const https = require('https');
const fs = require('fs');
const express = require('express');
const mw = require('./middleware');
const app = express();
const verbose = process.env.NODE_ENV != 'test';
app.map = function(a, route){
route = route || '';
for (var key in a) {
switch (typeof a[key]) {
// { '/path': { ... }}
case 'object':
if (!Array.isArray(a[key])) {
app.map(a[key], route + key);
break;
} // else drop through
// get: function(){ ... }
case 'function':
if (verbose) console.log('%s %s', key, route);
app[key](route, a[key]);
break;
}
}
};
app.use(express.json({type: "application/json", strict: false, limit: '10mb'}));
app.use(express.urlencoded({ type: "application/x-www-form-urlencoded", extended: true }));
app.use(express.text({type: "text/*", limit: '10mb'}));
app.use((req, res, next) => {
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE");
res.set("Access-Control-Allow-Headers", "Content-Type");
next();
});
app.map({
'/vt/v1/api/upload-file-encoded': {
post: [ mw.authorisation, mw.post ]
}
});
// Generic error handler. Stops stack dumps
// being sent to clients.
app.use(function (err, req, res, next) {
const title = `HTTP backend error at ${req.method} ${req.originalUrl}`;
const description = err.message;
const message = err.message;
const alert = {title, message, description, error: err};
console.log("Error:", err);
res.set("Content-Type", "application/json");
if (err instanceof Error && err.name != "UnauthorizedError") {
console.error(err.stack);
res.set("Content-Type", "text/plain");
res.status(500).send('General internal error');
} else if (typeof err === 'string') {
res.status(500).send({message: err});
} else {
res.status(err.status || 500).send({message: err.message || (err.inner && err.inner.message) || "Internal error"});
}
});
app.disable('x-powered-by');
app.enable('trust proxy');
console.log('trust proxy is ' + (app.get('trust proxy')? 'on' : 'off'));
const addr = "127.0.0.1";
if (!module.parent) {
var port = process.env.HTTP_PORT || 3000;
var server = http.createServer(app).listen(port, addr);
console.log('API started on port ' + port);
} else {
app.start = function (port = 3000, path, locals={}) {
app.locals = {...app.locals, ...locals};
var root = app;
if (path) {
root = express();
['x-powered-by', 'trust proxy'].forEach(k => root.set(k, app.get(k)));
root.use(path, app);
}
if (process.env.HTTPS_PEM || (process.env.HTTPS_CERT && process.env.HTTPS_KEY)) {
const options = {
cert: fs.readFileSync(process.env.HTTPS_CERT || process.env.HTTPS_PEM),
key: fs.readFileSync(process.env.HTTPS_KEY || process.env.HTTPS_PEM)
};
if (process.env.HTTPS_CA) {
// Enable client certificate authentication
options.requestCert = true;
options.ca = fs.readFileSync(process.env.HTTPS_CA);
options.rejectUnauthorized = true;
console.log("TLS client authentication requested");
}
const server = https.createServer(options, root).listen(port, addr);
if (server) {
console.log(`TLS API started on port ${port}, prefix: ${path || "/"}`);
}
return server;
} else {
const server = http.createServer(root).listen(port, addr);
if (server) {
console.log(`API started on port ${port}, prefix: ${path || "/"}`);
}
return server;
}
}
module.exports = app;
}

View File

@@ -0,0 +1,25 @@
/**
* This method imitates the ASAQC header-based
* authorisation.
*
* The valid subscription token should be defined
* in the $DOUGAL_ASAQC_SUBSCRIPTION_KEY environent
* variable (e.g., by inclusion in .dougalrc).
*/
module.exports = async function (req, res, next) {
try {
const authSent = req.get("Ocp-Apim-Subscription-Key");
const authExpected = process.env.DOUGAL_ASAQC_SUBSCRIPTION_KEY;
if (authSent == authExpected || !authSent) {
next()
} else {
throw {status: 401, message: "Not authorised"};
}
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,4 @@
module.exports = {
authorisation: require('./authorisation'),
post: require('./post')
};

View File

@@ -0,0 +1,68 @@
const fs = require('fs');
const path = require('path');
const uuid = require('uuid/v4');
/**
* Suggest new names for files.
*
* Takes a file name and adds a numeric suffix to it if
* there isn't one, or increments it otherwise.
*/
function rename(filename) {
const ext = path.extname(filename);
const base = path.basename(filename, ext);
const bare = base.match(/^(.*?)(-\d+)?$/)[1]; // Strips out any -\d+ suffix
const suffix = Number((base.match(/-(\d+)$/) || [])[1]) || 0; // Because NaN
return `${bare}-${suffix+1}${ext}`;
}
/**
* Save `data` as `filename` in `dirname`.
*/
async function saveFile (dirname, filename, data) {
const fname = path.resolve(dirname, filename);
if (fs.existsSync(fname)) {
return await saveFile(dirname, rename(filename), data);
} else {
await fs.promises.writeFile(fname, data);
return filename;
}
}
/**
* This method imitates the ASAQC endpoint
* /vt/v1/api/upload-file-encoded
*
* If OUTPUT_DIR is defined, it tries to save the
* received data as files in that directory, else
* it discards the data but still produces a
* plausible response.
*/
module.exports = async function (req, res, next) {
try {
const payload = req.body;
const response = {id: uuid()};
if (payload.fileName && payload.encodedData) {
const data = Buffer.from(payload.encodedData, 'base64');
console.log(`Received ${payload.fileName}, ${data.length} bytes`);
if (req.app.locals.OUTPUT_DIR) {
response.fileName = await saveFile(req.app.locals.OUTPUT_DIR, payload.fileName, data);
} else {
console.log("No disk output");
}
res.send(response);
} else {
res.status(400).send({message: "Bad syntax"});
}
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,29 @@
#!/usr/bin/node
/**
* A minimalist mock-up of the ASAQC server, used
* for testing.
*
* Use the following environment variables to
* configure its behaviour:
*
* ASAQC_DUMMY_OUTPUT_DIR Path to a directory in which
* to store the received data. If not provided, the
* data will be discarded but the API will still return
* a success code. Alternatively, a path may be given
* in the command line.
*
* See also api/index.js for other environment
* variable options.
*
* Example command line:
*
* HTTPS_CA="./certs/client.pem" HTTPS_PEM="./certs/dougal.pem" ./index.js ./store
*
*/
const api = require('./api');
const OUTPUT_DIR = process.argv[2] || process.env.ASAQC_DUMMY_OUTPUT_DIR;
const server = api.start(process.env.HTTP_PORT || 3077, process.env.HTTP_PATH, {OUTPUT_DIR});

View File

@@ -0,0 +1,29 @@
#!/usr/bin/node
/*
* Can be required as a module or called directly.
*
* In the latter case, it will do a queue run.
*
* The following environment variables may come in
* useful:
*
* DOUGAL_ROOT Use it to specify the path to Dougal's
* top directory (`software/`). Most of the time this
* is not needed unless running in a development
* environment.
*
* NODE_TLS_REJECT_UNAUTHORIZED=0 Use this when running
* against the internal test server or any other endpoint
* that has self-signed certificates. WARNING: think carefully
* if you really want to do this, most of the time you don't.
*/
module.exports = {
processQueue: require('./process')
}
if (!module.parent) {
module.exports.processQueue().then(() => process.exit());
}

View File

@@ -0,0 +1,67 @@
const { createHash } = require('crypto');
const { seisjson, pdf } = require('../../lib/sse/present');
const { configuration } = require('../../lib/db');
function digestOf(content) {
const hash = createHash('sha256');
const data = (typeof content.data == "string" || Buffer.isBuffer(content.data))
? content.data
: JSON.stringify(content.data);
hash.update(data);
return {
sha256: {hex: hash.digest('hex')}
};
}
/**
* Create the payloads to send to the ASAQC endpoint
* for a queue item.
*
* At present this consists of two files, which must be
* sent in two separate requests. One is the SeisJSON
* file and the other is its PDF representation.
*
* In principle, other options are possible, such as
* GeoJSON and CSV, and this could be made configurable.
*
* Likewise, it would be possible to upload P1 and P2 files,
* etc.
*/
async function getPayloads(item) {
const asaqcConfig = await configuration.get(item.payload.project, '/asaqc');
const surveyName = await configuration.get(item.payload.project, 'id');
const template = {
type: "acqlinelog",
imo: asaqcConfig.imo,
mmsi: asaqcConfig.mmsi,
surveyName: surveyName,
surveyId: asaqcConfig.id
};
const seisjsonData = await seisjson(item.payload);
const pdfData = await pdf(item.payload, seisjsonData);
return [
{
payload: {
...template,
fileName: seisjsonData.name,
encodedData: Buffer.from(JSON.stringify(seisjsonData.data)).toString("base64")
},
digest: digestOf(seisjsonData)
},
{
payload: {
...template,
fileName: pdfData.name,
encodedData: pdfData.data.toString("base64")
},
digest: digestOf(pdfData)
}
]
}
module.exports = getPayloads;

View File

@@ -0,0 +1,44 @@
const getPayloads = require('./payloads');
const despatchPayload = require('./despatch');
const {fetchItems, markFailed, markSent, rescheduleFailed} = require('./db');
function passed (result) {
return "id" in result;
}
/**
* Process the queue.
*
* Try to send up to a certain number of
* items from the queue.
* Reschedule any failed items.
*/
async function processQueue () {
const items = await fetchItems({status: "queued"});
for (const item of items) {
const payloads = await getPayloads(item, (digestInfo) => {item.digest = digestInfo});
const results = [];
for (const {payload, digest} of payloads) {
const response = await despatchPayload(payload);
results.push({response, digest});
}
if (results.some(result => !passed(result.response))) {
await markFailed(item, results);
} else {
await markSent(item, results);
}
}
await rescheduleFailed(items);
}
module.exports = processQueue;
if (!module.parent) {
processQueue().then(() => process.exit());
}

View File

@@ -1,6 +1,6 @@
openapi: 3.0.0
info:
version: 0.1.0
version: 0.2.0
title: Dougal API
description: >
Public API of the Dougal seismic production & data analysis tool.
@@ -34,10 +34,10 @@ info:
servers:
- url: https://dougal.aaltronav.eu/api
description: Dougal demo server
- url: https://siddis.dougal.aaltronav.eu/api
description: M/V Siddis Sailor, external access
- url: https://lan.siddis.dougal.aaltronav.eu/api
description: M/V Siddis Sailor, LAN access only
- url: https://248.dougal.aaltronav.eu/api
description: Crew 248, external access
- url: https://lan.248.dougal.aaltronav.eu/api
description: Crew 248, LAN access only
tags:
-
@@ -61,6 +61,15 @@ tags:
-
name: navdata
description: Data from navigation headers
-
name: asaqc
description: |
Interface to [Equinor](https://equinor.com/)'s [ASAQC Vessel Track](https://api.equinor.com/docs/services/vessel-track/) application programming interface.
Used for transferring line log data from the vessel to the Equinor office. As implemented as of October 2021, for each sequence that is queued for sending it transfers the following files:
* SeisJSON file containing sequence event data.
* PDF file which is a representation of the corresponding SeisJSON.
components:
securitySchemes:
@@ -103,7 +112,7 @@ components:
This authentication is used by users having the `user` role, giving read and write access to the Dougal system, except for administrative endpoints. See the description of `CookieAuthGuest` for more information on how to obtain a token.
responses:
401Error:
"401":
description: Unauthorised. Either no credentials were provided or the user does not have sufficient privileges to perform this operation.
content:
application/json:
@@ -701,9 +710,103 @@ components:
description: Internal database reference to the current project (only present when `_online` is `true`)
QueueRequestASAQC:
description: >
Parameters of an ASAQC transfer item.
type: object
required: [ "project", "sequence", "mimetype" ]
properties:
project:
type: string
description: Project ID.
sequence:
type: integer
description: Sequence number.
example:
{
"project": "eq21205",
"sequence": 55
}
QueueItem:
description: Information about an item in the transfers queue
type: object
properties:
item_id:
type: integer
description: The unique ID of this queue item.
queue_id:
type: integer
description: The ID of the queue that this item belongs to.
status:
type: string
enum: [ "queued", "cancelled", "failed", "sent" ]
description: Status of the queue request. Failed transfers may get rescheduled as a new item, in which case `parent_id` will reference the original request.
payload:
$ref: "#/components/schemas/QueueRequestASAQC"
results:
type: object
description: Information about the transfer after an attempt has been made. It is non-empty only for items with status `failed` or `sent`.
properties:
digest:
type: object
description: Cryptographic digest of the transmitted payload.
properties:
sha256:
type: object
description: SHA-256 digest.
properties:
hex:
type: string
description: Hexadecimal representation of the SHA-256 hash of the contents of the file that was sent. Can be used to check, with a high degree of confidence, whether a copy of the file is byte-by-byte the same as the data that was sent.
response:
type: object
description: If the request was successful, the contents of this property are the response body sent by the remote server. If there was an error, the `error` property will be present and may contain details of the problem.
created_on:
type: string
format: date-time
description: Creation timestamp of this request.
updated_on:
type: string
format: date-time
description: Last update timestamp of this request.
not_before:
type: string
format: date-time
description: The request will not be processed before this.
parent_id:
type: integer
description: If this request is a retry of a failed attempt, this item references the ID of the original request.
example:
{
"item_id": 87,
"status": "sent",
"payload": {
"project": "eq21205",
"sequence": 55
},
"results": [
{
"id": "849e8659-953d-466c-8cfc-ad8d615d1ac0",
"fileName": "1065960055S00000-NavLog.json"
},
{
"id": "517864a7-0981-46c6-8f39-aa53da865de7",
"fileName": "1065960055S00000-NavLog.pdf"
}
],
"created_on": "2021-10-03T01:47:02.312Z",
"updated_on": "2021-10-03T01:51:19.656Z",
"not_before": "1970-01-01T00:00:00.000Z",
"parent_id": null
}
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
- BearerAuthUser: []
- CookieAuthUser: []
paths:
@@ -711,6 +814,9 @@ paths:
get:
description: Get list of projects.
tags: [ "project" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
responses:
"200":
description: List of projects
@@ -730,13 +836,16 @@ paths:
meta:
type: object
"401":
$ref: "#/components/responses/401Error"
$ref: "#/components/responses/401"
/project/{project}:
# Synonym: /project/{project}/summary:
get:
description: Get details about a specific project.
tags: [ "project" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
@@ -747,13 +856,16 @@ paths:
schema:
$ref: "#/components/schemas/ProjectSummary"
"401":
$ref: "#/components/responses/401Error"
$ref: "#/components/responses/401"
/project/{project}/line/:
get:
description: Get list of preplot lines.
tags: [ "preplot" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
@@ -766,7 +878,7 @@ paths:
items:
$ref: "#/components/schemas/PreplotLine"
"401":
$ref: "#/components/responses/401Error"
$ref: "#/components/responses/401"
/project/{project}/line/{line}:
@@ -809,6 +921,9 @@ paths:
get:
description: Get list of sequences.
tags: [ "sequences" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
@@ -821,13 +936,16 @@ paths:
items:
$ref: "#/components/schemas/Sequence"
"401":
$ref: "#/components/responses/401Error"
$ref: "#/components/responses/401"
/project/{project}/sequence/{sequence}:
get:
description: Get a sequence
tags: [ "sequences" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
- $ref: "#/components/parameters/Sequence"
@@ -880,12 +998,17 @@ paths:
responses:
"204":
description: The resource has been successfully updated.
"401":
$ref: "#/components/responses/401"
/project/{project}/plan:
get:
description: Get list of planned sequences.
tags: [ "planner" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
@@ -912,6 +1035,8 @@ paths:
responses:
"201":
description: Sequence successfully added
"401":
$ref: "#/components/responses/401"
put:
summary: Modify a planned sequence
@@ -928,6 +1053,8 @@ paths:
responses:
"201":
description: Sequence successfully modified
"401":
$ref: "#/components/responses/401"
/project/{project}/plan/{sequence}:
@@ -950,6 +1077,8 @@ paths:
responses:
"201":
description: Sequence successfully modified
"401":
$ref: "#/components/responses/401"
delete:
description: Remove a sequence from the plan.
@@ -960,12 +1089,17 @@ paths:
responses:
"204":
description: Returned when the sequence no longer exists in the plan. Note that it is possible that the sequence never existed in the first place, as supplying a valid but non-existing sequence number is not an error.
"401":
$ref: "#/components/responses/401"
/project/{project}/event:
get:
summary: Get project events.
tags: [ "log" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
@@ -987,6 +1121,8 @@ paths:
application/vnd.seis+json:
schema:
$ref: "#/components/schemas/SeisExport"
"401":
$ref: "#/components/responses/401"
post:
summary: Add a new event
tags: [ "log" ]
@@ -1004,6 +1140,8 @@ paths:
responses:
"201":
description: New event created successfully
"401":
$ref: "#/components/responses/401"
put:
summary: Update an existing event.
tags: [ "log" ]
@@ -1021,6 +1159,8 @@ paths:
responses:
"201":
description: Event updated successfully.
"401":
$ref: "#/components/responses/401"
delete:
summary: Delete an event.
tags: [ "log" ]
@@ -1046,6 +1186,8 @@ paths:
responses:
"204":
description: The event no longer exists.
"401":
$ref: "#/components/responses/401"
/project/{project}/event/{type}/{id}:
@@ -1084,6 +1226,8 @@ paths:
responses:
"201":
description: Event updated successfully.
"401":
$ref: "#/components/responses/401"
delete:
summary: Delete an event.
@@ -1114,12 +1258,17 @@ paths:
responses:
"204":
description: The event no longer exists.
"401":
$ref: "#/components/responses/401"
/project/{project}/label:
get:
summary: Get project labels.
tags: [ "project" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
@@ -1131,12 +1280,17 @@ paths:
type: array
items:
$ref: "#/components/schemas/Label"
"401":
$ref: "#/components/responses/401"
/project/{project}/configuration/{path}:
get:
summary: Get project configuration data.
tags: [ "project" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
-
@@ -1169,6 +1323,8 @@ paths:
"sequence"
]
}
"401":
$ref: "#/components/responses/401"
"404":
description: The requested parameter does not exist. Note that the HTTP return code allows us to differentiate between a non-existent parameter and one which is `null` or `undefined`.
@@ -1179,6 +1335,9 @@ paths:
summary: Get project information data.
description: Each project has a key/value store where arbitrary information can be stored. The keys are simple strings and the values are JSON objects. This endpoint allows access to this information; either the full object can be returned or a subset of it, by setting the `{path}` accordingly.
tags: [ "project" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
-
@@ -1201,6 +1360,8 @@ paths:
- type: string
- type: number
- type: boolean
"401":
$ref: "#/components/responses/401"
/project/{project}/meta/{type}/{kind}/{path}:
@@ -1216,6 +1377,9 @@ paths:
`lines` | ✔ | ✘ | ✘
tags: [ "project", "metadata" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
-
@@ -1258,6 +1422,8 @@ paths:
- type: string
- type: number
- type: boolean
"401":
$ref: "#/components/responses/401"
"404":
description: The requested data does not exist. Note that the HTTP return code allows us to differentiate between a non-existent item and one which is `null` or `undefined`.
@@ -1303,12 +1469,17 @@ paths:
responses:
"201":
description: Metadata updated successfully.
"401":
$ref: "#/components/responses/401"
/navdata:
get:
summary: Get navigation data.
tags: [ "navdata" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/QueryLimit"
- $ref: "#/components/parameters/QueryOffset"
@@ -1321,12 +1492,17 @@ paths:
type: array
items:
$ref: "#/components/schemas/NavData"
"401":
$ref: "#/components/responses/401"
/navdata/gis/point:
get:
summary: Get navigation data as GeoJSON point features
tags: [ "navdata" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/QueryLimit"
- $ref: "#/components/parameters/QueryOffset"
@@ -1339,12 +1515,17 @@ paths:
type: array
items:
$ref: "#/components/schemas/GeoJSONFeature"
"401":
$ref: "#/components/responses/401"
/navdata/gis/line:
get:
summary: Get navigation data as GeoJSON line features
tags: [ "navdata" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
-
description: Maximum number of line vertices
@@ -1361,6 +1542,203 @@ paths:
application/geo+json:
schema:
$ref: "#/components/schemas/GeoJSONFeature"
"401":
$ref: "#/components/responses/401"
/queue/outgoing/asaqc:
get:
summary: Get list of queued items.
tags: [ "asaqc" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
-
name: status
in: query
schema:
type: string
enum: [ "queued", "cancelled", "failed", "sent" ]
description: Return only items with matching status. If unspecified, items will not be filtered by status.
-
name: order
in: query
schema:
type: string
enum:
- created_on
- updated_on
- not_before
- item_id
- status
- parent_id
- project
- sequence
default: updated_on
description: Order the returned items by the specified property.
-
name: dir
in: query
schema:
type: string
enum: [ '-', '+' ]
default: '-'
description: |
Specify whether to order items in ascending or descending order.
* `-` indicates descending order.
* `+` indicates ascending order (in fact, *anything at all* other than `-` means ascending order).
-
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 1000
description: Maximum number of items to retrieve.
- $ref: "#/components/parameters/QueryOffset"
responses:
"200":
description: List of queue items
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/QueueItem"
example:
[
{
"item_id": 167,
"status": "sent",
"payload": {
"project": "eq21203",
"sequence": 176
},
"results": [
{
"digest": {
"sha256": {
"hex": "b67e3a925e0b0845a6e6cfd386acb7841a781b19b994295e251caa635b7c0753"
}
},
"response": {
"id": "118d5a69-a235-4306-887c-90a5fd178e79",
"fileName": "2053721176S00000-NavLog-1.json"
}
},
{
"digest": {
"sha256": {
"hex": "9ea30986db8d44400a3352767a8d4cf794ee68b96c793e0e12fba82363d2f5b1"
}
},
"response": {
"id": "300d291b-7355-44eb-a15a-5ba5637dac65",
"fileName": "2053721176S00000-NavLog-1.pdf"
}
}
],
"created_on": "2021-10-03T18:32:25.480Z",
"updated_on": "2021-10-03T18:35:10.632Z",
"not_before": "1970-01-01T00:00:00.000Z",
"parent_id": 166
},
{
"item_id": 166,
"status": "failed",
"payload": {
"project": "eq21203",
"sequence": 176
},
"results": [
{
"digest": {
"sha256": {
"hex": "2fbacfc0414a51d07629c35efa7d29c5012461d16ddcccb89804d1522c10b553"
}
},
"response": {
"error": {}
}
},
{
"digest": {
"sha256": {
"hex": "4910443e3be2c34990783bb7968746f62dd4ae1fea1c5499ce81f5ba2f262e68"
}
},
"response": {
"error": {
"code": "ECONNREFUSED",
"type": "system",
"errno": "ECONNREFUSED",
"message": "request to https://localhost:3077/vt/v1/api/upload-file-encoded failed, reason: connect ECONNREFUSED 127.0.0.1:3077"
}
}
}
],
"created_on": "2021-10-03T18:32:05.585Z",
"updated_on": "2021-10-03T18:32:25.392Z",
"not_before": "1970-01-01T00:00:00.000Z",
"parent_id": null
}
]
"401":
$ref: "#/components/responses/401"
post:
summary: Add one or more items to the queue.
tags: [ "asaqc" ]
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/QueueRequestASAQC"
responses:
"202":
description: |
All items were added to the queue for processing.
Note that this response does not imply that items have been sent or will be delivered, or that an entity matching the request even exists. It is merely a promise to attempt a transfer at some later time, if the item exists and the remote server is reachable.
"400":
description: |
The request was malformed or had invalid data.
If a request consisting of multiple items fails, none of the items get added to the queue.
"401":
$ref: "#/components/responses/401"
/queue/outgoing/asaqc/{id}:
delete:
summary: Cancel a queued transfer.
description: Use this request to cancel a queued transfer before it is sent. This endpoint does not return a body, success or failure is denoted by the HTTP response status code.
tags: [ "asaqc" ]
parameters:
-
name: id
in: path
schema:
type: integer
description: The ID of a queued request.
responses:
"204":
description: |
Request successfully cancelled. No data has been sent or will be sent.
* If the referenced request had status `queued`, its status was changed to `cancelled`.
* If the referenced request had status `failed` and there existed one or more requests with status `queued` and `parent_id` equal to this request's ID, their status was changed to `cancelled`.
"401":
$ref: "#/components/responses/401"
"404":
description: The request does not exist or it has already been fulfilled and cannot therefore be cancelled.
/login:
@@ -1384,6 +1762,8 @@ paths:
responses:
"204":
description: Login successful. The token is returned in a `Set-Cookie` header.
"401":
$ref: "#/components/responses/401"
/logout: