@silexlabs/silex 3.0.6 → 3.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/client/grapesjs/upload-progress.d.ts +2 -0
- package/dist/client/constants.d.ts +1 -0
- package/dist/client/js/main.js +107 -110
- package/dist/client/silex-lib/src/ts/client/grapesjs/upload-progress.d.ts +2 -0
- package/dist/client/silex-lib/src/ts/constants.d.ts +1 -0
- package/dist/plugins/server/constants.d.ts +1 -0
- package/dist/plugins/server/constants.js +2 -1
- package/dist/plugins/server/constants.js.map +1 -1
- package/dist/plugins/server/page.d.ts +2 -0
- package/dist/plugins/server/page.js +35 -0
- package/dist/plugins/server/page.js.map +1 -0
- package/dist/plugins/server/plugins/server/GitlabConnector.d.ts +17 -7
- package/dist/plugins/server/plugins/server/GitlabConnector.js +255 -114
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js +6 -0
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js.map +1 -1
- package/dist/server/constants.js +2 -1
- package/dist/server/server/api/publicationApi.js +2 -2
- package/package.json +1 -1
- package/src/ts/client/grapesjs/PublicationUi.ts +20 -23
- package/src/ts/client/grapesjs/index.ts +2 -0
- package/src/ts/client/grapesjs/storage.ts +8 -2
- package/src/ts/client/grapesjs/upload-progress.ts +48 -0
- package/src/ts/constants.ts +1 -0
- package/src/ts/plugins/server/GitlabConnector.ts +326 -140
- package/src/ts/plugins/server/GitlabHostingConnector.ts +6 -0
- package/src/ts/server/api/publicationApi.ts +2 -2
|
@@ -15,14 +15,15 @@
|
|
|
15
15
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { API_CONNECTOR_LOGIN_CALLBACK, API_CONNECTOR_PATH, API_PATH, WEBSITE_DATA_FILE } from '../../constants'
|
|
18
|
+
import { API_CONNECTOR_LOGIN_CALLBACK, API_CONNECTOR_PATH, API_PATH, WEBSITE_DATA_FILE, WEBSITE_PAGES_FOLDER } from '../../constants'
|
|
19
19
|
import { ServerConfig } from '../../server/config'
|
|
20
20
|
import { ConnectorFile, ConnectorFileContent, StatusCallback, StorageConnector, contentToBuffer, contentToString, toConnectorData } from '../../server/connectors/connectors'
|
|
21
|
-
import { ApiError, ConnectorType, ConnectorUser, WebsiteData, WebsiteId, WebsiteMeta, WebsiteMetaFileContent, JobStatus } from '../../types'
|
|
21
|
+
import { ApiError, ConnectorType, ConnectorUser, WebsiteData, WebsiteId, WebsiteMeta, WebsiteMetaFileContent, JobStatus, Page } from '../../types'
|
|
22
22
|
import fetch from 'node-fetch'
|
|
23
23
|
import crypto, { createHash } from 'crypto'
|
|
24
24
|
import { join } from 'path'
|
|
25
25
|
import { Agent } from 'https'
|
|
26
|
+
import { getPageSlug } from '../../page'
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Gitlab connector
|
|
@@ -30,17 +31,19 @@ import { Agent } from 'https'
|
|
|
30
31
|
* @see https://docs.gitlab.com/ee/api/oauth2.html
|
|
31
32
|
*/
|
|
32
33
|
|
|
34
|
+
const MAX_BATCH_UPLOAD_SIZE = 100
|
|
35
|
+
|
|
33
36
|
export interface GitlabOptions {
|
|
34
37
|
clientId: string
|
|
35
38
|
clientSecret: string
|
|
36
39
|
branch: string
|
|
37
40
|
assetsFolder: string
|
|
38
|
-
//metaRepo: string
|
|
39
|
-
//metaRepoFile: string
|
|
40
41
|
repoPrefix: string
|
|
41
42
|
scope: string
|
|
42
43
|
domain: string
|
|
43
44
|
timeOut: number
|
|
45
|
+
//metaRepo: string
|
|
46
|
+
//metaRepoFile: string
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export interface GitlabToken {
|
|
@@ -112,16 +115,26 @@ interface GitlabFetchCommits {
|
|
|
112
115
|
since: string
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
interface
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
createdAt: string,
|
|
120
|
-
updatedAt: string,
|
|
121
|
-
}
|
|
122
|
-
}
|
|
118
|
+
interface GitlabPage {
|
|
119
|
+
id: string
|
|
120
|
+
name: string
|
|
121
|
+
isFile: true
|
|
123
122
|
}
|
|
124
123
|
|
|
124
|
+
interface GitlabWebsiteData {
|
|
125
|
+
pages: GitlabPage[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// interface MetaRepoFileContent {
|
|
129
|
+
// websites: {
|
|
130
|
+
// [websiteId: string]: {
|
|
131
|
+
// meta: WebsiteMetaFileContent,
|
|
132
|
+
// createdAt: string,
|
|
133
|
+
// updatedAt: string,
|
|
134
|
+
// }
|
|
135
|
+
// }
|
|
136
|
+
// }
|
|
137
|
+
|
|
125
138
|
const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
126
139
|
<svg
|
|
127
140
|
xmlns:svg="http://www.w3.org/2000/svg"
|
|
@@ -200,16 +213,18 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
200
213
|
scope: 'api', // 'api+read_api+read_user+read_repository+write_repository+email+sudo+profile+openid'
|
|
201
214
|
...opts,
|
|
202
215
|
} as GitlabOptions
|
|
203
|
-
if(!this.options.clientId) throw new Error('Missing Gitlab client ID')
|
|
204
|
-
if(!this.options.clientSecret) throw new Error('Missing Gitlab client secret')
|
|
205
|
-
if(!this.options.domain) throw new Error('Missing Gitlab domain')
|
|
206
|
-
if(!this.options.timeOut) this.options.timeOut = 15000 /* default value */
|
|
216
|
+
if (!this.options.clientId) throw new Error('Missing Gitlab client ID')
|
|
217
|
+
if (!this.options.clientSecret) throw new Error('Missing Gitlab client secret')
|
|
218
|
+
if (!this.options.domain) throw new Error('Missing Gitlab domain')
|
|
219
|
+
if (!this.options.timeOut) this.options.timeOut = 15000 /* default value */
|
|
207
220
|
}
|
|
208
221
|
|
|
209
222
|
// **
|
|
210
223
|
// Convenience methods for the Gitlab API
|
|
211
|
-
private getAssetPath(path: string): string {
|
|
212
|
-
|
|
224
|
+
private getAssetPath(path: string, encode = true): string {
|
|
225
|
+
const resolvedPath = join(this.options.assetsFolder, path)
|
|
226
|
+
if (encode) return encodeURIComponent(resolvedPath)
|
|
227
|
+
return resolvedPath
|
|
213
228
|
}
|
|
214
229
|
|
|
215
230
|
isUsingOfficialInstance(): boolean {
|
|
@@ -244,55 +259,15 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
async readFile(session: GitlabSession, websiteId: string, fileName: string): Promise<Buffer> {
|
|
262
|
+
// Remove leading slash
|
|
247
263
|
const safePath = fileName.replace(/^\//, '')
|
|
248
|
-
|
|
249
|
-
const url = `${this.options.domain}/api/v4/projects/${websiteId}/repository/files/${safePath}?ref=${this.options.branch}&access_token=${this.getSessionToken(session).token?.access_token}`
|
|
250
|
-
const response = await fetch(url, {
|
|
251
|
-
agent: this.getAgent(),
|
|
252
|
-
method: 'GET',
|
|
253
|
-
headers: {
|
|
254
|
-
'Content-Type': 'application/json',
|
|
255
|
-
},
|
|
256
|
-
})
|
|
257
|
-
const json = await response.json()
|
|
258
|
-
if(!response.ok) throw new ApiError(`Error reading file "${fileName}" from Gitlab: ${json?.message ?? json?.error ?? response.statusText}`, response.status)
|
|
259
|
-
// From base64 string to buffer
|
|
260
|
-
const buf = Buffer.from(json.content, 'base64')
|
|
261
|
-
// Return the image bytes
|
|
262
|
-
return buf
|
|
264
|
+
return this.downloadRawFile(session, websiteId, safePath)
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
/*
|
|
266
|
-
* Get the meta repo path for the current user
|
|
267
|
-
* The meta repo contains a JSON file which contains the list of websites
|
|
268
|
-
*/
|
|
269
|
-
//private getMetaRepoPath(session: GitlabSession): string {
|
|
270
|
-
// if(!this.getSessionToken(session).username) throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
|
|
271
|
-
// return encodeURIComponent(`${this.getSessionToken(session).username}/${this.options.metaRepo}`)
|
|
272
|
-
//}
|
|
273
|
-
|
|
274
|
-
///**
|
|
275
|
-
// * Initialize the storage with a meta repo
|
|
276
|
-
// */
|
|
277
|
-
//private async initStorage(session: GitlabSession): Promise<void> {
|
|
278
|
-
// // Create the meta repo
|
|
279
|
-
// try {
|
|
280
|
-
// const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
|
|
281
|
-
// name: this.options.metaRepo,
|
|
282
|
-
// }) as any
|
|
283
|
-
// return this.createFile(session, this.getMetaRepoPath(session), this.options.metaRepoFile, JSON.stringify({
|
|
284
|
-
// websites: {}
|
|
285
|
-
// } as MetaRepoFileContent))
|
|
286
|
-
// } catch (e) {
|
|
287
|
-
// console.error('Could not init storage', e.statusCode, e.httpStatusCode, e)
|
|
288
|
-
// throw e
|
|
289
|
-
// }
|
|
290
|
-
//}
|
|
291
|
-
|
|
292
267
|
/**
|
|
293
268
|
* Call the Gitlab API with the user's token and handle errors
|
|
294
269
|
*/
|
|
295
|
-
async callApi(session: GitlabSession, path: string, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'GET',
|
|
270
|
+
async callApi(session: GitlabSession, path: string, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'GET', requestBody: GitlabWriteFile | GitlabGetToken | GitlabWebsiteName | GitlabCreateBranch | GitlabGetTags | GitlabCreateTag | GitlabFetchCommits | null = null, params: any = {}): Promise<any> {
|
|
296
271
|
const token = this.getSessionToken(session).token
|
|
297
272
|
const tokenParam = token ? `access_token=${token.access_token}&` : ''
|
|
298
273
|
const paramsStr = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent((v as any).toString())}`).join('&')
|
|
@@ -300,17 +275,21 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
300
275
|
const headers = {
|
|
301
276
|
'Content-Type': 'application/json',
|
|
302
277
|
}
|
|
303
|
-
if (method === 'GET' &&
|
|
304
|
-
console.error('Gitlab API error (4) - GET request with body', {url, method, body, params})
|
|
278
|
+
if (method === 'GET' && requestBody) {
|
|
279
|
+
console.error('Gitlab API error (4) - GET request with body', { url, method, body: requestBody, params })
|
|
305
280
|
}
|
|
306
281
|
// With or without body
|
|
307
282
|
let response
|
|
283
|
+
const body = requestBody ? JSON.stringify(requestBody) : undefined
|
|
308
284
|
try {
|
|
309
|
-
|
|
285
|
+
if(body && Buffer.byteLength(body) > 1024 * 100) {
|
|
286
|
+
console.warn('Gitlab API warning - body too big', Buffer.byteLength(body), 'bytes', { url, method, params })
|
|
287
|
+
}
|
|
288
|
+
response = await fetch(url, requestBody && method !== 'GET' ? {
|
|
310
289
|
agent: this.getAgent(),
|
|
311
290
|
method,
|
|
312
291
|
headers,
|
|
313
|
-
body
|
|
292
|
+
body,
|
|
314
293
|
} : {
|
|
315
294
|
agent: this.getAgent(),
|
|
316
295
|
method,
|
|
@@ -318,12 +297,19 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
318
297
|
})
|
|
319
298
|
} catch (e) {
|
|
320
299
|
console.error('Gitlab API error (0)', e)
|
|
321
|
-
throw new ApiError(`Gitlab API error (0): ${e.message} ${e.code} ${e.name} ${e.type}`, 500)
|
|
300
|
+
throw new ApiError(`Gitlab API error (0): ${e.message} ${e.text} ${e.code} ${e.name} ${e.type}`, 500)
|
|
322
301
|
}
|
|
323
|
-
let json: { message: string, error: string } | any
|
|
324
302
|
// Handle the case when the server returns a non-JSON response (e.g. 400 Bad Request)
|
|
325
|
-
const text = await
|
|
303
|
+
const text = await async function () {
|
|
304
|
+
try {
|
|
305
|
+
return await response.text()
|
|
306
|
+
} catch (e) {
|
|
307
|
+
console.error('Gitlab API error (6) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params }, e)
|
|
308
|
+
throw new ApiError(`Gitlab API error (6): response body not available. ${e.message}`, 500)
|
|
309
|
+
}
|
|
310
|
+
}()
|
|
326
311
|
if (!response.ok) {
|
|
312
|
+
console.error('Gitlab API error (7) - response not ok', response.status, response.statusText, { url, method, body: requestBody, params, text: text })
|
|
327
313
|
if (text.includes('A file with this name doesn\'t exist')) {
|
|
328
314
|
throw new ApiError('Gitlab API error (5): Not Found', 404)
|
|
329
315
|
} else if (response.status === 401 && this.getSessionToken(session).token?.refresh_token) {
|
|
@@ -354,7 +340,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
354
340
|
return await this.callApi(session, path, method, body as any, params)
|
|
355
341
|
} else {
|
|
356
342
|
const message = typeof refreshJson?.message === 'object' ? Object.entries(refreshJson.message).map(entry => entry.join(' ')).join(' ') : refreshJson?.message ?? refreshJson?.error ?? response.statusText
|
|
357
|
-
console.error('Gitlab API error (2) - could not refresh token', response.status, response.statusText, {message}, 'refresh_token:', token?.refresh_token)
|
|
343
|
+
console.error('Gitlab API error (2) - could not refresh token', response.status, response.statusText, { message }, 'refresh_token:', token?.refresh_token)
|
|
358
344
|
// Workaround for when the token is invalid
|
|
359
345
|
// It happens often which is not normal (refresh token should last 6 months)
|
|
360
346
|
this.logout(session)
|
|
@@ -362,26 +348,97 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
362
348
|
throw new ApiError(`Gitlab API error (2): ${message}`, response.status)
|
|
363
349
|
}
|
|
364
350
|
} else {
|
|
365
|
-
const message =
|
|
366
|
-
console.error('Gitlab API error (1)', response.status, response.statusText, {url, method, body, params, text, message})
|
|
367
|
-
throw new ApiError(`Gitlab API error (1): ${message}`, response.status)
|
|
351
|
+
const message = response.statusText
|
|
352
|
+
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body: requestBody, params, text: text, message })
|
|
353
|
+
throw new ApiError(`Gitlab API error (1): ${message} (${text})`, response.status)
|
|
368
354
|
}
|
|
369
355
|
}
|
|
356
|
+
let json: { message: string, error: string } | any
|
|
370
357
|
try {
|
|
371
358
|
json = JSON.parse(text)
|
|
372
359
|
} catch (e) {
|
|
373
|
-
if(!response.ok) {
|
|
360
|
+
if (!response.ok) {
|
|
374
361
|
// A real error
|
|
375
362
|
throw e
|
|
376
363
|
} else {
|
|
377
364
|
// Useless error linked to the fact that the response is not JSON
|
|
378
|
-
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, {url, method, body, params, text})
|
|
365
|
+
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params, text: text })
|
|
379
366
|
return text
|
|
380
367
|
}
|
|
381
368
|
}
|
|
382
369
|
return json
|
|
383
370
|
}
|
|
384
371
|
|
|
372
|
+
// Cache for projects path
|
|
373
|
+
private projectPathCache = new Map<string, string>()
|
|
374
|
+
|
|
375
|
+
async downloadRawFile(session: GitlabSession, projectId: string, filePath: string): Promise<Buffer> {
|
|
376
|
+
const token = this.getSessionToken(session).token?.access_token
|
|
377
|
+
const domain = this.options.domain
|
|
378
|
+
const branch = this.options.branch
|
|
379
|
+
|
|
380
|
+
// Get the project path with namespace
|
|
381
|
+
let pathWithNamespace = this.projectPathCache.get(projectId)
|
|
382
|
+
if (!pathWithNamespace) {
|
|
383
|
+
const metaUrl = `${domain}/api/v4/projects/${projectId}`
|
|
384
|
+
const metaRes = await fetch(metaUrl, {
|
|
385
|
+
agent: this.getAgent(),
|
|
386
|
+
headers: {
|
|
387
|
+
'Authorization': `Bearer ${token}`,
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
const metaText = await metaRes.text()
|
|
392
|
+
if (!metaRes.ok) {
|
|
393
|
+
console.error('GitLab raw error (meta)', metaRes.status, metaRes.statusText, { metaUrl, metaText })
|
|
394
|
+
throw new ApiError(`GitLab raw error (meta): ${metaRes.statusText}`, metaRes.status)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const meta = JSON.parse(metaText)
|
|
399
|
+
pathWithNamespace = meta.path_with_namespace
|
|
400
|
+
} catch (e) {
|
|
401
|
+
throw new ApiError('GitLab raw error (meta): could not parse project info', 500)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Limit cache size, remove least recently used
|
|
405
|
+
this.projectPathCache.set(projectId, pathWithNamespace!)
|
|
406
|
+
if (this.projectPathCache.size > 500) {
|
|
407
|
+
this.projectPathCache.delete(this.projectPathCache.keys().next().value)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Construct the raw URL
|
|
412
|
+
// GET /projects/:id/repository/files/:file_path/raw
|
|
413
|
+
const rawUrl = `${domain}/api/v4/projects/${encodeURIComponent(pathWithNamespace!)}/repository/files/${encodeURIComponent(filePath)}/raw?ref=${branch}&access_token=${token}`
|
|
414
|
+
const fileRes = await fetch(rawUrl, {
|
|
415
|
+
agent: this.getAgent(),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
const contentType = fileRes.headers.get('content-type')
|
|
419
|
+
if (contentType?.includes('text/html')) {
|
|
420
|
+
const html = await fileRes.text()
|
|
421
|
+
throw new ApiError('GitLab returned HTML instead of file (unauthorized or not found).', 401)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!fileRes.ok) {
|
|
425
|
+
const errText = await fileRes.text()
|
|
426
|
+
if (errText.includes('not found') || fileRes.status === 404) {
|
|
427
|
+
throw new ApiError('GitLab raw error (5): Not Found', 404)
|
|
428
|
+
}
|
|
429
|
+
console.error('GitLab raw error (1)', fileRes.status, fileRes.statusText, { rawUrl, errText })
|
|
430
|
+
throw new ApiError(`GitLab raw error (1): ${fileRes.statusText} (${errText})`, fileRes.status)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const buffer = await fileRes.buffer()
|
|
435
|
+
return buffer
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.error('GitLab raw error (3): could not read buffer', e)
|
|
438
|
+
throw new ApiError('GitLab raw error (3): failed to read binary content', 500)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
385
442
|
private generateCodeVerifier() {
|
|
386
443
|
return crypto.randomBytes(64).toString('base64')
|
|
387
444
|
.replace(/\+/g, '-')
|
|
@@ -472,15 +529,15 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
472
529
|
*/
|
|
473
530
|
async setToken(session: GitlabSession, loginResult: any): Promise<void> {
|
|
474
531
|
const sessionToken = this.getSessionToken(session)
|
|
475
|
-
if(!loginResult.state || loginResult.state !== sessionToken?.state) {
|
|
532
|
+
if (!loginResult.state || loginResult.state !== sessionToken?.state) {
|
|
476
533
|
this.logout(session)
|
|
477
534
|
throw new ApiError('Invalid state', 401)
|
|
478
535
|
}
|
|
479
|
-
if(!sessionToken?.codeVerifier) {
|
|
536
|
+
if (!sessionToken?.codeVerifier) {
|
|
480
537
|
this.logout(session)
|
|
481
538
|
throw new ApiError('Missing code verifier', 401)
|
|
482
539
|
}
|
|
483
|
-
if(!sessionToken?.codeChallenge) {
|
|
540
|
+
if (!sessionToken?.codeChallenge) {
|
|
484
541
|
this.logout(session)
|
|
485
542
|
throw new ApiError('Missing code challenge', 401)
|
|
486
543
|
}
|
|
@@ -558,7 +615,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
558
615
|
// }
|
|
559
616
|
//}
|
|
560
617
|
const userId = this.getSessionToken(session).userId
|
|
561
|
-
if(!userId) {
|
|
618
|
+
if (!userId) {
|
|
562
619
|
this.logout(session)
|
|
563
620
|
throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
|
|
564
621
|
}
|
|
@@ -574,16 +631,45 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
574
631
|
}))
|
|
575
632
|
}
|
|
576
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Read the website data
|
|
636
|
+
* The website data file is named `website.json` and the pages are named `page-{id}.json`
|
|
637
|
+
* The pages are stored in the `src` folder by default
|
|
638
|
+
*/
|
|
577
639
|
async readWebsite(session: GitlabSession, websiteId: string): Promise<WebsiteData> {
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
640
|
+
const websiteDataBuf = await this.downloadRawFile(session, websiteId, WEBSITE_DATA_FILE)
|
|
641
|
+
const websiteData = JSON.parse(websiteDataBuf.toString('utf8')) as GitlabWebsiteData | WebsiteData
|
|
642
|
+
|
|
643
|
+
// If the website pages are not in the main file, we need to read them
|
|
644
|
+
// This happens when the website was just created
|
|
645
|
+
// Let grapesjs create the pages in the frontend
|
|
646
|
+
if (!websiteData.pages) {
|
|
647
|
+
return websiteData as WebsiteData
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Load each page in parallel
|
|
651
|
+
const pages = await Promise.all(websiteData.pages.map(async (page: GitlabPage | Page) => {
|
|
652
|
+
if ((page as GitlabPage).isFile) {
|
|
653
|
+
const name = getPageSlug(page.name)
|
|
654
|
+
const fileName = (`${(getPageSlug(page.name))}-${page.id}`)
|
|
655
|
+
const filePath = `${WEBSITE_PAGES_FOLDER}/${fileName}.json`
|
|
656
|
+
const pageContent = await this.downloadRawFile(session, websiteId, filePath)
|
|
657
|
+
const res = JSON.parse(pageContent.toString('utf8')) as Page
|
|
658
|
+
return res
|
|
659
|
+
}
|
|
660
|
+
return page as Page
|
|
661
|
+
}))
|
|
662
|
+
|
|
663
|
+
// Read each page file if needed
|
|
664
|
+
return {
|
|
665
|
+
...websiteData,
|
|
666
|
+
pages,
|
|
667
|
+
} as WebsiteData
|
|
585
668
|
}
|
|
586
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Create a new website, i.e. a new Gitlab repository with an empty website data file
|
|
672
|
+
*/
|
|
587
673
|
async createWebsite(session: GitlabSession, websiteMeta: WebsiteMetaFileContent): Promise<WebsiteId> {
|
|
588
674
|
const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
|
|
589
675
|
name: this.options.repoPrefix + websiteMeta.name,
|
|
@@ -595,13 +681,76 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
595
681
|
return project.id
|
|
596
682
|
}
|
|
597
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Update the website data
|
|
686
|
+
* Split the website data into 1 file per page + 1 file for the website data itself
|
|
687
|
+
* Use gitlab batch API to create/update the files
|
|
688
|
+
*/
|
|
598
689
|
async updateWebsite(session: GitlabSession, websiteId: WebsiteId, websiteData: WebsiteData): Promise<void> {
|
|
599
|
-
const
|
|
690
|
+
const batchActions: GitlabAction[] = []
|
|
691
|
+
|
|
692
|
+
// List existing files in the pages folder
|
|
693
|
+
let existingFiles: string[] = []
|
|
694
|
+
try {
|
|
695
|
+
const files = await this.callApi(session, `api/v4/projects/${websiteId}/repository/tree`, 'GET', null, {
|
|
696
|
+
path: WEBSITE_PAGES_FOLDER,
|
|
697
|
+
recursive: false,
|
|
698
|
+
}) as any[]
|
|
699
|
+
existingFiles = files.map(file => file.path)
|
|
700
|
+
} catch (e) {
|
|
701
|
+
if (e.statusCode !== 404 && e.httpStatusCode !== 404) {
|
|
702
|
+
throw e
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Prepare actions for each page
|
|
707
|
+
const pages = websiteData.pages.map((page: Page) => {
|
|
708
|
+
const file_name = encodeURIComponent(`${(getPageSlug(page.name))}-${page.id}`)
|
|
709
|
+
const file_path = (`${WEBSITE_PAGES_FOLDER}/${file_name}.json`)
|
|
710
|
+
const content = JSON.stringify(page)
|
|
711
|
+
|
|
712
|
+
// Determine whether to create or update the file
|
|
713
|
+
if (existingFiles.includes(file_path)) {
|
|
714
|
+
batchActions.push({
|
|
715
|
+
action: 'update',
|
|
716
|
+
file_path,
|
|
717
|
+
content,
|
|
718
|
+
})
|
|
719
|
+
} else {
|
|
720
|
+
batchActions.push({
|
|
721
|
+
action: 'create',
|
|
722
|
+
file_path,
|
|
723
|
+
content,
|
|
724
|
+
})
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
name: page.name,
|
|
729
|
+
id: page.id,
|
|
730
|
+
isFile: true,
|
|
731
|
+
}
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
// Prepare the main website data file
|
|
735
|
+
const websiteDataWithGitlabPages = {
|
|
736
|
+
...websiteData,
|
|
737
|
+
pages,
|
|
738
|
+
} as GitlabWebsiteData
|
|
739
|
+
|
|
740
|
+
batchActions.push({
|
|
741
|
+
action: 'update',
|
|
742
|
+
file_path: WEBSITE_DATA_FILE,
|
|
743
|
+
content: JSON.stringify(websiteDataWithGitlabPages),
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
// Perform a single batch commit
|
|
747
|
+
const batch = {
|
|
600
748
|
branch: this.options.branch,
|
|
601
749
|
commit_message: 'Update website data from Silex',
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
750
|
+
actions: batchActions,
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
await this.callApi(session, `api/v4/projects/${websiteId}/repository/commits`, 'POST', batch)
|
|
605
754
|
}
|
|
606
755
|
|
|
607
756
|
async deleteWebsite(session: GitlabSession, websiteId: WebsiteId): Promise<void> {
|
|
@@ -621,7 +770,6 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
621
770
|
// branch: this.options.branch,
|
|
622
771
|
// commit_message: `Delete meta data of ${data.meta.name} (${websiteId}) from Silex`,
|
|
623
772
|
// content: JSON.stringify(metaRepo),
|
|
624
|
-
// file_path: this.options.metaRepoFile,
|
|
625
773
|
//})
|
|
626
774
|
}
|
|
627
775
|
|
|
@@ -641,12 +789,12 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
641
789
|
name: meta.name + ' Copy ' + new Date().toISOString().replace(/T.*/, '') + ' ' + Math.random().toString(36).substring(2, 4),
|
|
642
790
|
})
|
|
643
791
|
// Upload all files
|
|
644
|
-
for(const file of files) {
|
|
645
|
-
const
|
|
646
|
-
const content = await this.readFile(session, websiteId, path)
|
|
792
|
+
for (const file of files) {
|
|
793
|
+
const content = await this.readFile(session, websiteId, file)
|
|
647
794
|
// From buffer to string
|
|
648
795
|
const contentStr = content.toString('base64')
|
|
649
|
-
|
|
796
|
+
const path = encodeURIComponent(file)
|
|
797
|
+
switch (file) {
|
|
650
798
|
case WEBSITE_DATA_FILE:
|
|
651
799
|
await this.updateFile(session, newId, path, contentStr, true)
|
|
652
800
|
break
|
|
@@ -686,68 +834,78 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
686
834
|
}
|
|
687
835
|
|
|
688
836
|
async setWebsiteMeta(session: GitlabSession, websiteId: WebsiteId, websiteMeta: WebsiteMetaFileContent): Promise<void> {
|
|
689
|
-
//// Load the meta repo data
|
|
690
|
-
//const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
|
|
691
|
-
// ref: this.options.branch,
|
|
692
|
-
//})
|
|
693
|
-
//const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
|
|
694
|
-
//// Update or create the website meta data
|
|
695
|
-
//metaRepo.websites[websiteId] = {
|
|
696
|
-
// updatedAt: new Date().toISOString(),
|
|
697
|
-
// createdAt: metaRepo.websites[websiteId]?.createdAt ?? new Date().toISOString(),
|
|
698
|
-
// meta: websiteMeta,
|
|
699
|
-
//}
|
|
700
|
-
//// Save the meta repo data
|
|
701
|
-
//const project = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'PUT', {
|
|
702
|
-
// branch: this.options.branch,
|
|
703
|
-
// commit_message: `Update website meta data of ${websiteMeta.name} (${websiteId}) from Silex`,
|
|
704
|
-
// content: JSON.stringify(metaRepo),
|
|
705
|
-
// file_path: this.options.metaRepoFile,
|
|
706
|
-
//})
|
|
707
|
-
|
|
708
837
|
// Rename the repo if needed
|
|
709
838
|
const oldMeta = await this.getWebsiteMeta(session, websiteId)
|
|
710
|
-
if(websiteMeta.name !== oldMeta.name) {
|
|
839
|
+
if (websiteMeta.name !== oldMeta.name) {
|
|
711
840
|
await this.callApi(session, `api/v4/projects/${websiteId}`, 'PUT', {
|
|
712
841
|
name: this.options.repoPrefix + websiteMeta.name,
|
|
713
842
|
})
|
|
714
843
|
}
|
|
715
|
-
//// Update the metadata file
|
|
716
|
-
//await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`, 'PUT', {
|
|
717
|
-
// branch: this.options.branch,
|
|
718
|
-
// commit_message: 'Update website meta data from Silex',
|
|
719
|
-
// content: JSON.stringify(websiteMeta),
|
|
720
|
-
// file_path: WEBSITE_META_DATA_FILE,
|
|
721
|
-
// id: websiteId,
|
|
722
|
-
//})
|
|
723
844
|
}
|
|
724
845
|
|
|
725
|
-
async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback
|
|
726
|
-
status && await status({ message:
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
846
|
+
async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback): Promise<void> {
|
|
847
|
+
status && await status({ message: `Preparing ${files.length} files`, status: JobStatus.IN_PROGRESS })
|
|
848
|
+
|
|
849
|
+
// List all the files in the repo
|
|
850
|
+
const existingPaths = new Set<string>()
|
|
851
|
+
let page = 1
|
|
852
|
+
let keepGoing = true
|
|
853
|
+
while (keepGoing) {
|
|
854
|
+
const tree = await this.callApi(session, `api/v4/projects/${websiteId}/repository/tree`, 'GET', null, {
|
|
855
|
+
recursive: true,
|
|
856
|
+
per_page: 100,
|
|
857
|
+
page,
|
|
858
|
+
})
|
|
859
|
+
for (const f of tree) {
|
|
860
|
+
if (f.type === 'blob') existingPaths.add(f.path)
|
|
861
|
+
}
|
|
862
|
+
keepGoing = tree.length === 100
|
|
863
|
+
page++
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Split the files into chunks to avoid the number of files limit
|
|
867
|
+
const chunks: ConnectorFile[][] = []
|
|
868
|
+
for (let i = 0; i < files.length; i += MAX_BATCH_UPLOAD_SIZE) {
|
|
869
|
+
chunks.push(files.slice(i, i + MAX_BATCH_UPLOAD_SIZE))
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
873
|
+
const chunk = chunks[chunkIndex]
|
|
874
|
+
status && await status({ message: `Uploading batch ${chunkIndex + 1}/${chunks.length}`, status: JobStatus.IN_PROGRESS })
|
|
875
|
+
|
|
876
|
+
// Create the actions for the batch
|
|
877
|
+
const actions = await Promise.all(
|
|
878
|
+
chunk.map(async (file) => {
|
|
879
|
+
const content = (await contentToBuffer(file.content)).toString('base64')
|
|
880
|
+
const file_path = this.getAssetPath(file.path, false)
|
|
881
|
+
const actionType = existingPaths.has(file_path) ? 'update' : 'create'
|
|
882
|
+
return {
|
|
883
|
+
action: actionType,
|
|
884
|
+
file_path,
|
|
885
|
+
content,
|
|
886
|
+
encoding: 'base64',
|
|
887
|
+
}
|
|
888
|
+
})
|
|
889
|
+
) as GitlabAction[]
|
|
732
890
|
|
|
733
891
|
try {
|
|
734
|
-
await this.
|
|
892
|
+
await this.callApi(session, `api/v4/projects/${websiteId}/repository/commits`, 'POST', {
|
|
893
|
+
branch: 'main',
|
|
894
|
+
commit_message: `Batch update assets (${chunkIndex + 1}/${chunks.length})`,
|
|
895
|
+
actions,
|
|
896
|
+
})
|
|
735
897
|
} catch (e) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
} else {
|
|
740
|
-
status && await status({ message: 'Error', status: JobStatus.ERROR })
|
|
741
|
-
throw e
|
|
742
|
-
}
|
|
898
|
+
console.error(`Batch ${chunkIndex + 1} failed`, e)
|
|
899
|
+
status && await status({ message: `Error in batch ${chunkIndex + 1}`, status: JobStatus.ERROR })
|
|
900
|
+
throw e
|
|
743
901
|
}
|
|
744
902
|
}
|
|
745
|
-
|
|
903
|
+
|
|
904
|
+
status && await status({ message: 'All files uploaded successfully', status: JobStatus.SUCCESS })
|
|
746
905
|
}
|
|
747
906
|
|
|
748
907
|
async readAsset(session: GitlabSession, websiteId: string, fileName: string): Promise<ConnectorFileContent> {
|
|
749
|
-
|
|
750
|
-
const finalPath = this.getAssetPath(fileName)
|
|
908
|
+
const finalPath = this.getAssetPath(fileName, false)
|
|
751
909
|
return this.readFile(session, websiteId, finalPath)
|
|
752
910
|
}
|
|
753
911
|
|
|
@@ -762,4 +920,32 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
762
920
|
})),
|
|
763
921
|
})
|
|
764
922
|
}
|
|
923
|
+
|
|
924
|
+
/*
|
|
925
|
+
* Get the meta repo path for the current user
|
|
926
|
+
* The meta repo contains a JSON file which contains the list of websites
|
|
927
|
+
*/
|
|
928
|
+
//private getMetaRepoPath(session: GitlabSession): string {
|
|
929
|
+
// if(!this.getSessionToken(session).username) throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
|
|
930
|
+
// return encodeURIComponent(`${this.getSessionToken(session).username}/${this.options.metaRepo}`)
|
|
931
|
+
//}
|
|
932
|
+
|
|
933
|
+
///**
|
|
934
|
+
// * Initialize the storage with a meta repo
|
|
935
|
+
// */
|
|
936
|
+
//private async initStorage(session: GitlabSession): Promise<void> {
|
|
937
|
+
// // Create the meta repo
|
|
938
|
+
// try {
|
|
939
|
+
// const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
|
|
940
|
+
// name: this.options.metaRepo,
|
|
941
|
+
// }) as any
|
|
942
|
+
// return this.createFile(session, this.getMetaRepoPath(session), this.options.metaRepoFile, JSON.stringify({
|
|
943
|
+
// websites: {}
|
|
944
|
+
// } as MetaRepoFileContent))
|
|
945
|
+
// } catch (e) {
|
|
946
|
+
// console.error('Could not init storage', e.statusCode, e.httpStatusCode, e)
|
|
947
|
+
// throw e
|
|
948
|
+
// }
|
|
949
|
+
//}
|
|
950
|
+
|
|
765
951
|
}
|