├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── adhoc-specs ├── githubClient.spec.ts ├── mockCache.ts └── staticSettingsProvider.ts ├── package.json └── src ├── IFileReference.ts ├── IGithubBlob.ts ├── IGithubClient.ts ├── IGithubCommit.ts ├── IGithubConfig.ts ├── IGithubCreateBlobReponse.ts ├── IGithubCreateTreeResponse.ts ├── IGithubFile.ts ├── IGithubGetBlobResponse.ts ├── IGithubGetTreeResponse.ts ├── IGithubObject.ts ├── IGithubReference.ts ├── IGithubTree.ts ├── IGithubTreeItem.ts ├── github.design.module.ts ├── github.publish.module.ts ├── githubBlobStorage.ts ├── githubChangeCommitter.ts ├── githubClient.ts ├── githubMode.ts ├── githubObjectStorage.ts ├── githubTreeItemType.ts └── index.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pfx binary 2 | *.ico binary 3 | *.svg binary 4 | *.jpg binary 5 | *.png binary 6 | *.exe binary 7 | *.eot binary 8 | *.woff binary 9 | *.woff2 binary 10 | *.ttf binary 11 | *.otf binary 12 | *.cdr binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Paperbits. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paperbits for GitHub 2 | This repository contains implementation, tools and examples for GitHub as a backend. 3 | -------------------------------------------------------------------------------- /adhoc-specs/githubClient.spec.ts: -------------------------------------------------------------------------------- 1 | import * as atob from "atob"; 2 | import * as Utils from "@paperbits/common"; 3 | import { XmlHttpRequestClient } from "@paperbits/common/http/xmlHttpRequestClient"; 4 | import { GithubClient } from "../src/githubClient"; 5 | import { StaticSettingsProvider } from "./staticSettingsProvider"; 6 | import { IGithubTreeItem } from "../src/IGithubTreeItem"; 7 | 8 | 9 | describe("GithubClient", async () => { 10 | it("Can upload binary files.", async () => { 11 | const settingsProvider = new StaticSettingsProvider({ 12 | github: { 13 | authorizationKey: "...", 14 | repositoryName: "...", 15 | repositoryOwner: "..." 16 | } 17 | }); 18 | 19 | global["atob"] = atob; 20 | const httpClient = new XmlHttpRequestClient(); 21 | const githubClient = new GithubClient(settingsProvider, httpClient); 22 | const content = Utils.stringToUnit8Array("Test content"); 23 | const newTree = new Array(); 24 | 25 | for (let i = 0; i < 2; i++) { 26 | const filename = `bulk/file${i + 1}.txt`; 27 | 28 | try { 29 | const response = await githubClient.createBlob(filename, content); 30 | 31 | const newTreeItem: IGithubTreeItem = { 32 | path: filename, 33 | sha: response.sha 34 | }; 35 | 36 | newTree.push(newTreeItem); 37 | } 38 | catch (error) { 39 | console.log(error); 40 | } 41 | } 42 | 43 | await githubClient.push("Test 1"); 44 | }); 45 | }); -------------------------------------------------------------------------------- /adhoc-specs/mockCache.ts: -------------------------------------------------------------------------------- 1 | import { ILocalCache } from "@paperbits/common/caching"; 2 | import { Bag } from "@paperbits/common"; 3 | 4 | export class MockCache implements ILocalCache { 5 | private cache: Bag; 6 | 7 | constructor() { 8 | this.cache = {}; 9 | } 10 | 11 | /** 12 | * Returns keys of all cached items. 13 | */ 14 | public getKeys(): string[] { 15 | return Object.keys(this.cache); 16 | } 17 | 18 | /** 19 | * Creates/updates cached item. 20 | * @param key 21 | * @param value 22 | */ 23 | public setItem(key: string, value: any): void { 24 | this.cache[key] = value; 25 | } 26 | 27 | /** 28 | * Retuns cached item by key. 29 | * @param key 30 | */ 31 | public getItem(key: string): T { 32 | return this.cache[key]; 33 | } 34 | 35 | /** 36 | * Returns space occupied by cache (if supported); 37 | */ 38 | public getOccupiedSpace?(): number { 39 | return Object.keys(this.cache).length; 40 | } 41 | 42 | /** 43 | * Returns remaining space (if supported) 44 | */ 45 | public getRemainingSpace?(): number { 46 | return 99999999; 47 | } 48 | 49 | /** 50 | * Registers a listener for cache changes. 51 | * @param callback 52 | */ 53 | public addChangeListener(callback: () => void): void { 54 | // 55 | } 56 | 57 | /** 58 | * Removes element by key. 59 | * @param key 60 | */ 61 | public removeItem(key: string): void { 62 | delete this.cache[key]; 63 | } 64 | 65 | /** 66 | * Clears cache. 67 | */ 68 | public clear(): void { 69 | this.cache = {}; 70 | } 71 | } -------------------------------------------------------------------------------- /adhoc-specs/staticSettingsProvider.ts: -------------------------------------------------------------------------------- 1 | import { ISettingsProvider } from "@paperbits/common/configuration"; 2 | 3 | export class StaticSettingsProvider implements ISettingsProvider { 4 | constructor(private readonly configuration: Object) { } 5 | 6 | public async getSetting(name: string): Promise { 7 | return this.configuration[name]; 8 | } 9 | 10 | public async setSetting(name: string, value: T): Promise { 11 | this.configuration[name] = value; 12 | } 13 | 14 | public async getSettings(): Promise { 15 | return this.configuration; 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@paperbits/github", 3 | "version": "0.1.646", 4 | "description": "GitHub client library for Paperbits components.", 5 | "license": "MIT", 6 | "author": "Paperbits", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/paperbits/paperbits-github.git" 10 | }, 11 | "keywords": [ 12 | "paperbits", 13 | "github" 14 | ], 15 | "dependencies": { 16 | "@paperbits/common": "0.1.646", 17 | "lodash": "^4.17.15", 18 | "moment": "^2.25.3" 19 | } 20 | } -------------------------------------------------------------------------------- /src/IFileReference.ts: -------------------------------------------------------------------------------- 1 | import { Bag } from "@paperbits/common/bag"; 2 | 3 | export interface IFileReference { 4 | path: string; 5 | metadata: Bag; 6 | } -------------------------------------------------------------------------------- /src/IGithubBlob.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubBlob { 2 | content: string; 3 | encoding?: string; 4 | path?: string; 5 | } -------------------------------------------------------------------------------- /src/IGithubClient.ts: -------------------------------------------------------------------------------- 1 | import { IGithubFile } from "./IGithubFile"; 2 | import { IGithubCommit } from "./IGithubCommit"; 3 | import { IGithubReference } from "./IGithubReference"; 4 | import { IGithubGetTreeResponse } from "./IGithubGetTreeResponse"; 5 | import { IGithubCreateTreeResponse } from "./IGithubCreateTreeResponse"; 6 | import { IGithubTreeItem } from "./IGithubTreeItem"; 7 | import { IGithubCreateBlobReponse } from "./IGithubCreateBlobReponse"; 8 | import { IGithubBlob } from "./IGithubBlob"; 9 | 10 | 11 | export interface IGithubClient { 12 | repositoryName: string; 13 | 14 | getFileContent(path: string): Promise; 15 | 16 | getHeads(): Promise; 17 | 18 | getCommit(commitSha: string): Promise; 19 | 20 | createCommit(parentCommitSha: string, tree: string, message: string): Promise; 21 | 22 | createTree(baseTreeSha: string, treeItems: IGithubTreeItem[]): Promise; 23 | 24 | createReference(branch: string, commitSha: string): Promise; 25 | 26 | deleteReference(branch: string): Promise; 27 | 28 | deleteFile(path: string, blobSha: string, commitMsg: string): Promise; 29 | 30 | updateReference(branch: string, commitSha: string): Promise; 31 | 32 | push(message: string, branch?: string): Promise; 33 | 34 | pushTree(treeItems: IGithubTreeItem[], message?: string, branch?: string): Promise; 35 | 36 | getBlob(blobSha: string): Promise; 37 | 38 | createBlob(path: string, content: Uint8Array): Promise; 39 | 40 | getLatestCommitTree(): Promise; 41 | 42 | getLatestCommit(): Promise; 43 | } -------------------------------------------------------------------------------- /src/IGithubCommit.ts: -------------------------------------------------------------------------------- 1 | import { IGithubTree } from "./IGithubTree"; 2 | 3 | export interface IGithubCommit { 4 | author: any; 5 | committer: any; 6 | sha: string; 7 | tree: IGithubTree; 8 | url: string; 9 | } -------------------------------------------------------------------------------- /src/IGithubConfig.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubConfig { 2 | repository: string; 3 | repositoryOwner: string; 4 | authorizationKey: string; 5 | } -------------------------------------------------------------------------------- /src/IGithubCreateBlobReponse.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubCreateBlobReponse { 2 | sha: string; 3 | } -------------------------------------------------------------------------------- /src/IGithubCreateTreeResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubCreateTreeResponse { 2 | sha: string; 3 | url: string; 4 | truncated: boolean; 5 | } -------------------------------------------------------------------------------- /src/IGithubFile.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubFile { 2 | type: string; 3 | encoding: string; 4 | size: number; 5 | name: string; 6 | path: string; 7 | content: string; 8 | sha: string; 9 | url: string; 10 | git_url: string; 11 | html_url: string; 12 | download_url: string; 13 | } -------------------------------------------------------------------------------- /src/IGithubGetBlobResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubGetBlobResponse { 2 | sha: string; 3 | content: string; 4 | encoding: string; 5 | size: number; 6 | } -------------------------------------------------------------------------------- /src/IGithubGetTreeResponse.ts: -------------------------------------------------------------------------------- 1 | import { IGithubCommit } from "./IGithubCommit"; 2 | import { IGithubTreeItem } from "./IGithubTreeItem"; 3 | 4 | export interface IGithubGetTreeResponse { 5 | sha: string; 6 | tree: IGithubTreeItem[]; 7 | truncated: boolean; 8 | url: string; 9 | lastCommit?: IGithubCommit; 10 | } -------------------------------------------------------------------------------- /src/IGithubObject.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubObject { 2 | sha: string; 3 | type: string; 4 | } -------------------------------------------------------------------------------- /src/IGithubReference.ts: -------------------------------------------------------------------------------- 1 | import { IGithubObject } from "./IGithubObject"; 2 | 3 | export interface IGithubReference { 4 | ref: string; 5 | url: string; 6 | object: IGithubObject; 7 | } -------------------------------------------------------------------------------- /src/IGithubTree.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubTree { 2 | sha: string; 3 | } -------------------------------------------------------------------------------- /src/IGithubTreeItem.ts: -------------------------------------------------------------------------------- 1 | export interface IGithubTreeItem { 2 | sha?: string; 3 | path: string; 4 | size?: number; 5 | type?: string; 6 | mode?: string; 7 | url?: string; 8 | content?: string; 9 | } -------------------------------------------------------------------------------- /src/github.design.module.ts: -------------------------------------------------------------------------------- 1 | import { GithubClient } from "./githubClient"; 2 | import { GithubBlobStorage } from "./githubBlobStorage"; 3 | import { IInjector, IInjectorModule } from "@paperbits/common/injection"; 4 | import { GithubObjectStorage } from "./githubObjectStorage"; 5 | 6 | 7 | export class GithubDesignModule implements IInjectorModule { 8 | public register(injector: IInjector): void { 9 | injector.bindSingleton("githubClient", GithubClient); 10 | injector.bindSingleton("blobStorage", GithubBlobStorage); 11 | injector.bindSingleton("objectStorage", GithubObjectStorage); 12 | } 13 | } -------------------------------------------------------------------------------- /src/github.publish.module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IInjector, IInjectorModule } from "@paperbits/common/injection"; 3 | import { GithubClient } from "./githubClient"; 4 | import { GithubBlobStorage } from "./githubBlobStorage"; 5 | import { GithubChangeCommitter } from "./githubChangeCommitter"; 6 | 7 | 8 | export class GithubPublishModule implements IInjectorModule { 9 | public register(injector: IInjector): void { 10 | injector.bindSingleton("githubClient", GithubClient); 11 | injector.bindSingleton("outputBlobStorage", GithubBlobStorage); 12 | injector.bindSingleton("changeCommitter", GithubChangeCommitter); 13 | } 14 | } -------------------------------------------------------------------------------- /src/githubBlobStorage.ts: -------------------------------------------------------------------------------- 1 | import { IBlobStorage } from "@paperbits/common/persistence/IBlobStorage"; 2 | import { IGithubClient } from "./IGithubClient"; 3 | import { IGithubTreeItem } from "./IGithubTreeItem"; 4 | 5 | export class GithubBlobStorage implements IBlobStorage { 6 | private readonly changes: IGithubTreeItem[]; 7 | 8 | constructor(private readonly githubClient: IGithubClient) { 9 | this.changes = []; 10 | } 11 | 12 | public getChanges(): IGithubTreeItem[] { 13 | return this.changes; 14 | } 15 | 16 | public async uploadBlob(key: string, content: Uint8Array): Promise { 17 | key = key.replaceAll("\\", "/"); 18 | 19 | if (key.startsWith("/")) { 20 | key = key.substr(1); 21 | } 22 | 23 | const response = await this.githubClient.createBlob(key, content); 24 | 25 | const newTreeItem: IGithubTreeItem = { 26 | path: key, 27 | sha: response.sha 28 | }; 29 | 30 | this.changes.push(newTreeItem); 31 | } 32 | 33 | private base64ToUnit8Array(base64: string): Uint8Array { 34 | const rawData = atob(base64); 35 | const rawDataLength = rawData.length; 36 | const byteArray = new Uint8Array(new ArrayBuffer(rawDataLength)); 37 | 38 | for (let i = 0; i < rawDataLength; i++) { 39 | byteArray[i] = rawData.charCodeAt(i); 40 | } 41 | 42 | return byteArray; 43 | } 44 | 45 | public async downloadBlob(path: string): Promise { 46 | const githubFile = await this.githubClient.getFileContent(path); 47 | 48 | return this.base64ToUnit8Array(githubFile.content); 49 | } 50 | 51 | public async getDownloadUrl(permalink: string): Promise { 52 | throw new Error("Not supported"); 53 | } 54 | 55 | public deleteBlob(path: string): Promise { 56 | throw new Error("Not supported"); 57 | } 58 | 59 | public async listBlobs(): Promise { 60 | const latestCommitTree = await this.githubClient.getLatestCommitTree(); 61 | const blobPaths = latestCommitTree.tree.filter(item => item.type === "blob").map(item => item.path); 62 | return blobPaths; 63 | } 64 | } -------------------------------------------------------------------------------- /src/githubChangeCommitter.ts: -------------------------------------------------------------------------------- 1 | import { ChangeCommitter } from "@paperbits/common/persistence"; 2 | import { GithubClient } from "./githubClient"; 3 | 4 | export class GithubChangeCommitter implements ChangeCommitter { 5 | constructor(private readonly githubClient: GithubClient) { } 6 | 7 | public async commit(): Promise { 8 | await this.githubClient.push("Published website."); 9 | } 10 | } -------------------------------------------------------------------------------- /src/githubClient.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment"; 2 | import * as Utils from "@paperbits/common/utils"; 3 | import { ISettingsProvider } from "@paperbits/common/configuration"; 4 | import { HttpHeader, HttpMethod, HttpClient } from "@paperbits/common/http"; 5 | import { HttpHeaders } from "@paperbits/common/http/httpHeaders"; 6 | import { IGithubClient } from "./IGithubClient"; 7 | import { IGithubFile } from "./IGithubFile"; 8 | import { IGithubCommit } from "./IGithubCommit"; 9 | import { IGithubReference } from "./IGithubReference"; 10 | import { IGithubGetTreeResponse } from "./IGithubGetTreeResponse"; 11 | import { IGithubCreateTreeResponse } from "./IGithubCreateTreeResponse"; 12 | import { IGithubTreeItem } from "./IGithubTreeItem"; 13 | import { IGithubCreateBlobReponse } from "./IGithubCreateBlobReponse"; 14 | import { IGithubBlob } from "./IGithubBlob"; 15 | import { IGithubGetBlobResponse } from "./IGithubGetBlobResponse"; 16 | import { IGithubObject } from "./IGithubObject"; 17 | import { GithubMode } from "./githubMode"; 18 | import { GithubTreeItemType } from "./githubTreeItemType"; 19 | 20 | 21 | export class GithubClient implements IGithubClient { 22 | private baseUrl: string; 23 | private baseRepositoriesUrl: string; 24 | private repositoryOwner: string; 25 | private authorizationToken: string; 26 | private mandatoryHttpHeaders: HttpHeader[]; 27 | private changes: IGithubTreeItem[]; 28 | 29 | public repositoryName: string; 30 | 31 | constructor( 32 | private readonly settingsProvider: ISettingsProvider, 33 | private readonly httpClient: HttpClient 34 | ) { 35 | // initialization... 36 | this.settingsProvider = settingsProvider; 37 | this.httpClient = httpClient; 38 | 39 | // rebinding... 40 | this.getHeads = this.getHeads.bind(this); 41 | this.ensureConfig = this.ensureConfig.bind(this); 42 | 43 | this.changes = []; 44 | } 45 | 46 | private applyConfiguration(githubSettings: Object): Promise { 47 | this.authorizationToken = githubSettings["authorizationKey"]; 48 | this.repositoryName = githubSettings["repositoryName"]; 49 | this.repositoryOwner = githubSettings["repositoryOwner"]; 50 | this.baseUrl = `https://api.github.com/repos/${this.repositoryOwner}/${this.repositoryName}`; 51 | this.baseRepositoriesUrl = `${this.baseUrl}/git`; 52 | this.mandatoryHttpHeaders = [{ name: HttpHeaders.Authorization, value: "token " + this.authorizationToken }]; 53 | 54 | return Promise.resolve(); 55 | } 56 | 57 | private async ensureConfig(): Promise { 58 | const settings = await this.settingsProvider.getSetting("github"); 59 | await this.applyConfiguration(settings); 60 | } 61 | 62 | public async getFileContent(path: string): Promise { 63 | await this.ensureConfig(); 64 | 65 | const response = await this.httpClient.send({ 66 | url: `${this.baseUrl}/contents/${path}`, 67 | headers: this.mandatoryHttpHeaders 68 | }); 69 | 70 | return response.toObject(); 71 | } 72 | 73 | /** 74 | * Deletes a file in a single commit. 75 | * Please see https://developer.github.com/v3/repos/contents/ 76 | */ 77 | public async deleteFile(path: string, blobSha: string, commitMsg: string): Promise { 78 | await this.ensureConfig(); 79 | 80 | const requestBody = { 81 | sha: blobSha, 82 | message: commitMsg, 83 | branch: "master" 84 | }; 85 | 86 | await this.httpClient.send({ 87 | url: `${this.baseUrl}/contents/${path}`, 88 | method: HttpMethod.delete, 89 | headers: this.mandatoryHttpHeaders, 90 | body: JSON.stringify(requestBody) 91 | }); 92 | } 93 | 94 | /** 95 | * Please see http://developer.github.com/v3/git/refs/ 96 | */ 97 | public async getHeads(): Promise { 98 | await this.ensureConfig(); 99 | 100 | const response = await this.httpClient.send({ 101 | url: `${this.baseRepositoriesUrl}/refs/heads`, 102 | method: HttpMethod.get, 103 | headers: this.mandatoryHttpHeaders 104 | }); 105 | 106 | return response.toObject(); 107 | } 108 | 109 | /** 110 | * Please see http://developer.github.com/v3/git/commits/ 111 | */ 112 | public async getCommit(commitSha: string): Promise { 113 | await this.ensureConfig(); 114 | 115 | const response = await this.httpClient.send({ 116 | url: `${this.baseRepositoriesUrl}/commits/${commitSha}`, 117 | method: HttpMethod.get, 118 | headers: this.mandatoryHttpHeaders 119 | }); 120 | 121 | return response.toObject(); 122 | } 123 | 124 | /** 125 | * Please see http://developer.github.com/v3/git/commits/ 126 | */ 127 | public async createCommit(parentCommitSha: string, tree: string, message: string): Promise { 128 | await this.ensureConfig(); 129 | 130 | const requestBody = { 131 | message: message, 132 | tree: tree, 133 | parents: parentCommitSha ? [parentCommitSha] : [] 134 | }; 135 | 136 | const response = await this.httpClient.send({ 137 | url: `${this.baseRepositoriesUrl}/commits`, 138 | method: HttpMethod.post, 139 | headers: this.mandatoryHttpHeaders, 140 | body: JSON.stringify(requestBody) 141 | }); 142 | 143 | return response.toObject(); 144 | } 145 | 146 | /** 147 | * Please see http://developer.github.com/v3/git/trees/ 148 | */ 149 | public async getTree(treeSha: string): Promise { 150 | await this.ensureConfig(); 151 | 152 | const response = await this.httpClient.send({ 153 | url: `${this.baseRepositoriesUrl}/trees/${treeSha}?recursive=1`, 154 | method: HttpMethod.get, 155 | headers: this.mandatoryHttpHeaders 156 | }); 157 | 158 | return response.toObject(); 159 | } 160 | 161 | /** 162 | * Please see http://developer.github.com/v3/git/trees/ 163 | */ 164 | public async createTree(baseTreeSha: string, treeItems: IGithubTreeItem[]): Promise { 165 | await this.ensureConfig(); 166 | 167 | const tree = new Array(); 168 | 169 | treeItems.forEach(treeItem => { 170 | if (treeItem.path.startsWith("/")) { 171 | treeItem.path = treeItem.path.substr(1); 172 | } 173 | 174 | tree.push({ 175 | path: treeItem.path, 176 | sha: treeItem.sha, 177 | mode: GithubMode.file, 178 | type: GithubTreeItemType.blob 179 | }); 180 | }); 181 | 182 | const requestBody = { 183 | base_tree: baseTreeSha, 184 | tree: tree 185 | }; 186 | 187 | const response = await this.httpClient.send({ 188 | url: `${this.baseRepositoriesUrl}/trees`, 189 | method: HttpMethod.post, 190 | headers: this.mandatoryHttpHeaders, 191 | body: JSON.stringify(requestBody) 192 | }); 193 | 194 | return response.toObject(); 195 | } 196 | 197 | /** 198 | * Please see http://developer.github.com/v3/git/refs/ 199 | */ 200 | public async createReference(branch: string, commitSha: string): Promise { 201 | await this.ensureConfig(); 202 | 203 | const requestBody = { 204 | ref: `refs/heads/${branch}`, 205 | sha: commitSha 206 | }; 207 | 208 | const response = await this.httpClient.send({ 209 | url: `${this.baseRepositoriesUrl}/refs`, 210 | method: HttpMethod.post, 211 | headers: this.mandatoryHttpHeaders, 212 | body: JSON.stringify(requestBody) 213 | }); 214 | 215 | return response.toObject(); 216 | } 217 | 218 | /** 219 | * Please see http://developer.github.com/v3/git/refs/ 220 | */ 221 | public async deleteReference(branch: string): Promise { 222 | await this.ensureConfig(); 223 | 224 | await this.httpClient.send({ 225 | url: `${this.baseRepositoriesUrl}/refs/heads/${branch}`, 226 | method: HttpMethod.delete, 227 | headers: this.mandatoryHttpHeaders 228 | }); 229 | } 230 | 231 | /** 232 | * Please see http://developer.github.com/v3/git/refs/ 233 | */ 234 | public async updateReference(branch: string, commitSha: string): Promise { 235 | await this.ensureConfig(); 236 | 237 | const requestBody = { 238 | sha: commitSha, 239 | force: true 240 | }; 241 | 242 | const response = await this.httpClient.send({ 243 | url: `${this.baseRepositoriesUrl}/refs/heads/${branch}`, 244 | method: HttpMethod.patch, 245 | headers: this.mandatoryHttpHeaders, 246 | body: JSON.stringify(requestBody) 247 | }); 248 | 249 | return response.toObject(); 250 | } 251 | 252 | public async push(message: string = null, branch: string = "master"): Promise { 253 | await this.pushTree(this.changes, message, branch); 254 | this.changes = []; 255 | } 256 | 257 | public async pushTree(treeItems: IGithubTreeItem[], message: string = null, branch: string = "master"): Promise { 258 | await this.ensureConfig(); 259 | 260 | console.log(`Pushing ${treeItems.length} files to branch ${branch}.`); 261 | 262 | // get the head of the master branch 263 | const heads = await this.getHeads(); 264 | const lastHead = heads.pop(); 265 | 266 | // get the last commit 267 | const lastCommitReference = lastHead.object; 268 | const lastCommit = await this.getCommit(lastCommitReference.sha); 269 | 270 | // create tree object (also implicitly creates a blob based on content) 271 | const createTreeResponse = await this.createTree(lastCommit.tree.sha, treeItems); 272 | 273 | if (!message) { 274 | message = moment().format("MM/DD/YYYY, hh:mm:ss"); 275 | } 276 | 277 | // create new commit 278 | const newCommit = await this.createCommit(lastCommit.sha, createTreeResponse.sha, message); 279 | 280 | // update branch to point to new commit 281 | const head = await this.updateReference(branch, newCommit.sha); 282 | 283 | return head; 284 | } 285 | 286 | public async getBlob(blobSha: string): Promise { 287 | await this.ensureConfig(); 288 | 289 | const response = await this.httpClient.send({ 290 | url: `${this.baseRepositoriesUrl}/blobs/${blobSha}`, 291 | method: HttpMethod.get, 292 | headers: this.mandatoryHttpHeaders 293 | }); 294 | 295 | const getBlobReponse = response.toObject(); 296 | 297 | const blob: IGithubBlob = { 298 | content: atob(getBlobReponse.content), 299 | path: "" 300 | }; 301 | 302 | return blob; 303 | } 304 | 305 | public async createBlob(path: string, content: Uint8Array): Promise { 306 | await this.ensureConfig(); 307 | 308 | const base64 = Utils.arrayBufferToBase64(content); 309 | 310 | const requestBody = { 311 | content: base64, 312 | encoding: "base64" 313 | }; 314 | 315 | const httpResponse = await this.httpClient.send({ 316 | url: `${this.baseRepositoriesUrl}/blobs`, 317 | method: HttpMethod.post, 318 | headers: this.mandatoryHttpHeaders, 319 | body: JSON.stringify(requestBody) 320 | }); 321 | 322 | const response = httpResponse.toObject(); 323 | 324 | const treeItem: IGithubTreeItem = { 325 | path: path, 326 | sha: response.sha 327 | }; 328 | 329 | this.changes.push(treeItem); 330 | 331 | return response; 332 | } 333 | 334 | public async getLatestCommitTree(): Promise { 335 | await this.ensureConfig(); 336 | 337 | // get the head of the master branch 338 | const heads = await this.getHeads(); 339 | const lastHead = heads.pop(); 340 | 341 | // get the last commit 342 | const lastCommitReference: IGithubObject = lastHead.object; 343 | const lastCommit = await this.getCommit(lastCommitReference.sha); 344 | 345 | // get the last commit tree 346 | const getTreeResponse = await this.getTree(lastCommit.tree.sha); 347 | getTreeResponse.lastCommit = lastCommit; 348 | 349 | return getTreeResponse; 350 | } 351 | 352 | public async getLatestCommit(): Promise { 353 | await this.ensureConfig(); 354 | 355 | // get the head of the master branch 356 | const heads = await this.getHeads(); 357 | const lastHead = heads.pop(); 358 | 359 | const lastCommitReference: IGithubObject = lastHead.object; 360 | 361 | // get the last commit 362 | const commit = await this.getCommit(lastCommitReference.sha); 363 | 364 | return commit; 365 | } 366 | } -------------------------------------------------------------------------------- /src/githubMode.ts: -------------------------------------------------------------------------------- 1 | export enum GithubMode { 2 | file = "100644", // blob 3 | executable = "100755", // blob 4 | subdirectory = "040000", // tree 5 | submodule = "160000", // commit 6 | pathOfSymlink = "120000" // blob 7 | } -------------------------------------------------------------------------------- /src/githubObjectStorage.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from "@paperbits/common"; 2 | import * as Objects from "@paperbits/common/objects"; 3 | import { Bag } from "@paperbits/common/bag"; 4 | import { IObjectStorage, Operator, OrderDirection, Query } from "@paperbits/common/persistence"; 5 | import { IGithubClient } from "./IGithubClient"; 6 | import { IGithubTreeItem } from "./IGithubTreeItem"; 7 | import { ISettingsProvider } from "@paperbits/common/configuration"; 8 | 9 | 10 | export class GithubObjectStorage implements IObjectStorage { 11 | private loadDataPromise: Promise; 12 | protected storageDataObject: Object; 13 | private splitter: string = "/"; 14 | private pathToData: string; 15 | 16 | constructor( 17 | private readonly settingsProvider: ISettingsProvider, 18 | private readonly githubClient: IGithubClient 19 | ) { } 20 | 21 | protected async getData(): Promise { 22 | if (this.loadDataPromise) { 23 | return this.loadDataPromise; 24 | } 25 | 26 | this.loadDataPromise = new Promise(async (resolve) => { 27 | const githubSettings = await this.settingsProvider.getSetting("github"); 28 | this.pathToData = githubSettings["pathToData"]; 29 | 30 | const response = await this.githubClient.getFileContent(this.pathToData); 31 | this.storageDataObject = JSON.parse(atob(response.content)); 32 | 33 | resolve(this.storageDataObject); 34 | }); 35 | 36 | return this.loadDataPromise; 37 | } 38 | 39 | public async addObject(path: string, dataObject: Object): Promise { 40 | if (path) { 41 | const pathParts = path.split(this.splitter); 42 | const mainNode = pathParts[0]; 43 | 44 | if (pathParts.length === 1 || (pathParts.length === 2 && !pathParts[1])) { 45 | this.storageDataObject[mainNode] = dataObject; 46 | } 47 | else { 48 | if (!this.storageDataObject.hasOwnProperty(mainNode)) { 49 | this.storageDataObject[mainNode] = {}; 50 | } 51 | this.storageDataObject[mainNode][pathParts[1]] = dataObject; 52 | } 53 | } 54 | else { 55 | Object.keys(dataObject).forEach(prop => { 56 | const obj = dataObject[prop]; 57 | const pathParts = prop.split(this.splitter); 58 | const mainNode = pathParts[0]; 59 | 60 | if (pathParts.length === 1 || (pathParts.length === 2 && !pathParts[1])) { 61 | this.storageDataObject[mainNode] = obj; 62 | } 63 | else { 64 | if (!this.storageDataObject.hasOwnProperty(mainNode)) { 65 | this.storageDataObject[mainNode] = {}; 66 | } 67 | this.storageDataObject[mainNode][pathParts[1]] = obj; 68 | } 69 | }); 70 | } 71 | } 72 | 73 | public async getObject(path: string): Promise { 74 | const data = await this.getData(); 75 | 76 | return Objects.getObjectAt(path, Objects.clone(data)); 77 | } 78 | 79 | public async deleteObject(path: string): Promise { 80 | if (!path) { 81 | return; 82 | } 83 | 84 | Objects.deleteNodeAt(path, this.storageDataObject); 85 | } 86 | 87 | public async updateObject(path: string, dataObject: T): Promise { 88 | if (!path) { 89 | return; 90 | } 91 | 92 | const clone: any = Objects.clone(dataObject); 93 | Objects.setValue(path, this.storageDataObject, clone); 94 | Objects.cleanupObject(clone); // Ensure all "undefined" are cleaned up 95 | } 96 | 97 | public async searchObjects(path: string, query: Query): Promise> { 98 | const searchResultObject: Bag = {}; 99 | const data = await this.getData(); 100 | 101 | if (!data) { 102 | return searchResultObject; 103 | } 104 | 105 | const searchObj = Objects.getObjectAt(path, data); 106 | 107 | if (!searchObj) { 108 | return {}; 109 | } 110 | 111 | let collection = Object.values(searchObj); 112 | 113 | if (query) { 114 | if (query.filters.length > 0) { 115 | collection = collection.filter(x => { 116 | let meetsCriteria = true; 117 | 118 | for (const filter of query.filters) { 119 | let left = Objects.getObjectAt(filter.left, x); 120 | let right = filter.right; 121 | 122 | if (left === undefined) { 123 | meetsCriteria = false; 124 | continue; 125 | } 126 | 127 | if (typeof left === "string") { 128 | left = left.toUpperCase(); 129 | } 130 | 131 | if (typeof right === "string") { 132 | right = right.toUpperCase(); 133 | } 134 | 135 | const operator = filter.operator; 136 | 137 | switch (operator) { 138 | case Operator.contains: 139 | if (left && !left.includes(right)) { 140 | meetsCriteria = false; 141 | } 142 | break; 143 | 144 | case Operator.equals: 145 | if (left !== right) { 146 | meetsCriteria = false; 147 | } 148 | break; 149 | 150 | default: 151 | throw new Error("Cannot translate operator into Firebase Realtime Database query."); 152 | } 153 | } 154 | 155 | return meetsCriteria; 156 | }); 157 | } 158 | 159 | if (query.orderingBy) { 160 | const property = query.orderingBy; 161 | 162 | collection = collection.sort((x, y) => { 163 | const a = Objects.getObjectAt(property, x); 164 | const b = Objects.getObjectAt(property, y); 165 | const modifier = query.orderDirection === OrderDirection.accending ? 1 : -1; 166 | 167 | if (a > b) { 168 | return modifier; 169 | } 170 | 171 | if (a < b) { 172 | return -modifier; 173 | } 174 | 175 | return 0; 176 | }); 177 | } 178 | } 179 | 180 | collection.forEach(item => { 181 | const segments = item.key.split("/"); 182 | const key = segments[1]; 183 | 184 | Objects.setValue(key, searchResultObject, item); 185 | Objects.cleanupObject(item); // Ensure all "undefined" are cleaned up 186 | }); 187 | 188 | return searchResultObject; 189 | } 190 | 191 | private async createChangesTree(): Promise { 192 | const newTree = new Array(); 193 | const content = Utils.stringToUnit8Array(JSON.stringify(this.storageDataObject)); 194 | const response = await this.githubClient.createBlob(this.pathToData, content); 195 | 196 | const newTreeItem: IGithubTreeItem = { 197 | path: this.pathToData, 198 | sha: response.sha 199 | }; 200 | 201 | newTree.push(newTreeItem); 202 | 203 | return newTree; 204 | } 205 | 206 | public async saveChanges(delta: Object): Promise { 207 | const saveTasks = []; 208 | const keys = []; 209 | 210 | Object.keys(delta).map(key => { 211 | const firstLevelObject = delta[key]; 212 | 213 | Object.keys(firstLevelObject).forEach(subkey => { 214 | keys.push(`${key}/${subkey}`); 215 | }); 216 | }); 217 | 218 | keys.forEach(key => { 219 | const changeObject = Objects.getObjectAt(key, delta); 220 | 221 | if (changeObject) { 222 | saveTasks.push(this.updateObject(key, changeObject)); 223 | } 224 | else { 225 | saveTasks.push(this.deleteObject(key)); 226 | } 227 | }); 228 | 229 | await Promise.all(saveTasks); 230 | 231 | const newTree = await this.createChangesTree(); 232 | const lastCommit = await this.githubClient.getLatestCommit(); 233 | const tree = await this.githubClient.createTree(lastCommit.tree.sha, newTree); 234 | 235 | const message = `Updating website content.`; 236 | const commit = await this.githubClient.createCommit(lastCommit.sha, tree.sha, message); 237 | await this.githubClient.updateReference("master", commit.sha); 238 | } 239 | } -------------------------------------------------------------------------------- /src/githubTreeItemType.ts: -------------------------------------------------------------------------------- 1 | export enum GithubTreeItemType { 2 | blob = "blob", 3 | tree = "tree", 4 | commit = "commit" 5 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./github.design.module"; 2 | export * from "./github.publish.module"; 3 | export * from "./githubBlobStorage"; 4 | export * from "./githubChangeCommitter"; 5 | export * from "./githubClient"; 6 | export * from "./githubMode"; 7 | export * from "./githubObjectStorage"; 8 | export * from "./githubTreeItemType"; 9 | export * from "./IFileReference"; 10 | export * from "./IGithubBlob"; 11 | export * from "./IGithubClient"; 12 | export * from "./IGithubCommit"; 13 | export * from "./IGithubConfig"; 14 | export * from "./IGithubCreateBlobReponse"; 15 | export * from "./IGithubCreateTreeResponse"; 16 | export * from "./IGithubFile"; 17 | export * from "./IGithubGetBlobResponse"; 18 | export * from "./IGithubGetTreeResponse"; 19 | export * from "./IGithubObject"; 20 | export * from "./IGithubReference"; 21 | export * from "./IGithubTree"; 22 | export * from "./IGithubTreeItem"; 23 | --------------------------------------------------------------------------------