Skip to content

Commit

Permalink
Merge pull request #263 from dfpc-coe/attachment-attach
Browse files Browse the repository at this point in the history
Attachment Attach
  • Loading branch information
ingalls authored Aug 6, 2024
2 parents 91b51a5 + b026324 commit a37d218
Show file tree
Hide file tree
Showing 13 changed files with 830 additions and 926 deletions.
42 changes: 42 additions & 0 deletions api/lib/control/attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import os from 'node:os';
import { DataPackage } from '@tak-ps/node-cot';
import { randomUUID } from 'node:crypto';
import type { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import fs from 'node:fs';
import S3 from '../aws/s3.js';
import Config from '../config.js';

export default class AttachmentControl {
config: Config;

constructor(config: Config) {
this.config = config;
}

async upload(name: string, body: Readable): Promise<{
hash: string
}> {
const tmp = os.tmpdir() + '/' + randomUUID();

try {
fs.mkdirSync(tmp)
await pipeline(
body,
fs.createWriteStream(tmp + '/' + name)
);

const hash = await DataPackage.hash(tmp + '/' + name);

await S3.put(
`attachment/${hash}/${name}`,
fs.createReadStream(tmp + '/' + name)
);

return { hash };
} catch (err) {
fs.unlinkSync(tmp + '/' + name);
throw err;
}
}
}
772 changes: 358 additions & 414 deletions api/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@
"@types/tough-cookie": "^4.0.2",
"@types/ws": "^8.5.4",
"@types/xml2js": "^0.4.11",
"eslint": "^8.0.0",
"eslint": "^9.0.0",
"path-to-regexp": "^6.0.0",
"sinon": "^18.0.0",
"tape": "^5.6.1",
"typescript": "^5.0.0",
"typescript-eslint": "^7.6.0"
"typescript-eslint": "^8.0.0"
}
}
51 changes: 51 additions & 0 deletions api/routes/attachments.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import path from 'path';
import busboy from 'busboy';
import { Type } from '@sinclair/typebox'
import AttachmentControl from '../lib/control/attachment.js';
import Schema from '@openaddresses/batch-schema';
import Err from '@openaddresses/batch-error';
import Auth from '../lib/auth.js';
import S3 from '../lib/aws/s3.js';
import Config from '../lib/config.js';

