Create project
This commit is contained in:
commit
eea288b28d
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
data/*
|
|
@ -0,0 +1,9 @@
|
||||||
|
FROM node:lts-alpine
|
||||||
|
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
COPY ./dist /code/dist
|
||||||
|
COPY ./package*.json /code/
|
||||||
|
RUN npm install --only-production
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
|
@ -0,0 +1,315 @@
|
||||||
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const express_1 = __importDefault(require("express"));
|
||||||
|
const promises_1 = __importDefault(require("fs/promises"));
|
||||||
|
const fs_1 = require("fs");
|
||||||
|
const os_1 = require("os");
|
||||||
|
const path_1 = require("path");
|
||||||
|
const crypto_1 = require("crypto");
|
||||||
|
const sqlite3_1 = require("sqlite3");
|
||||||
|
const body_parser_1 = __importDefault(require("body-parser"));
|
||||||
|
const multer_1 = __importDefault(require("multer"));
|
||||||
|
const uuid_1 = require("uuid");
|
||||||
|
const mime_1 = require("mime");
|
||||||
|
const port = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 3333;
|
||||||
|
const data = typeof process.env['DATADIR'] !== 'undefined' ? process.env['DATADIR'] : './data';
|
||||||
|
const dbPath = (0, path_1.join)(data, 'queue.sqlite');
|
||||||
|
const app = (0, express_1.default)();
|
||||||
|
const tmp = (0, os_1.tmpdir)();
|
||||||
|
const db = new sqlite3_1.Database(dbPath);
|
||||||
|
const accepted = ['application/zip', 'application/x-zip-compressed'];
|
||||||
|
const storage = multer_1.default.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, tmp);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
cb(null, `${+new Date()}_${file.originalname}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function fileFilter(req, file, cb) {
|
||||||
|
if (accepted.indexOf(file.mimetype) !== -1) {
|
||||||
|
cb(null, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn(`Filetype ${file.mimetype} is not of type zip`);
|
||||||
|
cb(new Error("Dataset is not of type zip"), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const upload = (0, multer_1.default)({ storage, fileFilter });
|
||||||
|
app.use(body_parser_1.default.json());
|
||||||
|
app.use(body_parser_1.default.urlencoded({ extended: true }));
|
||||||
|
function hash(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hashSum = (0, crypto_1.createHash)('sha256');
|
||||||
|
const stream = (0, fs_1.createReadStream)(path);
|
||||||
|
stream.on('error', (err) => reject(err));
|
||||||
|
stream.on('data', (chunk) => hashSum.update(chunk));
|
||||||
|
stream.on('end', () => resolve(hashSum.digest('hex')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function exists(path) {
|
||||||
|
try {
|
||||||
|
await promises_1.default.access(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function add(email, name, dataset, model) {
|
||||||
|
const query = `INSERT INTO queue
|
||||||
|
(id, email, name, dataset, model)
|
||||||
|
VALUES ( ?, ?, ?, ?, ?);`;
|
||||||
|
const id = (0, uuid_1.v4)();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
return db.run(query, [id, email, name, dataset, model], (err, row) => {
|
||||||
|
if (err)
|
||||||
|
return reject(err);
|
||||||
|
console.log(`Added job ${id} to queue`);
|
||||||
|
return resolve(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function status(id) {
|
||||||
|
const query = `SELECT name, model, started, completed, failed, meta FROM queue WHERE id = ? LIMIT 1;`;
|
||||||
|
let jobStatus = 'Unknown';
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
return db.all(query, [id], (err, rows) => {
|
||||||
|
if (err)
|
||||||
|
return reject(err);
|
||||||
|
if (rows[0].started === null) {
|
||||||
|
jobStatus = `Has not started`;
|
||||||
|
}
|
||||||
|
else if (rows[0].failed !== null) {
|
||||||
|
jobStatus = `Failed <br /> <pre>${rows[0].meta}</pre>`;
|
||||||
|
}
|
||||||
|
else if (rows[0].completed !== null) {
|
||||||
|
jobStatus = `Completed ${rows[0].completed} <a href="/model/${rows[0].model}/${id}">Download</a>`;
|
||||||
|
}
|
||||||
|
console.log(`Got status for job ${id}: ${jobStatus}`);
|
||||||
|
return resolve(jobStatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function name(id) {
|
||||||
|
const query = `SELECT name, meta FROM queue WHERE id = ? LIMIT 1;`;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
return db.all(query, [id], (err, rows) => {
|
||||||
|
if (err)
|
||||||
|
return reject(err);
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return reject(new Error(`Job ${id} does not exist`));
|
||||||
|
}
|
||||||
|
return resolve(rows[0].name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function dataset(id) {
|
||||||
|
const query = `SELECT dataset FROM queue WHERE id = ? LIMIT 1;`;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
return db.all(query, [id], (err, rows) => {
|
||||||
|
if (err)
|
||||||
|
return reject(err);
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return reject(new Error(`Dataset ${id} does not exist`));
|
||||||
|
}
|
||||||
|
return resolve(rows[0].dataset);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function job() {
|
||||||
|
const query = `SELECT id FROM queue WHERE
|
||||||
|
started IS NULL
|
||||||
|
AND completed IS NULL
|
||||||
|
AND failed IS NULL
|
||||||
|
ORDER BY created ASC
|
||||||
|
LIMIT 1;`;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
return db.all(query, [], (err, rows) => {
|
||||||
|
if (err)
|
||||||
|
return reject(err);
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
return resolve(rows[0].id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
app.get('/', async (req, res, next) => {
|
||||||
|
let html;
|
||||||
|
try {
|
||||||
|
html = await promises_1.default.readFile('./views/index.html', 'utf8');
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
res.send(html);
|
||||||
|
});
|
||||||
|
app.post('/', upload.single('dataset'), async (req, res, next) => {
|
||||||
|
let fileHash;
|
||||||
|
let filePath;
|
||||||
|
let fileExists;
|
||||||
|
let id;
|
||||||
|
req.setTimeout(0);
|
||||||
|
if (typeof req.file === 'undefined' && req.file === null) {
|
||||||
|
console.error('No file in upload');
|
||||||
|
return next('ERROR: Please upload dataset as zip file');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fileHash = await hash(req.file.path);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error hashing file ${req.file.originalname}`);
|
||||||
|
}
|
||||||
|
filePath = (0, path_1.join)(data, `${fileHash}.zip`);
|
||||||
|
try {
|
||||||
|
fileExists = await exists(filePath);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
if (!fileExists) {
|
||||||
|
try {
|
||||||
|
await promises_1.default.rename(req.file.path, filePath);
|
||||||
|
console.log(`Saved dataset with hash ${fileHash}`);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.warn(`Dataset with hash ${fileHash} already exists...`);
|
||||||
|
try {
|
||||||
|
await promises_1.default.unlink(req.file.path);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
id = await add(req.body.email, req.body.name, fileHash, req.body.model);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return next(`Error adding training job ${req.body.name}`);
|
||||||
|
}
|
||||||
|
res.send(`<html><body>Dataset for job ${req.body.name} has been uploaded successfully. You will be emailed when your job has started and when it has completed training. <br /> Monitor job status here: <a href="/job/${id}">${id}</a></body></html>`);
|
||||||
|
});
|
||||||
|
app.get('/job/:id', async (req, res, next) => {
|
||||||
|
let jobStatus;
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No job id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
if (req.params.id.length !== 36) {
|
||||||
|
console.error(`Job id ${req.params.id} is invalid`);
|
||||||
|
return next('Invalid job id');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
jobStatus = await status(req.params.id);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next('Error getting job status');
|
||||||
|
}
|
||||||
|
return res.send(`<html><body>Job: ${req.params.id}<br /> Status: ${jobStatus}</body></html>`);
|
||||||
|
});
|
||||||
|
app.get('/model/:id', async (req, res, next) => {
|
||||||
|
let filePath;
|
||||||
|
let fileExists = false;
|
||||||
|
let id;
|
||||||
|
let fileName;
|
||||||
|
let fileStream;
|
||||||
|
let mimeType;
|
||||||
|
let stream;
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No job id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
id = req.params.id;
|
||||||
|
filePath = (0, path_1.join)(data, `${id}.onnx`);
|
||||||
|
try {
|
||||||
|
fileExists = await exists(filePath);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error checking whether model for job ${id} exists`);
|
||||||
|
}
|
||||||
|
if (!fileExists) {
|
||||||
|
console.warn(`Model for job ${id} does not exist`);
|
||||||
|
return next(`Model for job ${id} does not exist`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fileName = await name(id);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error getting job ${id}`);
|
||||||
|
}
|
||||||
|
mimeType = (0, mime_1.getType)(filePath);
|
||||||
|
res.setHeader('Content-disposition', `attachment; filename=${fileName}.onnx`);
|
||||||
|
res.setHeader('Content-type', mimeType);
|
||||||
|
stream = (0, fs_1.createReadStream)(filePath);
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
app.get('/dataset/:id', async (req, res, next) => {
|
||||||
|
let filePath;
|
||||||
|
let fileExists = false;
|
||||||
|
let id;
|
||||||
|
let datasetHash;
|
||||||
|
let fileStream;
|
||||||
|
let mimeType;
|
||||||
|
let stream;
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No dataset id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
id = req.params.id;
|
||||||
|
try {
|
||||||
|
datasetHash = await dataset(id);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error getting dataset for job ${id}`);
|
||||||
|
}
|
||||||
|
filePath = (0, path_1.join)(data, `${datasetHash}.zip`);
|
||||||
|
try {
|
||||||
|
fileExists = await exists(filePath);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error checking whether dataset for job ${id} exists`);
|
||||||
|
}
|
||||||
|
if (!fileExists) {
|
||||||
|
console.warn(`Dataset for job ${id} does not exist`);
|
||||||
|
return next(`Dataset for job ${id} does not exist`);
|
||||||
|
}
|
||||||
|
mimeType = (0, mime_1.getType)(filePath);
|
||||||
|
res.setHeader('Content-disposition', `attachment; filename=${datasetHash}.zip`);
|
||||||
|
res.setHeader('Content-type', mimeType);
|
||||||
|
stream = (0, fs_1.createReadStream)(filePath);
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
app.get('/job', async (req, res, next) => {
|
||||||
|
let jobId;
|
||||||
|
try {
|
||||||
|
jobId = await job();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next('Error getting job');
|
||||||
|
}
|
||||||
|
res.json([jobId]);
|
||||||
|
});
|
||||||
|
//app.get('/jobs');
|
||||||
|
//app.post('/job/started/:id', )
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`yolo_web running on port ${port}`);
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=index.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "yolo_web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"compile": "./node_modules/.bin/tsc --project tsconfig.json",
|
||||||
|
"start": "node dist"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/mime": "^3.0.1",
|
||||||
|
"@types/multer": "^1.4.7",
|
||||||
|
"@types/sqlite3": "^3.1.8",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"mime": "^3.0.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"sqlite3": "^5.1.6",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
npm run compile
|
||||||
|
sudo docker build -t yolo_web .
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir -p data
|
||||||
|
touch data/queue.sqlite
|
||||||
|
npm i
|
||||||
|
|
||||||
|
cat ./sql/setup.sql | sqlite3 data/queue.sqlite
|
||||||
|
|
||||||
|
bash ./scripts/build_web.sh
|
|
@ -0,0 +1,12 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS queue (
|
||||||
|
id TEXT(36) PRIMARY KEY,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
dataset TEXT NOT NULL,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
started TIMESTAMP,
|
||||||
|
completed TIMESTAMP,
|
||||||
|
failed TIMESTAMP,
|
||||||
|
meta TEXT
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
|
@ -0,0 +1,415 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { Express, Request, Response, NextFunction } from 'express'
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import { createReadStream } from 'fs';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { createHash, Hash } from 'crypto';
|
||||||
|
import { Database } from 'sqlite3';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { getType } from 'mime';
|
||||||
|
|
||||||
|
const port : number = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 3333;
|
||||||
|
const data : string = typeof process.env['DATADIR'] !== 'undefined' ? process.env['DATADIR'] : './data';
|
||||||
|
const dbPath : string = join(data, 'queue.sqlite');
|
||||||
|
const app : Express = express();
|
||||||
|
const tmp : string = tmpdir();
|
||||||
|
const db : Database = new Database(dbPath);
|
||||||
|
|
||||||
|
const accepted : string[] = ['application/zip', 'application/x-zip-compressed'];
|
||||||
|
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req : any, file : any, cb : any) {
|
||||||
|
cb(null, tmp)
|
||||||
|
},
|
||||||
|
filename: function (req: any, file: any, cb: any) {
|
||||||
|
cb(null, `${+new Date()}_${file.originalname}`)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function fileFilter (req: any, file: any, cb: any) {
|
||||||
|
if (accepted.indexOf(file.mimetype) !== -1) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
console.warn(`Filetype ${file.mimetype} is not of type zip`);
|
||||||
|
cb(new Error("Dataset is not of type zip"), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const upload : any = multer({ storage, fileFilter });
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
function hash (path : string) : Promise<string> {
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
const hashSum : Hash = createHash('sha256');
|
||||||
|
const stream : any = createReadStream(path);
|
||||||
|
stream.on('error', (err : Error) => reject(err));
|
||||||
|
stream.on('data', (chunk : Buffer) => hashSum.update(chunk));
|
||||||
|
stream.on('end', () => resolve(hashSum.digest('hex')));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists (path : string) : Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(path);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add (email : string, name : string, dataset : string, model : string) : Promise<string> {
|
||||||
|
const query : string = `INSERT INTO queue
|
||||||
|
(id, email, name, dataset, model)
|
||||||
|
VALUES ( ?, ?, ?, ?, ?);`;
|
||||||
|
const id : string = uuid();
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
return db.run(query, [id, email, name, dataset, model], (err : Error, row : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
console.log(`Added job ${id} to queue`);
|
||||||
|
return resolve(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function status (id : string) : Promise<string> {
|
||||||
|
const query : string = `SELECT name, model, started, completed, failed, meta FROM queue WHERE id = ? LIMIT 1;`;
|
||||||
|
let jobStatus : string = 'Unknown';
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
return db.all(query, [id], (err : Error, rows : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
if (rows[0].started === null) {
|
||||||
|
jobStatus = `Has not started`
|
||||||
|
} else if (rows[0].failed !== null) {
|
||||||
|
jobStatus = `Failed <br /> <pre>${rows[0].meta}</pre>`;
|
||||||
|
} else if (rows[0].completed !== null) {
|
||||||
|
jobStatus = `Completed ${rows[0].completed} <a href="/model/${rows[0].model}/${id}">Download</a>`;
|
||||||
|
}
|
||||||
|
console.log(`Got status for job ${id}: ${jobStatus}`);
|
||||||
|
return resolve(jobStatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function name (id : string) : Promise<string> {
|
||||||
|
const query : string = `SELECT name, meta FROM queue WHERE id = ? LIMIT 1;`;
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
return db.all(query, [id], (err : Error, rows : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return reject(new Error(`Job ${id} does not exist`));
|
||||||
|
}
|
||||||
|
return resolve(rows[0].name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dataset (id : string) : Promise<string> {
|
||||||
|
const query : string = `SELECT dataset FROM queue WHERE id = ? LIMIT 1;`;
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
return db.all(query, [id], (err : Error, rows : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return reject(new Error(`Dataset ${id} does not exist`));
|
||||||
|
}
|
||||||
|
return resolve(rows[0].dataset);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function job () : Promise<string|null> {
|
||||||
|
const query : string = `SELECT id FROM queue WHERE
|
||||||
|
started IS NULL
|
||||||
|
AND completed IS NULL
|
||||||
|
AND failed IS NULL
|
||||||
|
ORDER BY created ASC
|
||||||
|
LIMIT 1;`;
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
return db.all(query, [], (err : Error, rows : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
return resolve(rows[0].id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function claim (id : string) : Promise<string> {
|
||||||
|
const query : string = `SELECT * FROM queue WHERE id = ? LIMIT 1;`;
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
return db.all(query, [id], (err : Error, rows : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
if (rows.length < 1) {
|
||||||
|
return reject(new Error(`Dataset ${id} does not exist`));
|
||||||
|
}
|
||||||
|
if (rows[0].started !== null) {
|
||||||
|
return reject(new Error(`Job ${id} is already claimed`));
|
||||||
|
}
|
||||||
|
const claimQuery : string = `UPDATE queue SET started = CURRENT_TIMESTAMP WHERE id = ? LIMIT 1;`;
|
||||||
|
return db.run(claimQuery, [id], (err : Error, row : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
return resolve(rows[0]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fail (id : string, meta : string) : Promise<boolean> {
|
||||||
|
const query : string = `UPDATE queue SET failed = CURRENT_TIMESTAMP WHERE id = ? LIMIT 1;`;
|
||||||
|
return new Promise((resolve : Function, reject : Function) => {
|
||||||
|
return db.run(query, [ id ], (err : Error, row : any) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
return resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/', async (req : Request, res : Response, next : NextFunction) => {
|
||||||
|
let html : string;
|
||||||
|
try {
|
||||||
|
html = await fs.readFile('./views/index.html', 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
res.send(html);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/', upload.single('dataset'), async (req : Request, res : Response, next : NextFunction) => {
|
||||||
|
let fileHash : string;
|
||||||
|
let filePath : string;
|
||||||
|
let fileExists : boolean;
|
||||||
|
let id : string;
|
||||||
|
|
||||||
|
req.setTimeout(0);
|
||||||
|
|
||||||
|
if (typeof req.file === 'undefined' && req.file === null) {
|
||||||
|
console.error('No file in upload');
|
||||||
|
return next('ERROR: Please upload dataset as zip file');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileHash = await hash(req.file.path);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error hashing file ${req.file.originalname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = join(data, `${fileHash}.zip`);
|
||||||
|
try {
|
||||||
|
fileExists = await exists(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileExists) {
|
||||||
|
try {
|
||||||
|
await fs.rename(req.file.path, filePath);
|
||||||
|
console.log(`Saved dataset with hash ${fileHash}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.warn(`Dataset with hash ${fileHash} already exists...`);
|
||||||
|
try {
|
||||||
|
await fs.unlink(req.file.path);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
id = await add(req.body.email, req.body.name, fileHash, req.body.model);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return next(`Error adding training job ${req.body.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(`<html><body>Dataset for job ${req.body.name} has been uploaded successfully. You will be emailed when your job has started and when it has completed training. <br /> Monitor job status here: <a href="/job/${id}">${id}</a></body></html>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/job/:id', async (req : Request, res : Response, next : NextFunction) => {
|
||||||
|
let jobStatus : string;
|
||||||
|
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No job id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.params.id.length !== 36) {
|
||||||
|
console.error(`Job id ${req.params.id} is invalid`);
|
||||||
|
return next('Invalid job id');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
jobStatus = await status(req.params.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next('Error getting job status');
|
||||||
|
}
|
||||||
|
return res.send(`<html><body>Job: ${req.params.id}<br /> Status: ${jobStatus}</body></html>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/model/:id', async (req : Request, res: Response, next : NextFunction) => {
|
||||||
|
let filePath : string;
|
||||||
|
let fileExists : boolean = false;
|
||||||
|
let id : string;
|
||||||
|
let fileName : string;
|
||||||
|
let fileStream : any
|
||||||
|
let mimeType : string;
|
||||||
|
let stream : any;
|
||||||
|
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No job id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
|
||||||
|
id = req.params.id;
|
||||||
|
filePath = join(data, `${id}.onnx`);
|
||||||
|
try {
|
||||||
|
fileExists = await exists(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error checking whether model for job ${id} exists`);
|
||||||
|
}
|
||||||
|
if (!fileExists) {
|
||||||
|
console.warn(`Model for job ${id} does not exist`)
|
||||||
|
return next(`Model for job ${id} does not exist`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fileName = await name(id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error getting job ${id}`);
|
||||||
|
}
|
||||||
|
mimeType = getType(filePath);
|
||||||
|
|
||||||
|
res.setHeader('Content-disposition', `attachment; filename=${fileName}.onnx`);
|
||||||
|
res.setHeader('Content-type', mimeType);
|
||||||
|
|
||||||
|
stream = createReadStream(filePath);
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/dataset/:id', async (req : Request, res: Response, next : NextFunction) => {
|
||||||
|
let filePath : string;
|
||||||
|
let fileExists : boolean = false;
|
||||||
|
let id : string;
|
||||||
|
let datasetHash : string;
|
||||||
|
let fileStream : any
|
||||||
|
let mimeType : string;
|
||||||
|
let stream : any;
|
||||||
|
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No dataset id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
|
||||||
|
id = req.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
datasetHash = await dataset(id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error getting dataset for job ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = join(data, `${datasetHash}.zip`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileExists = await exists(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next(`Error checking whether dataset for job ${id} exists`);
|
||||||
|
}
|
||||||
|
if (!fileExists) {
|
||||||
|
console.warn(`Dataset for job ${id} does not exist`)
|
||||||
|
return next(`Dataset for job ${id} does not exist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType = getType(filePath);
|
||||||
|
|
||||||
|
res.setHeader('Content-disposition', `attachment; filename=${datasetHash}.zip`);
|
||||||
|
res.setHeader('Content-type', mimeType);
|
||||||
|
|
||||||
|
stream = createReadStream(filePath);
|
||||||
|
stream.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/job', async (req : Request, res: Response, next : NextFunction) => {
|
||||||
|
let jobId : string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jobId = await job();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next('Error getting job');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json([jobId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/job/claim/:id', async (req : Request, res: Response, next : NextFunction) => {
|
||||||
|
let id : string;
|
||||||
|
let jobObj : any;
|
||||||
|
let resObj : any = {};
|
||||||
|
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No dataset id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
|
||||||
|
id = req.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jobObj = await claim(id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next('Error claiming job');
|
||||||
|
}
|
||||||
|
|
||||||
|
resJob.id = id;
|
||||||
|
resJob.datasetPath = `/dataset/${id}`;
|
||||||
|
resJob.model = jobObj.model;
|
||||||
|
|
||||||
|
res.json(resJob);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/job/fail/:id', async (req : Request, res: Response, next : NextFunction) => {
|
||||||
|
let id : string;
|
||||||
|
let jobObj : any;
|
||||||
|
let resObj : any = {};
|
||||||
|
|
||||||
|
if (typeof req.params.id === 'undefined' || req.params.id === null) {
|
||||||
|
console.error(`No dataset id provided`);
|
||||||
|
return next('Invalid request');
|
||||||
|
}
|
||||||
|
|
||||||
|
id = req.params.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jobObj = await claim(id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return next('Error claiming job');
|
||||||
|
}
|
||||||
|
|
||||||
|
resJob.id = id;
|
||||||
|
resJob.datasetPath = `/dataset/${id}`;
|
||||||
|
resJob.model = jobObj.model;
|
||||||
|
|
||||||
|
res.json(resJob);
|
||||||
|
});
|
||||||
|
//app.get('/jobs');
|
||||||
|
//app.post('/job/started/:id', )
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`yolo_web running on port ${port}`);
|
||||||
|
})
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"target": "ES2020",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments" : false,
|
||||||
|
"baseUrl" : "dist",
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"rootDir" : "./src/",
|
||||||
|
"paths" : {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude" : [
|
||||||
|
"./dist"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<title>YOLOv5 Training Web</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" integrity="sha512-EZLkOqwILORob+p0BXZc+Vm3RgJBOe1Iq/0fiI7r/wJgzOFZMlsqTa29UEl6v6U6gsV4uIpsNZoV32YZqrCRCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<body>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<select name="model" id="model" required>
|
||||||
|
<option value="yolov5_onnx">YOLOv5 -> ONNX</option>
|
||||||
|
</select>
|
||||||
|
<br />
|
||||||
|
<input type="text" name="name" id="name" placeholder="Model name" required />
|
||||||
|
<br />
|
||||||
|
<input type="email" name="email" id="email" placeholder="Email" required />
|
||||||
|
<br />
|
||||||
|
<input type="file" name="dataset" id="dataset" required />
|
||||||
|
<br />
|
||||||
|
<input type="submit" />
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue