Separate backend from algorithm-visualizer repo

master
Jason Park 7 years ago
commit 5c48b3088d

BIN
.DS_Store vendored

Binary file not shown.

6
.gitignore vendored

@ -0,0 +1,6 @@
/.idea
/node_modules
/npm-debug.log
/public
/pm2.config.js
.DS_Store

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>

@ -0,0 +1,8 @@
<?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>

@ -0,0 +1,12 @@
<?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>

@ -0,0 +1,8 @@
<?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>

@ -0,0 +1,14 @@
<?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>

@ -0,0 +1,266 @@
<?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>

@ -0,0 +1,42 @@
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;
}

@ -0,0 +1,78 @@
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;

@ -0,0 +1,30 @@
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;

@ -0,0 +1,7 @@
#!/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

@ -0,0 +1,21 @@
#!/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}`);
}

@ -0,0 +1,13 @@
{
"name": "@algorithm-visualizer/server",
"version": "2.0.0",
"description": "Server for Algorithm Visualizer",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/algorithm-visualizer/server.git"
},
"license": "MIT"
}

@ -0,0 +1,23 @@
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

@ -0,0 +1,22 @@
#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,82 @@
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,
};

@ -0,0 +1,7 @@
const memoryLimit = 256; // in megabytes
const timeLimit = 5000; // in milliseconds
export {
memoryLimit,
timeLimit,
};

@ -0,0 +1,18 @@
class ClientError extends Error {
}
class NotFoundError extends ClientError {
}
class ForbiddenError extends ClientError {
}
class UnauthorizedError extends ClientError {
}
export {
ClientError,
NotFoundError,
ForbiddenError,
UnauthorizedError,
};

@ -0,0 +1,7 @@
import { Hierarchy } from '/models';
import path from 'path';
const repoPath = path.resolve(__dirname, '..', 'public', 'algorithms');
const hierarchy = new Hierarchy(repoPath);
export default hierarchy;

@ -0,0 +1,52 @@
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,
};

@ -0,0 +1,6 @@
import GithubWebHook from 'express-github-webhook';
import { githubWebhookSecret } from '/environment';
const webhook = GithubWebHook({ path: '/', secret: githubWebhookSecret });
export default webhook;

@ -0,0 +1,49 @@
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;

@ -0,0 +1,32 @@
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;

@ -0,0 +1,4 @@
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,81 @@
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;

@ -0,0 +1,42 @@
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;

@ -0,0 +1,41 @@
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,25 @@
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,24 @@
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,21 @@
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,84 @@
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,4 @@
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,19 @@
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,18 @@
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,17 @@
FROM rikorose/gcc-cmake
ARG tag_name
RUN curl --create-dirs -o /usr/local/include/nlohmann/json.hpp -L "https://github.com/nlohmann/json/releases/download/v3.1.2/json.hpp" \
&& curl --create-dirs -o /usr/tmp/algorithm-visualizer.tar.gz -L "https://github.com/algorithm-visualizer/tracers.cpp/archive/${tag_name}.tar.gz" \
&& cd /usr/tmp \
&& mkdir algorithm-visualizer \
&& tar xvzf algorithm-visualizer.tar.gz -C algorithm-visualizer --strip-components=1 \
&& cd /usr/tmp/algorithm-visualizer \
&& mkdir build \
&& cd build \
&& cmake .. \
&& make install
CMD g++ Main.cpp -o Main -O2 -std=c++11 -lcurl \
&& ./Main

@ -0,0 +1,2 @@
export { default as ImageBuilder } from './ImageBuilder';
export { default as WorkerBuilder } from './WorkerBuilder';

@ -0,0 +1,8 @@
FROM openjdk:8
ARG tag_name
RUN curl --create-dirs -o /usr/local/lib/algorithm-visualizer.jar -L "https://github.com/algorithm-visualizer/tracers.java/releases/download/${tag_name}/algorithm-visualizer.jar"
CMD javac -cp /usr/local/lib/algorithm-visualizer.jar Main.java \
&& java -cp /usr/local/lib/algorithm-visualizer.jar:. Main

@ -0,0 +1,14 @@
const process = { env: { ALGORITHM_VISUALIZER: '1' } };
importScripts('/api/tracers/js');
const sandbox = code => {
const require = name => ({ 'algorithm-visualizer': AlgorithmVisualizer }[name]); // fake require
eval(code);
};
onmessage = e => {
const lines = e.data.split('\n').map((line, i) => line.replace(/(\.\s*delay\s*)\(\s*\)/g, `$1(${i})`));
const code = lines.join('\n');
sandbox(code);
postMessage(AlgorithmVisualizer.Commander.commands);
};

@ -0,0 +1,45 @@
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…
Cancel
Save