2019-05-02 21:12:06 +02:00
|
|
|
import fs from 'fs';
|
|
|
|
import rp from 'request-promise';
|
|
|
|
import Q from 'q';
|
2019-08-30 20:26:57 +02:00
|
|
|
import { ok } from 'assert';
|
2019-05-02 21:12:06 +02:00
|
|
|
|
|
|
|
// TODO: Change branchName if necessary
|
|
|
|
const branchName = 'release';
|
|
|
|
const branchRef = `?ref=${branchName}`;
|
|
|
|
const baseUrl = 'https://api.github.com/repos/ml5js/ml5-examples/contents';
|
|
|
|
const clientId = process.env.GITHUB_ID;
|
|
|
|
const clientSecret = process.env.GITHUB_SECRET;
|
|
|
|
const editorUsername = process.env.ML5_EXAMPLES_USERNAME;
|
2019-08-30 20:26:57 +02:00
|
|
|
const personalAccessToken = process.env.EDITOR_API_ACCESS_TOKEN;
|
|
|
|
const editorApiUrl = process.env.EDITOR_API_URL;
|
2019-05-02 21:12:06 +02:00
|
|
|
const headers = {
|
|
|
|
'User-Agent': 'p5js-web-editor/0.0.1'
|
|
|
|
};
|
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
ok(clientId, 'GITHUB_ID is required');
|
|
|
|
ok(clientSecret, 'GITHUB_SECRET is required');
|
|
|
|
ok(editorUsername, 'ML5_EXAMPLES_USERNAME is required');
|
|
|
|
ok(personalAccessToken, 'EDITOR_API_ACCESS_TOKEN is required');
|
|
|
|
ok(editorApiUrl, 'EDITOR_API_URL is required');
|
|
|
|
|
|
|
|
//
|
|
|
|
const githubRequestOptions = {
|
2019-05-02 21:12:06 +02:00
|
|
|
url: baseUrl,
|
|
|
|
qs: {
|
|
|
|
client_id: clientId,
|
|
|
|
client_secret: clientSecret
|
|
|
|
},
|
|
|
|
method: 'GET',
|
|
|
|
headers,
|
|
|
|
json: true
|
|
|
|
};
|
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
const editorRequestOptions = {
|
|
|
|
url: `${editorApiUrl}/${editorUsername}`,
|
|
|
|
method: 'GET',
|
|
|
|
headers: {
|
|
|
|
...headers,
|
|
|
|
Authorization: `Basic ${Buffer.from(`${editorUsername}:${personalAccessToken}`).toString('base64')}`
|
|
|
|
},
|
|
|
|
json: true
|
|
|
|
};
|
2019-05-02 21:12:06 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* ---------------------------------------------------------
|
|
|
|
* --------------------- helper functions --------------------
|
|
|
|
* ---------------------------------------------------------
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* fatten a nested array
|
|
|
|
*/
|
|
|
|
function flatten(list) {
|
|
|
|
return list.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
|
|
|
|
}
|
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
/**
|
|
|
|
* Fetch data for a single HTML/JS file, or return
|
|
|
|
* an url to the file's CDN location
|
|
|
|
*/
|
|
|
|
async function fetchFileContent(item) {
|
|
|
|
const { name } = item;
|
|
|
|
const file = { url: item.url };
|
|
|
|
|
|
|
|
// if it is an html or js file
|
|
|
|
if (
|
|
|
|
(file.url != null && name.endsWith('.html')) ||
|
|
|
|
name.endsWith('.js')
|
|
|
|
) {
|
|
|
|
const options = Object.assign({}, githubRequestOptions);
|
|
|
|
options.url = `${file.url}`;
|
|
|
|
|
|
|
|
if (
|
|
|
|
options.url !== undefined ||
|
|
|
|
options.url !== null ||
|
|
|
|
options.url !== ''
|
|
|
|
) {
|
|
|
|
file.content = await rp(options);
|
|
|
|
// NOTE: remove the URL property if there's content
|
|
|
|
// Otherwise the p5 editor will try to pull from that url
|
|
|
|
if (file.content !== null) delete file.url;
|
|
|
|
}
|
|
|
|
|
|
|
|
return file;
|
|
|
|
// if it is NOT an html or js file
|
|
|
|
}
|
|
|
|
|
|
|
|
if (file.url) {
|
|
|
|
const cdnRef = `https://cdn.jsdelivr.net/gh/ml5js/ml5-examples@${branchName}${file.url.split(branchName)[1]}`;
|
|
|
|
file.url = cdnRef;
|
|
|
|
}
|
|
|
|
|
|
|
|
return file;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-05-02 21:12:06 +02:00
|
|
|
/**
|
|
|
|
* STEP 1: Get the top level cateogories
|
|
|
|
*/
|
|
|
|
async function getCategories() {
|
|
|
|
try {
|
2019-08-30 20:26:57 +02:00
|
|
|
const options = Object.assign({}, githubRequestOptions);
|
2019-05-02 21:12:06 +02:00
|
|
|
options.url = `${options.url}/p5js${branchRef}`;
|
|
|
|
const results = await rp(options);
|
|
|
|
|
|
|
|
return results;
|
|
|
|
} catch (err) {
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* STEP 2: Get the examples for each category
|
|
|
|
* e.g. Posenet:
|
|
|
|
* - /posenet_image_single
|
|
|
|
* - /posenet_part_selection
|
|
|
|
*/
|
|
|
|
async function getCategoryExamples(sketchRootList) {
|
|
|
|
const output = [];
|
|
|
|
const sketchRootCategories = sketchRootList.map(async (categories) => {
|
|
|
|
// let options = Object.assign({url: `${requestOptions.url}/${categories.path}${branchRef}`}, requestOptions)
|
2019-08-30 20:26:57 +02:00
|
|
|
const options = Object.assign({}, githubRequestOptions);
|
2019-05-02 21:12:06 +02:00
|
|
|
options.url = `${options.url}${categories.path}${branchRef}`;
|
|
|
|
// console.log(options)
|
|
|
|
const sketchDirs = await rp(options);
|
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
try {
|
|
|
|
const result = flatten(sketchDirs);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
} catch (err) {
|
|
|
|
return [];
|
|
|
|
}
|
2019-05-02 21:12:06 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const sketchList = await Q.all(sketchRootCategories);
|
|
|
|
|
|
|
|
sketchList.forEach((sketch) => {
|
|
|
|
sketch.forEach((item) => {
|
|
|
|
if (item.type === 'dir') output.push(item);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* STEP 3.1: Recursively get the tree of files for each directory
|
|
|
|
* @param parentObject - one sketch directory object
|
|
|
|
*/
|
|
|
|
async function traverseSketchTree(parentObject) {
|
|
|
|
const output = Object.assign({}, parentObject);
|
|
|
|
|
|
|
|
if (parentObject.type !== 'dir') {
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
// let options = `https://api.github.com/repos/ml5js/ml5-examples/contents/${sketches.path}${branchRef}`
|
2019-08-30 20:26:57 +02:00
|
|
|
const options = Object.assign({}, githubRequestOptions);
|
2019-05-02 21:12:06 +02:00
|
|
|
options.url = `${options.url}${parentObject.path}${branchRef}`;
|
|
|
|
|
|
|
|
output.tree = await rp(options);
|
|
|
|
|
|
|
|
output.tree = output.tree.map(file => traverseSketchTree(file));
|
|
|
|
|
|
|
|
output.tree = await Q.all(output.tree);
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* STEP 3.2: Traverse the sketchtree for all of the sketches
|
|
|
|
* @param {*} categoryExamples - all of the categories in an array
|
|
|
|
*/
|
|
|
|
async function traverseSketchTreeAll(categoryExamples) {
|
|
|
|
const sketches = categoryExamples.map(async sketch => traverseSketchTree(sketch));
|
|
|
|
|
|
|
|
const result = await Q.all(sketches);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Traverse the tree and format into parent child relation
|
|
|
|
* @param {*} parentObject
|
|
|
|
*/
|
|
|
|
function traverseAndFormat(parentObject) {
|
|
|
|
const parent = Object.assign({}, parentObject);
|
|
|
|
|
|
|
|
if (!parentObject.tree) {
|
|
|
|
// returns the files
|
|
|
|
return {
|
|
|
|
name: parent.name,
|
2019-08-30 20:26:57 +02:00
|
|
|
url: parent.download_url
|
2019-05-02 21:12:06 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const subdir = parentObject.tree.map((item) => {
|
|
|
|
if (!item.tree) {
|
|
|
|
// returns the files
|
|
|
|
return {
|
|
|
|
name: item.name,
|
2019-08-30 20:26:57 +02:00
|
|
|
url: item.download_url
|
2019-05-02 21:12:06 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const feat = {
|
|
|
|
name: item.name,
|
|
|
|
children: traverseAndFormat(item)
|
|
|
|
};
|
|
|
|
return feat;
|
|
|
|
});
|
|
|
|
return subdir;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-08-30 20:26:57 +02:00
|
|
|
* Traverse the tree and download all the content,
|
|
|
|
* transforming into an object keyed by file/directory name
|
2019-05-02 21:12:06 +02:00
|
|
|
* @param {*} projectFileTree
|
|
|
|
*/
|
2019-08-30 20:26:57 +02:00
|
|
|
async function traverseAndDownload(projectFileTree) {
|
|
|
|
return projectFileTree.reduce(
|
|
|
|
async (previousPromise, item, idx) => {
|
|
|
|
const result = await previousPromise;
|
|
|
|
|
|
|
|
if (Array.isArray(item.children)) {
|
|
|
|
result[item.name] = {
|
|
|
|
files: await traverseAndDownload(item.children)
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
result[item.name] = await fetchFileContent(item);
|
2019-05-02 21:12:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
},
|
2019-08-30 20:26:57 +02:00
|
|
|
{}
|
2019-05-02 21:12:06 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* STEP 4
|
|
|
|
* Take a parent directory and prepare it for injestion!
|
|
|
|
* @param {*} sketch
|
|
|
|
* @param {*} user
|
|
|
|
*/
|
2019-08-30 20:26:57 +02:00
|
|
|
async function formatSketchForStorage(sketch, user) {
|
|
|
|
const newProject = {
|
2019-05-02 21:12:06 +02:00
|
|
|
name: sketch.name,
|
2019-08-30 20:26:57 +02:00
|
|
|
files: {} // <== add files to this object
|
|
|
|
};
|
2019-05-02 21:12:06 +02:00
|
|
|
|
|
|
|
let projectFiles = traverseAndFormat(sketch);
|
2019-08-30 20:26:57 +02:00
|
|
|
projectFiles = await traverseAndDownload(projectFiles);
|
2019-05-02 21:12:06 +02:00
|
|
|
newProject.files = projectFiles;
|
|
|
|
return newProject;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* format all the sketches using the formatSketchForStorage()
|
|
|
|
*/
|
|
|
|
function formatSketchForStorageAll(sketchWithItems, user) {
|
|
|
|
let sketchList = sketchWithItems.slice(0);
|
|
|
|
|
|
|
|
sketchList = sketchList.map(sketch => formatSketchForStorage(sketch, user));
|
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
return Promise.all(sketchList);
|
2019-05-02 21:12:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-08-30 20:26:57 +02:00
|
|
|
* Fetch a list of all projects from the API
|
2019-05-02 21:12:06 +02:00
|
|
|
*/
|
2019-08-30 20:26:57 +02:00
|
|
|
async function getProjectsList() {
|
|
|
|
const options = Object.assign({}, editorRequestOptions);
|
|
|
|
options.url = `${options.url}/sketches`;
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
const results = await rp(options);
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
return results.sketches;
|
|
|
|
}
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
/**
|
|
|
|
* Delete a project
|
|
|
|
*/
|
|
|
|
async function deleteProject(project) {
|
|
|
|
const options = Object.assign({}, editorRequestOptions);
|
|
|
|
options.method = 'DELETE';
|
|
|
|
options.url = `${options.url}/sketches/${project.id}`;
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
const results = await rp(options);
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
return results;
|
2019-05-02 21:12:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-08-30 20:26:57 +02:00
|
|
|
* Create a new project
|
2019-05-02 21:12:06 +02:00
|
|
|
*/
|
2019-08-30 20:26:57 +02:00
|
|
|
async function createProject(project) {
|
|
|
|
try {
|
|
|
|
const options = Object.assign({}, editorRequestOptions);
|
|
|
|
options.method = 'POST';
|
|
|
|
options.url = `${options.url}/sketches`;
|
|
|
|
options.body = project;
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
const results = await rp(options);
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
return results;
|
|
|
|
} catch (err) {
|
|
|
|
throw err;
|
|
|
|
}
|
2019-05-02 21:12:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* STEP 6
|
|
|
|
* Remove existing projects, then fill the db
|
|
|
|
* @param {*} filledProjectList
|
|
|
|
* @param {*} user
|
|
|
|
*/
|
|
|
|
async function createProjectsInP5User(filledProjectList, user) {
|
2019-08-30 20:26:57 +02:00
|
|
|
console.log('Finding existing projects...');
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
const existingProjects = await getProjectsList();
|
|
|
|
|
|
|
|
console.log(`Will delete ${existingProjects.length} projects`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
await Q.all(existingProjects.map(deleteProject));
|
|
|
|
console.log('deleted old projects!');
|
|
|
|
} catch (error) {
|
|
|
|
console.log('Problem deleting projects');
|
|
|
|
console.log(error);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
try {
|
|
|
|
const newProjects = filledProjectList.map(async (project) => {
|
|
|
|
console.log(`saving ${project.name}`);
|
|
|
|
await createProject(project);
|
2019-05-02 21:12:06 +02:00
|
|
|
});
|
2019-08-30 20:26:57 +02:00
|
|
|
await Q.all(newProjects);
|
|
|
|
console.log(`Projects saved to User: ${editorUsername}!`);
|
|
|
|
} catch (error) {
|
|
|
|
console.log('Error saving projects');
|
|
|
|
console.log(error);
|
2019-05-02 21:12:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ---------------------------------------------------------
|
|
|
|
* --------------------- main ------------------------------
|
|
|
|
* ---------------------------------------------------------
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* MAKE
|
|
|
|
* Get all the sketches from the ml5-examples repo
|
|
|
|
* Get the p5 examples
|
|
|
|
* Dive down into each sketch and get all the files
|
|
|
|
* Format the sketch files to be save to the db
|
|
|
|
* Delete existing and save
|
|
|
|
*/
|
|
|
|
async function make() {
|
|
|
|
// Get the categories and their examples
|
|
|
|
const categories = await getCategories();
|
|
|
|
const categoryExamples = await getCategoryExamples(categories);
|
2019-08-30 20:26:57 +02:00
|
|
|
|
2019-05-02 21:12:06 +02:00
|
|
|
const examplesWithResourceTree = await traverseSketchTreeAll(categoryExamples);
|
2019-08-30 20:26:57 +02:00
|
|
|
const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree);
|
|
|
|
|
|
|
|
await createProjectsInP5User(formattedSketchList);
|
2019-05-02 21:12:06 +02:00
|
|
|
console.log('done!');
|
|
|
|
process.exit();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* TEST - same as make except reads from file for testing purposes
|
|
|
|
* Get all the sketches from the ml5-examples repo
|
|
|
|
* Get the p5 examples
|
|
|
|
* Dive down into each sketch and get all the files
|
|
|
|
* Format the sketch files to be save to the db
|
|
|
|
* Delete existing and save
|
|
|
|
*/
|
2019-08-30 20:26:57 +02:00
|
|
|
// eslint-disable-next-line no-unused-vars
|
2019-05-02 21:12:06 +02:00
|
|
|
async function test() {
|
|
|
|
// read from file while testing
|
|
|
|
const examplesWithResourceTree = JSON.parse(fs.readFileSync('./ml5-examplesWithResourceTree.json'));
|
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
const formattedSketchList = await formatSketchForStorageAll(examplesWithResourceTree);
|
2019-05-02 21:12:06 +02:00
|
|
|
|
2019-08-30 20:26:57 +02:00
|
|
|
await createProjectsInP5User(formattedSketchList);
|
2019-05-02 21:12:06 +02:00
|
|
|
console.log('done!');
|
|
|
|
process.exit();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ---------------------------------------------------------
|
|
|
|
* --------------------- Run -------------------------------
|
|
|
|
* ---------------------------------------------------------
|
|
|
|
* Usage:
|
|
|
|
* If you're testing, change the make() function to test()
|
|
|
|
* ensure when testing that you've saved some JSON outputs to
|
|
|
|
* read from so you don't have to make a billion requests all the time
|
|
|
|
*
|
|
|
|
* $ GITHUB_ID=<....> GITHUB_SECRET=<...> NODE_ENV=development npm run fetch-examples-ml5
|
|
|
|
* $ GITHUB_ID=<....> GITHUB_SECRET=<...> npm run fetch-examples-ml5
|
|
|
|
*/
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
|
|
// test()
|
|
|
|
make(); // replace with test() if you don't want to run all the fetch functions over and over
|
|
|
|
} else {
|
|
|
|
make();
|
|
|
|
}
|