export default async function router(schema: Schema, config: Config) {
const attachmentControl = new AttachmentControl(config);

await schema.get('/attachment', {
name: 'List Attachments',
group: 'Attachments',
Expand Down Expand Up @@ -52,6 +56,53 @@ export default async function router(schema: Schema, config: Config) {
}
});

await schema.put('/attachment', {
name: 'Upload Attachment',
group: 'Attachments',
description: 'Upload an attachment that is assigned to a given CoT',
res: Type.Object({
hash: Type.String()
})
}, async (req, res) => {
try {
await Auth.is_auth(config, req);

if (
!req.headers['content-type']
|| !req.headers['content-type'].startsWith('multipart/form-data')
) {
throw new Err(400, null, 'Unsupported Content-Type');
}

const bb = busboy({
headers: req.headers,
limits: { files: 1 }
});

const uploads: Promise<{
hash: string;
}>[] = [];

bb.on('file', async (fieldname, file, blob) => {
uploads.push(attachmentControl.upload(blob.filename, file));
}).on('finish', async () => {
try {
const files = await Promise.all(uploads);

return res.json({
...files[0]
})
} catch (err) {
Err.respond(err, res);
}
});

return req.pipe(bb);
} catch (err) {
return Err.respond(err, res);
}
});

await schema.get('/attachment/:hash', {
name: 'Get Attachment',
group: 'Attachments',
Expand Down
129 changes: 67 additions & 62 deletions api/test/fixtures/get_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
"query": true,
"res": true
},
"PUT /attachment": {
"body": false,
"query": false,
"res": true
},
"GET /attachment/:hash": {
"body": false,
"query": true,
Expand Down Expand Up @@ -504,160 +509,160 @@
"query": true,
"res": false
},
"GET /iconset": {
"GET /marti/missions/:name/layer": {
"body": false,
"query": true,
"query": false,
"res": true
},
"POST /iconset": {
"POST /marti/missions/:name/layer": {
"body": true,
"query": false,
"res": true
},
"PATCH /iconset/:iconset": {
"PATCH /marti/missions/:name/layer/:uid": {
"body": true,
"query": false,
"res": true
},
"GET /iconset/:iconset": {
"body": false,
"query": true,
"res": true
},
"DELETE /iconset/:iconset": {
"DELETE /marti/missions/:name/layer/:uid": {
"body": false,
"query": false,
"res": true
},
"POST /iconset/:iconset/icon": {
"POST /marti/missions/:name/log": {
"body": true,
"query": false,
"res": true
},
"GET /icon": {
"DELETE /marti/missions/:name/log/:log": {
"body": false,
"query": true,
"query": false,
"res": true
},
"GET /iconset/:iconset/icon/:icon": {
"GET /marti/missions/:name": {
"body": false,
"query": false,
"query": true,
"res": true
},
"PATCH /iconset/:iconset/icon/:icon": {
"body": true,
"GET /marti/missions/:name/cot": {
"body": false,
"query": false,
"res": true
},
"DELETE /iconset/:iconset/icon/:icon": {
"GET /marti/missions/:name/changes": {
"body": false,
"query": false,
"query": true,
"res": true
},
"GET /iconset/:iconset/icon/:icon/raw": {
"DELETE /marti/missions/:name": {
"body": false,
"query": true,
"res": false
"res": true
},
"GET /icon/sprite:size?.json": {
"POST /marti/missions/:name": {
"body": false,
"query": true,
"res": false
"res": true
},
"GET /icon/sprite:size?.png": {
"GET /marti/mission": {
"body": false,
"query": true,
"res": false
"res": true
},
"GET /marti/missions/:name/layer": {
"GET /marti/missions/:name/role": {
"body": false,
"query": false,
"res": true
},
"POST /marti/missions/:name/layer": {
"body": true,
"GET /marti/missions/:name/subscriptions": {
"body": false,
"query": false,
"res": true
},
"PATCH /marti/missions/:name/layer/:uid": {
"body": true,
"GET /marti/missions/:name/subscriptions/roles": {
"body": false,
"query": false,
"res": true
},
"DELETE /marti/missions/:name/layer/:uid": {
"GET /marti/missions/:name/contacts": {
"body": false,
"query": false,
"res": true
},
"POST /marti/missions/:name/log": {
"body": true,
"query": false,
"POST /marti/missions/:name/upload": {
"body": false,
"query": true,
"res": true
},
"DELETE /marti/missions/:name/log/:log": {
"DELETE /marti/missions/:name/upload/:hash": {
"body": false,
"query": false,
"res": true
},
"GET /marti/missions/:name": {
"GET /iconset": {
"body": false,
"query": true,
"res": true
},
"GET /marti/missions/:name/cot": {
"body": false,
"POST /iconset": {
"body": true,
"query": false,
"res": true
},
"GET /marti/missions/:name/changes": {
"body": false,
"query": true,
"PATCH /iconset/:iconset": {
"body": true,
"query": false,
"res": true
},
"DELETE /marti/missions/:name": {
"GET /iconset/:iconset": {
"body": false,
"query": true,
"res": true
},
"POST /marti/missions/:name": {
"DELETE /iconset/:iconset": {
"body": false,
"query": true,
"query": false,
"res": true
},
"GET /marti/mission": {
"POST /iconset/:iconset/icon": {
"body": true,
"query": false,
"res": true
},
"GET /icon": {
"body": false,
"query": true,
"res": true
},
"GET /marti/missions/:name/role": {
"GET /iconset/:iconset/icon/:icon": {
"body": false,
"query": false,
"res": true
},
"GET /marti/missions/:name/subscriptions": {
"body": false,
"PATCH /iconset/:iconset/icon/:icon": {
"body": true,
"query": false,
"res": true
},
"GET /marti/missions/:name/subscriptions/roles": {
"DELETE /iconset/:iconset/icon/:icon": {
"body": false,
"query": false,
"res": true
},
"GET /marti/missions/:name/contacts": {
"GET /iconset/:iconset/icon/:icon/raw": {
"body": false,
"query": false,
"res": true
"query": true,
"res": false
},
"POST /marti/missions/:name/upload": {
"GET /icon/sprite:size?.json": {
"body": false,
"query": true,
"res": true
"res": false
},
"DELETE /marti/missions/:name/upload/:hash": {
"GET /icon/sprite:size?.png": {
"body": false,
"query": false,
"res": true
"query": true,
"res": false
},
"POST /marti/package": {
"body": false,
Expand Down Expand Up @@ -824,11 +829,6 @@
"query": false,
"res": true
},
"GET /swagger": {
"body": false,
"query": false,
"res": true
},
"GET /search/reverse/:longitude/:latitude": {
"body": false,
"query": false,
Expand All @@ -844,6 +844,11 @@
"query": true,
"res": true
},
"GET /swagger": {
"body": false,
"query": false,
"res": true
},
"GET /task": {
"body": false,
"query": true,
Expand Down
6 changes: 5 additions & 1 deletion api/web/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<!DOCTYPE html>
<html class='h-full' lang="en">
<html
class='h-full'
lang="en"
style='overflow: hidden;'
>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
Expand Down
Loading

0 comments on commit a37d218

Please sign in to comment.