Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"@types/mime-types": "^2.1.0",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.0.5",
"@types/proper-lockfile": "^4.1.1",
"@types/pump": "^1.1.0",
"@types/range-parser": "^1.2.3",
"@types/request": "^2.48.5",
Expand Down Expand Up @@ -102,6 +103,7 @@
"jest-transform-stub": "^2.0.0",
"mime-types": "^2.1.27",
"mock-fs": "^4.13.0",
"proper-lockfile": "^4.1.1",
"pump": "^3.0.0",
"range-parser": "^1.2.1",
"request": "^2.88.2",
Expand Down
24 changes: 19 additions & 5 deletions client/platform/desktop/backend/native/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fs from 'fs-extra';
import { shell } from 'electron';
import mime from 'mime-types';
import moment from 'moment';
import lockfile from 'proper-lockfile';
import {
DatasetType, MultiTrackRecord, Pipelines, SaveDetectionsArgs, FrameImage, DatasetMetaMutable,
} from 'viame-web-common/apispec';
Expand All @@ -26,6 +27,13 @@ const JsonTrackFileName = /^result(_.*)?\.json$/;
const JsonMetaFileName = 'meta.json';
const CsvFileName = /^.*\.csv$/;

async function acquireDirLock(dir: string) {
const release = await lockfile.lock(dir, {
lockfilePath: npath.join(dir, 'dir.lock'),
});
return release;
}

/**
* locate json track file in a directory
* @param path path to a directory
Expand Down Expand Up @@ -214,7 +222,7 @@ async function createKwiverRunWorkingDir(
// eslint won't recognize \. as valid escape
// eslint-disable-next-line no-useless-escape
const safeDatasetName = jsonMetaList[0].name.replace(/[\.\s/]+/g, '_');
const runFolderName = moment().format(`[${safeDatasetName}_${pipeline}]_MM-DD-yy_hh-mm-ss`);
const runFolderName = moment().format(`[${safeDatasetName}_${pipeline}]_MM-DD-yy_hh-mm-ss.SSS`);
const runFolderPath = npath.join(jobFolderPath, runFolderName);
if (!fs.existsSync(jobFolderPath)) {
await fs.mkdir(jobFolderPath);
Expand All @@ -230,10 +238,12 @@ async function _saveSerialized(
settings: Settings,
datasetId: string,
trackData: MultiTrackRecord,
allowEmpty = false,
) {
const time = moment().format('MM-DD-YYYY_hh-mm-ss');
const time = moment().format('MM-DD-YYYY_hh-mm-ss.SSS');
const newFileName = `result_${time}.json`;
const projectInfo = getProjectDir(settings, datasetId);
const release = await acquireDirLock(projectInfo.basePath);

try {
const validatedInfo = await getValidatedProjectDir(settings, datasetId);
Expand All @@ -246,9 +256,11 @@ async function _saveSerialized(
);
} catch (err) {
// Some part of the project dir didn't exist
if (!allowEmpty) throw err;
}
const serialized = JSON.stringify(trackData);
await fs.writeFile(npath.join(projectInfo.basePath, newFileName), serialized);
await release();
}

/**
Expand All @@ -275,14 +287,16 @@ async function _saveAsJson(absPath: string, data: unknown) {

async function saveMetadata(settings: Settings, datasetId: string, args: DatasetMetaMutable) {
const projectDirInfo = await getValidatedProjectDir(settings, datasetId);
const release = await acquireDirLock(projectDirInfo.basePath);
const existing = await loadJsonMetadata(projectDirInfo.metaFileAbsPath);
if (args.confidenceFilters) {
existing.confidenceFilters = args.confidenceFilters;
}
if (args.customTypeStyling) {
existing.customTypeStyling = args.customTypeStyling;
}
_saveAsJson(projectDirInfo.metaFileAbsPath, existing);
await _saveAsJson(projectDirInfo.metaFileAbsPath, existing);
await release();
}

/**
Expand Down Expand Up @@ -317,7 +331,7 @@ async function processOtherAnnotationFiles(
const data: MultiTrackRecord = {};
tracks.forEach((t) => { data[t.trackId.toString()] = t; });
// eslint-disable-next-line no-await-in-loop
await _saveSerialized(settings, datasetId, data);
await _saveSerialized(settings, datasetId, data, true);
processedFiles.push(path);
break; // Exit on first successful detection load
} catch (err) {
Expand Down Expand Up @@ -456,7 +470,7 @@ async function importMedia(settings: Settings, path: string): Promise<JsonMeta>
}
/* Finally create an empty file as fallback */
if (!foundDetections) {
await _saveSerialized(settings, dsId, {});
await _saveSerialized(settings, dsId, {}, true);
}

return jsonMeta;
Expand Down
89 changes: 60 additions & 29 deletions client/platform/desktop/backend/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,90 +42,107 @@ function makeMediaUrl(filepath: string): string {
return `http://${addr.address}:${addr.port}/api/media?path=${filepath}`;
}

function fail(res: express.Response, code: number, message: string) {
return res.status(code).json({ message }).end();
}

apirouter.get('/', (_, res) => {
res.send('Electron REST backend.');
});

/* LOAD metadata */
apirouter.get('/dataset/:id/meta', async (req, res) => {
apirouter.get('/dataset/:id/meta', async (req, res, next) => {
try {
const ds = await common.loadMetadata(settings.get(), req.params.id, makeMediaUrl);
res.json(ds);
} catch (err) {
fail(res, 500, err);
err.status = 500;
next(err);
}
});

/* SAVE metadata */
apirouter.post('/dataset/:id/meta', async (req, res) => {
apirouter.post('/dataset/:id/meta', async (req, res, next) => {
try {
await common.saveMetadata(settings.get(), req.params.id, req.body);
res.status(200);
res.status(200).send('done');
} catch (err) {
fail(res, 500, err);
err.status = 500;
next(err);
}
});

/* SAVE detections */
apirouter.post('/dataset/:id/detections', async (req, res) => {
apirouter.post('/dataset/:id/detections', async (req, res, next) => {
try {
const args = req.body as SaveDetectionsArgs;
await common.saveDetections(settings.get(), req.params.id, args);
return res.status(200);
res.status(200).send('done');
} catch (err) {
console.error(err);
return fail(res, 500, err);
throw err;
err.status = 500;
next(err);
}
return null;
});

/* IMPORT dataset */
apirouter.post('/import', async (req, res) => {
apirouter.post('/import', async (req, res, next) => {
const { path } = req.query;
if (!path || Array.isArray(path)) {
return fail(res, 400, `Invalid path: ${path}`);
return next({
status: 500,
statusMessage: `Invalid path: ${path}`,
});
}
try {
const meta = await common.importMedia(settings.get(), path.toString());
return res.json(meta);
res.json(meta);
} catch (err) {
return fail(res, 500, err);
err.status = 500;
next(err);
}
return null;
});

/* STREAM media */
apirouter.get('/media', (req, res) => {
apirouter.get('/media', (req, res, next) => {
let { path } = req.query;
if (!path || Array.isArray(path)) {
return fail(res, 400, `Invalid path: ${path}`);
return next({
status: 400,
statusMessage: `Invalid path: ${path}`,
});
}
path = path.toString();

let filestat: fs.Stats;
try {
filestat = fs.statSync(path);
if (!filestat.isFile()) {
return fail(res, 404, `Invalid file for path: ${path}`);
return next({
status: 404,
statusMessage: `Invalid file for path: ${path}`,
});
}
} catch (err) {
return fail(res, 404, `No such path ${path}`);
return next({
status: 404,
statusMessage: `No such path ${path}`,
});
}

const mimetype = mime.lookup(path);
if (mimetype === false) {
return fail(res, 400, `Mime lookup failed for path: ${path}`);
return next({
status: 400,
statusMessage: `Mime lookup failed for path: ${path}`,
});
}
if (!supportedMediaTypes.includes(mimetype)) {
return fail(res, 400, 'Cannot request this mime type');
return next({
status: 400,
statusMessage: 'Cannot request this mime type',
});
}

const ranges = req.headers.range && rangeParser(filestat.size, req.headers.range);
if (ranges === -1 || ranges === -2) {
return fail(res, 400, `Range parse failed: ${req.headers.range}`);
return next({
status: 400,
statusMessage: `Range parse failed: ${req.headers.range}`,
});
}

if (ranges === undefined || ranges === '') {
Expand Down Expand Up @@ -163,6 +180,20 @@ if (process.env.NODE_ENV === 'development') {
app.use(bodyparser.json());
app.use('/api', apirouter);

function fail(
err: { status?: number; statusMessage?: string },
req: express.Request,
res: express.Response,
) {
res.status(err.status || 500).json({ message: err.statusMessage || err });
}

apirouter.get('/', (_, res) => {
res.send('Electron REST backend.');
});

app.use(fail);

/* Singleton listen function will only create a server once */
function listen(callback: (server: http.Server) => void) {
if (!server) {
Expand Down
10 changes: 7 additions & 3 deletions client/viame-web-common/components/ConfidenceFilter.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script>
import { throttle } from 'lodash';
import { throttle, debounce } from 'lodash';

export default {
name: 'ConfidenceFilter',
Expand All @@ -11,11 +11,15 @@ export default {
},
created() {
this.updateConfidence = throttle(this.updateConfidence, 100);
this.emitEnd = debounce(this.emitEnd, 200);
},
methods: {
updateConfidence(value) {
this.$emit('update:confidence', value);
},
emitEnd() {
this.$emit('end');
},
},
};
</script>
Expand All @@ -32,8 +36,8 @@ export default {
class="px-3 mb-2"
persistent-hint
@input="updateConfidence"
@end="$emit('end')"
@mouseup="$emit('end')"
@end="emitEnd"
@mouseup="emitEnd"
/>
</div>
</template>
21 changes: 21 additions & 0 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2318,6 +2318,13 @@
dependencies:
"@types/node" "*"

"@types/proper-lockfile@^4.1.1":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz#99f026cbfdbe6305bdd454ffd5fefc1bd064beb1"
integrity sha512-HAjVfDa73pFgivViHyDu8HHHcds+W4MgOuZZAdyFJrHS8ngtCXmhl4hc2YXqSOwO6Bsa+iF2Sgxb2+gv874VOQ==
dependencies:
"@types/retry" "*"

"@types/pump@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@types/pump/-/pump-1.1.0.tgz#ed5214af511da32b6ee85c8d33ad3d59bb79ad8f"
Expand Down Expand Up @@ -2355,6 +2362,11 @@
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.4.tgz#84879a4d6d4aaefde53d4b29c91c0c4cbcffc26f"
integrity sha512-rPvqs+1hL/5hbES/0HTdUu4lvNmneiwKwccbWe7HGLWbnsLdqKnQHyWLg4Pj0AMO7PLHCwBM1Cs8orChdkDONg==

"@types/retry@*":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==

"@types/serve-static@*":
version "1.13.8"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.8.tgz#851129d434433c7082148574ffec263d58309c46"
Expand Down Expand Up @@ -12980,6 +12992,15 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.4"

proper-lockfile@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.1.tgz#284cf9db9e30a90e647afad69deb7cb06881262c"
integrity sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg==
dependencies:
graceful-fs "^4.1.11"
retry "^0.12.0"
signal-exit "^3.0.2"

property-information@^5.0.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
Expand Down