UNPKG

@tak-ps/node-tak

Version:

Lightweight JavaScript library for communicating with TAK Server

213 lines 7.59 kB
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