@xrengine/server-core
Version:
Shared components for XREngine server
462 lines (435 loc) • 14.7 kB
text/typescript
import { BadRequest, Forbidden } from '@feathersjs/errors'
import { Octokit } from '@octokit/rest'
import appRootPath from 'app-root-path'
import fs from 'fs'
import path from 'path'
import { GITHUB_PER_PAGE, GITHUB_URL_REGEX } from '@xrengine/common/src/constants/GitHubConstants'
import { ProjectInterface } from '@xrengine/common/src/interfaces/ProjectInterface'
import { UserInterface } from '@xrengine/common/src/interfaces/User'
import {
AudioFileTypes,
ImageFileTypes,
VideoFileTypes,
VolumetricFileTypes
} from '@xrengine/engine/src/assets/constants/fileTypes'
import { Application } from '../../../declarations'
import config from '../../appconfig'
import { getStorageProvider } from '../../media/storageprovider/storageprovider'
import { getFileKeysRecursive } from '../../media/storageprovider/storageProviderUtils'
import logger from '../../ServerLogger'
import { deleteFolderRecursive, writeFileSyncRecursive } from '../../util/fsHelperFunctions'
import { useGit } from '../../util/gitHelperFunctions'
import { ProjectParams } from './project.class'
let app
export const getAuthenticatedRepo = async (token: string, repositoryPath: string) => {
try {
if (!/.git$/.test(repositoryPath)) repositoryPath = repositoryPath + '.git'
repositoryPath = repositoryPath.toLowerCase()
const user = await getUser(token)
return repositoryPath.replace('https://', `https://${user.data.login}:${token}@`)
} catch (error) {
logger.error(error)
return null
}
}
export const getUser = async (token: string) => {
const octoKit = new Octokit({ auth: token })
return octoKit.rest.users.getAuthenticated()
}
export const getInstallationOctokit = async (repo) => {
if (!repo) return null
let installationId
await app.eachInstallation(({ installation }) => {
if (repo.user == installation.account?.login) installationId = installation.id
})
const installationAuth = await app.octokit.auth({
type: 'installation',
installationId: installationId
})
return new Octokit({
auth: installationAuth.token // directly pass the token
})
}
export const checkUserRepoWriteStatus = async (owner, repo, token): Promise<number> => {
const userApp = new Octokit({ auth: token })
try {
const { data } = await userApp.rest.repos.get({
owner,
repo
})
if (!data.permissions) return 403
return data.permissions.push || data.permissions.admin ? 200 : 403
} catch (err) {
logger.error(err, 'Error getting repo')
return err.status
}
}
export const checkUserOrgWriteStatus = async (org, token) => {
const octo = new Octokit({ auth: token })
try {
const authUser = await octo.rest.users.getAuthenticated()
if (org === authUser.data.login) return 200
const { data } = await octo.rest.orgs.getMembershipForAuthenticatedUser({
org
})
return data.role === 'admin' || data.role === 'member' ? 200 : 403
} catch (err) {
logger.error(err, 'Org does not exist')
return err.status
}
}
export const checkAppOrgStatus = async (organization, token) => {
const octo = new Octokit({ auth: token })
const authUser = await octo.rest.users.getAuthenticated()
if (organization === authUser.data.login) return 200
const orgs = await getUserOrgs(token)
return orgs.find((org) => org.login.toLowerCase() === organization.toLowerCase())
}
export const getUserRepos = async (token: string): Promise<any[]> => {
let page = 1
let end = false
let repos = []
const octoKit = new Octokit({ auth: token })
while (!end) {
const repoResponse = (await octoKit.rest.repos.listForAuthenticatedUser({
per_page: GITHUB_PER_PAGE,
page
})) as any
repos = repos.concat(repoResponse.data)
page++
if (repoResponse.data.length < GITHUB_PER_PAGE) end = true
}
return repos
}
export const getUserOrgs = async (token: string): Promise<any[]> => {
let page = 1
let end = false
let orgs = []
const octoKit = new Octokit({ auth: token })
while (!end) {
const repoResponse = (await octoKit.rest.orgs.listForAuthenticatedUser({
per_page: GITHUB_PER_PAGE,
page
})) as any
orgs = orgs.concat(repoResponse.data)
page++
if (repoResponse.data.length < GITHUB_PER_PAGE) end = true
}
return orgs
}
export const getRepo = async (owner: string, repo: string, token: string): Promise<any> => {
const octoKit = new Octokit({ auth: token })
const repoResponse = await octoKit.rest.repos.get({ owner, repo })
return repoResponse.data.html_url
}
export const pushProjectToGithub = async (
app: Application,
project: ProjectInterface,
user: UserInterface,
reset = false,
commitSHA?: string,
storageProviderName?: string
) => {
const storageProvider = getStorageProvider(storageProviderName)
try {
logger.info(`[ProjectPush]: Getting files for project "${project.name}"...`)
let files = await getFileKeysRecursive(`projects/${project.name}/`)
files = files.filter((file) => /\.\w+$/.test(file))
logger.info('[ProjectPush]: Found files:' + files)
const localProjectDirectory = path.join(appRootPath.path, 'packages/projects/projects', project.name)
if (fs.existsSync(localProjectDirectory)) {
logger.info('[Project temp debug]: fs exists, deleting')
deleteFolderRecursive(localProjectDirectory)
}
await Promise.all(
files.map(async (filePath) => {
logger.info(`[ProjectLoader]: - downloading "${filePath}"`)
const fileResult = await storageProvider.getObject(filePath)
if (fileResult.Body.length === 0) logger.info(`[ProjectLoader]: WARNING file "${filePath}" is empty`)
writeFileSyncRecursive(path.join(appRootPath.path, 'packages/projects', filePath), fileResult.Body)
})
)
const repoPath = project.repositoryPath.toLowerCase()
const githubIdentityProvider = await app.service('identity-provider').Model.findOne({
where: {
userId: user.id,
type: 'github'
}
})
const githubPathRegexExec = GITHUB_URL_REGEX.exec(repoPath)
if (!githubPathRegexExec) throw new BadRequest('Invalid Github URL')
const split = githubPathRegexExec[2].split('/')
const owner = split[0]
const repo = split[1].replace('.git', '')
const repos = await getUserRepos(githubIdentityProvider.oauthToken)
const octoKit = githubIdentityProvider
? new Octokit({ auth: githubIdentityProvider.oauthToken })
: await (async () => {
return getInstallationOctokit(
repos.find((repo) => {
repo.repositoryPath = repo.repositoryPath.toLowerCase()
return repo.repositoryPath === repoPath || repo.repositoryPath === repoPath + '.git'
})
)
})()
if (!octoKit) return
try {
await octoKit.rest.repos.get({
owner,
repo
})
} catch (err) {
if (err.status === 404) {
const authUser = await octoKit.rest.users.getAuthenticated()
if (authUser.data.login === owner)
await octoKit.repos.createForAuthenticatedUser({
name: repo,
auto_init: true
})
else await octoKit.repos.createInOrg({ org: owner, name: repo, auto_init: true })
} else throw err
}
const deploymentBranch = `${config.server.releaseName}-deployment`
if (reset) {
const projectDirectory = path.resolve(appRootPath.path, `packages/projects/projects/${project.name}/`)
const git = useGit(projectDirectory)
if (commitSHA) git.checkout(commitSHA)
await git.checkoutLocalBranch(deploymentBranch)
await git.push('origin', deploymentBranch, ['-f'])
} else await uploadToRepo(octoKit, files, owner, repo, deploymentBranch, project, app)
} catch (err) {
logger.error(err)
throw err
}
}
// Credit to https://dev.to/lucis/how-to-push-files-programatically-to-a-repository-using-octokit-with-typescript-1nj0
// for much of the following code.
const uploadToRepo = async (
octo: Octokit,
filePaths: string[],
org: string,
repo: string,
branch: string = `master`,
project: ProjectInterface,
app: Application
) => {
let currentCommit
try {
currentCommit = await getCurrentCommit(octo, org, repo, branch)
} catch (err) {
if (err.status === 409 && err.message === 'Git Repository is empty.') {
await octo.repos.delete({ owner: org, repo })
await octo.repos.createInOrg({ org, name: repo, auto_init: true })
currentCommit = await getCurrentCommit(octo, org, repo, branch)
} else throw err
}
//Get the GH user for use in commit message
const user = (await octo.users.getAuthenticated()).data
//Create blobs from all the files
const fileBlobs = await Promise.all(filePaths.map(createBlobForFile(octo, org, repo)))
// Create a new tree from all of the files, so that a new commit can be made from it
const newTree = await createNewTree(
octo,
org,
repo,
fileBlobs,
filePaths.map((path) => path.replace(`projects/${project.name}/`, '')),
currentCommit.treeSha
)
const date = Date.now()
const commitMessage = `Update by ${user.login} at ${new Date(date).toJSON()}`
//Create the new commit with all of the file changes
const newCommit = await createNewCommit(octo, org, repo, commitMessage, newTree.sha, currentCommit.commitSha)
await app.service('project').Model.update(
{
commitSHA: newCommit.sha,
commitDate: new Date()
},
{
where: {
id: project.id
}
}
)
try {
//This pushes the commit to the main branch in GitHub
await setBranchToCommit(octo, org, repo, branch, newCommit.sha)
} catch (err) {
// Couldn't push directly to branch for some reason, so making a new branch and opening a PR instead
await octo.git.createRef({
owner: org,
repo,
ref: `refs/heads/${user.login}-${date}`,
sha: newCommit.sha
})
await octo.pulls.create({
owner: org,
repo,
head: `refs/heads/${user.login}-${date}`,
base: `refs/heads/${branch}`,
title: commitMessage
})
}
}
export const getCurrentCommit = async (octo: Octokit, org: string, repo: string, branch: string = 'master') => {
try {
await octo.repos.getBranch({ owner: org, repo, branch })
} catch (err) {
// If the branch for this deployment somehow doesn't exist, push the default branch to it so it exists
if (err.status === 404 && err.message === 'Branch not found') {
const repoResult = await octo.repos.get({ owner: org, repo })
const baseBranchRef = await octo.git.getRef({
owner: org,
repo,
ref: `heads/${repoResult.data.default_branch}`
})
await octo.git.createRef({
owner: org,
repo,
ref: `refs/heads/${branch}`,
sha: baseBranchRef.data.object.sha
})
} else throw err
}
const { data: refData } = await octo.git.getRef({
owner: org,
repo,
ref: `heads/${branch}`
})
const commitSha = refData.object.sha
const { data: commitData } = await octo.git.getCommit({
owner: org,
repo,
commit_sha: commitSha
})
return {
commitSha,
treeSha: commitData.tree.sha
}
}
export const getGithubOwnerRepo = (url: string) => {
url = url.toLowerCase()
const githubPathRegexExec = GITHUB_URL_REGEX.exec(url)
if (!githubPathRegexExec)
return {
error: 'invalidUrl',
text: 'Project URL is not a valid GitHub URL, or the GitHub repo is private'
}
const split = githubPathRegexExec[2].split('/')
if (!split[0] || !split[1])
return {
error: 'invalidUrl',
text: 'Project URL is not a valid GitHub URL, or the GitHub repo is private'
}
const owner = split[0]
const repo = split[1].replace('.git', '')
return {
owner,
repo
}
}
export const getOctokitForChecking = async (app: Application, url: string, params: ProjectParams) => {
url = url.toLowerCase()
const githubIdentityProvider = await app.service('identity-provider').Model.findOne({
where: {
userId: params!.user.id,
type: 'github'
}
})
if (!githubIdentityProvider) throw new Forbidden('You must have a connected GitHub account to access public repos')
const { owner, repo } = getGithubOwnerRepo(url)
const octoKit = new Octokit({ auth: githubIdentityProvider.oauthToken })
return {
owner,
repo,
octoKit,
token: githubIdentityProvider.oauthToken
}
}
const createBlobForFile = (octo: Octokit, org: string, repo: string) => async (filePath: string) => {
const encoding = isBase64Encoded(filePath) ? 'base64' : 'utf-8'
const bytes = await fs.readFileSync(path.join(appRootPath.path, 'packages/projects', filePath), 'binary')
const buffer = Buffer.from(bytes, 'binary')
const content = buffer.toString(encoding)
const blobData = await octo.git.createBlob({
owner: org,
repo,
content,
encoding
})
return blobData.data
}
const createNewTree = async (
octo: Octokit,
owner: string,
repo: string,
blobs: any[],
paths: string[],
parentTreeSha: string
) => {
const oldTree = await octo.git.getTree({
owner,
repo,
tree_sha: parentTreeSha,
recursive: 'true'
})
const committableFiles = oldTree.data.tree.filter((file) => file.type === 'blob')
const committableFilesMap = committableFiles.map((file) => file.path)
// My custom config. Could be taken as parameters
const tree = blobs.map(({ sha }, index) => ({
path: paths[index],
mode: `100644`,
type: `blob`,
sha
})) as any[]
committableFilesMap.forEach((fileName) => {
if (fileName && paths.indexOf(fileName) < 0) {
tree.push({
path: fileName,
mode: `100644`,
type: 'blob',
sha: null
})
}
})
const { data } = await octo.git.createTree({
owner,
repo,
tree,
base_tree: parentTreeSha
})
return data
}
const createNewCommit = async (
octo: Octokit,
org: string,
repo: string,
message: string,
currentTreeSha: string,
currentCommitSha: string
) =>
(
await octo.git.createCommit({
owner: org,
repo,
message,
tree: currentTreeSha,
parents: [currentCommitSha]
})
).data
const setBranchToCommit = (octo: Octokit, org: string, repo: string, branch: string = `master`, commitSha: string) =>
octo.git.updateRef({
owner: org,
repo,
ref: `heads/${branch}`,
sha: commitSha
})
const isBase64Encoded = (filePath: string) => {
const extension = `.${filePath.split('.').pop()!}`
return (
ImageFileTypes.indexOf(extension) > -1 ||
AudioFileTypes.indexOf(extension) > -1 ||
VolumetricFileTypes.indexOf(extension) > -1 ||
VideoFileTypes.indexOf(extension) > -1
)
}