Files
PicList/src/main/manage/apis/github.ts

502 lines
16 KiB
TypeScript

import got from 'got'
import { ManageLogger } from '../utils/logger'
import { formatHttpProxy, isImage } from '~/renderer/manage/utils/common'
import windowManager from 'apis/app/window/windowManager'
import { IWindowList } from '#/types/enum'
import { ipcMain, IpcMainEvent } from 'electron'
import { gotUpload, trimPath, NewDownloader, getAgent, getOptions, ConcurrencyPromisePool, formatError } from '../utils/common'
import UpDownTaskQueue,
{
commonTaskStatus
} from '../datastore/upDownTaskQueue'
import fs from 'fs-extra'
import path from 'path'
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '@/manage/utils/static'
class GithubApi {
token: string
username: string
logger: ManageLogger
proxy: any
proxyStr: string | undefined
baseUrl = 'https://api.github.com'
commonHeaders : IStringKeyMap
constructor (token: string, username: string, proxy: string | undefined, logger: ManageLogger) {
this.logger = logger
this.token = token.startsWith('Bearer ') ? token : `Bearer ${token}`.trim()
this.username = username
this.proxy = proxy
this.proxyStr = formatHttpProxy(proxy, 'string') as string | undefined
this.commonHeaders = {
Authorization: this.token,
Accept: 'application/vnd.github+json'
}
}
formatFolder (item: any, slicedPrefix: string) {
let key = ''
if (slicedPrefix === '') {
key = `${item.path}/`
} else {
key = `${slicedPrefix}/${item.path}/`
}
return {
...item,
Key: key,
key,
fileSize: 0,
formatedTime: '',
fileName: item.path,
isDir: true,
checked: false,
isImage: false,
match: false
}
}
formatFile (item: any, slicedPrefix: string, branch: string, repo: string, cdnUrl: string | undefined) {
let rawUrl = ''
if (cdnUrl) {
const placeholder = ['{username}', '{repo}', '{branch}', '{path}']
if (placeholder.some(item => cdnUrl.includes(item))) {
rawUrl = cdnUrl.replace('{username}', this.username)
.replace('{repo}', repo)
.replace('{branch}', branch)
.replace('{path}', `${slicedPrefix}/${item.path}`)
} else {
rawUrl = `${cdnUrl}/${slicedPrefix}/${item.path}`
}
} else {
rawUrl = `https://raw.githubusercontent.com/${this.username}/${repo}/${branch}/${slicedPrefix}/${item.path}`
}
rawUrl = rawUrl.replace(/(?<!https?:)\/{2,}/g, '/')
let key = ''
if (slicedPrefix === '') {
key = item.path
} else {
key = `${slicedPrefix}/${item.path}`
}
const result = {
...item,
Key: key,
key,
fileSize: item.size,
formatedTime: '',
fileName: item.path,
isDir: false,
checked: false,
match: false,
isImage: isImage(item.path),
rawUrl
}
const temp = result.rawUrl
result.rawUrl = result.url
result.url = temp
return result
}
/**
* get repo list
*/
async getBucketList (): Promise<any> {
let initPage = 1
let res
const result = [] as any[]
do {
res = await got(
`${this.baseUrl}/user/repos`,
getOptions('GET', this.commonHeaders, { page: initPage, per_page: 100 }, 'json', undefined, undefined, this.proxy)
) as any
if (res.statusCode === 200) {
res.body.forEach((item: any) => {
result.push({
...item,
Name: item.name,
Location: item.id,
CreationDate: item.created_at
})
})
} else {
return []
}
initPage++
} while (res.body.length > 0)
return result
}
/**
* 获取branch列表
*/
async getBucketDomain (param: IStringKeyMap): Promise<any> {
const { bucketName: repo } = param
let initPage = 1
let res
const result = [] as string[]
do {
res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/branches`,
getOptions('GET', this.commonHeaders, { page: initPage, per_page: 100 }, 'json', undefined, undefined, this.proxy)
) as any
if (res.statusCode === 200) {
res.body.forEach((item: any) => result.push(item.name))
} else {
return []
}
initPage++
} while (res.body.length > 0)
return result
}
async getBucketListRecursively (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
const slicedPrefix = prefix.replace(/^\//, '').replace(/\/$/, '')
const cancelTask = [false]
ipcMain.on(cancelDownloadLoadingFileList, (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
}
})
let res = {} as any
const result = {
fullList: <any>[],
success: false,
finished: false
}
const treeQueue = [slicedPrefix]
while (treeQueue.length) {
if (cancelTask[0]) {
result.finished = true
return result
}
const currentPrefix = treeQueue[0]
res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${treeQueue.shift()}`,
getOptions('GET', this.commonHeaders, {}, 'json', undefined, undefined, this.proxy)
) as any
if (res && res.statusCode === 200) {
const { tree } = res.body
tree.forEach((item: any) => {
if (item.type === 'tree') {
treeQueue.push(`${currentPrefix}/${item.path}`)
} else {
result.fullList.push(this.formatFile(item, currentPrefix, branch, repo, cdnUrl))
}
})
window.webContents.send(refreshDownloadFileTransferList, result)
} else {
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
return
}
}
result.success = true
result.finished = true
window.webContents.send(refreshDownloadFileTransferList, result)
ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
}
async getBucketListBackstage (configMap: IStringKeyMap): Promise<any> {
const window = windowManager.get(IWindowList.SETTING_WINDOW)!
const { bucketName: repo, customUrl: branch, prefix, cancelToken, cdnUrl } = configMap
const slicedPrefix = prefix.replace(/^\//, '').replace(/\/$/, '')
const cancelTask = [false]
ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => {
if (token === cancelToken) {
cancelTask[0] = true
ipcMain.removeAllListeners('cancelLoadingFileList')
}
})
let res = {} as any
const result = {
fullList: <any>[],
success: false,
finished: false
}
res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${slicedPrefix}`,
getOptions('GET', this.commonHeaders, undefined, 'json', undefined, undefined, this.proxy)
)
if (res && res.statusCode === 200) {
res.body.tree.forEach((item: any) => {
if (item.type === 'tree') {
result.fullList.push(this.formatFolder(item, slicedPrefix))
} else {
result.fullList.push(this.formatFile(item, slicedPrefix, branch, repo, cdnUrl))
}
})
} else {
result.finished = true
window.webContents.send('refreshFileTransferList', result)
ipcMain.removeAllListeners('cancelLoadingFileList')
return
}
result.success = true
result.finished = true
window.webContents.send('refreshFileTransferList', result)
ipcMain.removeAllListeners('cancelLoadingFileList')
}
/**
* 删除文件
* @param configMap
* configMap = {
* bucketName: string,
* region: string,
* key: string
* }
*/
async deleteBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName: repo, githubBranch: branch, key, DeleteHash: sha } = configMap
const body = {
message: 'deleted by PicList',
sha,
branch
}
const res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/contents/${key}`,
getOptions('DELETE', this.commonHeaders, undefined, 'json', JSON.stringify(body), undefined, this.proxy)
)
return res.statusCode === 200
}
/**
* create a new tree to delete a folder
* @param configMap
*/
async deleteBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName: repo, githubBranch: branch, key } = configMap
const refRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`,
getOptions('GET', this.commonHeaders, undefined, 'json', undefined, undefined, this.proxy)
) as any
if (refRes.statusCode !== 200) {
return false
}
const refSha = refRes.body.object.sha
const rootRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/branches/${branch}`,
getOptions('GET', undefined, undefined, 'json', undefined, undefined, this.proxy)
) as any
if (rootRes.statusCode !== 200) {
return false
}
const rootSha = rootRes.body.commit.commit.tree.sha
// TODO: if there are more than 10000 files in the folder, it will be truncated
// Rare cases, not considered for now
const treeRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/trees/${branch}:${key.replace(/^\//, '').replace(/\/$/, '')}`,
getOptions('GET', this.commonHeaders, {
recursive: true
}, 'json', undefined, undefined, this.proxy)
) as any
if (treeRes.statusCode !== 200) {
return false
}
const oldTree = treeRes.body.tree
const newTree = oldTree.filter((item: any) => item.type === 'blob')
.map((item:any) => ({
path: `${key.replace(/^\//, '').replace(/\/$/, '')}/${item.path}`,
mode: item.mode,
type: item.type,
sha: null
}))
const newTreeShaRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/trees`,
getOptions('POST', this.commonHeaders, undefined, 'json', JSON.stringify({
base_tree: rootSha,
tree: newTree
}), undefined, this.proxy)
) as any
if (newTreeShaRes.statusCode !== 201) {
return false
}
const newTreeSha = newTreeShaRes.body.sha
const commitRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/commits`,
getOptions('POST', this.commonHeaders, undefined, 'json', JSON.stringify({
message: 'deleted by PicList',
tree: newTreeSha,
parents: [refSha]
}), undefined, this.proxy)
) as any
if (commitRes.statusCode !== 201) {
return false
}
const commitSha = commitRes.body.sha
const updateRefRes = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/git/refs/heads/${branch}`,
getOptions('PATCH', this.commonHeaders, undefined, 'json', JSON.stringify({
sha: commitSha
}), undefined, this.proxy)
) as any
if (updateRefRes.statusCode !== 200) {
return false
}
return true
}
/**
* 获取预签名url
* @param configMap
* configMap = {
* bucketName: string,
* region: string,
* key: string,
* expires: number,
* customUrl: string
* }
*/
async getPreSignedUrl (configMap: IStringKeyMap): Promise<string> {
const { bucketName: repo, customUrl: branch, key, rawUrl, githubPrivate: isPrivate } = configMap
if (!isPrivate) {
return rawUrl
}
const res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/contents/${key}`,
getOptions('GET', this.commonHeaders, {
ref: branch
}, 'json', undefined, undefined, this.proxy)
) as any
if (res.statusCode === 200) {
return res.body.download_url
} else {
return ''
}
}
/**
* 新建文件夹
* @param configMap
*/
async createBucketFolder (configMap: IStringKeyMap): Promise<boolean> {
const { bucketName: repo, githubBranch: branch, key } = configMap
const newFileKey = `${trimPath(key)}/.gitkeep`
const base64Content = Buffer.from('created by PicList').toString('base64')
const body = {
message: `created a new folder named ${key} by PicList`,
content: base64Content,
branch
}
const res = await got(
`${this.baseUrl}/repos/${this.username}/${repo}/contents/${newFileKey}`,
getOptions('PUT', this.commonHeaders, undefined, 'json', JSON.stringify(body), undefined, this.proxy)
)
return res.statusCode === 201
}
/**
* 上传文件
* @param configMap
*/
async uploadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { fileArray } = configMap
const instance = UpDownTaskQueue.getInstance()
fileArray.forEach((item: any) => {
item.key.startsWith('/') && (item.key = item.key.slice(1))
})
const filteredFileArray = fileArray.filter((item: any) => item.fileSize < 100 * 1024 * 1024)
for (const item of filteredFileArray) {
const { bucketName: repo, region, githubBranch: branch, key, filePath, fileName } = item
const id = `${repo}-${branch}-${key}-${filePath}`
if (instance.getUploadTask(id)) {
continue
}
const trimKey = trimPath(key)
const base64Content = fs.readFileSync(filePath, { encoding: 'base64' })
instance.addUploadTask({
id,
progress: 0,
status: commonTaskStatus.queuing,
sourceFileName: fileName,
sourceFilePath: filePath,
targetFilePath: key,
targetFileBucket: repo,
targetFileRegion: region
})
gotUpload(
instance,
`${this.baseUrl}/repos/${this.username}/${repo}/contents/${trimKey}`,
'PUT',
JSON.stringify({
message: 'uploaded by PicList',
branch,
content: base64Content
}),
this.commonHeaders,
id,
this.logger,
30000,
false,
getAgent(this.proxy)
)
}
return true
}
/**
* 下载文件
* @param configMap
*/
async downloadBucketFile (configMap: IStringKeyMap): Promise<boolean> {
const { downloadPath, fileArray, maxDownloadFileCount } = configMap
const instance = UpDownTaskQueue.getInstance()
const promises = [] as any
for (const item of fileArray) {
const { bucketName: repo, customUrl: branch, key, fileName, githubPrivate, githubUrl } = item
const id = `${repo}-${branch}-${key}-${fileName}`
const savedFilePath = path.join(downloadPath, fileName)
if (instance.getDownloadTask(id)) {
continue
}
instance.addDownloadTask({
id,
progress: 0,
status: commonTaskStatus.queuing,
sourceFileName: fileName,
targetFilePath: savedFilePath
})
let downloadUrl: string
if (githubPrivate) {
const preSignedUrl = await this.getPreSignedUrl({
bucketName: repo,
customUrl: branch,
key,
rawUrl: githubUrl,
githubPrivate
})
downloadUrl = preSignedUrl
} else {
downloadUrl = githubUrl
}
promises.push(() => new Promise((resolve, reject) => {
NewDownloader(
instance,
downloadUrl,
id,
savedFilePath,
this.logger,
this.proxyStr
)
.then((res: boolean) => {
if (res) {
resolve(res)
} else {
reject(res)
}
})
}))
}
const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
pool.all(promises).catch((error) => {
this.logger.error(formatError(error, { class: 'GithubApi', method: 'downloadBucketFile' }))
})
return true
}
}
export default GithubApi