@nnsay/dji-terra-api-sdk
Version:
607 lines (606 loc) • 26.2 kB
JavaScript
;
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;