Merge pull request #153 from pierotofy/win32

Native NodeODM on Windows
pull/155/head
Piero Toffanin 2021-05-25 11:28:35 -04:00 zatwierdzone przez GitHub
commit 4eb5e48b77
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
19 zmienionych plików z 533 dodań i 194 usunięć

Wyświetl plik

@ -1,3 +1,5 @@
node_modules node_modules
tests tests
tmp tmp
nodeodm.exe
dist

Wyświetl plik

@ -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

Wyświetl plik

@ -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

3
.gitignore vendored
Wyświetl plik

@ -43,3 +43,6 @@ jspm_packages
.vscode .vscode
package-lock.json package-lock.json
apps/
nodeodm.exe
dist/

Wyświetl plik

@ -1,9 +0,0 @@
sudo: required
services:
- docker
before_install:
- docker build -t opendronemap/node-opendronemap .
script: docker run opendronemap/node-opendronemap --powercycle

Wyświetl plik

@ -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:

1
SOURCE 100644
Wyświetl plik

@ -0,0 +1 @@
NodeODM is free software. You can download the source code from https://github.com/OpenDroneMap/NodeODM/

Wyświetl plik

@ -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;

Wyświetl plik

@ -0,0 +1,8 @@
@echo off
setlocal
call %ODM_PATH%\win32env.bat
python %*
endlocal

Wyświetl plik

@ -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
}; };
} }

Wyświetl plik

@ -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];
} }
} }

34
libs/apps.js 100644
Wyświetl plik

@ -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
};

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
});
}
} }
}; };

Wyświetl plik

@ -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]);
}, },

Wyświetl plik

@ -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);

Wyświetl plik

@ -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",

Wyświetl plik

@ -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">

Wyświetl plik

@ -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}`);
});