@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.
@@ -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', body: GitlabWriteFile | GitlabGetToken | GitlabWebsiteName | GitlabCreateBranch | GitlabGetTags | GitlabCreateTag | GitlabFetchCommits | null = null, params: any = {}): Promise<any> {
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' && body) {
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
- response = await fetch(url, body && method !== 'GET' ? {
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: body ? JSON.stringify(body) : undefined
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 | undefined): Promise<void> {
847
- status && await status({ message: 'in progress...', status: JobStatus.IN_PROGRESS })
848
- // For each file
849
- for (const file of files) {
850
- // Convert to base64
851
- const content = (await contentToBuffer(file.content)).toString('base64')
852
- const path = this.getAssetPath(file.path)
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.updateFile(session, websiteId, path, content, true)
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
- // If the file does not exist, create it
858
- if (e.statusCode === 404 || e.httpStatusCode === 404 || e.message.endsWith('A file with this name doesn\'t exist')) {
859
- await this.createFile(session, websiteId, path, content, true)
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
- status && await status({ message: 'Successfull', status: JobStatus.SUCCESS })
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 src = fileWithSrc.src
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: fileWithContent.content ?? await storage.readAsset(session, websiteId, fileWithSrc.src),
135
+ content,
136
136
  }
137
137
  }))
138
138