8bitworkshop/src/services.ts

322 lines
9.8 KiB
TypeScript
Raw Normal View History

2019-05-07 19:37:37 +00:00
import { getFolderForPath, isProbablyBinary, stringToByteArray, byteArrayToString, byteArrayToUTF8 } from "./util";
2019-05-07 19:37:37 +00:00
import { FileData } from "./workertypes";
import { CodeProject } from "./project";
// in index.html
declare var exports;
declare var firebase;
export interface GHRepoMetadata {
url : string; // github url
platform_id : string; // e.g. "vcs"
mainPath?: string; // main file path
}
export interface GHSession extends GHRepoMetadata {
2019-05-08 23:15:26 +00:00
url : string; // github url
user : string; // user name
reponame : string; // repo name
repopath : string; // "user/repo"
subtreepath : string; // tree/master/[...]
2019-05-08 23:15:26 +00:00
prefix : string; // file prefix, "local/" or ""
2019-05-10 19:26:12 +00:00
branch : string; // "master" is default
2019-05-08 23:15:26 +00:00
repo : any; // [repo object]
2019-05-10 19:26:12 +00:00
tree? : any; // [tree object]
head? : any; // [head ref]
2019-05-07 19:37:37 +00:00
paths? : string[];
}
const README_md_template = "$NAME\n=====\n\nCompatible with the [$PLATFORM](http://8bitworkshop.com/redir.html?platform=$PLATFORM&importURL=$GITHUBURL) platform in [8bitworkshop](http://8bitworkshop.com/). Main file is [$MAINFILE]($MAINFILE#mainfile).\n";
export function getRepos() : {[key:string]:GHRepoMetadata} {
return JSON.parse(localStorage.getItem('__repos') || '{}');
}
2019-05-09 17:22:24 +00:00
export function parseGithubURL(ghurl:string) {
2019-05-10 19:26:12 +00:00
var toks = ghurl.split('/', 8);
2019-05-09 17:22:24 +00:00
if (toks.length < 5) return null;
if (toks[0] != 'https:') return null;
if (toks[2] != 'github.com') return null;
2019-05-10 19:26:12 +00:00
if (toks[5] && toks[5] != 'tree') return null;
return {user:toks[3], repo:toks[4], repopath:toks[3]+'/'+toks[4], branch:toks[6], subtreepath:toks[7]};
2019-05-09 17:22:24 +00:00
}
2019-05-07 19:37:37 +00:00
export class GithubService {
2019-05-08 17:42:24 +00:00
githubCons;
githubToken;
2019-05-07 19:37:37 +00:00
github;
store;
project : CodeProject;
2019-05-08 17:42:24 +00:00
constructor(githubCons:() => any, githubToken:string, store, project : CodeProject) {
this.githubCons = githubCons;
this.githubToken = githubToken;
2019-05-07 19:37:37 +00:00
this.store = store;
this.project = project;
2019-05-08 17:42:24 +00:00
this.recreateGithub();
}
recreateGithub() {
this.github = new this.githubCons({token:this.githubToken});
}
login() : Promise<void> {
// already logged in? return immediately
if (this.githubToken && this.githubToken.length) {
return new Promise<void>( (yes,no) => {
yes();
});
}
// login via popup
var provider = new firebase.auth.GithubAuthProvider();
provider.addScope('repo');
return firebase.auth().signInWithPopup(provider).then( (result) => {
this.githubToken = result.credential.accessToken;
var user = result.user;
this.recreateGithub();
document.cookie = "__github_key=" + this.githubToken + ";path=/;max-age=31536000";
console.log("Stored GitHub OAUTH key");
}).catch( (error) => {
console.log(error);
bootbox.alert("Could not login to GitHub: " + error);
2019-05-08 17:42:24 +00:00
});
2019-05-07 19:37:37 +00:00
}
isFileIgnored(s : string) : boolean {
s = s.toUpperCase();
if (s.startsWith("LICENSE")) return true;
if (s.startsWith("README")) return true;
if (s.startsWith(".")) return true;
return false;
}
getGithubSession(ghurl:string) : Promise<GHSession> {
2019-05-07 19:37:37 +00:00
return new Promise( (yes,no) => {
2019-05-09 17:22:24 +00:00
var urlparse = parseGithubURL(ghurl);
2019-05-07 19:37:37 +00:00
if (!urlparse) {
no("Please enter a valid GitHub URL.");
}
var sess = {
url: ghurl,
2019-05-07 19:37:37 +00:00
user: urlparse.user,
reponame: urlparse.repo,
2019-05-08 23:15:26 +00:00
repopath: urlparse.repopath,
2019-05-10 19:26:12 +00:00
branch: urlparse.branch || "master",
subtreepath: urlparse.subtreepath,
2019-05-08 23:15:26 +00:00
prefix: '', //this.getPrefix(urlparse.user, urlparse.repo),
repo: this.github.repos(urlparse.user, urlparse.repo),
2019-05-09 17:22:24 +00:00
platform_id: this.project ? this.project.platform_id : null
2019-05-07 19:37:37 +00:00
};
yes(sess);
});
}
2019-05-10 19:26:12 +00:00
getGithubHEADTree(ghurl:string) : Promise<GHSession> {
var sess;
return this.getGithubSession(ghurl).then( (session) => {
sess = session;
return sess.repo.git.refs.heads(sess.branch).fetch();
})
.then( (head) => {
sess.head = head;
return sess.repo.git.trees(head.object.sha).fetch();
})
.then( (tree) => {
if (sess.subtreepath) {
for (let subtree of tree.tree) {
if (subtree.type == 'tree' && subtree.path == sess.subtreepath && subtree.sha) {
return sess.repo.git.trees(subtree.sha).fetch();
}
}
throw "Cannot find subtree '" + sess.subtreepath + "' in tree " + tree.sha;
}
return tree;
})
2019-05-10 19:26:12 +00:00
.then( (tree) => {
sess.tree = tree;
return sess;
});
}
2019-05-08 23:15:26 +00:00
bind(sess:GHSession, dobind:boolean) {
var repos = getRepos();
2019-05-08 23:15:26 +00:00
if (dobind) {
repos[sess.repopath] = {url:sess.url, platform_id:sess.platform_id, mainPath:sess.mainPath};
2019-05-08 23:15:26 +00:00
} else {
delete repos[sess.repopath];
}
localStorage.setItem('__repos', JSON.stringify(repos));
2019-05-07 19:37:37 +00:00
}
2019-05-07 19:37:37 +00:00
import(ghurl:string) : Promise<GHSession> {
var sess : GHSession;
return this.getGithubSession(ghurl).then( (session) => {
sess = session;
// load README
return sess.repo.contents('README.md').read();
})
2019-05-09 17:22:24 +00:00
.catch( (e) => {
console.log(e);
2019-05-08 17:42:24 +00:00
console.log('no README.md found')
return ''; // empty README
})
.then( (readme) => {
var m;
// check README for main file
const re8main = /\(([^)]+)#mainfile\)/;
m = re8main.exec(readme);
2019-05-08 17:42:24 +00:00
if (m && m[1]) {
console.log("main path: '" + m[1] + "'");
sess.mainPath = m[1];
}
// check README for proper platform
2019-05-09 17:22:24 +00:00
// unless we use githubURL=
const re8plat = /8bitworkshop.com[^)]+platform=(\w+)/;
m = re8plat.exec(readme);
if (m) {
console.log("platform id: '" + m[1] + "'");
2019-05-09 17:22:24 +00:00
if (sess.platform_id && !sess.platform_id.startsWith(m[1]))
throw "Platform mismatch: Repository is " + m[1] + ", you have " + this.project.platform_id + " selected.";
2019-05-09 17:22:24 +00:00
sess.platform_id = m[1];
}
// bind to repository
this.bind(sess, true);
// get head commit
2019-05-08 17:42:24 +00:00
return sess;
});
}
2019-05-08 23:15:26 +00:00
pull(ghurl:string, deststore?) : Promise<GHSession> {
var sess : GHSession;
2019-05-10 19:26:12 +00:00
return this.getGithubHEADTree(ghurl).then( (session) => {
2019-05-07 19:37:37 +00:00
sess = session;
let blobreads = [];
sess.paths = [];
2019-05-10 19:26:12 +00:00
sess.tree.tree.forEach( (item) => {
2019-05-07 19:37:37 +00:00
console.log(item.path, item.type, item.size);
sess.paths.push(item.path);
if (item.type == 'blob' && !this.isFileIgnored(item.path)) {
var read = sess.repo.git.blobs(item.sha).fetch().then( (blob) => {
2019-05-07 19:37:37 +00:00
var path = sess.prefix + item.path;
var size = item.size;
var encoding = blob.encoding;
var isBinary = isProbablyBinary(item.path, blob);
var data = blob.content;
if (blob.encoding == 'base64') {
var bindata = stringToByteArray(atob(data));
data = isBinary ? bindata : byteArrayToUTF8(bindata);
}
if (blob.size != data.length) {
data = data.slice(0, blob.size);
}
2019-05-08 23:15:26 +00:00
return (deststore || this.store).setItem(path, data);
2019-05-07 19:37:37 +00:00
});
blobreads.push(read);
} else {
console.log("ignoring " + item.path);
}
});
return Promise.all(blobreads);
})
.then( (blobs) => {
return sess;
});
}
2019-05-08 17:42:24 +00:00
importAndPull(ghurl:string) {
return this.import(ghurl).then((sess) => {
return this.pull(ghurl);
});
}
2019-05-07 19:37:37 +00:00
publish(reponame:string, desc:string, license:string, isprivate:boolean) : Promise<GHSession> {
var repo;
2019-05-07 19:37:37 +00:00
return this.github.user.repos.create({
name: reponame,
description: desc,
private: isprivate,
auto_init: false,
2019-05-07 19:37:37 +00:00
license_template: license
})
.then( (_repo) => {
repo = _repo;
// create README.md
var s = README_md_template;
s = s.replace(/\$NAME/g, encodeURIComponent(reponame));
s = s.replace(/\$PLATFORM/g, encodeURIComponent(this.project.platform_id));
s = s.replace(/\$GITHUBURL/g, encodeURIComponent(repo.html_url));
s = s.replace(/\$MAINFILE/g, encodeURIComponent(this.project.stripLocalPath(this.project.mainPath)));
var config = {
message: '8bitworkshop: updated metadata in README.md',
content: btoa(s)
}
return repo.contents('README.md').add(config);
})
.then( () => {
return this.getGithubSession(repo.htmlUrl);
})
.then( (sess) => {
2019-05-07 19:37:37 +00:00
this.bind(sess, true);
return sess;
});
}
commitPush( ghurl:string, message:string, files:{path:string,data:FileData}[] ) : Promise<GHSession> {
var sess : GHSession;
2019-05-10 19:26:12 +00:00
return this.getGithubHEADTree(ghurl).then( (session) => {
2019-05-07 19:37:37 +00:00
sess = session;
if (sess.subtreepath) {
throw "Sorry, right now you can only commit files to the root directory of a repository.";
}
2019-05-07 19:37:37 +00:00
return Promise.all(files.map( (file) => {
if (typeof file.data === 'string') {
2019-05-10 19:26:12 +00:00
return sess.repo.git.blobs.create({
content: file.data,
encoding: 'utf-8'
});
} else {
2019-05-10 19:26:12 +00:00
return sess.repo.git.blobs.create({
content: btoa(byteArrayToString(file.data)),
encoding: 'base64'
});
}
2019-05-07 19:37:37 +00:00
}));
}).then( (blobs) => {
2019-05-10 19:26:12 +00:00
return sess.repo.git.trees.create({
2019-05-07 19:37:37 +00:00
tree: files.map( (file, index) => {
return {
path: file.path,
mode: '100644',
type: 'blob',
sha: blobs[index]['sha']
};
}),
2019-05-10 19:26:12 +00:00
base_tree: sess.tree.sha
2019-05-07 19:37:37 +00:00
});
2019-05-10 19:26:12 +00:00
}).then( (newtree) => {
return sess.repo.git.commits.create({
2019-05-07 19:37:37 +00:00
message: message,
2019-05-10 19:26:12 +00:00
tree: newtree.sha,
2019-05-07 19:37:37 +00:00
parents: [
2019-05-10 19:26:12 +00:00
sess.head.object.sha
2019-05-07 19:37:37 +00:00
]
});
}).then( (commit) => {
2019-05-10 19:26:12 +00:00
return sess.head.update({
2019-05-07 19:37:37 +00:00
sha: commit.sha
});
}).then( (update) => {
return sess;
});
}
deleteRepository(ghurl:string) {
return this.getGithubSession(ghurl).then( (session) => {
return session.repo.remove();
});
}
2019-05-07 19:37:37 +00:00
}