Skip to content

Commit 4263b1f

Browse files
committed
backup releases with images and assets
closes #7
1 parent 59e3a11 commit 4263b1f

File tree

2 files changed

+65
-31
lines changed

2 files changed

+65
-31
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ Local backup of your GitHub data.
55
## In Scope
66

77
- ✅ Repositories: cloned, public and private ones
8+
- ✅ Releases: including images and assets
89
- ✅ Issues: including comments and images, open and closed ones
910
- ✅ User: details and starred repositories
1011

1112
## In Progress
1213
- ⏳ Readme: Images and file attachments
14+
- ⏳ Releases: File attachments
1315
- ⏳ Issues: File attachments
14-
- ⏳ Releases: including images, files and artifacts
1516
- ⏳ Projects: classic per repo and new projects per user
1617

1718
## Usage

index.js

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs-extra'
22
import shell from 'shelljs'
3-
import { dirname, basename } from 'path'
3+
import { dirname, basename, extname } from 'path'
44
import fetch from 'node-fetch'
55
import { extension } from 'mime-types'
66

@@ -12,7 +12,7 @@ const retryDelayRateLimit = 6 * 60
1212
const retryDelayOthers = 6
1313

1414
const { USERNAME, TOKEN } = process.env
15-
const FOLDER = '/usr/src/backup'
15+
const folder = '/usr/src/backup'
1616

