UNPKG

@nnsay/dji-terra-api-sdk

Version:
607 lines (606 loc) 26.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TerraAPI = void 0; const utc_1 = __importDefault(require("dayjs/plugin/utc")); const timezone_1 = __importDefault(require("dayjs/plugin/timezone")); const duration_1 = __importDefault(require("dayjs/plugin/duration")); const dayjs_1 = __importDefault(require("dayjs")); const crypto_1 = __importDefault(require("crypto")); const url_1 = __importDefault(require("url")); const axios_1 = __importDefault(require("axios")); const axios_retry_1 = __importDefault(require("axios-retry")); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const client_s3_1 = require("@aws-sdk/client-s3"); dayjs_1.default.extend(utc_1.default); dayjs_1.default.extend(timezone_1.default); dayjs_1.default.extend(duration_1.default); /** * Terra API */ class TerraAPI { constructor(appKey = process.env.DJI_APP_KEY, secretKey = process.env.DJI_SECRET_KEY, apiHost = 'https://openapi-cn.dji.com') { Object.defineProperty(this, "appKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "secretKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "apiHost", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "headers", { enumerable: true, configurable: true, writable: true, value: 'date @request-target digest' }); Object.defineProperty(this, "algorithm", { enumerable: true, configurable: true, writable: true, value: 'hmac-sha256' }); Object.defineProperty(this, "reqClient", { enumerable: true, configurable: true, writable: true, value: axios_1.default.create() }); if (!appKey || !secretKey) { throw new Error('DJI_APP_KEY or DJI_SECRET_KEY is not set'); } this.appKey = appKey; this.secretKey = secretKey; this.apiHost = apiHost; (0, axios_retry_1.default)(this.reqClient, { retries: 3, retryDelay: axios_retry_1.default.exponentialDelay, }); } getFormattedDate() { return (0, dayjs_1.default)().utc().format('ddd, DD MMM YYYY HH:mm:ss [GMT]'); } calculateDigest(payloadStr) { return crypto_1.default .createHash('sha256') .update(payloadStr, 'utf-8') .digest() .toString('base64'); } generateSignature(signingStr) { return crypto_1.default .createHmac('sha256', this.secretKey) .update(signingStr, 'utf-8') .digest() .toString('base64'); } buildRequestParam(method, reqUrl, payload) { const requestTarget = `${method} ${url_1.default.parse(reqUrl).path}`; const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload); const digest = this.calculateDigest(payloadStr); const date = this.getFormattedDate(); const signingStr = `date: ${date}\n@request-target: ${requestTarget}\ndigest: SHA-256=${digest}`; const signature = this.generateSignature(signingStr); const headers = { Date: date, Authorization: `hmac username="${this.appKey}", algorithm="${this.algorithm}", headers="${this.headers}", signature="${signature}"`, Digest: `SHA-256=${digest}`, 'Content-Type': payloadStr === '' ? undefined : 'application/json;charset=UTF-8', }; return { headers, payloadStr }; } traverseDirectory(rootDir) { return __awaiter(this, void 0, void 0, function* () { const files = []; const traverse = (dirPath) => __awaiter(this, void 0, void 0, function* () { const fileOrDirNames = yield promises_1.default.readdir(dirPath); for (const fileOrDirName of fileOrDirNames) { const filePath = path_1.default.resolve(path_1.default.join(dirPath, fileOrDirName)); const stat = yield promises_1.default.stat(filePath); if (stat.isDirectory()) { yield traverse(filePath); } else { files.push(path_1.default.relative(rootDir, filePath)); } } }); yield traverse(rootDir); return files; }); } /** * Get token * @returns STS token */ obtainToken() { return __awaiter(this, void 0, void 0, function* () { const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/store/obtain_token`; const method = 'POST'.toLowerCase(); const { headers, payloadStr: payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } const stsToken = data.data; console.log(`[obtainToken]: ${JSON.stringify(stsToken)}`); return stsToken; }); } /** * Upload complete callback * @param callbackParam string, callback parameter which comes from sts token * @param uploadedFiles list, the list of uploaded files etag result * @param resourceUUID string, resource id * @returns resource file list */ uploadCallback(callbackParam, uploadedFiles, resourceUUID) { return __awaiter(this, void 0, void 0, function* () { const reqUrl = `${this.apiHost}/terra-rescon-be/v2/store/upload_callback`; const method = 'POST'.toLowerCase(); const maxCallbackFileCount = 50; let resoruceFiles = []; while (uploadedFiles.length) { const files = uploadedFiles.splice(0, maxCallbackFileCount); const payload = { callbackParam, files, resourceUUID, }; const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const res = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (res.data.result.code !== 0) { throw new Error(res.data.result.msg); } console.log(`[uploadCallback]: ${JSON.stringify(res.data.data)}`); resoruceFiles = resoruceFiles.concat(res.data.data); } return resoruceFiles; }); } /** * Upload file * @param stsToken STS token, comes from get token api * @param imageDir local file root directory * @returns uploaded files etag list */ uploadFile(stsToken, imageDir) { return __awaiter(this, void 0, void 0, function* () { const s3Config = { region: stsToken.region, credentials: { accessKeyId: stsToken.accessKeyID, secretAccessKey: stsToken.secretAccessKey, sessionToken: stsToken.sessionToken, }, }; if (this.apiHost.includes('-cn')) { s3Config.endpoint = `https://${stsToken.region}.aliyuncs.com`; } const ossClient = new client_s3_1.S3Client(s3Config); const uploadedFiles = []; const files = yield this.traverseDirectory(path_1.default.resolve(imageDir)); const concurrent = 50; while (files.length) { const currentBatchFiles = files .splice(0, concurrent) .filter((file) => /.*(jpg|jpeg|dng|heic|heif)$/i.test(file)); const batchUpload = currentBatchFiles.map((file) => __awaiter(this, void 0, void 0, function* () { const key = stsToken.storePath.replace('{fileName}', file); const iamgeFile = path_1.default.resolve(`${imageDir}/${file}`); const { ETag } = yield ossClient.send(new client_s3_1.PutObjectCommand({ Bucket: stsToken.cloudBucketName, Key: key, // WARN: aliyun oss error: aws-chunked encoding is not supported with the specified x-amz-content-sha256 value // HINT: upload with buffer but not stream for resolve the above aliyun oss error Body: yield promises_1.default.readFile(iamgeFile), })); console.log(`[uploadFile] ${key} ${ETag}`); uploadedFiles.push({ name: file, etag: ETag, checksum: ETag }); })); yield Promise.all(batchUpload); } console.log(`[uploadFile] ${JSON.stringify(uploadedFiles)}`); return uploadedFiles; }); } /** * Create resource * @param payload json, create resource parameters * - meta?, string, user extension information * - files?, string[], the file uuid to be added to the resource * - name, string, resource name * - type, string, resource type, available values : map * @returns resource information */ createResource(payload) { return __awaiter(this, void 0, void 0, function* () { const reqUrl = `${this.apiHost}/terra-rescon-be/v2/resources`; const method = 'POST'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers: Object.assign(Object.assign({}, headers), { 'Return-Detail': true }), data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } const resource = data.data; console.log(`[createResource] ${JSON.stringify(resource)}`); return resource; }); } /** * Delete resource * @param resourceUUID string, resource uuid * @param deleteMode delete mode. 0 - do not delete. 1 - delete files that are not linked to other resource. uint * @returns execute result */ deleteResource(resourceUUID_1) { return __awaiter(this, arguments, void 0, function* (resourceUUID, deleteMode = 0) { const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/resources/${resourceUUID}?deleteMode=${deleteMode}`; const method = 'DELETE'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const res = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (res.data.result.code !== 0) { throw new Error(res.data.result.msg); } console.log(`[deleteResource] ${JSON.stringify(res.data.result)}`); return res.data.result; }); } /** * Get resource information * @param uuid resource uuid * @return resource information */ getResource(uuid) { return __awaiter(this, void 0, void 0, function* () { const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/resources/${uuid}`; const method = 'GET'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } console.log(`[getResource] ${JSON.stringify(data.data)}`); return data.data; }); } /** * Get resource list * @param query json, query parameters * - rows?: page rows. uint * - page?: page code. Starting from 1. uint * - search?: search option * - uuids?: get resource list with specified uuid. The uuids are separated by ",". * - type?: pecify the resource type to search for, available values : map * @return resource paged list */ listResources() { return __awaiter(this, arguments, void 0, function* (query = { rows: 10 }) { const urlParams = new URLSearchParams(); Object.entries(query).forEach(([key, value]) => { urlParams.append(key, String(value)); }); const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/resources?` + urlParams.toString(); const method = 'GET'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } console.log(`[listResources] ${JSON.stringify(data.data)}`); return data.data; }); } /** * Create job * @param payload json, job parameters * - meta?, string, User extension information * - name, string, Job name * @returns job details */ createJob(payload) { return __awaiter(this, void 0, void 0, function* () { const reqUrl = `${this.apiHost}/terra-rescon-be/v2/jobs`; const method = 'POST'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers: Object.assign(Object.assign({}, headers), { 'Return-Detail': true }), data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } const job = data.data; console.log(`[createJob] ${JSON.stringify(job)}`); return job; }); } /** * Get job details * @param uuid string, job ID * @returns job details */ getJob(uuid) { return __awaiter(this, void 0, void 0, function* () { const reqUrl = `${this.apiHost}/terra-rescon-be/v2/jobs/${uuid}`; const payload = ''; const method = 'GET'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } const job = data.data; console.log(`[getJob] ${JSON.stringify(job)}`); return job; }); } /** * Delete job * @param uuid string, job uuid * @param deleteMode delete mode. 0 - do not delete. 1 - delete files that are not linked to other resource. uint * @returns execute result */ deleteJob(uuid) { return __awaiter(this, void 0, void 0, function* () { const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/jobs/${uuid}`; const method = 'DELETE'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const res = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (res.data.result.code !== 0) { throw new Error(res.data.result.msg); } console.log(`[deleteJob] ${JSON.stringify(res.data.result)}`); return res.data.result; }); } /** * Start job * @param uuid job id * @param payload json, start job parameters * - outputResourceUuid?: string, When the type is 4, you can specify the output resource, indicating the merging into that resource. * - parameters: string, json, reference: https://developer.dji.com/doc/terra_api_tutorial/cn/terra-cloud-algo.html * - parameters.parameter: json, the configuration of 2D, 3D, and LiDAR reconstruction jobs * - parameters.predefine_AOI?: json, is an optional parameter, and is at the same level as the parameter. The predefine_AOI parameter only takes effect in 2D and 3D jobs. * - parameters.export_parameter?: json, is optional and sets the directory structure and content of reconstruction output. * - resourceUuid: string, Resource uuid * - type: 13 | 14 | 15, Job type. 14 - 2D reconstruction, 15 - 3D reconstruction, 13 - LiDAR reconstruction * @returns execute result */ startJob(uuid, payload) { return __awaiter(this, void 0, void 0, function* () { const reqUrl = `${this.apiHost}/terra-rescon-be/v2/jobs/${uuid}/start`; const method = 'POST'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, Object.assign(Object.assign({}, payload), { parameters: JSON.stringify(payload.parameters) })); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } const result = data.result; console.log(`[startJob] ${JSON.stringify(result)}`); return result; }); } /** * Get file list * @param query json, query Paramater * - rows?: number, page rows. uint * - page?: number, page code. Starting from 1. uint * - search?: string, search option * - uuids?: string, get file list with specified uuid. IDs are separated by ",". UUID is a 36-character string, with a maximum support of 1000 UUIDs * - type?: number, job type. 14 - 2D reconstruction, 15 - 3D reconstruction, 13 - LiDAR reconstruction * - originResourceUuid?: string, origin resource uuid * - outputResourceUuid?: string, resource uuid of reconstruction result * @return file paged list */ listJobs() { return __awaiter(this, arguments, void 0, function* (query = { rows: 10 }) { const urlParams = new URLSearchParams(); Object.entries(query).forEach(([key, value]) => { urlParams.append(key, String(value)); }); const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/jobs?` + urlParams.toString(); const method = 'GET'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } console.log(`[listJobs] ${JSON.stringify(data.data)}`); return data.data; }); } /** * Get file list * @param query json, query Paramater * - rows?: number, page rows. uint * - page?: number, page code. Starting from 1. uint * - search?: string, search option * - needURL?: boolean * - name?: string * - uuids?: string, get file list with specified uuid. IDs are separated by ",". UUID is a 36-character string, with a maximum support of 1000 UUIDs * - resourceUuid?: string, Linked resource uuid * - orderAsc?: boolean, The default sorting order for Files is descending based on created_at. When this condition is set to true, the results are returned in ascending order. * @return file paged list */ listFiles() { return __awaiter(this, arguments, void 0, function* (query = { rows: 10 }) { const urlParams = new URLSearchParams(); Object.entries(query).forEach(([key, value]) => { urlParams.append(key, String(value)); }); const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/files?` + urlParams.toString(); const method = 'GET'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } console.log(`[listFiles] ${JSON.stringify(data.data)}`); return data.data; }); } /** * Get file information * @param uuid, string, file id * @returns file information */ getFile(uuid) { return __awaiter(this, void 0, void 0, function* () { const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/files/${uuid}`; const method = 'GET'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } console.log(`[getFile] ${JSON.stringify(data.data)}`); return data.data; }); } /** * Delete file * @param uuid, string, file id * @returns execute result */ deleteFile(uuid) { return __awaiter(this, void 0, void 0, function* () { const payload = ''; const reqUrl = `${this.apiHost}/terra-rescon-be/v2/files/${uuid}`; const method = 'DELETE'.toLowerCase(); const { headers, payloadStr } = this.buildRequestParam(method, reqUrl, payload); const { data } = yield this.reqClient.request({ url: reqUrl, method: method, headers, data: payloadStr, }); if (data.result.code !== 0) { throw new Error(data.result.msg); } console.log(`[deleteFile] ${JSON.stringify(data.data)}`); return data.data; }); } /** * Download files * @param outputResourceUuid, string, output resource id * @param rootDir, string, download root directory * @returns void */ downloadFiles(outputResourceUuid, rootDir) { return __awaiter(this, void 0, void 0, function* () { const { fileUuids } = yield this.getResource(outputResourceUuid); const downloadDir = path_1.default.resolve(rootDir); const maxDownloadFileCount = 100; while (fileUuids.length) { const batchFiles = fileUuids.splice(0, maxDownloadFileCount); const downlaodFileTasks = batchFiles.map((uuid) => __awaiter(this, void 0, void 0, function* () { const fileInfo = yield this.getFile(uuid); const fileStream = yield this.reqClient.get(fileInfo.url, { responseType: 'arraybuffer', }); const downlaodFilePath = path_1.default.resolve(downloadDir, fileInfo.name); yield promises_1.default.mkdir(path_1.default.dirname(downlaodFilePath), { recursive: true }); yield promises_1.default.writeFile(downlaodFilePath, fileStream.data); console.log(`[downloadFiles] ${fileInfo.name} downlaod done`); })); yield Promise.all(downlaodFileTasks); } console.log(`[downloadFiles] all files download done`); }); } } exports.TerraAPI = TerraAPI;