Skip to content

Commit c019fdd

Browse files
committed
initial version
1 parent 7aaa332 commit c019fdd

File tree

7 files changed

+553
-0
lines changed

7 files changed

+553
-0
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# System Files
2+
.DS_Store
3+
4+
# NPM Modules
5+
node_modules/
6+
7+
# Credentials File
8+
credentials.js

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.PHONY: backup
2+
3+
backup: install
4+
node index.js
5+
6+
install:
7+
npm install
8+

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@ Local backup of your GitHub data.
1212
- ⏳ Issues: File attachments
1313
- ⏳ Releases: including images, files and artifacts
1414
- ⏳ Projects: classic per repo and new projects per user
15+
16+
## Usage
17+
18+
1. Clone this repository
19+
2. Update and save `credentials.template.js` as `credentials.js`
20+
3. Run `make` to start the backup process

credentials.template.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
username: '<username>',
3+
token: '<token>',
4+
folder: '<folder>'
5+
}

index.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import fs from 'fs-extra'
2+
import shell from 'shelljs'
3+
import { dirname, basename } from 'path'
4+
import fetch from 'node-fetch'
5+
import { extension } from 'mime-types'
6+
import credentials from './credentials.js'
7+
8+
const { username, token, folder } = credentials
9+
10+
function request(path, options = {}) {
11+
return new Promise((resolve, reject) => {
12+
const baseUrl = path.substr(0, 4) !== 'http' ? 'https://api.github.com' : ''
13+
fetch(`${baseUrl}${path}`, {
14+
headers: {
15+
Authorization: `Token ${token}`
16+
},
17+
...options
18+
})
19+
.then(resp => {
20+
resolve(resp)
21+
})
22+
.catch(err => {
23+
reject(err)
24+
})
25+
})
26+
}
27+
28+
function requestJson(path, options) {
29+
return new Promise(async (resolve, reject) => {
30+
try {
31+
const response = await request(path, options)
32+
const json = await response.json()
33+
resolve(json)
34+
} catch (err) {
35+
reject(err)
36+
}
37+
})
38+
}
39+
40+
function requestAll(path, options) {
41+
return new Promise(async (resolve, reject) => {
42+
try {
43+
let items = []
44+
let page = 1
45+
while (page !== null) {
46+
const separator = path.indexOf('?') === -1 ? '?' : '&'
47+
const moreItemsResponse = await request(`${path}${separator}page=${page}`, options)
48+
const moreItems = await moreItemsResponse.json()
49+
if (moreItems.length) {
50+
items = [...items, ...moreItems]
51+
page++
52+
} else {
53+
page = null
54+
}
55+
}
56+
resolve(items)
57+
} catch (err) {
58+
reject(err)
59+
}
60+
})
61+
}
62+
63+
function downloadFile(sourceFileUrl, targetFilePath) {
64+
return new Promise(async (resolve, reject) => {
65+
const response = await request(sourceFileUrl)
66+
const ext = extension([ ...response.headers ].filter(obj => obj[0] === 'content-type')[0][1])
67+
targetFilePath = targetFilePath + (ext ? '.' + ext : '')
68+
const fileStream = fs.createWriteStream(targetFilePath)
69+
response.body.pipe(fileStream)
70+
response.body.on('error', reject)
71+
fileStream.on('finish', () => {
72+
resolve(targetFilePath)
73+
})
74+
})
75+
}
76+
77+
function downloadAssets(body, folder, filename) {
78+
return new Promise(async (resolve, reject) => {
79+
try {
80+
const assets = body?.match(/["(]https:\/\/github\.com\/(.+)\/assets\/(.+)[)"]/g) || []
81+
for (const assetId in assets) {
82+
const targetFilename = filename.replace('{id}', assetId)
83+
const targetPath = folder + '/' + targetFilename
84+
const sourceUrl = assets[assetId].replace(/^["(](.+)[)"]$/, '$1')
85+
fs.ensureDirSync(folder)
86+
const realTargetFilename = basename(await downloadFile(sourceUrl, targetPath))
87+
body = body.replace(`"${sourceUrl}"`, '"file://./assets/' + realTargetFilename + '"')
88+
body = body.replace(`(${sourceUrl})`, '(file://./assets/' + realTargetFilename + ')')
89+
}
90+
resolve(body)
91+
} catch (err) {
92+
reject(err)
93+
}
94+
})
95+
}
96+
97+
function writeJSON(path, json) {
98+
fs.ensureDirSync(dirname(path))
99+
fs.writeJsonSync(path, json, { spaces: 2 })
100+
}
101+
102+
async function backup() {
103+
try {
104+
105+
// Reset the backup folder
106+
shell.exec(`rm -r ${folder}`)
107+
fs.ensureDirSync(folder)
108+
109+
// Get repositories
110+
const repositories = await requestAll('/user/repos')
111+
112+
// Save repositories
113+
writeJSON(`${folder}/repositories.json`, repositories)
114+
115+
// Loop repositories
116+
for (const repository of repositories) {
117+
118+
// Get issues
119+
const issues = await requestAll(`/repos/${username}/${repository.name}/issues?state=all`)
120+
121+
// Loop issues
122+
for (const issueId in issues) {
123+
124+
// Download issue assets
125+
issues[issueId].body = await downloadAssets(
126+
issues[issueId].body,
127+
`${folder}/repositories/${repository.name}/assets`,
128+
`issue_${issueId}_{id}`
129+
)
130+
131+
// Get issue comments
132+
const comments = await requestAll(issues[issueId].comments_url)
133+
134+
// Add issue comments to issues JSON
135+
issues[issueId].comments = comments
136+
137+
// Loop issue comments
138+
for (const commentId in comments) {
139+
140+
// Download issue assets
141+
issues[issueId].comments[commentId].body = await downloadAssets(
142+
issues[issueId].comments[commentId].body,
143+
`${folder}/repositories/${repository.name}/assets`,
144+
`issue_${issueId}_comment_${commentId}_{id}`
145+
)
146+
147+
}
148+
149+
}
150+
151+
// Save issues
152+
writeJSON(`${folder}/repositories/${repository.name}/issues.json`, issues)
153+
154+
// Clone repository
155+
shell.exec(`git clone https://${token}@github.com/${username}/${repository.name}.git ${folder}/repositories/${repository.name}/repository`)
156+
157+
}
158+
159+
// Get user details
160+
const user = await requestJson('/user')
161+
writeJSON(`${folder}/user/user.json`, user)
162+
163+
// Get starred repositories
164+
const starred = await requestAll('/user/starred')
165+
writeJSON(`${folder}/user/starred.json`, starred)
166+
167+
// Complete script
168+
console.log('Backup completed!')
169+
shell.exit(1)
170+
171+
} catch (err) {
172+
throw err
173+
}
174+
}
175+
176+
backup()

0 commit comments

Comments
 (0)