@silexlabs/silex 3.0.7 → 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/js/main.js +20 -23
- package/dist/plugins/server/plugins/server/GitlabConnector.d.ts +2 -7
- package/dist/plugins/server/plugins/server/GitlabConnector.js +66 -31
- 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/server/api/publicationApi.js +2 -2
- package/package.json +1 -1
- package/src/ts/client/grapesjs/PublicationUi.ts +20 -23
- package/src/ts/plugins/server/GitlabConnector.ts +69 -31
- package/src/ts/plugins/server/GitlabHostingConnector.ts +6 -0
- package/src/ts/server/api/publicationApi.ts +2 -2
|
@@ -31,6 +31,8 @@ import { getPageSlug } from '../../page'
|
|
|
31
31
|
* @see https://docs.gitlab.com/ee/api/oauth2.html
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
const MAX_BATCH_UPLOAD_SIZE = 100
|
|
35
|
+
|
|
34
36
|
export interface GitlabOptions {
|
|
35
37
|
clientId: string
|
|
36
38
|
clientSecret: string
|
|
@@ -265,7 +267,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
265
267
|
/**
|
|
266
268
|
* Call the Gitlab API with the user's token and handle errors
|
|
267
269
|
*/
|
|
268
|
-
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> {
|
|
269
271
|
const token = this.getSessionToken(session).token
|
|
270
272
|
const tokenParam = token ? `access_token=${token.access_token}&` : ''
|
|
271
273
|
const paramsStr = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent((v as any).toString())}`).join('&')
|
|
@@ -273,17 +275,21 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
273
275
|
const headers = {
|
|
274
276
|
'Content-Type': 'application/json',
|
|
275
277
|
}
|
|
276
|
-
if (method === 'GET' &&
|
|
277
|
-
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 })
|
|
278
280
|
}
|
|
279
281
|
// With or without body
|
|
280
282
|
let response
|
|
283
|
+
const body = requestBody ? JSON.stringify(requestBody) : undefined
|
|
281
284
|
try {
|
|
282
|
-
|
|
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' ? {
|
|
283
289
|
agent: this.getAgent(),
|
|
284
290
|
method,
|
|
285
291
|
headers,
|
|
286
|
-
body
|
|
292
|
+
body,
|
|
287
293
|
} : {
|
|
288
294
|
agent: this.getAgent(),
|
|
289
295
|
method,
|
|
@@ -298,11 +304,12 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
298
304
|
try {
|
|
299
305
|
return await response.text()
|
|
300
306
|
} catch (e) {
|
|
301
|
-
console.error('Gitlab API error (6) - could not parse response', response.status, response.statusText, { url, method, body, params }, e)
|
|
307
|
+
console.error('Gitlab API error (6) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params }, e)
|
|
302
308
|
throw new ApiError(`Gitlab API error (6): response body not available. ${e.message}`, 500)
|
|
303
309
|
}
|
|
304
310
|
}()
|
|
305
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 })
|
|
306
313
|
if (text.includes('A file with this name doesn\'t exist')) {
|
|
307
314
|
throw new ApiError('Gitlab API error (5): Not Found', 404)
|
|
308
315
|
} else if (response.status === 401 && this.getSessionToken(session).token?.refresh_token) {
|
|
@@ -342,7 +349,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
342
349
|
}
|
|
343
350
|
} else {
|
|
344
351
|
const message = response.statusText
|
|
345
|
-
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body, params, text: text, message })
|
|
352
|
+
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body: requestBody, params, text: text, message })
|
|
346
353
|
throw new ApiError(`Gitlab API error (1): ${message} (${text})`, response.status)
|
|
347
354
|
}
|
|
348
355
|
}
|
|
@@ -355,7 +362,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
355
362
|
throw e
|
|
356
363
|
} else {
|
|
357
364
|
// Useless error linked to the fact that the response is not JSON
|
|
358
|
-
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, { url, method, body, params, text: text })
|
|
365
|
+
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params, text: text })
|
|
359
366
|
return text
|
|
360
367
|
}
|
|
361
368
|
}
|
|
@@ -380,14 +387,12 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
380
387
|
'Authorization': `Bearer ${token}`,
|
|
381
388
|
},
|
|
382
389
|
})
|
|
383
|
-
console.log('metaRes', metaRes.status, metaRes.statusText, { metaUrl })
|
|
384
390
|
|
|
385
391
|
const metaText = await metaRes.text()
|
|
386
392
|
if (!metaRes.ok) {
|
|
387
393
|
console.error('GitLab raw error (meta)', metaRes.status, metaRes.statusText, { metaUrl, metaText })
|
|
388
394
|
throw new ApiError(`GitLab raw error (meta): ${metaRes.statusText}`, metaRes.status)
|
|
389
395
|
}
|
|
390
|
-
console.log('metaText', metaText)
|
|
391
396
|
|
|
392
397
|
try {
|
|
393
398
|
const meta = JSON.parse(metaText)
|
|
@@ -395,7 +400,6 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
395
400
|
} catch (e) {
|
|
396
401
|
throw new ApiError('GitLab raw error (meta): could not parse project info', 500)
|
|
397
402
|
}
|
|
398
|
-
console.log('pathWithNamespace', pathWithNamespace)
|
|
399
403
|
|
|
400
404
|
// Limit cache size, remove least recently used
|
|
401
405
|
this.projectPathCache.set(projectId, pathWithNamespace!)
|
|
@@ -410,7 +414,6 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
410
414
|
const fileRes = await fetch(rawUrl, {
|
|
411
415
|
agent: this.getAgent(),
|
|
412
416
|
})
|
|
413
|
-
console.log('fileRes', fileRes.status, fileRes.statusText, { rawUrl, token })
|
|
414
417
|
|
|
415
418
|
const contentType = fileRes.headers.get('content-type')
|
|
416
419
|
if (contentType?.includes('text/html')) {
|
|
@@ -426,12 +429,9 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
426
429
|
console.error('GitLab raw error (1)', fileRes.status, fileRes.statusText, { rawUrl, errText })
|
|
427
430
|
throw new ApiError(`GitLab raw error (1): ${fileRes.statusText} (${errText})`, fileRes.status)
|
|
428
431
|
}
|
|
429
|
-
console.log('fileRes ok', fileRes.status, fileRes.statusText)
|
|
430
432
|
|
|
431
433
|
try {
|
|
432
|
-
console.log('fileRes buffer', fileRes.headers.get('content-type'), fileRes.headers.get('content-length'))
|
|
433
434
|
const buffer = await fileRes.buffer()
|
|
434
|
-
console.log('fileRes buffer', buffer.length)
|
|
435
435
|
return buffer
|
|
436
436
|
} catch (e) {
|
|
437
437
|
console.error('GitLab raw error (3): could not read buffer', e)
|
|
@@ -843,27 +843,65 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
843
843
|
}
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
-
async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback
|
|
847
|
-
status && await status({ message:
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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[]
|
|
853
890
|
|
|
854
891
|
try {
|
|
855
|
-
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
|
+
})
|
|
856
897
|
} catch (e) {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
} else {
|
|
861
|
-
status && await status({ message: 'Error', status: JobStatus.ERROR })
|
|
862
|
-
throw e
|
|
863
|
-
}
|
|
898
|
+
console.error(`Batch ${chunkIndex + 1} failed`, e)
|
|
899
|
+
status && await status({ message: `Error in batch ${chunkIndex + 1}`, status: JobStatus.ERROR })
|
|
900
|
+
throw e
|
|
864
901
|
}
|
|
865
902
|
}
|
|
866
|
-
|
|
903
|
+
|
|
904
|
+
status && await status({ message: 'All files uploaded successfully', status: JobStatus.SUCCESS })
|
|
867
905
|
}
|
|
868
906
|
|
|
869
907
|
async readAsset(session: GitlabSession, websiteId: string, fileName: string): Promise<ConnectorFileContent> {
|
|
@@ -73,13 +73,19 @@ export default class GitlabHostingConnector extends GitlabConnector implements H
|
|
|
73
73
|
- if: '$CI_PIPELINE_SOURCE == "trigger"'
|
|
74
74
|
`
|
|
75
75
|
try {
|
|
76
|
+
job.message = `Checking if ${pathYml} needs to be created or updated...`
|
|
77
|
+
job.logs[0].push(job.message)
|
|
76
78
|
const originalCi = await this.readFile(session, websiteId, pathYml)
|
|
77
79
|
if(originalCi.toString().startsWith(SILEX_OVERWRITE_NOTICE)) {
|
|
80
|
+
job.message = `Updating ${pathYml}...`
|
|
81
|
+
job.logs[0].push(job.message)
|
|
78
82
|
await this.updateFile(session, websiteId, pathYml, contentYml)
|
|
79
83
|
}
|
|
80
84
|
} catch (e) {
|
|
81
85
|
// If the file .gitlab-ci.yml does not exist, create it, otherwise do nothing (do not overwriting existing one)
|
|
82
86
|
if (e.statusCode === 404 || e.httpStatusCode === 404 || e.message.endsWith('A file with this name doesn\'t exist')) {
|
|
87
|
+
job.message = `Creating ${pathYml}...`
|
|
88
|
+
job.logs[0].push(job.message)
|
|
83
89
|
await this.createFile(session, websiteId, pathYml, contentYml)
|
|
84
90
|
} else {
|
|
85
91
|
jobError(job.jobId, e.message)
|
|
@@ -127,12 +127,12 @@ export default function (config: ServerConfig): Router {
|
|
|
127
127
|
const fileWithContent = file as ClientSideFileWithContent
|
|
128
128
|
const fileWithSrc = file as ClientSideFileWithSrc
|
|
129
129
|
if(!fileWithContent.content && !fileWithSrc.src) throw new PublicationError('Missing content or src in file', 400)
|
|
130
|
-
const
|
|
130
|
+
const content = fileWithContent.content ?? await storage.readAsset(session, websiteId, fileWithSrc.src)
|
|
131
131
|
return {
|
|
132
132
|
// Destination
|
|
133
133
|
path: file.permalink ?? file.path,
|
|
134
134
|
// Content
|
|
135
|
-
content
|
|
135
|
+
content,
|
|
136
136
|
}
|
|
137
137
|
}))
|
|
138
138
|
|