@silexlabs/silex 3.0.5 → 3.0.7
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 +95 -95
- 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 +15 -0
- package/dist/plugins/server/plugins/server/GitlabConnector.js +199 -93
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/server/constants.js +2 -1
- package/package.json +1 -1
- package/src/ts/client/grapesjs/index.ts +2 -0
- package/src/ts/client/grapesjs/settings.ts +0 -3
- package/src/ts/client/grapesjs/storage.ts +15 -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 +268 -120
|
@@ -65,6 +65,7 @@ import { storagePlugin } from './storage'
|
|
|
65
65
|
import { API_PATH, API_WEBSITE_ASSETS_WRITE, API_WEBSITE_PATH } from '../../constants'
|
|
66
66
|
import { ClientConfig } from '../config'
|
|
67
67
|
import { titleCase } from '../utils'
|
|
68
|
+
import uploadProgress from './upload-progress'
|
|
68
69
|
|
|
69
70
|
const plugins = [
|
|
70
71
|
{name: './project-bar', value: projectBarPlugin}, // has to be before panels and dialogs
|
|
@@ -100,6 +101,7 @@ const plugins = [
|
|
|
100
101
|
{name: './resize-panel', value: resizePanelPlugin},
|
|
101
102
|
{name: '@silexlabs/grapesjs-notifications', value: notificationsPlugin},
|
|
102
103
|
{name: '@silexlabs/grapesjs-keymaps-dialog', value: keymapsDialogPlugin},
|
|
104
|
+
{name: './upload-progress', value: uploadProgress},
|
|
103
105
|
]
|
|
104
106
|
// Check that all plugins are loaded correctly
|
|
105
107
|
plugins
|
|
@@ -77,9 +77,6 @@ export const settingsDialog = (editor, opts) => {
|
|
|
77
77
|
const model = editor.getModel()
|
|
78
78
|
model.set('settings', data.settings || {})
|
|
79
79
|
model.set('name', data.name)
|
|
80
|
-
editor.once('canvas:frame:load', () => {
|
|
81
|
-
updateDom(editor)
|
|
82
|
-
})
|
|
83
80
|
})
|
|
84
81
|
// Normal way to apply the head content to the DOM
|
|
85
82
|
// fix #1559 Custom <head> code must be reapplied
|
|
@@ -61,8 +61,14 @@ export const storagePlugin = (editor: PublishableEditor) => {
|
|
|
61
61
|
if (data.assets) data.assets = addTempDataToAssetUrl(data.assets, options.id, user.storage.connectorId)
|
|
62
62
|
if (data.styles) data.styles = addTempDataToStyles(data.styles, options.id, user.storage.connectorId)
|
|
63
63
|
//setTimeout(() => progressiveLoadPages(editor, data))
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if (!data.pages) {
|
|
65
|
+
// This happens when the website was just created
|
|
66
|
+
// Let grapesjs create the pages in the frontend
|
|
67
|
+
return data
|
|
68
|
+
} else {
|
|
69
|
+
await progressiveLoadPages(editor, data)
|
|
70
|
+
return {}
|
|
71
|
+
}
|
|
66
72
|
} catch (err) {
|
|
67
73
|
editor.UndoManager.clear()
|
|
68
74
|
if (err.httpStatusCode === 401) {
|
|
@@ -207,6 +213,13 @@ async function progressiveLoadPages(editor: PublishableEditor, data: ProjectData
|
|
|
207
213
|
if (data.styles) editor.setStyle(data.styles)
|
|
208
214
|
if (data.assets) editor.AssetManager.add(data.assets)
|
|
209
215
|
|
|
216
|
+
await nextFrame()
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
// FIXME: Why is this setTimeout needed?
|
|
219
|
+
editor.trigger('storage:end:load', data)
|
|
220
|
+
editor.trigger('canvas:frame:load', editor)
|
|
221
|
+
})
|
|
222
|
+
|
|
210
223
|
// Sélectionner la première page
|
|
211
224
|
const firstPage = editor.Pages.getAll()[0]
|
|
212
225
|
if (firstPage) editor.Pages.select(firstPage)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Editor } from 'grapesjs'
|
|
2
|
+
|
|
3
|
+
export default function pluginUploadProgress(editor: Editor) {
|
|
4
|
+
const assetManager = editor.AssetManager
|
|
5
|
+
const barIntervals = new WeakMap<HTMLDivElement, number>()
|
|
6
|
+
|
|
7
|
+
editor.on('asset:upload:start', () => {
|
|
8
|
+
const header = document.querySelector('.gjs-am-assets-header') as HTMLDivElement | null
|
|
9
|
+
if (!header || header.querySelector('.gjs-upload-bar')) return
|
|
10
|
+
|
|
11
|
+
const bar = document.createElement('div')
|
|
12
|
+
bar.className = 'gjs-upload-bar'
|
|
13
|
+
bar.style.cssText = `
|
|
14
|
+
height: 4px;
|
|
15
|
+
background: #28a745;
|
|
16
|
+
width: 0%;
|
|
17
|
+
margin-top: 4px;
|
|
18
|
+
transition: width 0.2s ease;
|
|
19
|
+
`
|
|
20
|
+
header.appendChild(bar)
|
|
21
|
+
|
|
22
|
+
let percent = 0
|
|
23
|
+
const interval = setInterval(() => {
|
|
24
|
+
percent = Math.min(percent + Math.random() * 10, 95)
|
|
25
|
+
bar.style.width = `${percent}%`
|
|
26
|
+
}, 200)
|
|
27
|
+
|
|
28
|
+
barIntervals.set(bar, interval as unknown as number)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const cleanup = (success = true) => {
|
|
32
|
+
const bar = document.querySelector('.gjs-upload-bar') as HTMLDivElement | null
|
|
33
|
+
if (!bar) return
|
|
34
|
+
|
|
35
|
+
const interval = barIntervals.get(bar)
|
|
36
|
+
if (interval) {
|
|
37
|
+
clearInterval(interval)
|
|
38
|
+
barIntervals.delete(bar)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!success) bar.style.background = '#dc3545'
|
|
42
|
+
bar.style.width = '100%'
|
|
43
|
+
setTimeout(() => bar.remove(), success ? 1000 : 2000)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
editor.on('asset:upload:end', () => cleanup(true))
|
|
47
|
+
editor.on('asset:upload:error', () => cleanup(false))
|
|
48
|
+
}
|
package/src/ts/constants.ts
CHANGED
|
@@ -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
|
|
@@ -35,12 +36,12 @@ export interface GitlabOptions {
|
|
|
35
36
|
clientSecret: string
|
|
36
37
|
branch: string
|
|
37
38
|
assetsFolder: string
|
|
38
|
-
//metaRepo: string
|
|
39
|
-
//metaRepoFile: string
|
|
40
39
|
repoPrefix: string
|
|
41
40
|
scope: string
|
|
42
41
|
domain: string
|
|
43
42
|
timeOut: number
|
|
43
|
+
//metaRepo: string
|
|
44
|
+
//metaRepoFile: string
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export interface GitlabToken {
|
|
@@ -112,16 +113,26 @@ interface GitlabFetchCommits {
|
|
|
112
113
|
since: string
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
interface
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
createdAt: string,
|
|
120
|
-
updatedAt: string,
|
|
121
|
-
}
|
|
122
|
-
}
|
|
116
|
+
interface GitlabPage {
|
|
117
|
+
id: string
|
|
118
|
+
name: string
|
|
119
|
+
isFile: true
|
|
123
120
|
}
|
|
124
121
|
|
|
122
|
+
interface GitlabWebsiteData {
|
|
123
|
+
pages: GitlabPage[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// interface MetaRepoFileContent {
|
|
127
|
+
// websites: {
|
|
128
|
+
// [websiteId: string]: {
|
|
129
|
+
// meta: WebsiteMetaFileContent,
|
|
130
|
+
// createdAt: string,
|
|
131
|
+
// updatedAt: string,
|
|
132
|
+
// }
|
|
133
|
+
// }
|
|
134
|
+
// }
|
|
135
|
+
|
|
125
136
|
const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
126
137
|
<svg
|
|
127
138
|
xmlns:svg="http://www.w3.org/2000/svg"
|
|
@@ -200,16 +211,18 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
200
211
|
scope: 'api', // 'api+read_api+read_user+read_repository+write_repository+email+sudo+profile+openid'
|
|
201
212
|
...opts,
|
|
202
213
|
} 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 */
|
|
214
|
+
if (!this.options.clientId) throw new Error('Missing Gitlab client ID')
|
|
215
|
+
if (!this.options.clientSecret) throw new Error('Missing Gitlab client secret')
|
|
216
|
+
if (!this.options.domain) throw new Error('Missing Gitlab domain')
|
|
217
|
+
if (!this.options.timeOut) this.options.timeOut = 15000 /* default value */
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
// **
|
|
210
221
|
// Convenience methods for the Gitlab API
|
|
211
|
-
private getAssetPath(path: string): string {
|
|
212
|
-
|
|
222
|
+
private getAssetPath(path: string, encode = true): string {
|
|
223
|
+
const resolvedPath = join(this.options.assetsFolder, path)
|
|
224
|
+
if (encode) return encodeURIComponent(resolvedPath)
|
|
225
|
+
return resolvedPath
|
|
213
226
|
}
|
|
214
227
|
|
|
215
228
|
isUsingOfficialInstance(): boolean {
|
|
@@ -244,51 +257,11 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
244
257
|
}
|
|
245
258
|
|
|
246
259
|
async readFile(session: GitlabSession, websiteId: string, fileName: string): Promise<Buffer> {
|
|
260
|
+
// Remove leading slash
|
|
247
261
|
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
|
|
262
|
+
return this.downloadRawFile(session, websiteId, safePath)
|
|
263
263
|
}
|
|
264
264
|
|
|
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
265
|
/**
|
|
293
266
|
* Call the Gitlab API with the user's token and handle errors
|
|
294
267
|
*/
|
|
@@ -301,7 +274,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
301
274
|
'Content-Type': 'application/json',
|
|
302
275
|
}
|
|
303
276
|
if (method === 'GET' && body) {
|
|
304
|
-
console.error('Gitlab API error (4) - GET request with body', {url, method, body, params})
|
|
277
|
+
console.error('Gitlab API error (4) - GET request with body', { url, method, body, params })
|
|
305
278
|
}
|
|
306
279
|
// With or without body
|
|
307
280
|
let response
|
|
@@ -318,11 +291,17 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
318
291
|
})
|
|
319
292
|
} catch (e) {
|
|
320
293
|
console.error('Gitlab API error (0)', e)
|
|
321
|
-
throw new ApiError(`Gitlab API error (0): ${e.message} ${e.code} ${e.name} ${e.type}`, 500)
|
|
294
|
+
throw new ApiError(`Gitlab API error (0): ${e.message} ${e.text} ${e.code} ${e.name} ${e.type}`, 500)
|
|
322
295
|
}
|
|
323
|
-
let json: { message: string, error: string } | any
|
|
324
296
|
// Handle the case when the server returns a non-JSON response (e.g. 400 Bad Request)
|
|
325
|
-
const text = await
|
|
297
|
+
const text = await async function () {
|
|
298
|
+
try {
|
|
299
|
+
return await response.text()
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.error('Gitlab API error (6) - could not parse response', response.status, response.statusText, { url, method, body, params }, e)
|
|
302
|
+
throw new ApiError(`Gitlab API error (6): response body not available. ${e.message}`, 500)
|
|
303
|
+
}
|
|
304
|
+
}()
|
|
326
305
|
if (!response.ok) {
|
|
327
306
|
if (text.includes('A file with this name doesn\'t exist')) {
|
|
328
307
|
throw new ApiError('Gitlab API error (5): Not Found', 404)
|
|
@@ -354,7 +333,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
354
333
|
return await this.callApi(session, path, method, body as any, params)
|
|
355
334
|
} else {
|
|
356
335
|
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)
|
|
336
|
+
console.error('Gitlab API error (2) - could not refresh token', response.status, response.statusText, { message }, 'refresh_token:', token?.refresh_token)
|
|
358
337
|
// Workaround for when the token is invalid
|
|
359
338
|
// It happens often which is not normal (refresh token should last 6 months)
|
|
360
339
|
this.logout(session)
|
|
@@ -362,26 +341,104 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
362
341
|
throw new ApiError(`Gitlab API error (2): ${message}`, response.status)
|
|
363
342
|
}
|
|
364
343
|
} 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)
|
|
344
|
+
const message = response.statusText
|
|
345
|
+
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body, params, text: text, message })
|
|
346
|
+
throw new ApiError(`Gitlab API error (1): ${message} (${text})`, response.status)
|
|
368
347
|
}
|
|
369
348
|
}
|
|
349
|
+
let json: { message: string, error: string } | any
|
|
370
350
|
try {
|
|
371
351
|
json = JSON.parse(text)
|
|
372
352
|
} catch (e) {
|
|
373
|
-
if(!response.ok) {
|
|
353
|
+
if (!response.ok) {
|
|
374
354
|
// A real error
|
|
375
355
|
throw e
|
|
376
356
|
} else {
|
|
377
357
|
// 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})
|
|
358
|
+
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, { url, method, body, params, text: text })
|
|
379
359
|
return text
|
|
380
360
|
}
|
|
381
361
|
}
|
|
382
362
|
return json
|
|
383
363
|
}
|
|
384
364
|
|
|
365
|
+
// Cache for projects path
|
|
366
|
+
private projectPathCache = new Map<string, string>()
|
|
367
|
+
|
|
368
|
+
async downloadRawFile(session: GitlabSession, projectId: string, filePath: string): Promise<Buffer> {
|
|
369
|
+
const token = this.getSessionToken(session).token?.access_token
|
|
370
|
+
const domain = this.options.domain
|
|
371
|
+
const branch = this.options.branch
|
|
372
|
+
|
|
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
|
+
// Construct the raw URL
|
|
408
|
+
// GET /projects/:id/repository/files/:file_path/raw
|
|
409
|
+
const rawUrl = `${domain}/api/v4/projects/${encodeURIComponent(pathWithNamespace!)}/repository/files/${encodeURIComponent(filePath)}/raw?ref=${branch}&access_token=${token}`
|
|
410
|
+
const fileRes = await fetch(rawUrl, {
|
|
411
|
+
agent: this.getAgent(),
|
|
412
|
+
})
|
|
413
|
+
console.log('fileRes', fileRes.status, fileRes.statusText, { rawUrl, token })
|
|
414
|
+
|
|
415
|
+
const contentType = fileRes.headers.get('content-type')
|
|
416
|
+
if (contentType?.includes('text/html')) {
|
|
417
|
+
const html = await fileRes.text()
|
|
418
|
+
throw new ApiError('GitLab returned HTML instead of file (unauthorized or not found).', 401)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!fileRes.ok) {
|
|
422
|
+
const errText = await fileRes.text()
|
|
423
|
+
if (errText.includes('not found') || fileRes.status === 404) {
|
|
424
|
+
throw new ApiError('GitLab raw error (5): Not Found', 404)
|
|
425
|
+
}
|
|
426
|
+
console.error('GitLab raw error (1)', fileRes.status, fileRes.statusText, { rawUrl, errText })
|
|
427
|
+
throw new ApiError(`GitLab raw error (1): ${fileRes.statusText} (${errText})`, fileRes.status)
|
|
428
|
+
}
|
|
429
|
+
console.log('fileRes ok', fileRes.status, fileRes.statusText)
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
console.log('fileRes buffer', fileRes.headers.get('content-type'), fileRes.headers.get('content-length'))
|
|
433
|
+
const buffer = await fileRes.buffer()
|
|
434
|
+
console.log('fileRes buffer', buffer.length)
|
|
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,40 +834,13 @@ 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
846
|
async writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback | undefined): Promise<void> {
|
|
@@ -746,8 +867,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
746
867
|
}
|
|
747
868
|
|
|
748
869
|
async readAsset(session: GitlabSession, websiteId: string, fileName: string): Promise<ConnectorFileContent> {
|
|
749
|
-
|
|
750
|
-
const finalPath = this.getAssetPath(fileName)
|
|
870
|
+
const finalPath = this.getAssetPath(fileName, false)
|
|
751
871
|
return this.readFile(session, websiteId, finalPath)
|
|
752
872
|
}
|
|
753
873
|
|
|
@@ -762,4 +882,32 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
762
882
|
})),
|
|
763
883
|
})
|
|
764
884
|
}
|
|
885
|
+
|
|
886
|
+
/*
|
|
887
|
+
* Get the meta repo path for the current user
|
|
888
|
+
* The meta repo contains a JSON file which contains the list of websites
|
|
889
|
+
*/
|
|
890
|
+
//private getMetaRepoPath(session: GitlabSession): string {
|
|
891
|
+
// if(!this.getSessionToken(session).username) throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
|
|
892
|
+
// return encodeURIComponent(`${this.getSessionToken(session).username}/${this.options.metaRepo}`)
|
|
893
|
+
//}
|
|
894
|
+
|
|
895
|
+
///**
|
|
896
|
+
// * Initialize the storage with a meta repo
|
|
897
|
+
// */
|
|
898
|
+
//private async initStorage(session: GitlabSession): Promise<void> {
|
|
899
|
+
// // Create the meta repo
|
|
900
|
+
// try {
|
|
901
|
+
// const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
|
|
902
|
+
// name: this.options.metaRepo,
|
|
903
|
+
// }) as any
|
|
904
|
+
// return this.createFile(session, this.getMetaRepoPath(session), this.options.metaRepoFile, JSON.stringify({
|
|
905
|
+
// websites: {}
|
|
906
|
+
// } as MetaRepoFileContent))
|
|
907
|
+
// } catch (e) {
|
|
908
|
+
// console.error('Could not init storage', e.statusCode, e.httpStatusCode, e)
|
|
909
|
+
// throw e
|
|
910
|
+
// }
|
|
911
|
+
//}
|
|
912
|
+
|
|
765
913
|
}
|