/**
 * A service to access the github api v3.
 * It can read and write repositories and directories and files in the repositories.
 * Access is authenticated via an OAuth access token.
 */ import { Inject , Injectable } from '@angular/core' ; import { APP_CONFIG , AppConfig } from '../../app.config' ; import { HttpClient , HttpHeaders , HttpParams } from '@angular/common/http' ; import { isArray , isNullOrUndefined } from '../../common/util' ; import { Observable } from 'rxjs' ; import { map } from 'rxjs/operators' ; import { Base64 } from 'js-base64' ; import toBase64 = Base64 . toBase64 ; import fromBase64 = Base64 . fromBase64 ; /**
 * Representation of a repository.
 */ export interface GithubRepo { owner : string ; name : string ; } /**
 * Representation of a branch in a repository.
 */ export interface GithubBranch { repo : GithubRepo ; name : string ; } /**
 * A file.
 */ export interface GithubFileContents { type : 'file' ; branch : GithubBranch ; path : string ; name : string ; sha : string ; size : number ; content ? : string ; } export interface GithubDirectory { type : 'dir' ; branch : GithubBranch ; path : string ; name : string ; sha : string ; entries ? : ( GithubFileContents | GithubDirectory ) [ ] ; } interface RepoFromAPI { name : string ; owner : { login : string ; } ; } interface BranchFromAPI { name : string ; } interface FileContentsFromAPI { type : 'file' ; name : string ; path : string ; size : number ; encoding ? : 'base64' ; content ? : string ; sha : string ; } interface DirectoryEntryContentsFromAPI { type : 'dir' ; name : string ; path : string ; size : number ; sha : string ; } interface SymlinkContentsFromAPI { type : 'symlink' ; name : string ; path : string ; size : number ; target : string ; sha : string ; } interface SubmoduleContentsFromAPI { type : 'submodule' ; name : string ; path : string ; size : number ; sha : string ; } type ContentsFromAPI = FileContentsFromAPI | DirectoryEntryContentsFromAPI | SymlinkContentsFromAPI | SubmoduleContentsFromAPI ; /**
 * input type of content update call.
 */ interface ContentsUpdateInputAPI { message : string ; content : string ; sha : string ; branch ? : string ; } /**
 * return type of content update call.
 */ interface ContentsUpdateFromAPI { content : { name : string ; path : string ; sha : string ; size : number ; } ; } type FileOrDirectoryContentsFromAPI = ContentsFromAPI | [ ContentsFromAPI ] ; @ Injectable ( { providedIn : 'root' } ) export class GithubApiService { private readonly _rootUrl : string ; private _apiKey : string ; private failByDesign : boolean ; constructor ( @ Inject ( APP_CONFIG ) app_config , private httpClient ) { this . _rootUrl = app_config . GITHUB_API_ROOT_URL ; this . setApiKey ( app_config . GITHUB_API_KEY ) ; this . failByDesign = app_config . GITHUB_PROVOKE_FAILURES ; } /**
     * Headers used for every request.
     */ private headers ( apiKey ? ) { const key = ( apiKey ) ? apiKey : this . apiKey ( ) ; return new HttpHeaders ( ) . append ( 'Accept' , 'application/vnd.github.v3+json' ) . append ( 'Authorization' , 'token ' + key ) ; } param relativeUrl param apiKey param parameters return private get < T > ( relativeUrl , apiKey , parameters ? ) { return this . httpClient . get ( this . fullUrl ( relativeUrl ) , { headers : this . headers ( apiKey ) , params : parameters } ) . pipe ( map ( response => response as T ) ) ; } param relativeUrl param apiKey param body param parameters return private put < T , U > ( relativeUrl , apiKey , body , parameters ? ) { return this . httpClient . put ( this . fullUrl ( relativeUrl ) , body , { headers : this . headers ( apiKey ) , params : parameters } ) . pipe ( map ( response => response as U ) ) ; } private fullUrl ( relativeUrl ) { return this . _rootUrl + relativeUrl ; } public apiKey ( ) { return this . _apiKey ; } public setApiKey ( key ) { this . _apiKey = key ; } param apiKey public repos ( apiKey ? ) { return this . get < RepoFromAPI [ ] > ( 'user/repos' , apiKey ) . pipe ( map ( response => response . map ( ( repo ) => { return { name : repo . name , owner : repo . owner . login } ; } ) ) ) ; } param repo param apiKey public branches ( repo , apiKey ? ) { return this . get < BranchFromAPI [ ] > ( `repos/${ repo . owner }/${ repo . name }/branches` , apiKey ) . pipe ( map ( response => response . map ( ( branch ) => { return { name : branch . name , repo : repo } ; } ) ) ) ; } param branch param path param apiKey public content ( branch , path , apiKey ? ) { const repo = branch . repo ; const url = `repos/${ repo . owner }/${ repo . name }/contents/${ path }` ; return this . get < FileOrDirectoryContentsFromAPI > ( url , apiKey , new HttpParams ( ) . append ( 'ref' , branch . name ) ) . pipe ( map ( ( response ) => { if ( isArray ( response ) ) { return this . toDirectory ( branch , path , response as ContentsFromAPI [ ] ) ; } else { const singleResponse = response as ContentsFromAPI ; switch ( singleResponse . type ) { case 'file' : return this . toFileContents ( branch , response as FileContentsFromAPI ) ; case 'symlink' : default : } } } ) ) ; } param branch param newContents param message param apiKey public updateContent ( branch , newContents , message , apiKey ? ) { const repo = branch . repo ; const url = `repos/${ repo . owner }/${ repo . name }/contents/${ newContents . path }` ; return this . put < ContentsUpdateInputAPI , ContentsUpdateFromAPI > ( url , apiKey , { message : message , content : this . toBase64 ( newContents . content ) , sha : newContents . sha , branch : branch . name } as ContentsUpdateInputAPI ) . pipe ( map ( ( response ) => { return { type : 'file' , branch : branch , path : response . content . path , name : response . content . name , size : response . content . size , content : newContents . content , sha : response . content . sha } as GithubFileContents ; } ) ) ; } private toFileContents ( branch , contentApiResponse ) { const content = contentApiResponse . content ; let decodedContent ; if ( ! isNullOrUndefined ( content ) && ! isNullOrUndefined ( contentApiResponse . encoding ) ) { if ( contentApiResponse . encoding === 'base64' ) { decodedContent = this . fromBase64 ( content ) ; } else { decodedContent = '??? unknown encoding' + contentApiResponse . encoding ; } } else { decodedContent = undefined ; } return { type : 'file' , branch : branch , path : contentApiResponse . path , name : contentApiResponse . name , size : contentApiResponse . size , content : decodedContent , sha : contentApiResponse . sha } ; } private toDirectory ( branch , path , contentApiResponse ) { const entries = contentApiResponse . map ( ( entry ) => { switch ( entry . type ) { case 'file' : return this . toFileContents ( branch , entry ) ; case 'symlink' : return null ; case 'submodule' : return null ; case 'dir' : return { type : 'dir' , branch : branch , path : path , name : entry . name , sha : entry . sha } as GithubDirectory ; default : return null ; } } ) . filter ( entry => entry !== null ) ; return { type : 'dir' , branch : branch , path : path , name : this . basename ( path ) , sha : '' , entries : entries } ; } basename ( path ) { const index = path . lastIndexOf ( '/' ) ; if ( index >= 0 ) { return path . substr ( index + 1 ) ; } else { return path ; } } private toBase64 ( str ) { return toBase64 ( str ) ; } private fromBase64 ( b64str ) { return fromBase64 ( b64str ) ; } }