kopia lustrzana https://github.com/OpenDroneMap/NodeODM
commit
4eb5e48b77
|
@ -1,3 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
tests
|
tests
|
||||||
tmp
|
tmp
|
||||||
|
nodeodm.exe
|
||||||
|
dist
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: Publish Windows Bundle
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup NodeJS
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
- name: Setup Env
|
||||||
|
run: |
|
||||||
|
npm i
|
||||||
|
npm i -g nexe
|
||||||
|
- name: Build bundle
|
||||||
|
run: |
|
||||||
|
npm run winbundle
|
||||||
|
- name: Upload Bundle File
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: Bundle
|
||||||
|
path: dist\*.zip
|
||||||
|
- name: Upload Bundle to Release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: dist\*.zip
|
||||||
|
file_glob: true
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
overwrite: true
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
name: Build PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
tags: opendronemap/nodeodm:test
|
||||||
|
- name: Test Powercycle
|
||||||
|
run: |
|
||||||
|
docker run -ti --rm opendronemap/nodeodm:test --powercycle
|
|
@ -43,3 +43,6 @@ jspm_packages
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
apps/
|
||||||
|
nodeodm.exe
|
||||||
|
dist/
|
|
@ -1,9 +0,0 @@
|
||||||
sudo: required
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- docker build -t opendronemap/node-opendronemap .
|
|
||||||
|
|
||||||
script: docker run opendronemap/node-opendronemap --powercycle
|
|
|
@ -81,6 +81,14 @@ You're in good shape!
|
||||||
|
|
||||||
See https://github.com/NVIDIA/nvidia-docker and https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker for information on docker/NVIDIA setup.
|
See https://github.com/NVIDIA/nvidia-docker and https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker for information on docker/NVIDIA setup.
|
||||||
|
|
||||||
|
### Windows Bundle
|
||||||
|
|
||||||
|
NodeODM can run as a self-contained executable on Windows without the need for additional dependencies (except for [ODM](https://github.com/OpenDroneMap/ODM) which needs to be installed separately). You can download the latest `nodeodm-windows-x64.zip` bundle from the [releases](https://github.com/OpenDroneMap/NodeODM/releases) page. Extract the contents in a folder and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nodeodm.exe --odm_path c:\path\to\ODM
|
||||||
|
```
|
||||||
|
|
||||||
### Run it Natively
|
### Run it Natively
|
||||||
|
|
||||||
If you are already running [ODM](https://github.com/OpenDroneMap/ODM) on Ubuntu natively you can follow these steps:
|
If you are already running [ODM](https://github.com/OpenDroneMap/ODM) on Ubuntu natively you can follow these steps:
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
NodeODM is free software. You can download the source code from https://github.com/OpenDroneMap/NodeODM/
|
|
@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
let fs = require('fs');
|
let fs = require('fs');
|
||||||
let argv = require('minimist')(process.argv.slice(2));
|
let argv = require('minimist')(process.argv.slice(2));
|
||||||
let utils = require('./libs/utils');
|
let utils = require('./libs/utils');
|
||||||
|
let apps = require('./libs/apps');
|
||||||
const spawnSync = require('child_process').spawnSync;
|
const spawnSync = require('child_process').spawnSync;
|
||||||
|
|
||||||
if (argv.help){
|
if (argv.help){
|
||||||
|
@ -141,8 +142,8 @@ config.maxConcurrency = parseInt(argv.max_concurrency || fromConfigFile("maxConc
|
||||||
config.maxRuntime = parseInt(argv.max_runtime || fromConfigFile("maxRuntime", -1));
|
config.maxRuntime = parseInt(argv.max_runtime || fromConfigFile("maxRuntime", -1));
|
||||||
|
|
||||||
// Detect 7z availability
|
// Detect 7z availability
|
||||||
config.has7z = spawnSync("7z", ['--help']).status === 0;
|
config.has7z = spawnSync(apps.sevenZ, ['--help']).status === 0;
|
||||||
config.hasUnzip = spawnSync("unzip", ['--help']).status === 0;
|
config.hasUnzip = spawnSync(apps.unzip, ['--help']).status === 0;
|
||||||
|
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
call %ODM_PATH%\win32env.bat
|
||||||
|
python %*
|
||||||
|
|
||||||
|
endlocal
|
108
libs/Task.js
108
libs/Task.js
|
@ -19,12 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
|
const os = require('os');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const rmdir = require('rimraf');
|
const rmdir = require('rimraf');
|
||||||
const odmRunner = require('./odmRunner');
|
const odmRunner = require('./odmRunner');
|
||||||
|
const odmInfo = require('./odmInfo');
|
||||||
const processRunner = require('./processRunner');
|
const processRunner = require('./processRunner');
|
||||||
const Directories = require('./Directories');
|
const Directories = require('./Directories');
|
||||||
const kill = require('tree-kill');
|
const kill = require('tree-kill');
|
||||||
|
@ -36,16 +38,15 @@ const archiver = require('archiver');
|
||||||
const statusCodes = require('./statusCodes');
|
const statusCodes = require('./statusCodes');
|
||||||
|
|
||||||
module.exports = class Task{
|
module.exports = class Task{
|
||||||
constructor(uuid, name, options = [], webhook = null, skipPostProcessing = false, outputs = [], dateCreated = new Date().getTime(), done = () => {}){
|
constructor(uuid, name, options = [], webhook = null, skipPostProcessing = false, outputs = [], dateCreated = new Date().getTime(), imagesCountEstimate = -1){
|
||||||
assert(uuid !== undefined, "uuid must be set");
|
assert(uuid !== undefined, "uuid must be set");
|
||||||
assert(done !== undefined, "ready must be set");
|
|
||||||
|
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.name = name !== "" ? name : "Task of " + (new Date()).toISOString();
|
this.name = name !== "" ? name : "Task of " + (new Date()).toISOString();
|
||||||
this.dateCreated = isNaN(parseInt(dateCreated)) ? new Date().getTime() : parseInt(dateCreated);
|
this.dateCreated = isNaN(parseInt(dateCreated)) ? new Date().getTime() : parseInt(dateCreated);
|
||||||
this.dateStarted = 0;
|
this.dateStarted = 0;
|
||||||
this.processingTime = -1;
|
this.processingTime = -1;
|
||||||
this.setStatus(statusCodes.QUEUED);
|
this.setStatus(statusCodes.RUNNING);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.gcpFiles = [];
|
this.gcpFiles = [];
|
||||||
this.geoFiles = [];
|
this.geoFiles = [];
|
||||||
|
@ -56,8 +57,33 @@ module.exports = class Task{
|
||||||
this.skipPostProcessing = skipPostProcessing;
|
this.skipPostProcessing = skipPostProcessing;
|
||||||
this.outputs = utils.parseUnsafePathsList(outputs);
|
this.outputs = utils.parseUnsafePathsList(outputs);
|
||||||
this.progress = 0;
|
this.progress = 0;
|
||||||
|
this.imagesCountEstimate = imagesCountEstimate;
|
||||||
async.series([
|
this.initialized = false;
|
||||||
|
this.onInitialize = []; // Events to trigger on initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(done, additionalSteps = []){
|
||||||
|
async.series(additionalSteps.concat([
|
||||||
|
// Handle post-processing options logic
|
||||||
|
cb => {
|
||||||
|
// If we need to post process results
|
||||||
|
// if pc-ept is supported (build entwine point cloud)
|
||||||
|
// we automatically add the pc-ept option to the task options by default
|
||||||
|
if (this.skipPostProcessing) cb();
|
||||||
|
else{
|
||||||
|
odmInfo.supportsOption("pc-ept", (err, supported) => {
|
||||||
|
if (err){
|
||||||
|
console.warn(`Cannot check for supported option pc-ept: ${err}`);
|
||||||
|
}else if (supported){
|
||||||
|
if (!this.options.find(opt => opt.name === "pc-ept")){
|
||||||
|
this.options.push({ name: 'pc-ept', value: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Read images info
|
// Read images info
|
||||||
cb => {
|
cb => {
|
||||||
fs.readdir(this.getImagesFolderPath(), (err, files) => {
|
fs.readdir(this.getImagesFolderPath(), (err, files) => {
|
||||||
|
@ -91,35 +117,45 @@ module.exports = class Task{
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
], err => {
|
]), err => {
|
||||||
|
// Status might have changed due to user action
|
||||||
|
// in which case we leave it unchanged
|
||||||
|
if (this.getStatus() === statusCodes.RUNNING){
|
||||||
|
if (err) this.setStatus(statusCodes.FAILED, { errorMessage: err.message });
|
||||||
|
else this.setStatus(statusCodes.QUEUED);
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
this.onInitialize.forEach(evt => evt(this));
|
||||||
|
this.onInitialize = [];
|
||||||
done(err, this);
|
done(err, this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static CreateFromSerialized(taskJson, done){
|
static CreateFromSerialized(taskJson, done){
|
||||||
new Task(taskJson.uuid,
|
const task = new Task(taskJson.uuid,
|
||||||
taskJson.name,
|
taskJson.name,
|
||||||
taskJson.options,
|
taskJson.options,
|
||||||
taskJson.webhook,
|
taskJson.webhook,
|
||||||
taskJson.skipPostProcessing,
|
taskJson.skipPostProcessing,
|
||||||
taskJson.outputs,
|
taskJson.outputs,
|
||||||
taskJson.dateCreated,
|
taskJson.dateCreated);
|
||||||
(err, task) => {
|
|
||||||
if (err) done(err);
|
task.initialize((err, task) => {
|
||||||
else{
|
if (err) done(err);
|
||||||
// Override default values with those
|
else{
|
||||||
// provided in the taskJson
|
// Override default values with those
|
||||||
for (let k in taskJson){
|
// provided in the taskJson
|
||||||
task[k] = taskJson[k];
|
for (let k in taskJson){
|
||||||
}
|
task[k] = taskJson[k];
|
||||||
|
|
||||||
// Tasks that were running should be put back to QUEUED state
|
|
||||||
if (task.status.code === statusCodes.RUNNING){
|
|
||||||
task.status.code = statusCodes.QUEUED;
|
|
||||||
}
|
|
||||||
done(null, task);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Tasks that were running should be put back to QUEUED state
|
||||||
|
if (task.status.code === statusCodes.RUNNING){
|
||||||
|
task.status.code = statusCodes.QUEUED;
|
||||||
|
}
|
||||||
|
done(null, task);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get path where images are stored for this task
|
// Get path where images are stored for this task
|
||||||
|
@ -154,7 +190,10 @@ module.exports = class Task{
|
||||||
|
|
||||||
// Deletes files and folders related to this task
|
// Deletes files and folders related to this task
|
||||||
cleanup(cb){
|
cleanup(cb){
|
||||||
rmdir(this.getProjectFolderPath(), cb);
|
if (this.initialized) rmdir(this.getProjectFolderPath(), cb);
|
||||||
|
else this.onInitialize.push(() => {
|
||||||
|
rmdir(this.getProjectFolderPath(), cb);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(code, extra){
|
setStatus(code, extra){
|
||||||
|
@ -432,7 +471,15 @@ module.exports = class Task{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.skipPostProcessing) tasks.push(runPostProcessingScript());
|
// postprocess.sh is still here for legacy/backward compatibility
|
||||||
|
// purposes, but we might remove it in the future. The new logic
|
||||||
|
// instructs the processing engine to do the necessary processing
|
||||||
|
// of outputs without post processing steps (build EPT).
|
||||||
|
// We're leaving it here only for Linux/docker setups, but will not
|
||||||
|
// be triggered on Windows.
|
||||||
|
if (os.platform() !== "win32" && !this.skipPostProcessing){
|
||||||
|
tasks.push(runPostProcessingScript());
|
||||||
|
}
|
||||||
|
|
||||||
const taskOutputFile = path.join(this.getProjectFolderPath(), 'task_output.txt');
|
const taskOutputFile = path.join(this.getProjectFolderPath(), 'task_output.txt');
|
||||||
tasks.push(saveTaskOutput(taskOutputFile));
|
tasks.push(saveTaskOutput(taskOutputFile));
|
||||||
|
@ -547,8 +594,13 @@ module.exports = class Task{
|
||||||
|
|
||||||
// Re-executes the task (by setting it's state back to QUEUED)
|
// Re-executes the task (by setting it's state back to QUEUED)
|
||||||
// Only tasks that have been canceled, completed or have failed can be restarted.
|
// Only tasks that have been canceled, completed or have failed can be restarted.
|
||||||
|
// unless they are being initialized, in which case we switch them back to running
|
||||||
restart(options, cb){
|
restart(options, cb){
|
||||||
if ([statusCodes.CANCELED, statusCodes.FAILED, statusCodes.COMPLETED].indexOf(this.status.code) !== -1){
|
if (!this.initialized && this.status.code === statusCodes.CANCELED){
|
||||||
|
this.setStatus(statusCodes.RUNNING);
|
||||||
|
if (options !== undefined) this.options = options;
|
||||||
|
cb(null);
|
||||||
|
}else if ([statusCodes.CANCELED, statusCodes.FAILED, statusCodes.COMPLETED].indexOf(this.status.code) !== -1){
|
||||||
this.setStatus(statusCodes.QUEUED);
|
this.setStatus(statusCodes.QUEUED);
|
||||||
this.dateCreated = new Date().getTime();
|
this.dateCreated = new Date().getTime();
|
||||||
this.dateStarted = 0;
|
this.dateStarted = 0;
|
||||||
|
@ -571,7 +623,7 @@ module.exports = class Task{
|
||||||
processingTime: this.processingTime,
|
processingTime: this.processingTime,
|
||||||
status: this.status,
|
status: this.status,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
imagesCount: this.images.length,
|
imagesCount: this.images !== undefined ? this.images.length : this.imagesCountEstimate,
|
||||||
progress: this.progress
|
progress: this.progress
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,7 +184,7 @@ class TaskManager{
|
||||||
// Finds the first QUEUED task.
|
// Finds the first QUEUED task.
|
||||||
findNextTaskToProcess(){
|
findNextTaskToProcess(){
|
||||||
for (let uuid in this.tasks){
|
for (let uuid in this.tasks){
|
||||||
if (this.tasks[uuid].getStatus() === statusCodes.QUEUED){
|
if (this.tasks[uuid].getStatus() === statusCodes.QUEUED && this.tasks[uuid].initialized){
|
||||||
return this.tasks[uuid];
|
return this.tasks[uuid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Node-OpenDroneMap Node.js App and REST API to access OpenDroneMap.
|
||||||
|
Copyright (C) 2016 Node-OpenDroneMap Contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let sevenZ = "7z";
|
||||||
|
let unzip = "unzip";
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join("apps", "7z", "7z.exe"))){
|
||||||
|
sevenZ = path.resolve(path.join("apps", "7z", "7z.exe"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join("apps", "unzip", "unzip.exe"))){
|
||||||
|
unzip = path.resolve(path.join("apps", "unzip", "unzip.exe"));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sevenZ, unzip
|
||||||
|
};
|
|
@ -58,6 +58,15 @@ module.exports = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
supportsOption: function(optName, cb){
|
||||||
|
this.getOptions((err, json) => {
|
||||||
|
if (err) cb(err);
|
||||||
|
else{
|
||||||
|
cb(null, !!json.find(opt => opt.name === optName));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
getOptions: function(done){
|
getOptions: function(done){
|
||||||
if (odmOptions){
|
if (odmOptions){
|
||||||
done(null, odmOptions);
|
done(null, odmOptions);
|
||||||
|
|
|
@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
"use strict";
|
||||||
let fs = require('fs');
|
let fs = require('fs');
|
||||||
|
let os = require('os');
|
||||||
let path = require('path');
|
let path = require('path');
|
||||||
let assert = require('assert');
|
let assert = require('assert');
|
||||||
let spawn = require('child_process').spawn;
|
let spawn = require('child_process').spawn;
|
||||||
|
@ -28,8 +29,8 @@ module.exports = {
|
||||||
run: function(options, projectName, done, outputReceived){
|
run: function(options, projectName, done, outputReceived){
|
||||||
assert(projectName !== undefined, "projectName must be specified");
|
assert(projectName !== undefined, "projectName must be specified");
|
||||||
assert(options["project-path"] !== undefined, "project-path must be defined");
|
assert(options["project-path"] !== undefined, "project-path must be defined");
|
||||||
|
|
||||||
const command = path.join(config.odm_path, "run.sh"),
|
const command = path.join(config.odm_path, os.platform() === "win32" ? "run.bat" : "run.sh"),
|
||||||
params = [];
|
params = [];
|
||||||
|
|
||||||
for (var name in options){
|
for (var name in options){
|
||||||
|
@ -70,7 +71,9 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch
|
// Launch
|
||||||
let childProcess = spawn(command, params, {cwd: config.odm_path});
|
const env = utils.clone(process.env);
|
||||||
|
env.ODM_NONINTERACTIVE = 1;
|
||||||
|
let childProcess = spawn(command, params, {cwd: config.odm_path, env});
|
||||||
|
|
||||||
childProcess
|
childProcess
|
||||||
.on('exit', (code, signal) => done(null, code, signal))
|
.on('exit', (code, signal) => done(null, code, signal))
|
||||||
|
@ -123,6 +126,7 @@ module.exports = {
|
||||||
// Launch
|
// Launch
|
||||||
const env = utils.clone(process.env);
|
const env = utils.clone(process.env);
|
||||||
env.ODM_OPTIONS_TMP_FILE = utils.tmpPath(".json");
|
env.ODM_OPTIONS_TMP_FILE = utils.tmpPath(".json");
|
||||||
|
env.ODM_PATH = config.odm_path;
|
||||||
let childProcess = spawn(pythonExe, [path.join(__dirname, "..", "helpers", "odmOptionsToJson.py"),
|
let childProcess = spawn(pythonExe, [path.join(__dirname, "..", "helpers", "odmOptionsToJson.py"),
|
||||||
"--project-path", config.odm_path, "bogusname"], { env });
|
"--project-path", config.odm_path, "bogusname"], { env });
|
||||||
|
|
||||||
|
@ -154,11 +158,15 @@ module.exports = {
|
||||||
})
|
})
|
||||||
.on('error', handleResult);
|
.on('error', handleResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Python3 first
|
if (os.platform() === "win32"){
|
||||||
getOdmOptions("python3", (err, result) => {
|
getOdmOptions("helpers\\odm_python.bat", done);
|
||||||
if (err) getOdmOptions("python", done);
|
}else{
|
||||||
else done(null, result);
|
// Try Python3 first
|
||||||
});
|
getOdmOptions("python3", (err, result) => {
|
||||||
|
if (err) getOdmOptions("python", done);
|
||||||
|
else done(null, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
"use strict";
|
||||||
let fs = require('fs');
|
let fs = require('fs');
|
||||||
|
let apps = require('./apps');
|
||||||
let path = require('path');
|
let path = require('path');
|
||||||
let assert = require('assert');
|
let assert = require('assert');
|
||||||
let spawn = require('child_process').spawn;
|
let spawn = require('child_process').spawn;
|
||||||
|
@ -92,14 +93,14 @@ module.exports = {
|
||||||
},
|
},
|
||||||
["projectFolderPath"]),
|
["projectFolderPath"]),
|
||||||
|
|
||||||
sevenZip: makeRunner("7z", function(options){
|
sevenZip: makeRunner(apps.sevenZ, function(options){
|
||||||
return ["a", "-mx=0", "-y", "-r", "-bd", options.destination].concat(options.pathsToArchive);
|
return ["a", "-mx=0", "-y", "-r", "-bd", options.destination].concat(options.pathsToArchive);
|
||||||
},
|
},
|
||||||
["destination", "pathsToArchive", "cwd"],
|
["destination", "pathsToArchive", "cwd"],
|
||||||
null,
|
null,
|
||||||
false),
|
false),
|
||||||
|
|
||||||
sevenUnzip: makeRunner("7z", function(options){
|
sevenUnzip: makeRunner(apps.sevenZ, function(options){
|
||||||
let cmd = "x"; // eXtract files with full paths
|
let cmd = "x"; // eXtract files with full paths
|
||||||
if (options.noDirectories) cmd = "e"; //Extract files from archive (without using directory names)
|
if (options.noDirectories) cmd = "e"; //Extract files from archive (without using directory names)
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ module.exports = {
|
||||||
null,
|
null,
|
||||||
false),
|
false),
|
||||||
|
|
||||||
unzip: makeRunner("unzip", function(options){
|
unzip: makeRunner(apps.unzip, function(options){
|
||||||
const opts = options.noDirectories ? ["-j"] : [];
|
const opts = options.noDirectories ? ["-j"] : [];
|
||||||
return opts.concat(["-qq", "-o", options.file, "-d", options.destination]);
|
return opts.concat(["-qq", "-o", options.file, "-d", options.destination]);
|
||||||
},
|
},
|
||||||
|
|
299
libs/taskNew.js
299
libs/taskNew.js
|
@ -30,7 +30,7 @@ const async = require('async');
|
||||||
const odmInfo = require('./odmInfo');
|
const odmInfo = require('./odmInfo');
|
||||||
const request = require('request');
|
const request = require('request');
|
||||||
const ziputils = require('./ziputils');
|
const ziputils = require('./ziputils');
|
||||||
const { cancelJob } = require('node-schedule');
|
const statusCodes = require('./statusCodes');
|
||||||
|
|
||||||
const download = function(uri, filename, callback) {
|
const download = function(uri, filename, callback) {
|
||||||
request.head(uri, function(err, res, body) {
|
request.head(uri, function(err, res, body) {
|
||||||
|
@ -224,10 +224,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
createTask: (req, res) => {
|
createTask: (req, res) => {
|
||||||
// IMPROVEMENT: consider doing the file moving in the background
|
|
||||||
// and return a response more quickly instead of a long timeout.
|
|
||||||
req.setTimeout(1000 * 60 * 20);
|
|
||||||
|
|
||||||
const srcPath = path.join("tmp", req.id);
|
const srcPath = path.join("tmp", req.id);
|
||||||
|
|
||||||
// Print error message and cleanup
|
// Print error message and cleanup
|
||||||
|
@ -235,26 +231,149 @@ module.exports = {
|
||||||
res.json({error});
|
res.json({error});
|
||||||
removeDirectory(srcPath);
|
removeDirectory(srcPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let destPath = path.join(Directories.data, req.id);
|
||||||
|
let destImagesPath = path.join(destPath, "images");
|
||||||
|
let destGcpPath = path.join(destPath, "gcp");
|
||||||
|
|
||||||
|
const checkMaxImageLimits = (cb) => {
|
||||||
|
if (!config.maxImages) cb();
|
||||||
|
else{
|
||||||
|
fs.readdir(destImagesPath, (err, files) => {
|
||||||
|
if (err) cb(err);
|
||||||
|
else if (files.length > config.maxImages) cb(new Error(`${files.length} images uploaded, but this node can only process up to ${config.maxImages}.`));
|
||||||
|
else cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let initSteps = [
|
||||||
|
// Check if dest directory already exists
|
||||||
|
cb => {
|
||||||
|
if (req.files && req.files.length > 0) {
|
||||||
|
fs.stat(destPath, (err, stat) => {
|
||||||
|
if (err && err.code === 'ENOENT') cb();
|
||||||
|
else{
|
||||||
|
// Directory already exists, this could happen
|
||||||
|
// if a previous attempt at upload failed and the user
|
||||||
|
// used set-uuid to specify the same UUID over the previous run
|
||||||
|
// Try to remove it
|
||||||
|
removeDirectory(destPath, err => {
|
||||||
|
if (err) cb(new Error(`Directory exists and we couldn't remove it.`));
|
||||||
|
else cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Unzips zip URL to tmp/<uuid>/ (if any)
|
||||||
|
cb => {
|
||||||
|
if (req.body.zipurl) {
|
||||||
|
let archive = "zipurl.zip";
|
||||||
|
|
||||||
|
upload.storage.getDestination(req, archive, (err, dstPath) => {
|
||||||
|
if (err) cb(err);
|
||||||
|
else{
|
||||||
|
let archiveDestPath = path.join(dstPath, archive);
|
||||||
|
|
||||||
|
download(req.body.zipurl, archiveDestPath, cb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Move all uploads to data/<uuid>/images dir (if any)
|
||||||
|
cb => fs.mkdir(destPath, undefined, cb),
|
||||||
|
cb => fs.mkdir(destGcpPath, undefined, cb),
|
||||||
|
cb => mv(srcPath, destImagesPath, cb),
|
||||||
|
|
||||||
|
// Zip files handling
|
||||||
|
cb => {
|
||||||
|
const handleSeed = (cb) => {
|
||||||
|
const seedFileDst = path.join(destPath, "seed.zip");
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
// Move to project root
|
||||||
|
cb => mv(path.join(destImagesPath, "seed.zip"), seedFileDst, cb),
|
||||||
|
|
||||||
|
// Extract
|
||||||
|
cb => {
|
||||||
|
ziputils.unzip(seedFileDst, destPath, cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
cb => {
|
||||||
|
fs.exists(seedFileDst, exists => {
|
||||||
|
if (exists) fs.unlink(seedFileDst, cb);
|
||||||
|
else cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
], cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleZipUrl = (cb) => {
|
||||||
|
// Extract images
|
||||||
|
ziputils.unzip(path.join(destImagesPath, "zipurl.zip"),
|
||||||
|
destImagesPath,
|
||||||
|
cb, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and handle zip files and extract
|
||||||
|
fs.readdir(destImagesPath, (err, entries) => {
|
||||||
|
if (err) cb(err);
|
||||||
|
else {
|
||||||
|
async.eachSeries(entries, (entry, cb) => {
|
||||||
|
if (entry === "seed.zip"){
|
||||||
|
handleSeed(cb);
|
||||||
|
}else if (entry === "zipurl.zip") {
|
||||||
|
handleZipUrl(cb);
|
||||||
|
} else cb();
|
||||||
|
}, cb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Verify max images limit
|
||||||
|
cb => {
|
||||||
|
checkMaxImageLimits(cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
cb => {
|
||||||
|
// Find any *.txt (GCP) file and move it to the data/<uuid>/gcp directory
|
||||||
|
// also remove any lingering zipurl.zip
|
||||||
|
fs.readdir(destImagesPath, (err, entries) => {
|
||||||
|
if (err) cb(err);
|
||||||
|
else {
|
||||||
|
async.eachSeries(entries, (entry, cb) => {
|
||||||
|
if (/\.txt$/gi.test(entry)) {
|
||||||
|
mv(path.join(destImagesPath, entry), path.join(destGcpPath, entry), cb);
|
||||||
|
}else if (/\.zip$/gi.test(entry)){
|
||||||
|
fs.unlink(path.join(destImagesPath, entry), cb);
|
||||||
|
} else cb();
|
||||||
|
}, cb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
if (req.error !== undefined){
|
if (req.error !== undefined){
|
||||||
die(req.error);
|
die(req.error);
|
||||||
}else{
|
}else{
|
||||||
let destPath = path.join(Directories.data, req.id);
|
let imagesCountEstimate = -1;
|
||||||
let destImagesPath = path.join(destPath, "images");
|
|
||||||
let destGcpPath = path.join(destPath, "gcp");
|
|
||||||
|
|
||||||
const checkMaxImageLimits = (cb) => {
|
|
||||||
if (!config.maxImages) cb();
|
|
||||||
else{
|
|
||||||
fs.readdir(destImagesPath, (err, files) => {
|
|
||||||
if (err) cb(err);
|
|
||||||
else if (files.length > config.maxImages) cb(new Error(`${files.length} images uploaded, but this node can only process up to ${config.maxImages}.`));
|
|
||||||
else cb();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async.series([
|
async.series([
|
||||||
|
cb => {
|
||||||
|
// Basic path check
|
||||||
|
fs.exists(srcPath, exists => {
|
||||||
|
if (exists) cb();
|
||||||
|
else cb(new Error(`Invalid UUID`));
|
||||||
|
});
|
||||||
|
},
|
||||||
cb => {
|
cb => {
|
||||||
odmInfo.filterOptions(req.body.options, (err, options) => {
|
odmInfo.filterOptions(req.body.options, (err, options) => {
|
||||||
if (err) cb(err);
|
if (err) cb(err);
|
||||||
|
@ -264,134 +383,34 @@ module.exports = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check if dest directory already exists
|
|
||||||
cb => {
|
cb => {
|
||||||
if (req.files && req.files.length > 0) {
|
fs.readdir(srcPath, (err, entries) => {
|
||||||
fs.stat(destPath, (err, stat) => {
|
if (!err) imagesCountEstimate = entries.length;
|
||||||
if (err && err.code === 'ENOENT') cb();
|
|
||||||
else{
|
|
||||||
// Directory already exists, this could happen
|
|
||||||
// if a previous attempt at upload failed and the user
|
|
||||||
// used set-uuid to specify the same UUID over the previous run
|
|
||||||
// Try to remove it
|
|
||||||
removeDirectory(destPath, err => {
|
|
||||||
if (err) cb(new Error(`Directory exists and we couldn't remove it.`));
|
|
||||||
else cb();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cb();
|
cb();
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Unzips zip URL to tmp/<uuid>/ (if any)
|
|
||||||
cb => {
|
|
||||||
if (req.body.zipurl) {
|
|
||||||
let archive = "zipurl.zip";
|
|
||||||
|
|
||||||
upload.storage.getDestination(req, archive, (err, dstPath) => {
|
|
||||||
if (err) cb(err);
|
|
||||||
else{
|
|
||||||
let archiveDestPath = path.join(dstPath, archive);
|
|
||||||
|
|
||||||
download(req.body.zipurl, archiveDestPath, cb);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Move all uploads to data/<uuid>/images dir (if any)
|
|
||||||
cb => fs.mkdir(destPath, undefined, cb),
|
|
||||||
cb => fs.mkdir(destGcpPath, undefined, cb),
|
|
||||||
cb => mv(srcPath, destImagesPath, cb),
|
|
||||||
|
|
||||||
// Zip files handling
|
|
||||||
cb => {
|
|
||||||
const handleSeed = (cb) => {
|
|
||||||
const seedFileDst = path.join(destPath, "seed.zip");
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
// Move to project root
|
|
||||||
cb => mv(path.join(destImagesPath, "seed.zip"), seedFileDst, cb),
|
|
||||||
|
|
||||||
// Extract
|
|
||||||
cb => {
|
|
||||||
ziputils.unzip(seedFileDst, destPath, cb);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove
|
|
||||||
cb => {
|
|
||||||
fs.exists(seedFileDst, exists => {
|
|
||||||
if (exists) fs.unlink(seedFileDst, cb);
|
|
||||||
else cb();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleZipUrl = (cb) => {
|
|
||||||
// Extract images
|
|
||||||
ziputils.unzip(path.join(destImagesPath, "zipurl.zip"),
|
|
||||||
destImagesPath,
|
|
||||||
cb, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find and handle zip files and extract
|
|
||||||
fs.readdir(destImagesPath, (err, entries) => {
|
|
||||||
if (err) cb(err);
|
|
||||||
else {
|
|
||||||
async.eachSeries(entries, (entry, cb) => {
|
|
||||||
if (entry === "seed.zip"){
|
|
||||||
handleSeed(cb);
|
|
||||||
}else if (entry === "zipurl.zip") {
|
|
||||||
handleZipUrl(cb);
|
|
||||||
} else cb();
|
|
||||||
}, cb);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Verify max images limit
|
|
||||||
cb => {
|
cb => {
|
||||||
checkMaxImageLimits(cb);
|
const task = new Task(req.id, req.body.name, req.body.options,
|
||||||
},
|
req.body.webhook,
|
||||||
|
req.body.skipPostProcessing === 'true',
|
||||||
|
req.body.outputs,
|
||||||
|
req.body.dateCreated,
|
||||||
|
imagesCountEstimate
|
||||||
|
);
|
||||||
|
TaskManager.singleton().addNew(task);
|
||||||
|
res.json({ uuid: req.id });
|
||||||
|
cb();
|
||||||
|
|
||||||
cb => {
|
// We return a UUID right away but continue
|
||||||
// Find any *.txt (GCP) file and move it to the data/<uuid>/gcp directory
|
// doing processing in the background
|
||||||
// also remove any lingering zipurl.zip
|
|
||||||
fs.readdir(destImagesPath, (err, entries) => {
|
|
||||||
if (err) cb(err);
|
|
||||||
else {
|
|
||||||
async.eachSeries(entries, (entry, cb) => {
|
|
||||||
if (/\.txt$/gi.test(entry)) {
|
|
||||||
mv(path.join(destImagesPath, entry), path.join(destGcpPath, entry), cb);
|
|
||||||
}else if (/\.zip$/gi.test(entry)){
|
|
||||||
fs.unlink(path.join(destImagesPath, entry), cb);
|
|
||||||
} else cb();
|
|
||||||
}, cb);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Create task
|
task.initialize(err => {
|
||||||
cb => {
|
if (err) {
|
||||||
new Task(req.id, req.body.name, req.body.options,
|
// Cleanup
|
||||||
req.body.webhook,
|
removeDirectory(srcPath);
|
||||||
req.body.skipPostProcessing === 'true',
|
removeDirectory(destPath);
|
||||||
req.body.outputs,
|
} else TaskManager.singleton().processNextTask();
|
||||||
req.body.dateCreated,
|
}, initSteps);
|
||||||
(err, task) => {
|
|
||||||
if (err) cb(err);
|
|
||||||
else {
|
|
||||||
TaskManager.singleton().addNew(task);
|
|
||||||
res.json({ uuid: req.id });
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
], err => {
|
], err => {
|
||||||
if (err) die(err.message);
|
if (err) die(err.message);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "NodeODM",
|
"name": "NodeODM",
|
||||||
"version": "2.1.4",
|
"version": "2.1.5",
|
||||||
"description": "REST API to access ODM",
|
"description": "REST API to access ODM",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"winbundle": "node scripts/winbundle.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -37,6 +37,9 @@
|
||||||
background-color: #3d74d4;
|
background-color: #3d74d4;
|
||||||
border-color: #4582ec;
|
border-color: #4582ec;
|
||||||
}
|
}
|
||||||
|
.task{
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="css/main.css?t=1">
|
<link rel="stylesheet" href="css/main.css?t=1">
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const spawnSync = require('child_process').spawnSync;
|
||||||
|
const path = require('path');
|
||||||
|
const request = require('request');
|
||||||
|
const async = require('async');
|
||||||
|
const nodeUnzip = require('node-unzip-2');
|
||||||
|
const archiver = require('archiver');
|
||||||
|
|
||||||
|
const bundleName = "nodeodm-windows-x64.zip";
|
||||||
|
|
||||||
|
const download = function(uri, filename, callback) {
|
||||||
|
console.log(`Downloading ${uri}`);
|
||||||
|
request.head(uri, function(err, res, body) {
|
||||||
|
if (err) callback(err);
|
||||||
|
else{
|
||||||
|
request(uri).pipe(fs.createWriteStream(filename)).on('close', callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function downloadApp(destFolder, appUrl, cb){
|
||||||
|
if (!fs.existsSync(destFolder)) fs.mkdirSync(destFolder, { recursive: true });
|
||||||
|
else {
|
||||||
|
cb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zipPath = path.join(destFolder, "download.zip");
|
||||||
|
let _called = false;
|
||||||
|
|
||||||
|
const done = (err) => {
|
||||||
|
if (!_called){ // Bug in nodeUnzip, makes this get called twice
|
||||||
|
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
||||||
|
_called = true;
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
download(appUrl, zipPath, err => {
|
||||||
|
if (err) done(err);
|
||||||
|
else{
|
||||||
|
// Unzip
|
||||||
|
console.log(`Extracting ${zipPath}`);
|
||||||
|
fs.createReadStream(zipPath).pipe(nodeUnzip.Extract({ path: destFolder }))
|
||||||
|
.on('close', done)
|
||||||
|
.on('error', done);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
cb => {
|
||||||
|
// Cleanup directories
|
||||||
|
console.log("Cleaning up folders");
|
||||||
|
for (let dir of ["data", "tmp"]){
|
||||||
|
for (let entry of fs.readdirSync(dir)){
|
||||||
|
if (entry !== ".gitignore"){
|
||||||
|
console.log(`Removing ${dir}/${entry}`);
|
||||||
|
fs.rmdirSync(path.join(dir, entry), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
},
|
||||||
|
|
||||||
|
cb => {
|
||||||
|
downloadApp(path.join("apps", "7z"), "https://github.com/OpenDroneMap/NodeODM/releases/download/v2.1.0/7z19.zip", cb);
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
downloadApp(path.join("apps", "unzip"), "https://github.com/OpenDroneMap/NodeODM/releases/download/v2.1.0/unzip600.zip", cb);
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
console.log("Building executable");
|
||||||
|
const code = spawnSync('nexe.cmd', ['index.js', '-t', 'windows-x64-12.16.3', '-o', 'nodeodm.exe'], { stdio: "pipe"}).status;
|
||||||
|
|
||||||
|
if (code === 0) cb();
|
||||||
|
else cb(new Error(`nexe returned non-zero error code: ${code}`));
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
// Zip
|
||||||
|
const outFile = path.join("dist", bundleName);
|
||||||
|
if (!fs.existsSync("dist")) fs.mkdirSync("dist");
|
||||||
|
if (fs.existsSync(outFile)) fs.unlinkSync(outFile);
|
||||||
|
|
||||||
|
let output = fs.createWriteStream(outFile);
|
||||||
|
let archive = archiver.create('zip', {
|
||||||
|
zlib: { level: 5 } // Sets the compression level (1 = best speed since most assets are already compressed)
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.on('finish', () => {
|
||||||
|
console.log("Done!");
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.on('error', err => {
|
||||||
|
console.error(`Could not archive .zip file: ${err.message}`);
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
"apps",
|
||||||
|
"data",
|
||||||
|
"helpers",
|
||||||
|
"public",
|
||||||
|
"scripts",
|
||||||
|
"tmp",
|
||||||
|
"config-default.json",
|
||||||
|
"LICENSE",
|
||||||
|
"SOURCE",
|
||||||
|
"package.json",
|
||||||
|
"nodeodm.exe"
|
||||||
|
];
|
||||||
|
|
||||||
|
archive.pipe(output);
|
||||||
|
files.forEach(file => {
|
||||||
|
console.log(`Adding ${file}`);
|
||||||
|
let stat = fs.lstatSync(file);
|
||||||
|
if (stat.isFile()){
|
||||||
|
archive.file(file, {name: path.basename(file)});
|
||||||
|
}else if (stat.isDirectory()){
|
||||||
|
archive.directory(file, path.basename(file));
|
||||||
|
}else{
|
||||||
|
logger.error(`Could not add ${file}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
archive.finalize();
|
||||||
|
}
|
||||||
|
], (err) => {
|
||||||
|
if (err) console.log(`Bundle failed: ${err}`);
|
||||||
|
else console.log(`Bundle ==> dist/${bundleName}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue