parent
5c48b3088d
commit
08fa1c6dd7
@ -0,0 +1,2 @@
|
||||
CREDENTIALS_ENABLED = 0
|
||||
WEBHOOK_ENABLED = 0
|
||||
@ -0,0 +1,2 @@
|
||||
CREDENTIALS_ENABLED = 1
|
||||
WEBHOOK_ENABLED = 1
|
||||
@ -1,6 +1,16 @@
|
||||
# WebStorm settings
|
||||
/.idea
|
||||
|
||||
# npm
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/public
|
||||
/pm2.config.js
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# local .env* files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# downloaded files
|
||||
/public
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||
</project>
|
||||
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
||||
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/server.iml" filepath="$PROJECT_DIR$/.idea/server.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/public/algorithms" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/src/public/algorithms" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WebResourcesPaths">
|
||||
<contentEntries>
|
||||
<entry url="file://$PROJECT_DIR$">
|
||||
<entryData>
|
||||
<resourceRoots>
|
||||
<path value="file://$PROJECT_DIR$/src" />
|
||||
</resourceRoots>
|
||||
</entryData>
|
||||
</entry>
|
||||
</contentEntries>
|
||||
</component>
|
||||
</project>
|
||||
@ -1,266 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="33967b4c-80ab-44ef-aea7-fe45cd91b9b7" name="Default Changelist" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/encodings.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/server.iml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/webResources.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/app/backend.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/app/frontend.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/app/index.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bin/pull" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bin/www" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/pm2.config.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/public/algorithm-visualizer.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/public/codes/3fe76da7-b5cb-4925-8b32-9ff05204e78d/Main" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/public/codes/3fe76da7-b5cb-4925-8b32-9ff05204e78d/Main.cpp" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/apis/index.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/common/config.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/common/error.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/common/hierarchy.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/common/util.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/common/webhook.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/controllers/algorithms.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/controllers/auth.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/controllers/index.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/controllers/tracers.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/controllers/visualizations.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/index.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/models/Algorithm.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/models/Category.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/models/File.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/models/Hierarchy.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/models/index.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/tracers/ImageBuilder.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/tracers/WorkerBuilder.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/tracers/cpp/Dockerfile" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/tracers/index.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/tracers/java/Dockerfile" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/tracers/js/worker.js" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/webpack.backend.config.js" afterDir="false" />
|
||||
</list>
|
||||
<ignored path="$PROJECT_DIR$/.tmp/" />
|
||||
<ignored path="$PROJECT_DIR$/temp/" />
|
||||
<ignored path="$PROJECT_DIR$/tmp/" />
|
||||
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="FileEditorManager">
|
||||
<leaf>
|
||||
<file pinned="false" current-in-tab="true">
|
||||
<entry file="file://$PROJECT_DIR$/bin/pull">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="105">
|
||||
<caret line="7" lean-forward="true" selection-start-line="7" selection-end-line="7" />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</file>
|
||||
</leaf>
|
||||
</component>
|
||||
<component name="FindInProjectRecents">
|
||||
<findStrings>
|
||||
<find>robot</find>
|
||||
</findStrings>
|
||||
<dirStrings>
|
||||
<dir>$PROJECT_DIR$</dir>
|
||||
</dirStrings>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="IdeDocumentHistory">
|
||||
<option name="CHANGED_PATHS">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/package.json" />
|
||||
<option value="$PROJECT_DIR$/.gitignore" />
|
||||
<option value="$PROJECT_DIR$/bin/pull" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectFrameBounds" extendedState="6" fullScreen="true">
|
||||
<option name="width" value="1680" />
|
||||
<option name="height" value="1050" />
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
|
||||
<component name="ProjectView">
|
||||
<navigator proportions="" version="1">
|
||||
<foldersAlwaysOnTop value="true" />
|
||||
</navigator>
|
||||
<panes>
|
||||
<pane id="ProjectPane">
|
||||
<subPane>
|
||||
<expand>
|
||||
<path>
|
||||
<item name="server" type="b2602c69:ProjectViewProjectNode" />
|
||||
<item name="server" type="462c0819:PsiDirectoryNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="server" type="b2602c69:ProjectViewProjectNode" />
|
||||
<item name="server" type="462c0819:PsiDirectoryNode" />
|
||||
<item name="app" type="462c0819:PsiDirectoryNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="server" type="b2602c69:ProjectViewProjectNode" />
|
||||
<item name="server" type="462c0819:PsiDirectoryNode" />
|
||||
<item name="bin" type="462c0819:PsiDirectoryNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="server" type="b2602c69:ProjectViewProjectNode" />
|
||||
<item name="server" type="462c0819:PsiDirectoryNode" />
|
||||
<item name="src" type="462c0819:PsiDirectoryNode" />
|
||||
</path>
|
||||
<path>
|
||||
<item name="server" type="b2602c69:ProjectViewProjectNode" />
|
||||
<item name="server" type="462c0819:PsiDirectoryNode" />
|
||||
<item name="src" type="462c0819:PsiDirectoryNode" />
|
||||
<item name="controllers" type="462c0819:PsiDirectoryNode" />
|
||||
</path>
|
||||
</expand>
|
||||
<select />
|
||||
</subPane>
|
||||
</pane>
|
||||
<pane id="Scope" />
|
||||
</panes>
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
|
||||
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
|
||||
<property name="nodejs_package_manager_path" value="npm" />
|
||||
</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$" />
|
||||
</key>
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunDashboard">
|
||||
<option name="ruleStates">
|
||||
<list>
|
||||
<RuleState>
|
||||
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
|
||||
</RuleState>
|
||||
<RuleState>
|
||||
<option name="name" value="StatusDashboardGroupingRule" />
|
||||
</RuleState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="SvnConfiguration">
|
||||
<configuration />
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="33967b4c-80ab-44ef-aea7-fe45cd91b9b7" name="Default Changelist" comment="" />
|
||||
<created>1559885518055</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1559885518055</updated>
|
||||
<workItem from="1559885519397" duration="1598000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TimeTrackingManager">
|
||||
<option name="totallyTimeSpent" value="1598000" />
|
||||
</component>
|
||||
<component name="ToolWindowManager">
|
||||
<frame x="0" y="0" width="1680" height="1050" extended-state="6" />
|
||||
<editor active="true" />
|
||||
<layout>
|
||||
<window_info id="Favorites" side_tool="true" />
|
||||
<window_info active="true" content_ui="combo" id="Project" order="0" visible="true" weight="0.24954791" />
|
||||
<window_info id="Structure" order="1" side_tool="true" weight="0.25" />
|
||||
<window_info anchor="bottom" id="Docker" show_stripe_button="false" />
|
||||
<window_info anchor="bottom" id="Version Control" />
|
||||
<window_info anchor="bottom" id="Terminal" visible="true" weight="0.32959184" />
|
||||
<window_info anchor="bottom" id="Event Log" side_tool="true" />
|
||||
<window_info anchor="bottom" id="Message" order="0" />
|
||||
<window_info anchor="bottom" id="Find" order="1" />
|
||||
<window_info anchor="bottom" id="Run" order="2" />
|
||||
<window_info anchor="bottom" id="Debug" order="3" weight="0.4" />
|
||||
<window_info anchor="bottom" id="Cvs" order="4" weight="0.25" />
|
||||
<window_info anchor="bottom" id="Inspection" order="5" weight="0.4" />
|
||||
<window_info anchor="bottom" id="TODO" order="6" />
|
||||
<window_info anchor="right" id="Commander" internal_type="SLIDING" order="0" type="SLIDING" weight="0.4" />
|
||||
<window_info anchor="right" id="Ant Build" order="1" weight="0.25" />
|
||||
<window_info anchor="right" content_ui="combo" id="Hierarchy" order="2" weight="0.25" />
|
||||
</layout>
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="1" />
|
||||
</component>
|
||||
<component name="editorHistoryManager">
|
||||
<entry file="file://$PROJECT_DIR$/package.json">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="75">
|
||||
<caret line="5" column="56" selection-start-line="5" selection-start-column="56" selection-end-line="5" selection-end-column="56" />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/src/apis/index.js">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="30">
|
||||
<caret line="2" column="20" lean-forward="true" selection-start-line="2" selection-start-column="20" selection-end-line="2" selection-end-column="20" />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/pm2.config.js">
|
||||
<provider selected="true" editor-type-id="text-editor" />
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/webpack.backend.config.js">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="-47" />
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/app/backend.js">
|
||||
<provider selected="true" editor-type-id="text-editor" />
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/app/index.js">
|
||||
<provider selected="true" editor-type-id="text-editor" />
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/src/controllers/index.js">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="60">
|
||||
<caret line="4" lean-forward="true" selection-start-line="4" selection-end-line="4" />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/.gitignore">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="90">
|
||||
<caret line="6" lean-forward="true" selection-start-line="6" selection-end-line="6" />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/src/index.js">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="285">
|
||||
<caret line="19" column="13" selection-start-line="19" selection-start-column="13" selection-end-line="19" selection-end-column="13" />
|
||||
<folding>
|
||||
<element signature="e#0#30#0" expanded="true" />
|
||||
</folding>
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
<entry file="file://$PROJECT_DIR$/bin/pull">
|
||||
<provider selected="true" editor-type-id="text-editor">
|
||||
<state relative-caret-position="105">
|
||||
<caret line="7" lean-forward="true" selection-start-line="7" selection-end-line="7" />
|
||||
</state>
|
||||
</provider>
|
||||
</entry>
|
||||
</component>
|
||||
</project>
|
||||
@ -1,42 +0,0 @@
|
||||
const {
|
||||
__DEV__,
|
||||
backendBuildPath,
|
||||
} = require('../environment');
|
||||
|
||||
if (__DEV__) {
|
||||
const webpack = require('webpack');
|
||||
const webpackConfig = require('../webpack.backend.config.js');
|
||||
const compiler = webpack(webpackConfig);
|
||||
|
||||
let backend = null;
|
||||
let lastHash = null;
|
||||
compiler.watch({
|
||||
watchOptions: {
|
||||
ignored: /public/,
|
||||
},
|
||||
}, (err, stats) => {
|
||||
if (err) {
|
||||
lastHash = null;
|
||||
compiler.purgeInputFileSystem();
|
||||
console.error(err);
|
||||
} else if (stats.hash !== lastHash) {
|
||||
lastHash = stats.hash;
|
||||
console.info(stats.toString({
|
||||
cached: false,
|
||||
colors: true,
|
||||
}));
|
||||
|
||||
delete require.cache[require.resolve(backendBuildPath)];
|
||||
backend = require(backendBuildPath).default;
|
||||
}
|
||||
});
|
||||
|
||||
const backendWrapper = (req, res, next) => backend(req, res, next);
|
||||
backendWrapper.getHierarchy = () => backend.hierarchy;
|
||||
module.exports = backendWrapper;
|
||||
} else {
|
||||
const backend = require(backendBuildPath).default;
|
||||
const backendWrapper = (req, res, next) => backend(req, res, next);
|
||||
backendWrapper.getHierarchy = () => backend.hierarchy;
|
||||
module.exports = backendWrapper;
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
const packageJson = require('../package');
|
||||
|
||||
const {
|
||||
__DEV__,
|
||||
frontendSrcPath,
|
||||
frontendBuildPath,
|
||||
} = require('../environment');
|
||||
|
||||
const app = express();
|
||||
|
||||
if (__DEV__) {
|
||||
const webpack = require('webpack');
|
||||
const webpackDev = require('webpack-dev-middleware');
|
||||
const webpackHot = require('webpack-hot-middleware');
|
||||
const webpackConfig = require('../webpack.frontend.config.js');
|
||||
const compiler = webpack(webpackConfig);
|
||||
app.use(express.static(path.resolve(frontendSrcPath, 'static')));
|
||||
app.use(webpackDev(compiler, {
|
||||
stats: {
|
||||
cached: false,
|
||||
colors: true,
|
||||
},
|
||||
serverSideRender: true,
|
||||
index: false,
|
||||
}));
|
||||
app.use(webpackHot(compiler));
|
||||
app.use((req, res, next) => {
|
||||
const { fs } = res.locals;
|
||||
const outputPath = res.locals.webpackStats.toJson().outputPath;
|
||||
const filePath = path.resolve(outputPath, 'index.html');
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) return next(err);
|
||||
res.indexFile = data;
|
||||
next();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
app.use(express.static(frontendBuildPath, { index: false }));
|
||||
app.use((req, res, next) => {
|
||||
const filePath = path.resolve(frontendBuildPath, 'index.html');
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) return next(err);
|
||||
res.indexFile = data;
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.use((req, res) => {
|
||||
const backend = require('./backend');
|
||||
const hierarchy = backend.getHierarchy();
|
||||
|
||||
const [, categoryKey, algorithmKey] = url.parse(req.originalUrl).pathname.split('/');
|
||||
let { title, description } = packageJson;
|
||||
let algorithm = undefined;
|
||||
if (categoryKey && categoryKey !== 'scratch-paper') {
|
||||
algorithm = hierarchy.find(categoryKey, algorithmKey) || null;
|
||||
if (algorithm) {
|
||||
title = [algorithm.categoryName, algorithm.algorithmName].join(' - ');
|
||||
description = algorithm.description;
|
||||
} else {
|
||||
res.status(404);
|
||||
}
|
||||
}
|
||||
|
||||
const indexFile = res.indexFile
|
||||
.replace(/\$TITLE/g, title)
|
||||
.replace(/\$DESCRIPTION/g, description)
|
||||
.replace(/\$ALGORITHM/g, algorithm === undefined ? 'undefined' :
|
||||
JSON.stringify(algorithm).replace(/</g, '\\u003c'));
|
||||
res.send(indexFile);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@ -1,30 +0,0 @@
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const app = express();
|
||||
|
||||
const frontend = require('./frontend');
|
||||
const backend = require('./backend');
|
||||
|
||||
const {
|
||||
apiEndpoint,
|
||||
credentials,
|
||||
} = require('../environment');
|
||||
|
||||
app.use(compression());
|
||||
app.use((req, res, next) => {
|
||||
if (req.hostname === 'algo-visualizer.jasonpark.me') {
|
||||
res.redirect(301, 'https://algorithm-visualizer.org/');
|
||||
} else if (credentials && !req.secure) {
|
||||
res.redirect(301, `https://${req.hostname}${req.url}`);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
app.get('/robots.txt', (req, res) => {
|
||||
res.sendFile(path.resolve(__dirname, '..', 'robots.txt'));
|
||||
});
|
||||
app.use(apiEndpoint, backend);
|
||||
app.use(frontend);
|
||||
|
||||
module.exports = app;
|
||||
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
git fetch &&
|
||||
! git diff-index --quiet origin/master -- ':!package-lock.json' &&
|
||||
git reset --hard origin/master &&
|
||||
npm install &&
|
||||
npm run build
|
||||
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
||||
const app = require('../app');
|
||||
const {
|
||||
httpPort,
|
||||
httpsPort,
|
||||
credentials,
|
||||
} = require('../environment');
|
||||
|
||||
const httpServer = http.createServer(app);
|
||||
httpServer.listen(httpPort);
|
||||
console.info(`http: listening on port ${httpPort}`);
|
||||
|
||||
if (credentials) {
|
||||
const httpsServer = https.createServer(credentials, app);
|
||||
httpsServer.listen(httpsPort);
|
||||
console.info(`https: listening on port ${httpsPort}`);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,23 +0,0 @@
|
||||
const env = {
|
||||
NODE_ENV: 'production',
|
||||
HTTP_PORT: '80',
|
||||
HTTPS_PORT: '443',
|
||||
GITHUB_CLIENT_ID: '4ca41e5d1e4ae21b6f75',
|
||||
GITHUB_CLIENT_SECRET: '72fb530eb676b89a1a77e5371f8e21c5965f56ea',
|
||||
GITHUB_WEBHOOK_SECRET: 'flapdoodle_blandiloquence',
|
||||
CREDENTIALS_ENABLED: '1',
|
||||
CREDENTIALS_PATH: '/etc/letsencrypt/live/algorithm-visualizer.org',
|
||||
CREDENTIALS_CA: 'chain.pem',
|
||||
CREDENTIALS_KEY: 'privkey.pem',
|
||||
CREDENTIALS_CERT: 'cert.pem',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'algorithm-visualizer',
|
||||
script: 'npm run pm2',
|
||||
env,
|
||||
},
|
||||
],
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,22 +0,0 @@
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include "algorithm-visualizer/GraphTracer.h"
|
||||
#include "algorithm-visualizer/Randomize.h"
|
||||
|
||||
#define N 5
|
||||
|
||||
using namespace std;
|
||||
|
||||
int main() {
|
||||
int array[N][N];
|
||||
Randomize::Graph<int>(N, 1, *(new Randomize::Integer(1, 9))).weighted().directed(false).fill(&array[0][0]);
|
||||
GraphTracer graphTracer;
|
||||
graphTracer.set(array);
|
||||
for (int i = 0; i < N; i++) {
|
||||
for (int j = 0; j < N; j++) {
|
||||
cout << array[i][j] << " ";
|
||||
}
|
||||
cout << endl;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import morgan from 'morgan';
|
||||
import bodyParser from 'body-parser';
|
||||
import * as Controllers from 'controllers';
|
||||
import { NotFound } from 'ts-httpexceptions';
|
||||
import compression from 'compression';
|
||||
import { __PROD__, credentials, httpPort, httpsPort, webhookOptions } from 'config/environments';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import { Hierarchy } from 'models';
|
||||
import execa from 'execa';
|
||||
import * as Tracers from 'tracers';
|
||||
import { errorHandlerMiddleware, frontendMiddleware, redirectMiddleware } from 'middlewares';
|
||||
import { pull } from 'utils/misc';
|
||||
import { frontendBuildDir, frontendBuiltDir, frontendDir, rootDir } from 'config/paths';
|
||||
|
||||
const Webhook = require('express-github-webhook');
|
||||
|
||||
export default class Server {
|
||||
private readonly app = express();
|
||||
readonly hierarchy = new Hierarchy();
|
||||
readonly tracers = Object.values(Tracers).map(Tracer => new Tracer());
|
||||
private readonly webhook = webhookOptions && Webhook(webhookOptions);
|
||||
|
||||
constructor() {
|
||||
this.app
|
||||
.use(compression())
|
||||
.use(morgan(__PROD__ ? 'tiny' : 'dev'))
|
||||
.use(redirectMiddleware())
|
||||
.use(bodyParser.json())
|
||||
.use(bodyParser.urlencoded({extended: true}))
|
||||
.use('/api', this.getApiRouter())
|
||||
.use(frontendMiddleware(this));
|
||||
if (this.webhook) {
|
||||
this.app.use('/webhook', this.webhook);
|
||||
}
|
||||
this.app.use(errorHandlerMiddleware());
|
||||
|
||||
if (this.webhook) {
|
||||
this.webhook.on('push', async (repo: string, data: any) => {
|
||||
const {ref, head_commit} = data;
|
||||
if (ref !== 'refs/heads/master') return;
|
||||
if (!head_commit) throw new Error('The `head_commit` is empty.');
|
||||
|
||||
switch (repo) {
|
||||
case 'server':
|
||||
await this.update(head_commit.id);
|
||||
break;
|
||||
case 'algorithm-visualizer':
|
||||
await this.updateFrontend(head_commit.id);
|
||||
break;
|
||||
case 'algorithms':
|
||||
await this.hierarchy.update(head_commit.id);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Webhook from unknown repository '${repo}'.`);
|
||||
}
|
||||
});
|
||||
|
||||
this.webhook.on('release', async (repo: string, data: any) => {
|
||||
const tracer = this.tracers.find(tracer => repo === `tracers.${tracer.lang}`);
|
||||
if (!tracer) throw new Error(`Tracer not found for repository '${repo}'.`);
|
||||
await tracer.update(data.release);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getApiRouter() {
|
||||
const router = express.Router();
|
||||
Object.values(Controllers).forEach(Controller => new Controller(this).route(router));
|
||||
router.use((req: Request, res: Response, next: NextFunction) => {
|
||||
next(new NotFound('API not found.'));
|
||||
});
|
||||
return router;
|
||||
};
|
||||
|
||||
async update(commit?: string) {
|
||||
await pull(rootDir, 'server', commit);
|
||||
await execa.shell('npm install', {cwd: rootDir});
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
async updateFrontend(commit?: string) {
|
||||
await pull(frontendDir, 'algorithm-visualizer', commit);
|
||||
await execa.shell([
|
||||
'npm install',
|
||||
'npm run build',
|
||||
`rm -rf ${frontendBuiltDir}`,
|
||||
`mv ${frontendBuildDir} ${frontendBuiltDir}`,
|
||||
].join(' && '), {cwd: frontendDir});
|
||||
}
|
||||
|
||||
start() {
|
||||
const httpServer = http.createServer(this.app);
|
||||
httpServer.listen(httpPort);
|
||||
console.info(`http: listening on port ${httpPort}`);
|
||||
|
||||
if (credentials) {
|
||||
const httpsServer = https.createServer(credentials, this.app);
|
||||
httpsServer.listen(httpsPort);
|
||||
console.info(`https: listening on port ${httpsPort}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
import Promise from 'bluebird';
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import { githubClientId, githubClientSecret } from '/environment';
|
||||
|
||||
const instance = axios.create();
|
||||
|
||||
instance.interceptors.request.use(request => {
|
||||
request.params = { client_id: githubClientId, client_secret: githubClientSecret, ...request.params };
|
||||
return request;
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(response => {
|
||||
return response.data;
|
||||
}, error => {
|
||||
return Promise.reject(error.response.data);
|
||||
});
|
||||
|
||||
const request = (url, process) => {
|
||||
const tokens = url.split('/');
|
||||
const baseURL = /^https?:\/\//i.test(url) ? '' : 'https://api.github.com';
|
||||
return (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const mappedURL = baseURL + tokens.map((token, i) => token.startsWith(':') ? args.shift() : token).join('/');
|
||||
return resolve(process(mappedURL, args));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const GET = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [params] = args;
|
||||
return instance.get(mappedURL, { params });
|
||||
});
|
||||
};
|
||||
|
||||
const DELETE = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [params] = args;
|
||||
return instance.delete(mappedURL, { params });
|
||||
});
|
||||
};
|
||||
|
||||
const POST = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [body, params] = args;
|
||||
return instance.post(mappedURL, body, { params });
|
||||
});
|
||||
};
|
||||
|
||||
const PUT = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [body, params] = args;
|
||||
return instance.put(mappedURL, body, { params });
|
||||
});
|
||||
};
|
||||
|
||||
const PATCH = URL => {
|
||||
return request(URL, (mappedURL, args) => {
|
||||
const [body, params] = args;
|
||||
return instance.patch(mappedURL, body, { params });
|
||||
});
|
||||
};
|
||||
|
||||
const GitHubApi = {
|
||||
listCommits: GET('/repos/:owner/:repo/commits'),
|
||||
getAccessToken: code => instance.post('https://github.com/login/oauth/access_token', { code }, { headers: { Accept: 'application/json' } }),
|
||||
getLatestRelease: GET('/repos/:owner/:repo/releases/latest'),
|
||||
download: (url, path) => instance({
|
||||
method: 'get',
|
||||
url,
|
||||
responseType: 'stream',
|
||||
}).then(data => new Promise((resolve, reject) => {
|
||||
data.pipe(fs.createWriteStream(path));
|
||||
data.on('end', resolve);
|
||||
data.on('error', reject);
|
||||
})),
|
||||
};
|
||||
|
||||
export {
|
||||
GitHubApi,
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
const memoryLimit = 256; // in megabytes
|
||||
const timeLimit = 5000; // in milliseconds
|
||||
|
||||
export {
|
||||
memoryLimit,
|
||||
timeLimit,
|
||||
};
|
||||
@ -1,18 +0,0 @@
|
||||
class ClientError extends Error {
|
||||
}
|
||||
|
||||
class NotFoundError extends ClientError {
|
||||
}
|
||||
|
||||
class ForbiddenError extends ClientError {
|
||||
}
|
||||
|
||||
class UnauthorizedError extends ClientError {
|
||||
}
|
||||
|
||||
export {
|
||||
ClientError,
|
||||
NotFoundError,
|
||||
ForbiddenError,
|
||||
UnauthorizedError,
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { Hierarchy } from '/models';
|
||||
import path from 'path';
|
||||
|
||||
const repoPath = path.resolve(__dirname, '..', 'public', 'algorithms');
|
||||
const hierarchy = new Hierarchy(repoPath);
|
||||
|
||||
export default hierarchy;
|
||||
@ -1,52 +0,0 @@
|
||||
import Promise from 'bluebird';
|
||||
import axios from 'axios';
|
||||
import child_process from 'child_process';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
|
||||
const execute = (command, { stdout = process.stdout, stderr = process.stderr, ...options } = {}) => new Promise((resolve, reject) => {
|
||||
const child = child_process.exec(command, options, (error, stdout, stderr) => {
|
||||
if (error) return reject(error.code ? new Error(stderr) : error);
|
||||
resolve(stdout);
|
||||
});
|
||||
if (stdout) child.stdout.pipe(stdout);
|
||||
if (stderr) child.stderr.pipe(stderr);
|
||||
});
|
||||
|
||||
const createKey = name => name.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
|
||||
|
||||
const isDirectory = dirPath => fs.lstatSync(dirPath).isDirectory();
|
||||
|
||||
const listFiles = dirPath => fs.readdirSync(dirPath).filter(fileName => !fileName.startsWith('.'));
|
||||
|
||||
const listDirectories = dirPath => listFiles(dirPath).filter(fileName => isDirectory(path.resolve(dirPath, fileName)));
|
||||
|
||||
const getDescription = files => {
|
||||
const readmeFile = files.find(file => file.name === 'README.md');
|
||||
if (!readmeFile) return '';
|
||||
const lines = readmeFile.content.split('\n');
|
||||
lines.shift();
|
||||
while (lines.length && !lines[0].trim()) lines.shift();
|
||||
let descriptionLines = [];
|
||||
while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift());
|
||||
return removeMarkdown(descriptionLines.join(' '));
|
||||
};
|
||||
|
||||
const download = (url, localPath) => axios({ url, method: 'GET', responseType: 'stream' })
|
||||
.then(response => new Promise((resolve, reject) => {
|
||||
const writer = fs.createWriteStream(localPath);
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
response.data.pipe(writer);
|
||||
}));
|
||||
|
||||
export {
|
||||
execute,
|
||||
createKey,
|
||||
isDirectory,
|
||||
listFiles,
|
||||
listDirectories,
|
||||
getDescription,
|
||||
download,
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
import GithubWebHook from 'express-github-webhook';
|
||||
import { githubWebhookSecret } from '/environment';
|
||||
|
||||
const webhook = GithubWebHook({ path: '/', secret: githubWebhookSecret });
|
||||
|
||||
export default webhook;
|
||||
@ -0,0 +1,2 @@
|
||||
export const memoryLimit = 256; // in megabytes
|
||||
export const timeLimit = 5000; // in milliseconds
|
||||
@ -0,0 +1,73 @@
|
||||
import fs from 'fs';
|
||||
import { ServerOptions } from 'https';
|
||||
import path from 'path';
|
||||
|
||||
require('dotenv-flow').config();
|
||||
|
||||
declare var process: {
|
||||
env: {
|
||||
[key: string]: string,
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
NODE_ENV,
|
||||
|
||||
HTTP_PORT,
|
||||
HTTPS_PORT,
|
||||
|
||||
CREDENTIALS_ENABLED,
|
||||
CREDENTIALS_PATH,
|
||||
CREDENTIALS_CA,
|
||||
CREDENTIALS_KEY,
|
||||
CREDENTIALS_CERT,
|
||||
|
||||
WEBHOOK_ENABLED,
|
||||
WEBHOOK_SECRET,
|
||||
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET,
|
||||
} = process.env;
|
||||
|
||||
const isEnabled = (v: string) => v === '1';
|
||||
|
||||
const missingVars = [
|
||||
'NODE_ENV',
|
||||
'HTTP_PORT',
|
||||
'HTTPS_PORT',
|
||||
'CREDENTIALS_ENABLED',
|
||||
...(isEnabled(CREDENTIALS_ENABLED) ? [
|
||||
'CREDENTIALS_PATH',
|
||||
'CREDENTIALS_CA',
|
||||
'CREDENTIALS_KEY',
|
||||
'CREDENTIALS_CERT',
|
||||
] : []),
|
||||
'WEBHOOK_ENABLED',
|
||||
...(isEnabled(WEBHOOK_ENABLED) ? [
|
||||
'WEBHOOK_SECRET',
|
||||
] : []),
|
||||
'GITHUB_CLIENT_ID',
|
||||
'GITHUB_CLIENT_SECRET',
|
||||
].filter(variable => process.env[variable] === undefined);
|
||||
if (missingVars.length) throw new Error(`The following environment variables are missing: ${missingVars.join(', ')}`);
|
||||
|
||||
export const __PROD__ = NODE_ENV === 'production';
|
||||
export const __DEV__ = NODE_ENV === 'development';
|
||||
|
||||
export const httpPort = parseInt(HTTP_PORT);
|
||||
export const httpsPort = parseInt(HTTPS_PORT);
|
||||
|
||||
export const webhookOptions = isEnabled(WEBHOOK_ENABLED) ? {
|
||||
path: '/webhook',
|
||||
secret: WEBHOOK_SECRET,
|
||||
} : undefined;
|
||||
|
||||
const readCredentials = (file: string) => fs.readFileSync(path.resolve(CREDENTIALS_PATH, file));
|
||||
export const credentials: ServerOptions | undefined = isEnabled(CREDENTIALS_ENABLED) ? {
|
||||
ca: readCredentials(CREDENTIALS_CA),
|
||||
key: readCredentials(CREDENTIALS_KEY),
|
||||
cert: readCredentials(CREDENTIALS_CERT),
|
||||
} : undefined;
|
||||
|
||||
export const githubClientId = GITHUB_CLIENT_ID;
|
||||
export const githubClientSecret = GITHUB_CLIENT_SECRET;
|
||||
@ -0,0 +1,10 @@
|
||||
import path from 'path';
|
||||
|
||||
export const rootDir = path.resolve(__dirname, '..', '..');
|
||||
export const publicDir = path.resolve(rootDir, 'public');
|
||||
export const algorithmsDir = path.resolve(publicDir, 'algorithms');
|
||||
export const codesDir = path.resolve(publicDir, 'codes');
|
||||
export const visualizationsDir = path.resolve(publicDir, 'visualizations');
|
||||
export const frontendDir = path.resolve(publicDir, 'frontend');
|
||||
export const frontendBuildDir = path.resolve(frontendDir, 'build');
|
||||
export const frontendBuiltDir = path.resolve(publicDir, 'frontend-built');
|
||||
@ -0,0 +1,38 @@
|
||||
import express from 'express';
|
||||
import { Controller } from 'controllers/Controller';
|
||||
import { NotFound } from 'ts-httpexceptions';
|
||||
import Server from 'Server';
|
||||
|
||||
export class AlgorithmsController extends Controller {
|
||||
constructor(server: Server) {
|
||||
super(server);
|
||||
this.router
|
||||
.get('/', this.getHierarchy)
|
||||
.get('/:categoryKey/:algorithmKey', this.getAlgorithm)
|
||||
.get('/sitemap.txt', this.getSitemap);
|
||||
}
|
||||
|
||||
route = (router: express.Router): void => {
|
||||
router.use('/algorithms', this.router);
|
||||
};
|
||||
|
||||
getHierarchy = (req: express.Request, res: express.Response) => {
|
||||
res.json(this.server.hierarchy);
|
||||
};
|
||||
|
||||
getAlgorithm = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const {categoryKey, algorithmKey} = req.params;
|
||||
const algorithm = this.server.hierarchy.find(categoryKey, algorithmKey);
|
||||
if (!algorithm) return next(new NotFound('Algorithm not found.'));
|
||||
res.json({algorithm});
|
||||
};
|
||||
|
||||
getSitemap = (req: express.Request, res: express.Response) => {
|
||||
const urls: string[] = [];
|
||||
this.server.hierarchy.iterate((category, algorithm) => {
|
||||
urls.push(`https://algorithm-visualizer.org/${category.key}/${algorithm.key}`);
|
||||
});
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.send(urls.join('\n'));
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import express from 'express';
|
||||
import { githubClientId } from 'config/environments';
|
||||
import { GitHubApi } from 'utils/apis';
|
||||
import { Controller } from 'controllers/Controller';
|
||||
import Server from 'Server';
|
||||
|
||||
export class AuthController extends Controller {
|
||||
constructor(server: Server) {
|
||||
super(server);
|
||||
this.router
|
||||
.get('/request', this.request)
|
||||
.get('/response', this.response)
|
||||
.get('/destroy', this.destroy);
|
||||
}
|
||||
|
||||
route = (router: express.Router): void => {
|
||||
router.use('/auth', this.router);
|
||||
};
|
||||
|
||||
request = (req: express.Request, res: express.Response) => {
|
||||
res.redirect(`https://github.com/login/oauth/authorize?client_id=${githubClientId}&scope=user,gist`);
|
||||
};
|
||||
|
||||
response = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const {code} = req.query;
|
||||
|
||||
GitHubApi.getAccessToken(code).then(({data}) => {
|
||||
const {access_token} = data;
|
||||
res.send(`<script>window.opener.signIn('${access_token}');window.close();</script>`);
|
||||
}).catch(next);
|
||||
};
|
||||
|
||||
destroy = (req: express.Request, res: express.Response) => {
|
||||
res.send(`<script>window.opener.signOut();window.close();</script>`);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import express from "express";
|
||||
import Server from 'Server';
|
||||
|
||||
export abstract class Controller {
|
||||
protected readonly router: express.Router;
|
||||
|
||||
protected constructor(protected server: Server) {
|
||||
this.router = express.Router();
|
||||
}
|
||||
|
||||
abstract route(router: express.Router): void;
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import express from 'express';
|
||||
import { Controller } from 'controllers/Controller';
|
||||
import Server from 'Server';
|
||||
|
||||
export class TracersController extends Controller {
|
||||
constructor(server: Server) {
|
||||
super(server);
|
||||
this.server.tracers.forEach(tracer => tracer.route(this.router));
|
||||
}
|
||||
|
||||
route = (router: express.Router): void => {
|
||||
router.use('/tracers', this.router);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import uuid from 'uuid';
|
||||
import fs from 'fs-extra';
|
||||
import { Controller } from 'controllers/Controller';
|
||||
import Server from 'Server';
|
||||
import { visualizationsDir } from 'config/paths';
|
||||
|
||||
export class VisualizationsController extends Controller {
|
||||
constructor(server: Server) {
|
||||
super(server);
|
||||
this.router
|
||||
.post('/', this.uploadVisualization)
|
||||
.get('/:visualizationId', this.getVisualization);
|
||||
|
||||
fs.remove(visualizationsDir).catch(console.error);
|
||||
}
|
||||
|
||||
route = (router: express.Router): void => {
|
||||
router.use('/visualizations', this.router);
|
||||
};
|
||||
|
||||
uploadVisualization = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const {content} = req.body;
|
||||
const visualizationId = uuid.v4();
|
||||
const visualizationPath = path.resolve(visualizationsDir, `${visualizationId}.json`);
|
||||
const url = `https://algorithm-visualizer.org/scratch-paper/new?visualizationId=${visualizationId}`;
|
||||
fs.outputFile(visualizationPath, content)
|
||||
.then(() => res.send(url))
|
||||
.catch(next);
|
||||
};
|
||||
|
||||
getVisualization = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const {visualizationId} = req.params;
|
||||
const visualizationPath = path.resolve(visualizationsDir, `${visualizationId}.json`);
|
||||
res.sendFile(visualizationPath, err => {
|
||||
if (err) next(new Error('Visualization Expired'));
|
||||
fs.remove(visualizationPath);
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import express from 'express';
|
||||
import fs from 'fs-extra';
|
||||
import { execute } from '/common/util';
|
||||
import webhook from '/common/webhook';
|
||||
import hierarchy from '/common/hierarchy';
|
||||
import { NotFoundError } from '/common/error';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const downloadCategories = () => (
|
||||
fs.pathExistsSync(hierarchy.path) ?
|
||||
execute(`git fetch && git reset --hard origin/master`, { cwd: hierarchy.path }) :
|
||||
execute(`git clone https://github.com/algorithm-visualizer/algorithms.git ${hierarchy.path}`)
|
||||
).then(() => hierarchy.refresh());
|
||||
|
||||
downloadCategories().catch(console.error);
|
||||
|
||||
webhook.on('algorithms', event => {
|
||||
switch (event) {
|
||||
case 'push':
|
||||
downloadCategories().catch(console.error);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
router.route('/')
|
||||
.get((req, res, next) => {
|
||||
res.json(hierarchy);
|
||||
});
|
||||
|
||||
router.route('/:categoryKey/:algorithmKey')
|
||||
.get((req, res, next) => {
|
||||
const { categoryKey, algorithmKey } = req.params;
|
||||
const algorithm = hierarchy.find(categoryKey, algorithmKey);
|
||||
if (!algorithm) return next(new NotFoundError());
|
||||
res.json({ algorithm });
|
||||
});
|
||||
|
||||
router.route('/sitemap.txt')
|
||||
.get((req, res, next) => {
|
||||
const urls = [];
|
||||
hierarchy.iterate((category, algorithm) => {
|
||||
urls.push(`https://algorithm-visualizer.org/${category.key}/${algorithm.key}`);
|
||||
});
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.send(urls.join('\n'));
|
||||
});
|
||||
|
||||
export default router;
|
||||
@ -1,32 +0,0 @@
|
||||
import express from 'express';
|
||||
import { githubClientId } from '/environment';
|
||||
import { GitHubApi } from '/apis';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const request = (req, res, next) => {
|
||||
res.redirect(`https://github.com/login/oauth/authorize?client_id=${githubClientId}&scope=user,gist`);
|
||||
};
|
||||
|
||||
const response = (req, res, next) => {
|
||||
const { code } = req.query;
|
||||
|
||||
GitHubApi.getAccessToken(code).then(({ access_token }) => {
|
||||
res.send(`<script>window.opener.signIn('${access_token}');window.close();</script>`);
|
||||
}).catch(next);
|
||||
};
|
||||
|
||||
const destroy = (req, res, next) => {
|
||||
res.send(`<script>window.opener.signOut();window.close();</script>`);
|
||||
};
|
||||
|
||||
router.route('/request')
|
||||
.get(request);
|
||||
|
||||
router.route('/response')
|
||||
.get(response);
|
||||
|
||||
router.route('/destroy')
|
||||
.get(destroy);
|
||||
|
||||
export default router;
|
||||
@ -1,4 +0,0 @@
|
||||
export { default as auth } from './auth';
|
||||
export { default as algorithms } from './algorithms';
|
||||
export { default as tracers } from './tracers';
|
||||
export { default as visualizations } from './visualizations';
|
||||
@ -0,0 +1,4 @@
|
||||
export { AlgorithmsController } from './AlgorithmsController';
|
||||
export { AuthController } from './AuthController';
|
||||
export { TracersController } from './TracersController';
|
||||
export { VisualizationsController } from './VisualizationsController';
|
||||
@ -1,81 +0,0 @@
|
||||
import express from 'express';
|
||||
import fs from 'fs-extra';
|
||||
import Promise from 'bluebird';
|
||||
import uuid from 'uuid';
|
||||
import path from 'path';
|
||||
import { GitHubApi } from '/apis';
|
||||
import { execute } from '/common/util';
|
||||
import webhook from '/common/webhook';
|
||||
import { ImageBuilder, WorkerBuilder } from '/tracers';
|
||||
import { memoryLimit, timeLimit } from '/common/config';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const trace = lang => (req, res, next) => {
|
||||
const { code } = req.body;
|
||||
const tempPath = path.resolve(__dirname, '..', 'public', 'codes', uuid.v4());
|
||||
fs.outputFile(path.resolve(tempPath, `Main.${lang}`), code)
|
||||
.then(() => {
|
||||
const builder = builderMap[lang];
|
||||
const containerName = uuid.v4();
|
||||
let killed = false;
|
||||
const timer = setTimeout(() => {
|
||||
execute(`docker kill ${containerName}`).then(() => {
|
||||
killed = true;
|
||||
});
|
||||
}, timeLimit);
|
||||
return execute([
|
||||
'docker run --rm',
|
||||
`--name=${containerName}`,
|
||||
'-w=/usr/visualization',
|
||||
`-v=${tempPath}:/usr/visualization:rw`,
|
||||
`-m=${memoryLimit}m`,
|
||||
'-e ALGORITHM_VISUALIZER=1',
|
||||
builder.imageName,
|
||||
].join(' '), { stdout: null, stderr: null }).catch(error => {
|
||||
if (killed) throw new Error('Time Limit Exceeded');
|
||||
throw error;
|
||||
}).finally(() => clearTimeout(timer));
|
||||
})
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
const visualizationPath = path.resolve(tempPath, 'visualization.json');
|
||||
res.sendFile(visualizationPath, err => {
|
||||
if (err) return reject(new Error('Visualization Not Found'));
|
||||
resolve();
|
||||
});
|
||||
}))
|
||||
.catch(next)
|
||||
.finally(() => fs.remove(tempPath));
|
||||
};
|
||||
|
||||
const builderMap = {
|
||||
js: new WorkerBuilder(),
|
||||
cpp: new ImageBuilder('cpp'),
|
||||
java: new ImageBuilder('java'),
|
||||
};
|
||||
|
||||
Promise.map(Object.keys(builderMap), lang => {
|
||||
const builder = builderMap[lang];
|
||||
return GitHubApi.getLatestRelease('algorithm-visualizer', `tracers.${lang}`).then(builder.build);
|
||||
}).catch(console.error);
|
||||
|
||||
webhook.on('release', (repo, data) => {
|
||||
const result = /^tracers\.(\w+)$/.exec(repo);
|
||||
if (result) {
|
||||
const [, lang] = result;
|
||||
const builder = builderMap[lang];
|
||||
builder.build(data.release).catch(console.error);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(builderMap).forEach(lang => {
|
||||
const builder = builderMap[lang];
|
||||
if (builder instanceof ImageBuilder) {
|
||||
router.post(`/${lang}`, trace(lang));
|
||||
} else if (builder instanceof WorkerBuilder) {
|
||||
router.get(`/${lang}`, (req, res) => res.sendFile(builder.tracerPath));
|
||||
router.get(`/${lang}/worker`, (req, res) => res.sendFile(builder.workerPath));
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@ -1,42 +0,0 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import uuid from 'uuid';
|
||||
import fs from 'fs-extra';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const uploadPath = path.resolve(__dirname, '..', 'public', 'visualizations');
|
||||
const getVisualizationPath = visualizationId => path.resolve(uploadPath, `${visualizationId}.json`);
|
||||
|
||||
fs.remove(uploadPath).catch(console.error);
|
||||
|
||||
const uploadVisualization = (req, res, next) => {
|
||||
const { content } = req.body;
|
||||
const visualizationId = uuid.v4();
|
||||
const tracesPath = getVisualizationPath(visualizationId);
|
||||
const url = `https://algorithm-visualizer.org/scratch-paper/new?visualizationId=${visualizationId}`;
|
||||
fs.outputFile(tracesPath, content)
|
||||
.then(() => res.send(url))
|
||||
.catch(next);
|
||||
};
|
||||
|
||||
const getVisualization = (req, res, next) => {
|
||||
const { visualizationId } = req.params;
|
||||
const visualizationPath = getVisualizationPath(visualizationId);
|
||||
new Promise((resolve, reject) => {
|
||||
res.sendFile(visualizationPath, err => {
|
||||
if (err) return reject(new Error('Visualization Expired'));
|
||||
resolve();
|
||||
});
|
||||
}).catch(next)
|
||||
.finally(() => fs.remove(visualizationPath));
|
||||
};
|
||||
|
||||
router.route('/')
|
||||
.post(uploadVisualization);
|
||||
|
||||
router.route('/:visualizationId')
|
||||
.get(getVisualization);
|
||||
|
||||
export default router;
|
||||
@ -1,41 +0,0 @@
|
||||
import express from 'express';
|
||||
import morgan from 'morgan';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import bodyParser from 'body-parser';
|
||||
import * as controllers from '/controllers';
|
||||
import { ClientError, ForbiddenError, NotFoundError, UnauthorizedError } from '/common/error';
|
||||
import webhook from '/common/webhook';
|
||||
import hierarchy from '/common/hierarchy';
|
||||
|
||||
const app = express();
|
||||
app.use(morgan('tiny'));
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
Object.keys(controllers).forEach(key => app.use(`/${key}`, controllers[key]));
|
||||
app.use('/webhook', webhook);
|
||||
app.use((req, res, next) => next(new NotFoundError()));
|
||||
app.use((err, req, res, next) => {
|
||||
const statusMap = [
|
||||
[UnauthorizedError, 401],
|
||||
[ForbiddenError, 403],
|
||||
[NotFoundError, 404],
|
||||
[ClientError, 400],
|
||||
[Error, 500],
|
||||
];
|
||||
const [, status] = statusMap.find(([Error]) => err instanceof Error);
|
||||
res.status(status);
|
||||
res.send(err.message);
|
||||
console.error(err);
|
||||
});
|
||||
app.hierarchy = hierarchy;
|
||||
|
||||
webhook.on('algorithm-visualizer', event => {
|
||||
switch (event) {
|
||||
case 'push':
|
||||
process.exit(0);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
@ -0,0 +1,3 @@
|
||||
import Server from 'Server';
|
||||
|
||||
new Server().start();
|
||||
@ -0,0 +1,14 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { Exception, InternalServerError } from 'ts-httpexceptions';
|
||||
|
||||
export function errorHandlerMiddleware() {
|
||||
return (err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
if (!(err instanceof Exception)) {
|
||||
console.error(err);
|
||||
err = new InternalServerError(err.message, err);
|
||||
}
|
||||
|
||||
const {name, message, status} = err;
|
||||
res.status(status).json({name, message, status});
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import url from 'url';
|
||||
import Server from 'Server';
|
||||
import { frontendBuiltDir } from 'config/paths';
|
||||
|
||||
const packageJson = require('../../package.json');
|
||||
|
||||
export function frontendMiddleware(server: Server) {
|
||||
const staticMiddleware = express.static(frontendBuiltDir, {index: false});
|
||||
|
||||
if (!fs.pathExistsSync(frontendBuiltDir)) {
|
||||
server.updateFrontend().catch(console.error);
|
||||
}
|
||||
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
staticMiddleware(req, res, err => {
|
||||
if (err) return next(err);
|
||||
if (req.method !== 'GET') return next();
|
||||
|
||||
const filePath = path.resolve(frontendBuiltDir, 'index.html');
|
||||
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||
if (err) return next(err);
|
||||
|
||||
const {pathname} = url.parse(req.originalUrl);
|
||||
if (!pathname) return next(new Error('Failed to get `pathname`.'));
|
||||
const [, categoryKey, algorithmKey] = pathname.split('/');
|
||||
let {title, description} = packageJson;
|
||||
let algorithm = undefined;
|
||||
if (categoryKey && categoryKey !== 'scratch-paper') {
|
||||
algorithm = server.hierarchy.find(categoryKey, algorithmKey) || null;
|
||||
if (algorithm) {
|
||||
title = [algorithm.categoryName, algorithm.algorithmName].join(' - ');
|
||||
description = algorithm.description;
|
||||
} else {
|
||||
res.status(404);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const indexFile = data
|
||||
.replace(/\$TITLE/g, title)
|
||||
.replace(/\$DESCRIPTION/g, description)
|
||||
.replace(/\$ALGORITHM/g, algorithm === undefined ? 'undefined' :
|
||||
JSON.stringify(algorithm).replace(/</g, '\\u003c'));
|
||||
res.send(indexFile);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export { errorHandlerMiddleware } from './errorHandlerMiddleware';
|
||||
export { frontendMiddleware } from './frontendMiddleware';
|
||||
export { redirectMiddleware } from './redirectMiddleware';
|
||||
@ -0,0 +1,14 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { credentials } from 'config/environments';
|
||||
|
||||
export function redirectMiddleware() {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (req.hostname === 'algo-visualizer.jasonpark.me') {
|
||||
res.redirect(301, 'https://algorithm-visualizer.org/');
|
||||
} else if (credentials && !req.secure) {
|
||||
res.redirect(301, `https://${req.hostname}${req.url}`);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import path from 'path';
|
||||
import { createKey, getDescription, listFiles } from '/common/util';
|
||||
import { File } from '/models';
|
||||
|
||||
class Algorithm {
|
||||
constructor(path, name) {
|
||||
this.path = path;
|
||||
this.key = createKey(name);
|
||||
this.name = name;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.files = listFiles(this.path)
|
||||
.map(fileName => new File(path.resolve(this.path, fileName), fileName));
|
||||
this.description = getDescription(this.files);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const { key, name } = this;
|
||||
return { key, name };
|
||||
}
|
||||
}
|
||||
|
||||
export default Algorithm;
|
||||
@ -0,0 +1,26 @@
|
||||
import path from 'path';
|
||||
import { createKey, listFiles } from 'utils/hierarchy';
|
||||
import { File } from 'models';
|
||||
import { getDescription } from 'utils/misc';
|
||||
|
||||
export class Algorithm {
|
||||
key: string;
|
||||
files!: File[];
|
||||
description!: string;
|
||||
|
||||
constructor(private path: string, public name: string) {
|
||||
this.key = createKey(name);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.files = listFiles(this.path)
|
||||
.map(fileName => new File(path.resolve(this.path, fileName), fileName));
|
||||
this.description = getDescription(this.files);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const {key, name} = this;
|
||||
return {key, name};
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import path from 'path';
|
||||
import { createKey, listDirectories } from '/common/util';
|
||||
import { Algorithm } from '/models';
|
||||
|
||||
class Category {
|
||||
constructor(path, name) {
|
||||
this.path = path;
|
||||
this.key = createKey(name);
|
||||
this.name = name;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.algorithms = listDirectories(this.path)
|
||||
.map(algorithmName => new Algorithm(path.resolve(this.path, algorithmName), algorithmName));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const { key, name, algorithms } = this;
|
||||
return { key, name, algorithms };
|
||||
}
|
||||
}
|
||||
|
||||
export default Category;
|
||||
@ -0,0 +1,25 @@
|
||||
import path from 'path';
|
||||
import { createKey, listDirectories } from 'utils/hierarchy';
|
||||
import { Algorithm } from 'models';
|
||||
|
||||
export class Category {
|
||||
key: string;
|
||||
algorithms!: Algorithm[];
|
||||
|
||||
constructor(private path: string, public name: string) {
|
||||
this.key = createKey(name);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.algorithms = listDirectories(this.path)
|
||||
.map(algorithmName => new Algorithm(path.resolve(this.path, algorithmName), algorithmName));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const {key, name, algorithms} = this;
|
||||
return {key, name, algorithms};
|
||||
}
|
||||
}
|
||||
|
||||
export default Category;
|
||||
@ -1,21 +0,0 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
class File {
|
||||
constructor(path, name) {
|
||||
this.path = path;
|
||||
this.name = name;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.content = fs.readFileSync(this.path, 'utf-8');
|
||||
this.contributors = [];
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const { name, content, contributors } = this;
|
||||
return { name, content, contributors };
|
||||
}
|
||||
}
|
||||
|
||||
export default File;
|
||||
@ -0,0 +1,25 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export type Author = {
|
||||
login: string,
|
||||
avatar_url: string,
|
||||
}
|
||||
|
||||
export class File {
|
||||
content!: string;
|
||||
contributors!: Author[];
|
||||
|
||||
constructor(public path: string, public name: string) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.content = fs.readFileSync(this.path, 'utf-8');
|
||||
this.contributors = [];
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const {name, content, contributors} = this;
|
||||
return {name, content, contributors};
|
||||
}
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
import Promise from 'bluebird';
|
||||
import path from 'path';
|
||||
import { execute, listDirectories } from '/common/util';
|
||||
import { GitHubApi } from '/apis';
|
||||
import { Category } from '/models';
|
||||
|
||||
class Hierarchy {
|
||||
constructor(path) {
|
||||
this.path = path;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.categories = listDirectories(this.path)
|
||||
.map(categoryName => new Category(path.resolve(this.path, categoryName), categoryName));
|
||||
|
||||
const files = [];
|
||||
this.categories.forEach(category => category.algorithms.forEach(algorithm => files.push(...algorithm.files)));
|
||||
this.cacheCommitAuthors().then(commitAuthors => this.cacheContributors(files, commitAuthors));
|
||||
}
|
||||
|
||||
cacheCommitAuthors(page = 1, commitAuthors = {}) {
|
||||
const per_page = 100;
|
||||
return GitHubApi.listCommits('algorithm-visualizer', 'algorithms', {
|
||||
per_page,
|
||||
page,
|
||||
}).then(commits => {
|
||||
commits.forEach(({ sha, commit, author }) => {
|
||||
if (!author) return;
|
||||
const { login, avatar_url } = author;
|
||||
commitAuthors[sha] = { login, avatar_url };
|
||||
});
|
||||
if (commits.length < per_page) {
|
||||
return commitAuthors;
|
||||
} else {
|
||||
return this.cacheCommitAuthors(page + 1, commitAuthors);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cacheContributors(files, commitAuthors) {
|
||||
return Promise.each(files, file => {
|
||||
return execute(`git --no-pager log --follow --no-merges --format="%H" "${file.path}"`, {
|
||||
cwd: this.path, stdout: null,
|
||||
}).then(stdout => {
|
||||
const output = stdout.toString().replace(/\n$/, '');
|
||||
const shas = output.split('\n').reverse();
|
||||
const contributors = [];
|
||||
for (const sha of shas) {
|
||||
const author = commitAuthors[sha];
|
||||
if (author && !contributors.find(contributor => contributor.login === author.login)) {
|
||||
contributors.push(author);
|
||||
}
|
||||
}
|
||||
file.contributors = contributors;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
find(categoryKey, algorithmKey) {
|
||||
const category = this.categories.find(category => category.key === categoryKey);
|
||||
if (!category) return;
|
||||
const algorithm = category.algorithms.find(algorithm => algorithm.key === algorithmKey);
|
||||
if (!algorithm) return;
|
||||
|
||||
const categoryName = category.name;
|
||||
const algorithmName = algorithm.name;
|
||||
const files = algorithm.files;
|
||||
const description = algorithm.description;
|
||||
|
||||
return { categoryKey, categoryName, algorithmKey, algorithmName, files, description };
|
||||
}
|
||||
|
||||
iterate(callback) {
|
||||
this.categories.forEach(category => category.algorithms.forEach(algorithm => callback(category, algorithm)));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const { categories } = this;
|
||||
return { categories };
|
||||
}
|
||||
}
|
||||
|
||||
export default Hierarchy;
|
||||
@ -0,0 +1,93 @@
|
||||
import path from 'path';
|
||||
import { listDirectories } from 'utils/hierarchy';
|
||||
import { GitHubApi } from 'utils/apis';
|
||||
import { Algorithm, Category, File } from 'models';
|
||||
import { Author } from 'models/File';
|
||||
import { algorithmsDir } from 'config/paths';
|
||||
import { pull } from 'utils/misc';
|
||||
import execa from 'execa';
|
||||
|
||||
type CommitAuthors = {
|
||||
[sha: string]: Author,
|
||||
}
|
||||
|
||||
export class Hierarchy {
|
||||
private categories!: Category[];
|
||||
readonly path: string = algorithmsDir;
|
||||
|
||||
constructor() {
|
||||
this.refresh();
|
||||
this.update().catch(console.error);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.categories = listDirectories(this.path)
|
||||
.map(categoryName => new Category(path.resolve(this.path, categoryName), categoryName));
|
||||
|
||||
const files: File[] = [];
|
||||
this.categories.forEach(category => category.algorithms.forEach(algorithm => files.push(...algorithm.files)));
|
||||
this.cacheCommitAuthors().then(commitAuthors => this.cacheContributors(files, commitAuthors));
|
||||
}
|
||||
|
||||
async update(commit?: string) {
|
||||
await pull(this.path, 'algorithms', commit);
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
async cacheCommitAuthors(page = 1, commitAuthors: CommitAuthors = {}): Promise<CommitAuthors> {
|
||||
const per_page = 100;
|
||||
const {data} = await GitHubApi.listCommits('algorithm-visualizer', 'algorithms', {per_page, page});
|
||||
const commits: any[] = data;
|
||||
for (const {sha, author} of commits) {
|
||||
if (!author) continue;
|
||||
const {login, avatar_url} = author;
|
||||
commitAuthors[sha] = {login, avatar_url};
|
||||
}
|
||||
if (commits.length < per_page) {
|
||||
return commitAuthors;
|
||||
} else {
|
||||
return this.cacheCommitAuthors(page + 1, commitAuthors);
|
||||
}
|
||||
}
|
||||
|
||||
async cacheContributors(files: File[], commitAuthors: CommitAuthors) {
|
||||
for (const file of files) {
|
||||
const stdout = await execa('git', ['--no-pager', 'log', '--follow', '--no-merges', '--format="%H"', '--', `"${file.path}"`], {
|
||||
cwd: this.path,
|
||||
});
|
||||
const output = stdout.toString().replace(/\n$/, '');
|
||||
const shas = output.split('\n').reverse();
|
||||
const contributors: Author[] = [];
|
||||
for (const sha of shas) {
|
||||
const author = commitAuthors[sha];
|
||||
if (author && !contributors.find(contributor => contributor.login === author.login)) {
|
||||
contributors.push(author);
|
||||
}
|
||||
}
|
||||
file.contributors = contributors;
|
||||
}
|
||||
}
|
||||
|
||||
find(categoryKey: string, algorithmKey: string) {
|
||||
const category = this.categories.find(category => category.key === categoryKey);
|
||||
if (!category) return;
|
||||
const algorithm = category.algorithms.find(algorithm => algorithm.key === algorithmKey);
|
||||
if (!algorithm) return;
|
||||
|
||||
const categoryName = category.name;
|
||||
const algorithmName = algorithm.name;
|
||||
const files = algorithm.files;
|
||||
const description = algorithm.description;
|
||||
|
||||
return {categoryKey, categoryName, algorithmKey, algorithmName, files, description};
|
||||
}
|
||||
|
||||
iterate(callback: (category: Category, algorithm: Algorithm) => void) {
|
||||
this.categories.forEach(category => category.algorithms.forEach(algorithm => callback(category, algorithm)));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const {categories} = this;
|
||||
return {categories};
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export { default as Algorithm } from './Algorithm';
|
||||
export { default as Category } from './Category';
|
||||
export { default as File } from './File';
|
||||
export { default as Hierarchy } from './Hierarchy';
|
||||
@ -0,0 +1,4 @@
|
||||
export { Algorithm } from './Algorithm';
|
||||
export { Category } from './Category';
|
||||
export { File } from './File';
|
||||
export { Hierarchy } from './Hierarchy';
|
||||
@ -0,0 +1,64 @@
|
||||
import path from 'path';
|
||||
import { Release, Tracer } from 'tracers/Tracer';
|
||||
import express from 'express';
|
||||
import uuid from 'uuid';
|
||||
import fs from 'fs-extra';
|
||||
import { memoryLimit, timeLimit } from 'config/constants';
|
||||
import { codesDir } from 'config/paths';
|
||||
import execa = require('execa');
|
||||
|
||||
export class DockerTracer extends Tracer {
|
||||
private readonly directory: string;
|
||||
private readonly imageName: string;
|
||||
|
||||
constructor(lang: string) {
|
||||
super(lang);
|
||||
this.directory = path.resolve(__dirname, lang);
|
||||
this.imageName = `tracer-${this.lang}`;
|
||||
|
||||
this.build = this.build.bind(this);
|
||||
}
|
||||
|
||||
build(release: Release) {
|
||||
const {tag_name} = release;
|
||||
return execa('docker', ['build', '-t', this.imageName, '.', '--build-arg', `tag_name=${tag_name}`], {cwd: this.directory});
|
||||
}
|
||||
|
||||
route(router: express.Router) {
|
||||
router.post(`/${this.lang}`, (req, res, next) => {
|
||||
const {code} = req.body;
|
||||
const tempPath = path.resolve(codesDir, uuid.v4());
|
||||
fs.outputFile(path.resolve(tempPath, `Main.${this.lang}`), code)
|
||||
.then(() => {
|
||||
const containerName = uuid.v4();
|
||||
let killed = false;
|
||||
const timer = setTimeout(() => {
|
||||
execa('docker', ['kill', containerName]).then(() => {
|
||||
killed = true;
|
||||
});
|
||||
}, timeLimit);
|
||||
return execa('docker', [
|
||||
'run', '--rm',
|
||||
`--name=${containerName}`,
|
||||
'-w=/usr/visualization',
|
||||
`-v=${tempPath}:/usr/visualization:rw`,
|
||||
`-m=${memoryLimit}m`,
|
||||
'-e', 'ALGORITHM_VISUALIZER=1',
|
||||
this.imageName,
|
||||
]).catch(error => {
|
||||
if (killed) throw new Error('Time Limit Exceeded');
|
||||
throw error;
|
||||
}).finally(() => clearTimeout(timer));
|
||||
})
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
const visualizationPath = path.resolve(tempPath, 'visualization.json');
|
||||
res.sendFile(visualizationPath, (err: any) => {
|
||||
if (err) return reject(new Error('Visualization Not Found'));
|
||||
resolve();
|
||||
});
|
||||
}))
|
||||
.catch(next)
|
||||
.finally(() => fs.remove(tempPath));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
import path from 'path';
|
||||
import { execute } from '/common/util';
|
||||
|
||||
class ImageBuilder {
|
||||
constructor(lang) {
|
||||
this.lang = lang;
|
||||
this.directory = path.resolve(__dirname, lang);
|
||||
this.imageName = `tracer-${this.lang}`;
|
||||
|
||||
this.build = this.build.bind(this);
|
||||
}
|
||||
|
||||
build(release) {
|
||||
const { tag_name } = release;
|
||||
return execute(`docker build -t ${this.imageName} . --build-arg tag_name=${tag_name}`, { cwd: this.directory });
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageBuilder;
|
||||
@ -0,0 +1,24 @@
|
||||
import express from 'express';
|
||||
import { GitHubApi } from 'utils/apis';
|
||||
|
||||
export type Release = {
|
||||
tag_name: string;
|
||||
}
|
||||
|
||||
export abstract class Tracer {
|
||||
protected constructor(public lang: string) {
|
||||
this.update().catch(console.error);
|
||||
}
|
||||
|
||||
abstract build(release: Release): Promise<any>;
|
||||
|
||||
abstract route(router: express.Router): void;
|
||||
|
||||
async update(release?: Release) {
|
||||
if (release) {
|
||||
return this.build(release);
|
||||
}
|
||||
const {data} = await GitHubApi.getLatestRelease('algorithm-visualizer', `tracers.${this.lang}`);
|
||||
return this.build(data);
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
import path from 'path';
|
||||
import { download } from '/common/util';
|
||||
|
||||
class WorkerBuilder {
|
||||
constructor() {
|
||||
this.tracerPath = path.resolve(__dirname, '..', 'public', 'algorithm-visualizer.js');
|
||||
this.workerPath = path.resolve(__dirname, 'js', 'worker.js');
|
||||
|
||||
this.build = this.build.bind(this);
|
||||
}
|
||||
|
||||
build(release) {
|
||||
const { tag_name } = release;
|
||||
return download(`https://github.com/algorithm-visualizer/tracers.js/releases/download/${tag_name}/algorithm-visualizer.js`, this.tracerPath);
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkerBuilder;
|
||||
@ -0,0 +1,7 @@
|
||||
import { DockerTracer } from 'tracers/DockerTracer';
|
||||
|
||||
export class CppTracer extends DockerTracer {
|
||||
constructor() {
|
||||
super('cpp');
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export { default as ImageBuilder } from './ImageBuilder';
|
||||
export { default as WorkerBuilder } from './WorkerBuilder';
|
||||
@ -0,0 +1,3 @@
|
||||
export { CppTracer } from './cpp/CppTracer';
|
||||
export { JavaTracer } from './java/JavaTracer';
|
||||
export { JsTracer } from './js/JsTracer';
|
||||
@ -0,0 +1,7 @@
|
||||
import { DockerTracer } from 'tracers/DockerTracer';
|
||||
|
||||
export class JavaTracer extends DockerTracer {
|
||||
constructor() {
|
||||
super('java');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import path from 'path';
|
||||
import { download } from 'utils/misc';
|
||||
import { Release, Tracer } from 'tracers/Tracer';
|
||||
import express from 'express';
|
||||
import { publicDir } from 'config/paths';
|
||||
|
||||
export class JsTracer extends Tracer {
|
||||
readonly tracerPath: string;
|
||||
readonly workerPath: string;
|
||||
|
||||
constructor() {
|
||||
super('js');
|
||||
this.tracerPath = path.resolve(publicDir, 'algorithm-visualizer.js');
|
||||
this.workerPath = path.resolve(__dirname, 'worker.js');
|
||||
}
|
||||
|
||||
build(release: Release) {
|
||||
const {tag_name} = release;
|
||||
return download(`https://github.com/algorithm-visualizer/tracers.js/releases/download/${tag_name}/algorithm-visualizer.js`, this.tracerPath);
|
||||
}
|
||||
|
||||
route(router: express.Router) {
|
||||
router.get(`/${this.lang}`, (req, res) => res.sendFile(this.tracerPath));
|
||||
router.get(`/${this.lang}/worker`, (req, res) => res.sendFile(this.workerPath));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { githubClientId, githubClientSecret } from 'config/environments';
|
||||
|
||||
const instance = axios.create();
|
||||
|
||||
instance.interceptors.request.use(request => {
|
||||
request.params = {client_id: githubClientId, client_secret: githubClientSecret, ...request.params};
|
||||
return request;
|
||||
});
|
||||
|
||||
const request = (url: string, process: (mappedUrl: string, args: any[]) => Promise<AxiosResponse<any>>) => {
|
||||
const tokens = url.split('/');
|
||||
const baseURL = /^https?:\/\//i.test(url) ? '' : 'https://api.github.com';
|
||||
return (...args: any[]) => {
|
||||
const mappedUrl = baseURL + tokens.map(token => token.startsWith(':') ? args.shift() : token).join('/');
|
||||
return process(mappedUrl, args);
|
||||
};
|
||||
};
|
||||
|
||||
const GET = (url: string) => {
|
||||
return request(url, (mappedUrl: string, args: any[]) => {
|
||||
const [params] = args;
|
||||
return instance.get(mappedUrl, {params});
|
||||
});
|
||||
};
|
||||
|
||||
const DELETE = (url: string) => {
|
||||
return request(url, (mappedUrl: string, args: any[]) => {
|
||||
const [params] = args;
|
||||
return instance.delete(mappedUrl, {params});
|
||||
});
|
||||
};
|
||||
|
||||
const POST = (url: string) => {
|
||||
return request(url, (mappedUrl: string, args: any[]) => {
|
||||
const [body, params] = args;
|
||||
return instance.post(mappedUrl, body, {params});
|
||||
});
|
||||
};
|
||||
|
||||
const PUT = (url: string) => {
|
||||
return request(url, (mappedUrl: string, args: any[]) => {
|
||||
const [body, params] = args;
|
||||
return instance.put(mappedUrl, body, {params});
|
||||
});
|
||||
};
|
||||
|
||||
const PATCH = (url: string) => {
|
||||
return request(url, (mappedUrl: string, args: any[]) => {
|
||||
const [body, params] = args;
|
||||
return instance.patch(mappedUrl, body, {params});
|
||||
});
|
||||
};
|
||||
|
||||
export const GitHubApi = {
|
||||
listCommits: GET('/repos/:owner/:repo/commits'),
|
||||
|
||||
getAccessToken: (code: string) => instance.post('https://github.com/login/oauth/access_token', {code}, {headers: {Accept: 'application/json'}}),
|
||||
|
||||
getLatestRelease: GET('/repos/:owner/:repo/releases/latest'),
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export function createKey(name: string) {
|
||||
return name.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
|
||||
}
|
||||
|
||||
export function isDirectory(dirPath: string) {
|
||||
return fs.lstatSync(dirPath).isDirectory();
|
||||
}
|
||||
|
||||
export function listFiles(dirPath: string) {
|
||||
return fs.pathExistsSync(dirPath) ? fs.readdirSync(dirPath).filter(fileName => !fileName.startsWith('.')) : [];
|
||||
}
|
||||
|
||||
export function listDirectories(dirPath: string) {
|
||||
return listFiles(dirPath).filter(fileName => isDirectory(path.resolve(dirPath, fileName)));
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs-extra';
|
||||
import execa from 'execa';
|
||||
import { File } from 'models';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
|
||||
export function download(url: string, localPath: string) {
|
||||
return axios({url, method: 'GET', responseType: 'stream'})
|
||||
.then(response => new Promise((resolve, reject) => {
|
||||
const writer = fs.createWriteStream(localPath);
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
response.data.pipe(writer);
|
||||
}));
|
||||
}
|
||||
|
||||
export async function pull(dir: string, repo: string, commit = 'origin/master') {
|
||||
if (fs.pathExistsSync(dir)) {
|
||||
await execa.shell(`git fetch`, {cwd: dir});
|
||||
} else {
|
||||
await execa.shell(`git clone https://github.com/algorithm-visualizer/${repo}.git ${dir}`);
|
||||
}
|
||||
await execa.shell(`git reset --hard ${commit}`, {cwd: dir});
|
||||
}
|
||||
|
||||
export function getDescription(files: File[]) {
|
||||
const readmeFile = files.find(file => file.name === 'README.md');
|
||||
if (!readmeFile) return '';
|
||||
const lines = readmeFile.content.split('\n');
|
||||
lines.shift();
|
||||
while (lines.length && !lines[0].trim()) lines.shift();
|
||||
let descriptionLines = [];
|
||||
while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift());
|
||||
return removeMarkdown(descriptionLines.join(' '));
|
||||
}
|
||||
|
||||
export function rethrow(err: any) {
|
||||
throw err;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"es2015",
|
||||
"esnext"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"strict": true,
|
||||
"baseUrl": "src",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
{
|
||||
"rules": {
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"indent": [
|
||||
true,
|
||||
"spaces",
|
||||
2
|
||||
],
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"no-var-keyword": true,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always",
|
||||
"ignore-bound-class-methods"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-module",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
},
|
||||
{
|
||||
"call-signature": "onespace",
|
||||
"index-signature": "onespace",
|
||||
"parameter": "onespace",
|
||||
"property-declaration": "onespace",
|
||||
"variable-declaration": "onespace"
|
||||
}
|
||||
],
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-null-keyword": true,
|
||||
"prefer-const": true,
|
||||
"jsdoc-format": true
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const {
|
||||
__DEV__,
|
||||
backendBuildPath: buildPath,
|
||||
backendSrcPath: srcPath,
|
||||
} = require('./environment');
|
||||
|
||||
const alias = {
|
||||
'/environment': path.resolve(__dirname, 'environment.js'),
|
||||
};
|
||||
fs.readdirSync(srcPath).forEach(name => {
|
||||
alias['/' + name] = path.resolve(srcPath, name);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
target: 'node',
|
||||
node: {
|
||||
__dirname: true,
|
||||
},
|
||||
entry: srcPath,
|
||||
externals: [nodeExternals()],
|
||||
resolve: {
|
||||
modules: [srcPath],
|
||||
extensions: ['.js'],
|
||||
alias,
|
||||
},
|
||||
output: {
|
||||
path: buildPath,
|
||||
filename: 'index.js',
|
||||
libraryTarget: 'umd',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.js$/, use: 'babel-loader', include: srcPath },
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin([buildPath]),
|
||||
],
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
};
|
||||
Loading…
Reference in new issue