1717
function delay(seconds) {
1818
return new Promise(resolve => {
@@ -31,10 +31,11 @@ function request(path, options = {}) {
3131
let resp
3232
try {
3333
resp = await fetch(`${baseUrl}${path}`, {
34+
...options,
3435
headers: {
35-
Authorization: `Token ${TOKEN}`
36-
},
37-
...options
36+
Authorization: `Token ${TOKEN}`,
37+
...options.headers || {}
38+
}
3839
})
3940
} catch {
4041
console.log(`... failed at #${n} attempt`)
@@ -113,9 +114,12 @@ async function requestAllWithRetry(path, options) {
113114

114115
function downloadFile(sourceFileUrl, targetFilePath) {
115116
return new Promise(async (resolve, reject) => {
116-
const response = await request(sourceFileUrl)
117-
const ext = extension([ ...response.headers ].filter(obj => obj[0] === 'content-type')[0][1])
118-
targetFilePath = targetFilePath + (ext ? '.' + ext : '')
117+
const response = await request(sourceFileUrl, { headers: { Accept: 'application/octet-stream' }})
118+
if (!extname(targetFilePath)) {
119+
const ext = extension([ ...response.headers ].filter(obj => obj[0] === 'content-type')[0][1])
120+
targetFilePath = targetFilePath + (ext ? '.' + ext : '')
121+
}
122+
fs.ensureDirSync(dirname(targetFilePath))
119123
const fileStream = fs.createWriteStream(targetFilePath)
120124
response.body.pipe(fileStream)
121125
response.body.on('error', () => { return reject() })
@@ -125,18 +129,18 @@ function downloadFile(sourceFileUrl, targetFilePath) {
125129
})
126130
}
127131

128-
function downloadAssets(body, FOLDER, filename) {
132+
function downloadImages(body, folder, filename) {
129133
return new Promise(async (resolve, reject) => {
130134
try {
131-
const assets = body?.match(/["(]https:\/\/github\.com\/(.+)\/assets\/(.+)[)"]/g) || []
132-
for (let n = 0; n < assets.length; n++) {
133-
const targetFilename = filename.replace('{id}', (n+1).toString().padStart(assets.length.toString().length, '0'))
134-
const targetPath = FOLDER + '/' + targetFilename
135-
const sourceUrl = assets[n].replace(/^["(](.+)[)"]$/, '$1')
136-
fs.ensureDirSync(FOLDER)
135+
const images = body?.match(/["(]https:\/\/github\.com\/(.+)\/assets\/(.+)[)"]/g) || []
136+
for (let n = 0; n < images.length; n++) {
137+
const targetFilename = filename.replace('{id}', (n+1).toString().padStart(images.length.toString().length, '0'))
138+
const targetPath = folder + '/' + targetFilename
139+
const sourceUrl = images[n].replace(/^["(](.+)[)"]$/, '$1')
140+
fs.ensureDirSync(folder)
137141
const realTargetFilename = basename(await downloadFile(sourceUrl, targetPath))
138-
body = body.replace(`"${sourceUrl}"`, '"./assets/' + realTargetFilename + '"')
139-
body = body.replace(`(${sourceUrl})`, '(./assets/' + realTargetFilename + ')')
142+
body = body.replace(`"${sourceUrl}"`, '"./images/' + realTargetFilename + '"')
143+
body = body.replace(`(${sourceUrl})`, '(./images/' + realTargetFilename + ')')
140144
}
141145
return resolve(body)
142146
} catch (err) {
@@ -153,14 +157,14 @@ function writeJSON(path, json) {
153157
async function backup() {
154158
try {
155159

156-
// Reset the backup FOLDER
157-
fs.emptyDirSync(FOLDER)
160+
// Reset the backup folder
161+
fs.emptyDirSync(folder)
158162

159163
// Get repositories
160164
const repositories = await requestAllWithRetry('/user/repos')
161165

162166
// Save repositories
163-
writeJSON(`${FOLDER}/repositories.json`, repositories)
167+
writeJSON(`${folder}/repositories.json`, repositories)
164168

165169
// Loop repositories
166170
for (const repository of repositories.filter(rep => rep.name === 'test')) {
@@ -171,10 +175,10 @@ async function backup() {
171175
// Loop issues
172176
for (const issue of issues) {
173177

174-
// Download issue assets
175-
issue.body = await downloadAssets(
178+
// Download issue images
179+
issue.body = await downloadImages(
176180
issue.body,
177-
`${FOLDER}/repositories/${repository.name}/assets`,
181+
`${folder}/repositories/${repository.name}/images`,
178182
`issue_${issue.id}_{id}`
179183
)
180184

@@ -187,10 +191,10 @@ async function backup() {
187191
// Loop issue comments
188192
for (const comment of comments) {
189193

190-
// Download issue assets
191-
comment.body = await downloadAssets(
194+
// Download issue comment images
195+
comment.body = await downloadImages(
192196
comment.body,
193-
`${FOLDER}/repositories/${repository.name}/assets`,
197+
`${folder}/repositories/${repository.name}/images`,
194198
`issue_${issue.id}_comment_${comment.id}_{id}`
195199
)
196200

@@ -199,20 +203,49 @@ async function backup() {
199203
}
200204

201205
// Save issues
202-
writeJSON(`${FOLDER}/repositories/${repository.name}/issues.json`, issues)
206+
writeJSON(`${folder}/repositories/${repository.name}/issues.json`, issues)
207+
208+
// Get releases
209+
const releases = await requestAllWithRetry(`/repos/${USERNAME}/${repository.name}/releases`)
210+
211+
// Loop releases
212+
for (const release of releases) {
213+
214+
// Download release text images
215+
release.body = await downloadImages(
216+
release.body,
217+
`${folder}/repositories/${repository.name}/images`,
218+
`release_${release.id}_{id}`
219+
)
220+
221+
// Loop release assets
222+
for (const asset of release.assets) {
223+
224+
// Download release assets
225+
downloadFile(
226+
asset.url,
227+
`${folder}/repositories/${repository.name}/releases/${release.tag_name}/${asset.name}`
228+
)
229+
230+
}
231+
232+
}
233+
234+
// Save releases
235+
writeJSON(`${folder}/repositories/${repository.name}/releases.json`, releases)
203236

204237
// Clone repository
205-
shell.exec(`git clone https://${TOKEN}@github.com/${USERNAME}/${repository.name}.git ${FOLDER}/repositories/${repository.name}/repository`)
238+
shell.exec(`git clone https://${TOKEN}@github.com/${USERNAME}/${repository.name}.git ${folder}/repositories/${repository.name}/repository`)
206239

207240
}
208241

209242
// Get user details
210243
const user = await requestJson('/user')
211-
writeJSON(`${FOLDER}/user/user.json`, user)
244+
writeJSON(`${folder}/user/user.json`, user)
212245

213246
// Get starred repositories
214247
const starred = await requestAllWithRetry('/user/starred')
215-
writeJSON(`${FOLDER}/user/starred.json`, starred)
248+
writeJSON(`${folder}/user/starred.json`, starred)
216249

217250
// Complete script
218251
console.log('Backup completed!')

0 commit comments

Comments
 (0)