@silexlabs/silex 3.0.7 → 3.0.9
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 -8
- package/dist/plugins/server/plugins/server/GitlabConnector.js +70 -62
- 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 +73 -66
- package/src/ts/plugins/server/GitlabHostingConnector.ts +6 -0
- package/src/ts/server/api/publicationApi.ts +2 -2
|
@@ -31,6 +31,9 @@ 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
|
+
const MAX_BODY_SIZE_KB = 8 * 1000 * 1024 // 8MB (note that 10 MB PNG → becomes ~13.3 MB → ❌ often too big for Gitlab)
|
|
36
|
+
|
|
34
37
|
export interface GitlabOptions {
|
|
35
38
|
clientId: string
|
|
36
39
|
clientSecret: string
|
|
@@ -265,7 +268,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
265
268
|
/**
|
|
266
269
|
* Call the Gitlab API with the user's token and handle errors
|
|
267
270
|
*/
|
|
268
|
-
async callApi(session: GitlabSession, path: string, method: 'POST' | 'GET' | 'PUT' | 'DELETE' = 'GET',
|
|
271
|
+
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
272
|
const token = this.getSessionToken(session).token
|
|
270
273
|
const tokenParam = token ? `access_token=${token.access_token}&` : ''
|
|
271
274
|
const paramsStr = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent((v as any).toString())}`).join('&')
|
|
@@ -273,17 +276,22 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
273
276
|
const headers = {
|
|
274
277
|
'Content-Type': 'application/json',
|
|
275
278
|
}
|
|
276
|
-
if (method === 'GET' &&
|
|
277
|
-
console.error('Gitlab API error (4) - GET request with body', { url, method, body, params })
|
|
279
|
+
if (method === 'GET' && requestBody) {
|
|
280
|
+
console.error('Gitlab API error (4) - GET request with body', { url, method, body: requestBody, params })
|
|
278
281
|
}
|
|
279
282
|
// With or without body
|
|
280
283
|
let response
|
|
284
|
+
const body = requestBody ? JSON.stringify(requestBody) : undefined
|
|
281
285
|
try {
|
|
282
|
-
|
|
286
|
+
if(body && Buffer.byteLength(body) > MAX_BODY_SIZE_KB * 1024) {
|
|
287
|
+
// TODO: warn the end user
|
|
288
|
+
console.warn('Gitlab API warning - body too big', Buffer.byteLength(body), 'bytes', { url, method, params })
|
|
289
|
+
}
|
|
290
|
+
response = await fetch(url, requestBody && method !== 'GET' ? {
|
|
283
291
|
agent: this.getAgent(),
|
|
284
292
|
method,
|
|
285
293
|
headers,
|
|
286
|
-
body
|
|
294
|
+
body,
|
|
287
295
|
} : {
|
|
288
296
|
agent: this.getAgent(),
|
|
289
297
|
method,
|
|
@@ -298,11 +306,12 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
298
306
|
try {
|
|
299
307
|
return await response.text()
|
|
300
308
|
} catch (e) {
|
|
301
|
-
console.error('Gitlab API error (6) - could not parse response', response.status, response.statusText, { url, method, body, params }, e)
|
|
309
|
+
console.error('Gitlab API error (6) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params }, e)
|
|
302
310
|
throw new ApiError(`Gitlab API error (6): response body not available. ${e.message}`, 500)
|
|
303
311
|
}
|
|
304
312
|
}()
|
|
305
313
|
if (!response.ok) {
|
|
314
|
+
console.error('Gitlab API error (7) - response not ok', response.status, response.statusText, { url, method, body: requestBody, params, text: text })
|
|
306
315
|
if (text.includes('A file with this name doesn\'t exist')) {
|
|
307
316
|
throw new ApiError('Gitlab API error (5): Not Found', 404)
|
|
308
317
|
} else if (response.status === 401 && this.getSessionToken(session).token?.refresh_token) {
|
|
@@ -342,7 +351,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
342
351
|
}
|
|
343
352
|
} else {
|
|
344
353
|
const message = response.statusText
|
|
345
|
-
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body, params, text: text, message })
|
|
354
|
+
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body: requestBody, params, text: text, message })
|
|
346
355
|
throw new ApiError(`Gitlab API error (1): ${message} (${text})`, response.status)
|
|
347
356
|
}
|
|
348
357
|
}
|
|
@@ -355,62 +364,24 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
355
364
|
throw e
|
|
356
365
|
} else {
|
|
357
366
|
// 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 })
|
|
367
|
+
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params, text: text })
|
|
359
368
|
return text
|
|
360
369
|
}
|
|
361
370
|
}
|
|
362
371
|
return json
|
|
363
372
|
}
|
|
364
373
|
|
|
365
|
-
// Cache for projects path
|
|
366
|
-
private projectPathCache = new Map<string, string>()
|
|
367
|
-
|
|
368
374
|
async downloadRawFile(session: GitlabSession, projectId: string, filePath: string): Promise<Buffer> {
|
|
369
375
|
const token = this.getSessionToken(session).token?.access_token
|
|
370
376
|
const domain = this.options.domain
|
|
371
377
|
const branch = this.options.branch
|
|
372
378
|
|
|
373
|
-
// Get the project path with namespace
|
|
374
|
-
let pathWithNamespace = this.projectPathCache.get(projectId)
|
|
375
|
-
if (!pathWithNamespace) {
|
|
376
|
-
const metaUrl = `${domain}/api/v4/projects/${projectId}`
|
|
377
|
-
const metaRes = await fetch(metaUrl, {
|
|
378
|
-
agent: this.getAgent(),
|
|
379
|
-
headers: {
|
|
380
|
-
'Authorization': `Bearer ${token}`,
|
|
381
|
-
},
|
|
382
|
-
})
|
|
383
|
-
console.log('metaRes', metaRes.status, metaRes.statusText, { metaUrl })
|
|
384
|
-
|
|
385
|
-
const metaText = await metaRes.text()
|
|
386
|
-
if (!metaRes.ok) {
|
|
387
|
-
console.error('GitLab raw error (meta)', metaRes.status, metaRes.statusText, { metaUrl, metaText })
|
|
388
|
-
throw new ApiError(`GitLab raw error (meta): ${metaRes.statusText}`, metaRes.status)
|
|
389
|
-
}
|
|
390
|
-
console.log('metaText', metaText)
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
const meta = JSON.parse(metaText)
|
|
394
|
-
pathWithNamespace = meta.path_with_namespace
|
|
395
|
-
} catch (e) {
|
|
396
|
-
throw new ApiError('GitLab raw error (meta): could not parse project info', 500)
|
|
397
|
-
}
|
|
398
|
-
console.log('pathWithNamespace', pathWithNamespace)
|
|
399
|
-
|
|
400
|
-
// Limit cache size, remove least recently used
|
|
401
|
-
this.projectPathCache.set(projectId, pathWithNamespace!)
|
|
402
|
-
if (this.projectPathCache.size > 500) {
|
|
403
|
-
this.projectPathCache.delete(this.projectPathCache.keys().next().value)
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
379
|
// Construct the raw URL
|
|
408
380
|
// GET /projects/:id/repository/files/:file_path/raw
|
|
409
|
-
const rawUrl = `${domain}/api/v4/projects/${
|
|
381
|
+
const rawUrl = `${domain}/api/v4/projects/${projectId}/repository/files/${encodeURIComponent(filePath)}/raw?ref=${branch}&access_token=${token}`
|
|
410
382
|
const fileRes = await fetch(rawUrl, {
|
|
411
383
|
agent: this.getAgent(),
|
|
412
384
|
})
|
|
413
|
-
console.log('fileRes', fileRes.status, fileRes.statusText, { rawUrl, token })
|
|
414
385
|
|
|
415
386
|
const contentType = fileRes.headers.get('content-type')
|
|
416
387
|
if (contentType?.includes('text/html')) {
|
|
@@ -426,12 +397,9 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
426
397
|
console.error('GitLab raw error (1)', fileRes.status, fileRes.statusText, { rawUrl, errText })
|
|
427
398
|
throw new ApiError(`GitLab raw error (1): ${fileRes.statusText} (${errText})`, fileRes.status)
|
|
428
399
|
}
|
|
429
|
-
console.log('fileRes ok', fileRes.status, fileRes.statusText)
|
|
430
400
|
|
|
431
401
|
try {
|
|
432
|
-
console.log('fileRes buffer', fileRes.headers.get('content-type'), fileRes.headers.get('content-length'))
|
|
433
402
|
const buffer = await fileRes.buffer()
|
|
434
|
-
console.log('fileRes buffer', buffer.length)
|
|
435
403
|
return buffer
|
|
436
404
|
} catch (e) {
|
|
437
405
|
console.error('GitLab raw error (3): could not read buffer', e)
|
|
@@ -843,27 +811,66 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
843
811
|
}
|
|
844
812
|
}
|
|
845
813
|
|
|
846
|
-
async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback
|
|
847
|
-
status && await status({ message:
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
814
|
+
async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback): Promise<void> {
|
|
815
|
+
status && await status({ message: `Preparing ${files.length} files`, status: JobStatus.IN_PROGRESS })
|
|
816
|
+
|
|
817
|
+
// List all the files in the repo
|
|
818
|
+
const existingPaths = new Set<string>()
|
|
819
|
+
let page = 1
|
|
820
|
+
let keepGoing = true
|
|
821
|
+
while (keepGoing) {
|
|
822
|
+
const tree = await this.callApi(session, `api/v4/projects/${websiteId}/repository/tree`, 'GET', null, {
|
|
823
|
+
recursive: true,
|
|
824
|
+
per_page: 100,
|
|
825
|
+
page,
|
|
826
|
+
})
|
|
827
|
+
for (const f of tree) {
|
|
828
|
+
if (f.type === 'blob') existingPaths.add(f.path)
|
|
829
|
+
}
|
|
830
|
+
keepGoing = tree.length === 100
|
|
831
|
+
page++
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Split the files into chunks to avoid the number of files limit
|
|
835
|
+
const chunks: ConnectorFile[][] = []
|
|
836
|
+
for (let i = 0; i < files.length; i += MAX_BATCH_UPLOAD_SIZE) {
|
|
837
|
+
chunks.push(files.slice(i, i + MAX_BATCH_UPLOAD_SIZE))
|
|
838
|
+
}
|
|
853
839
|
|
|
840
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
841
|
+
const chunk = chunks[chunkIndex]
|
|
842
|
+
status && await status({ message: `Batch ${chunkIndex + 1}/${chunks.length}: Downloading assets`, status: JobStatus.IN_PROGRESS })
|
|
843
|
+
|
|
844
|
+
// Create the actions for the batch
|
|
845
|
+
const actions = await Promise.all(
|
|
846
|
+
chunk.map(async (file) => {
|
|
847
|
+
const content = (await contentToBuffer(file.content)).toString('base64')
|
|
848
|
+
const file_path = this.getAssetPath(file.path, false)
|
|
849
|
+
const actionType = existingPaths.has(file_path) ? 'update' : 'create'
|
|
850
|
+
return {
|
|
851
|
+
action: actionType,
|
|
852
|
+
file_path,
|
|
853
|
+
content,
|
|
854
|
+
encoding: 'base64',
|
|
855
|
+
}
|
|
856
|
+
})
|
|
857
|
+
) as GitlabAction[]
|
|
858
|
+
|
|
859
|
+
status && await status({ message: `Batch ${chunkIndex + 1}/${chunks.length}: Uploading assets`, status: JobStatus.IN_PROGRESS })
|
|
854
860
|
try {
|
|
855
|
-
await this.
|
|
861
|
+
await this.callApi(session, `api/v4/projects/${websiteId}/repository/commits`, 'POST', {
|
|
862
|
+
branch: 'main',
|
|
863
|
+
commit_message: `Batch update assets (${chunkIndex + 1}/${chunks.length})`,
|
|
864
|
+
actions,
|
|
865
|
+
})
|
|
856
866
|
} catch (e) {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
} else {
|
|
861
|
-
status && await status({ message: 'Error', status: JobStatus.ERROR })
|
|
862
|
-
throw e
|
|
863
|
-
}
|
|
867
|
+
console.error(`Batch ${chunkIndex + 1} failed`, e)
|
|
868
|
+
status && await status({ message: `Error in batch ${chunkIndex + 1}`, status: JobStatus.ERROR })
|
|
869
|
+
throw e
|
|
864
870
|
}
|
|
865
871
|
}
|
|
866
|
-
|
|
872
|
+
|
|
873
|
+
status && await status({ message: 'All files uploaded successfully', status: JobStatus.SUCCESS })
|
|
867
874
|
}
|
|
868
875
|
|
|
869
876
|
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
|
|