@tak-ps/node-tak
Version:
Lightweight JavaScript library for communicating with TAK Server
213 lines • 7.59 kB
JavaScript
import { Readable } from 'node:stream';
import mime from 'mime';
import Err from '@openaddresses/batch-error';
import Commands, { CommandOutputFormat } from '../commands.js';
import { TAKList } from './types.js';
import { Type } from '@sinclair/typebox';
export const Content = Type.Object({
UID: Type.String(),
SubmissionDateTime: Type.String(),
Keywords: Type.Array(Type.String()),
MIMEType: Type.String(),
SubmissionUser: Type.String(),
PrimaryKey: Type.String(),
Hash: Type.String(),
CreatorUid: Type.String(),
Name: Type.String()
});
export const TAKList_Content = TAKList(Type.Object({
filename: Type.String(),
keywords: Type.Array(Type.String()),
mimeType: Type.String(),
name: Type.String(),
submissionTime: Type.String(),
submitter: Type.String(),
uid: Type.String(),
size: Type.Integer(),
}));
export const Config = Type.Object({
uploadSizeLimit: Type.Integer()
});
export default class FileCommands extends Commands {
schema = {
list: {
description: 'List Files',
params: Type.Object({}),
query: Type.Object({}),
formats: [CommandOutputFormat.JSON]
}
};
async cli(args) {
if (args._[3] === 'list') {
const list = await this.list();
if (args.format === 'json') {
return list;
}
else {
return list.data.map((data) => {
return data.filename;
}).join('\n');
}
}
else {
throw new Error('Unsupported Subcommand');
}
}
async list() {
const url = new URL(`/Marti/api/sync/search`, this.api.url);
return await this.api.fetch(url, {
method: 'GET'
});
}
async meta(hash) {
const url = new URL(`/Marti/sync/${encodeURIComponent(hash)}/metadata`, this.api.url);
const res = await this.api.fetch(url, {
method: 'GET'
}, true);
const body = await res.text();
return body;
}
async download(hash) {
const url = new URL(`/Marti/sync/content`, this.api.url);
url.searchParams.append('hash', hash);
const res = await this.api.fetch(url, {
method: 'GET'
}, true);
// The raw fetch path skips status validation - without this check a
// TAK Server error page would be streamed as if it were file content
if (res.status < 200 || res.status >= 400) {
const body = await res.text().catch(() => '');
throw new Err(res.status, null, body || `Status Code: ${res.status}`);
}
return res.body;
}
async adminDelete(hash) {
const url = new URL(`/Marti/api/files/${hash}`, this.api.url);
return await this.api.fetch(url, {
method: 'DELETE',
});
}
async delete(hash) {
const url = new URL(`/Marti/sync/delete`, this.api.url);
url.searchParams.append('hash', hash);
return await this.api.fetch(url, {
method: 'DELETE',
});
}
// TODO Return a Content Object
/**
* Update a Package that should appear in the Public Data Packages List
*/
async uploadPackage(opts, body) {
const url = new URL(`/Marti/sync/missionupload`, this.api.url);
url.searchParams.append('filename', opts.name);
url.searchParams.append('creatorUid', opts.creatorUid);
url.searchParams.append('hash', opts.hash);
if (opts.mimetype) {
url.searchParams.append('mimetype', opts.mimetype);
}
// Needs this keywordt to appear in public list
url.searchParams.append('keyword', 'missionpackage');
if (opts.keywords) {
for (const keyword of opts.keywords) {
if (keyword.toLowerCase() === 'missionpackage')
continue;
url.searchParams.append('keyword', keyword);
}
}
if (opts.groups) {
for (const group of opts.groups) {
// This is intentionally case sensitive due to an apparent bug in TAK server
url.searchParams.append('Groups', group);
}
}
const form = new FormData();
const contentType = opts.mimetype || mime.getType(opts.name) || undefined;
const file = await toBlob(body, contentType);
form.append('assetfile', file, opts.name);
const res = await this.api.fetch(url, {
method: 'POST',
body: form
});
return res;
}
/**
* Update a Package that will not appear in the Public Data Packages List
* used primarily for sharing files between TAK clients
*/
async upload(opts, body) {
const url = new URL(`/Marti/sync/upload`, this.api.url);
url.searchParams.append('name', opts.name);
url.searchParams.append('keywords', opts.keywords.join(','));
url.searchParams.append('creatorUid', opts.creatorUid);
if (opts.altitude)
url.searchParams.append('altitude', opts.altitude);
if (opts.longitude)
url.searchParams.append('longitude', opts.longitude);
if (opts.latitude)
url.searchParams.append('latitude', opts.latitude);
if (body instanceof Buffer) {
body = Readable.from(body);
}
const res = await this.api.fetch(url, {
method: 'POST',
headers: {
'Content-Type': opts.contentType || mime.getType(opts.name) || 'application/octet-stream',
'Content-Length': opts.contentLength
},
body: body
});
return typeof res === 'string' ? JSON.parse(res) : res;
}
async count() {
const url = new URL('/Marti/api/files/metadata/count', this.api.url);
return await this.api.fetch(url, {
method: 'GET'
});
}
async update(hash, opts) {
if (opts.keywords !== undefined) {
const url = new URL(`/Marti/api/sync/metadata/${encodeURIComponent(hash)}/keywords`, this.api.url);
await this.api.fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: opts.keywords
});
}
if (opts.expiration !== undefined) {
const url = new URL(`/Marti/api/sync/metadata/${encodeURIComponent(hash)}/expiration`, this.api.url);
url.searchParams.append('expiration', String(opts.expiration));
await this.api.fetch(url, {
method: 'PUT',
});
}
}
async config() {
const url = new URL('/files/api/config', this.api.url);
return await this.api.fetch(url, {
method: 'GET'
});
}
}
async function toBlob(body, type) {
if (body instanceof Buffer) {
const bytes = new Uint8Array(body);
return type ? new Blob([bytes], { type }) : new Blob([bytes]);
}
const chunks = [];
for await (const chunk of body) {
if (chunk instanceof Buffer) {
chunks.push(chunk);
}
else if (typeof chunk === 'string') {
chunks.push(Buffer.from(chunk));
}
else {
chunks.push(Buffer.from(chunk));
}
}
const buff = Buffer.concat(chunks);
const bytes = new Uint8Array(buff);
return type ? new Blob([bytes], { type }) : new Blob([bytes]);
}
//# sourceMappingURL=files.js.map