@silexlabs/silex 3.3.1-3 → 3.3.1-5
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/README.md +27 -2
- package/dist/client/css/admin.css +26 -9
- package/dist/client/index.html +3 -3
- package/dist/client/js/main.js +285 -217
- package/dist/client/js/main.js.LICENSE.txt +2 -13
- package/dist/client/page.d.ts +1 -1
- package/dist/client/types.d.ts +1 -17
- package/dist/plugins/client/types.d.ts +1 -17
- package/dist/plugins/client/types.js.map +1 -1
- package/dist/plugins/server/page.d.ts +1 -1
- package/dist/plugins/server/page.js.map +1 -1
- package/dist/plugins/server/plugins/server/FtpConnector.js +53 -8
- package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
- package/dist/plugins/server/plugins/server/GitlabConnector.js +106 -85
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/plugins/server/server/connectors/FsStorage.js +50 -4
- package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
- package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +20 -0
- package/dist/plugins/server/server/utils/websiteDataSerialization.js +127 -0
- package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -0
- package/dist/plugins/server/types.d.ts +1 -17
- package/dist/plugins/server/types.js.map +1 -1
- package/dist/server/page.js +14 -0
- package/dist/server/server/cli.js +0 -0
- package/dist/server/server/connectors/FsStorage.js +40 -4
- package/dist/server/server/utils/websiteDataSerialization.js +85 -0
- package/package.json +13 -13
- package/src/css/_variables.scss +4 -0
- package/src/css/grapesjs-plugins.scss +5 -0
- package/src/css/grapesjs.scss +8 -2
- package/src/css/loader.scss +9 -6
- package/src/css/page-panel.scss +3 -2
- package/src/css/project-bar.scss +0 -1
- package/src/html/index.pug +1 -1
- package/src/ts/client/assetUrl.ts +1 -1
- package/src/ts/client/grapesjs/PublicationUi.ts +1 -1
- package/src/ts/client/grapesjs/css-props.ts +4 -3
- package/src/ts/client/grapesjs/flex.ts +8 -4
- package/src/ts/client/grapesjs/img.ts +9 -3
- package/src/ts/client/grapesjs/index.ts +9 -11
- package/src/ts/client/grapesjs/li.ts +3 -1
- package/src/ts/client/grapesjs/sectors.ts +3 -1
- package/src/ts/client/grapesjs/storage.ts +25 -30
- package/src/ts/client/index.ts +2 -3
- package/src/ts/page.ts +1 -1
- package/src/ts/plugins/server/FtpConnector.test.ts +2 -1
- package/src/ts/plugins/server/FtpConnector.ts +61 -9
- package/src/ts/plugins/server/GitlabConnector.ts +112 -102
- package/src/ts/server/connectors/FsStorage.test.ts +6 -2
- package/src/ts/server/connectors/FsStorage.ts +60 -5
- package/src/ts/server/utils/websiteDataSerialization.test.ts +59 -0
- package/src/ts/server/utils/websiteDataSerialization.ts +143 -0
- package/src/ts/types.ts +2 -20
- package/dist/client/css/admin.css.map +0 -1
- package/dist/client/grapesjs-filter-styles/dist/index.d.ts +0 -4
- package/dist/client/grapesjs-fonts/dist/index.d.ts +0 -4
- package/dist/client/grapesjs-fonts/src/commands.d.ts +0 -3
- package/dist/client/grapesjs-fonts/src/fonts.d.ts +0 -4
- package/dist/client/grapesjs-storage-rate-limit/dist/index.d.ts +0 -4
- package/dist/client/grapesjs-ui-suggest-classes/dist/index.d.ts +0 -4
- package/dist/client/silex-lib/src/ts/client/api.d.ts +0 -90
- package/dist/client/silex-lib/src/ts/client/assetUrl.d.ts +0 -46
- package/dist/client/silex-lib/src/ts/client/config.d.ts +0 -102
- package/dist/client/silex-lib/src/ts/client/events.d.ts +0 -23
- package/dist/client/silex-lib/src/ts/client/expose.d.ts +0 -8
- package/dist/client/silex-lib/src/ts/client/grapesjs/LoginDialog.d.ts +0 -15
- package/dist/client/silex-lib/src/ts/client/grapesjs/PublicationManager.d.ts +0 -74
- package/dist/client/silex-lib/src/ts/client/grapesjs/PublicationUi.d.ts +0 -59
- package/dist/client/silex-lib/src/ts/client/grapesjs/blocks.d.ts +0 -1
- package/dist/client/silex-lib/src/ts/client/grapesjs/breadcrumbs.d.ts +0 -1
- package/dist/client/silex-lib/src/ts/client/grapesjs/css-props.d.ts +0 -6
- package/dist/client/silex-lib/src/ts/client/grapesjs/flex.d.ts +0 -7
- package/dist/client/silex-lib/src/ts/client/grapesjs/footer.d.ts +0 -2
- package/dist/client/silex-lib/src/ts/client/grapesjs/img.d.ts +0 -6
- package/dist/client/silex-lib/src/ts/client/grapesjs/index.d.ts +0 -9
- package/dist/client/silex-lib/src/ts/client/grapesjs/internal-links.d.ts +0 -1
- package/dist/client/silex-lib/src/ts/client/grapesjs/keymaps.d.ts +0 -123
- package/dist/client/silex-lib/src/ts/client/grapesjs/li.d.ts +0 -7
- package/dist/client/silex-lib/src/ts/client/grapesjs/new-page-dialog.d.ts +0 -2
- package/dist/client/silex-lib/src/ts/client/grapesjs/openImport.d.ts +0 -9
- package/dist/client/silex-lib/src/ts/client/grapesjs/page-panel.d.ts +0 -9
- package/dist/client/silex-lib/src/ts/client/grapesjs/project-bar.d.ts +0 -21
- package/dist/client/silex-lib/src/ts/client/grapesjs/resize-panel.d.ts +0 -1
- package/dist/client/silex-lib/src/ts/client/grapesjs/rich-text.d.ts +0 -4
- package/dist/client/silex-lib/src/ts/client/grapesjs/scroll.d.ts +0 -7
- package/dist/client/silex-lib/src/ts/client/grapesjs/sectors.d.ts +0 -11
- package/dist/client/silex-lib/src/ts/client/grapesjs/semantic.d.ts +0 -1
- package/dist/client/silex-lib/src/ts/client/grapesjs/settings-sections.d.ts +0 -16
- package/dist/client/silex-lib/src/ts/client/grapesjs/settings.d.ts +0 -4
- package/dist/client/silex-lib/src/ts/client/grapesjs/storage.d.ts +0 -3
- package/dist/client/silex-lib/src/ts/client/grapesjs/symbolDialogs.d.ts +0 -3
- package/dist/client/silex-lib/src/ts/client/grapesjs/upload-progress.d.ts +0 -2
- package/dist/client/silex-lib/src/ts/client/grapesjs/view-buttons.d.ts +0 -1
- package/dist/client/silex-lib/src/ts/client/index.d.ts +0 -10
- package/dist/client/silex-lib/src/ts/client/publication-transformers.d.ts +0 -42
- package/dist/client/silex-lib/src/ts/client/publish-custom-code-block.d.ts +0 -4
- package/dist/client/silex-lib/src/ts/client/publish-fonts.d.ts +0 -4
- package/dist/client/silex-lib/src/ts/client/utils.d.ts +0 -22
- package/dist/client/silex-lib/src/ts/constants.d.ts +0 -33
- package/dist/client/silex-lib/src/ts/page.d.ts +0 -2
- package/dist/client/silex-lib/src/ts/types.d.ts +0 -341
- package/src/ts/client/grapesjs/dynamic-plugins.ts +0 -63
- package/src/ts/client/grapesjs/plugin-options.ts +0 -243
|
@@ -18,7 +18,7 @@
|
|
|
18
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
|
|
21
|
+
import { ApiError, ConnectorType, ConnectorUser, WebsiteData, WebsiteId, WebsiteMeta, WebsiteMetaFileContent, JobStatus } from '../../types'
|
|
22
22
|
import fetch from 'node-fetch'
|
|
23
23
|
import crypto, { createHash } from 'crypto'
|
|
24
24
|
import { join } from 'path'
|
|
@@ -26,6 +26,8 @@ import { Agent } from 'https'
|
|
|
26
26
|
import { getPageSlug } from '../../page'
|
|
27
27
|
import e from 'express'
|
|
28
28
|
import { fork } from 'child_process'
|
|
29
|
+
import { Page } from 'grapesjs'
|
|
30
|
+
import { stringify, split, merge } from '../../server/utils/websiteDataSerialization'
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* Gitlab connector
|
|
@@ -120,16 +122,6 @@ interface GitlabFetchCommits {
|
|
|
120
122
|
since: string
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
interface GitlabPage {
|
|
124
|
-
id: string
|
|
125
|
-
name: string
|
|
126
|
-
isFile: true
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
interface GitlabWebsiteData {
|
|
130
|
-
fileFormatVersion: string
|
|
131
|
-
pages: GitlabPage[]
|
|
132
|
-
}
|
|
133
125
|
|
|
134
126
|
// interface MetaRepoFileContent {
|
|
135
127
|
// websites: {
|
|
@@ -220,6 +212,7 @@ function sanitizeGitlabPath(name: string): string {
|
|
|
220
212
|
.toLowerCase()
|
|
221
213
|
}
|
|
222
214
|
|
|
215
|
+
|
|
223
216
|
export default class GitlabConnector implements StorageConnector {
|
|
224
217
|
connectorId = 'gitlab'
|
|
225
218
|
connectorType = ConnectorType.STORAGE
|
|
@@ -327,15 +320,21 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
327
320
|
'Content-Type': 'application/json',
|
|
328
321
|
}
|
|
329
322
|
if (method === 'GET' && requestBody) {
|
|
330
|
-
console.error('
|
|
323
|
+
console.error('[GitlabConnector] Invalid GET request: GET requests should not have a body', { url, method, body: requestBody, params })
|
|
331
324
|
}
|
|
332
325
|
// With or without body
|
|
333
|
-
let response
|
|
326
|
+
let response: Response
|
|
334
327
|
const body = requestBody ? JSON.stringify(requestBody) : undefined
|
|
335
328
|
try {
|
|
336
|
-
if(body && Buffer.byteLength(body) > MAX_BODY_SIZE_KB * 1024) {
|
|
337
|
-
|
|
338
|
-
|
|
329
|
+
if (body && Buffer.byteLength(body) > MAX_BODY_SIZE_KB * 1024) {
|
|
330
|
+
console.warn('[GitlabConnector] Request body size exceeds Gitlab API limit', {
|
|
331
|
+
size: Buffer.byteLength(body),
|
|
332
|
+
maxAllowed: MAX_BODY_SIZE_KB * 1024,
|
|
333
|
+
url,
|
|
334
|
+
method,
|
|
335
|
+
params,
|
|
336
|
+
session,
|
|
337
|
+
})
|
|
339
338
|
}
|
|
340
339
|
response = await fetch(url, requestBody && method !== 'GET' ? {
|
|
341
340
|
agent: this.getAgent(),
|
|
@@ -348,8 +347,16 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
348
347
|
headers,
|
|
349
348
|
})
|
|
350
349
|
} catch (e) {
|
|
351
|
-
console.error('Gitlab API
|
|
352
|
-
|
|
350
|
+
console.error('[GitlabConnector] Failed to reach Gitlab API endpoint', {
|
|
351
|
+
error: e,
|
|
352
|
+
url,
|
|
353
|
+
method,
|
|
354
|
+
body: requestBody,
|
|
355
|
+
params,
|
|
356
|
+
session,
|
|
357
|
+
stack: e?.stack || new Error().stack,
|
|
358
|
+
})
|
|
359
|
+
throw new ApiError(`Could not reach Gitlab API: ${e.message || e}`, 500)
|
|
353
360
|
}
|
|
354
361
|
// Pass the response headers to the caller
|
|
355
362
|
response.headers.forEach((value, name) => responseHeaders[name] = value)
|
|
@@ -358,14 +365,30 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
358
365
|
try {
|
|
359
366
|
return await response.text()
|
|
360
367
|
} catch (e) {
|
|
361
|
-
console.error('
|
|
362
|
-
|
|
368
|
+
console.error('[GitlabConnector] Failed to read response body from Gitlab API', {
|
|
369
|
+
status: response.status,
|
|
370
|
+
statusText: response.statusText,
|
|
371
|
+
url,
|
|
372
|
+
method,
|
|
373
|
+
body: requestBody,
|
|
374
|
+
params,
|
|
375
|
+
error: e
|
|
376
|
+
})
|
|
377
|
+
throw new ApiError(`Gitlab API: Could not read response body (${e.message})`, 500)
|
|
363
378
|
}
|
|
364
379
|
}()
|
|
365
380
|
if (!response.ok) {
|
|
366
|
-
console.error('Gitlab API
|
|
381
|
+
console.error('[GitlabConnector] Gitlab API responded with error status', {
|
|
382
|
+
status: response.status,
|
|
383
|
+
statusText: response.statusText,
|
|
384
|
+
url,
|
|
385
|
+
method,
|
|
386
|
+
body: requestBody,
|
|
387
|
+
params,
|
|
388
|
+
responseText: text
|
|
389
|
+
})
|
|
367
390
|
if (text.includes('A file with this name doesn\'t exist')) {
|
|
368
|
-
throw new ApiError('Gitlab API
|
|
391
|
+
throw new ApiError('Gitlab API: File not found', 404)
|
|
369
392
|
} else if (response.status === 401 && this.getSessionToken(session).token?.refresh_token) {
|
|
370
393
|
// Refresh the token
|
|
371
394
|
const token = this.getSessionToken(session).token
|
|
@@ -401,17 +424,28 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
401
424
|
})
|
|
402
425
|
} else {
|
|
403
426
|
const message = typeof refreshJson?.message === 'object' ? Object.entries(refreshJson.message).map(entry => entry.join(' ')).join(' ') : refreshJson?.message ?? refreshJson?.error ?? response.statusText
|
|
404
|
-
console.error('
|
|
405
|
-
|
|
406
|
-
|
|
427
|
+
console.error('[GitlabConnector] Failed to refresh Gitlab OAuth token', {
|
|
428
|
+
status: response.status,
|
|
429
|
+
statusText: response.statusText,
|
|
430
|
+
message,
|
|
431
|
+
refresh_token: token?.refresh_token
|
|
432
|
+
})
|
|
407
433
|
this.logout(session)
|
|
408
|
-
|
|
409
|
-
throw new ApiError(`Gitlab API error (2): ${message}`, response.status)
|
|
434
|
+
throw new ApiError(`Gitlab API: Could not refresh token (${message})`, response.status)
|
|
410
435
|
}
|
|
411
436
|
} else {
|
|
412
437
|
const message = response.statusText
|
|
413
|
-
console.error('Gitlab API error
|
|
414
|
-
|
|
438
|
+
console.error('[GitlabConnector] Unhandled Gitlab API error response', {
|
|
439
|
+
status: response.status,
|
|
440
|
+
statusText: response.statusText,
|
|
441
|
+
url,
|
|
442
|
+
method,
|
|
443
|
+
body: requestBody,
|
|
444
|
+
params,
|
|
445
|
+
responseText: text,
|
|
446
|
+
message
|
|
447
|
+
})
|
|
448
|
+
throw new ApiError(`Gitlab API error: ${message} (${text})`, response.status)
|
|
415
449
|
}
|
|
416
450
|
}
|
|
417
451
|
let json: { message: string, error: string } | any
|
|
@@ -419,11 +453,19 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
419
453
|
json = JSON.parse(text)
|
|
420
454
|
} catch (e) {
|
|
421
455
|
if (!response.ok) {
|
|
422
|
-
// A real error
|
|
423
456
|
throw e
|
|
424
457
|
} else {
|
|
425
|
-
|
|
426
|
-
|
|
458
|
+
console.error('[GitlabConnector] Response from Gitlab API is not valid JSON', {
|
|
459
|
+
statusText: response.statusText,
|
|
460
|
+
url,
|
|
461
|
+
method,
|
|
462
|
+
body: requestBody,
|
|
463
|
+
params,
|
|
464
|
+
responseText: text,
|
|
465
|
+
error: e,
|
|
466
|
+
session,
|
|
467
|
+
stack: e?.stack || new Error().stack,
|
|
468
|
+
})
|
|
427
469
|
return text
|
|
428
470
|
}
|
|
429
471
|
}
|
|
@@ -451,18 +493,30 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
451
493
|
if (!fileRes.ok) {
|
|
452
494
|
const errText = await fileRes.text()
|
|
453
495
|
if (errText.includes('not found') || fileRes.status === 404) {
|
|
454
|
-
|
|
496
|
+
console.error('[GitlabConnector] GitLab raw file not found', {
|
|
497
|
+
status: fileRes.status,
|
|
498
|
+
statusText: fileRes.statusText,
|
|
499
|
+
url: rawUrl,
|
|
500
|
+
responseText: errText
|
|
501
|
+
})
|
|
502
|
+
throw new ApiError(`GitLab raw file not found: filePath="${filePath}" (status ${fileRes.status})`, 404)
|
|
455
503
|
}
|
|
456
|
-
console.error('
|
|
457
|
-
|
|
504
|
+
console.error('[GitlabConnector] Failed to fetch raw file from GitLab', {
|
|
505
|
+
filePath,
|
|
506
|
+
url: rawUrl,
|
|
507
|
+
status: fileRes.status,
|
|
508
|
+
statusText: fileRes.statusText,
|
|
509
|
+
responseText: errText
|
|
510
|
+
})
|
|
511
|
+
throw new ApiError(`Failed to fetch raw file from GitLab: filePath="${filePath}" - ${fileRes.statusText} (${errText})`, fileRes.status)
|
|
458
512
|
}
|
|
459
513
|
|
|
460
514
|
try {
|
|
461
515
|
const buffer = await fileRes.buffer()
|
|
462
516
|
return buffer
|
|
463
517
|
} catch (e) {
|
|
464
|
-
console.error('
|
|
465
|
-
throw new ApiError('
|
|
518
|
+
console.error('[GitlabConnector] Error reading binary content from GitLab raw file', e)
|
|
519
|
+
throw new ApiError('Failed to read binary content from GitLab raw file', 500)
|
|
466
520
|
}
|
|
467
521
|
}
|
|
468
522
|
|
|
@@ -673,39 +727,15 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
673
727
|
*/
|
|
674
728
|
async readWebsite(session: GitlabSession, websiteId: string): Promise<WebsiteData> {
|
|
675
729
|
const websiteDataBuf = await this.downloadRawFile(session, websiteId, WEBSITE_DATA_FILE)
|
|
676
|
-
const
|
|
677
|
-
const { fileFormatVersion, ...websiteData } = websiteDataOb
|
|
678
|
-
|
|
679
|
-
// Check the file format version
|
|
680
|
-
if (fileFormatVersion !== WEBSITE_DATA_FILE_FORMAT_VERSION) {
|
|
681
|
-
// This should be handled by a migration mechanism
|
|
682
|
-
console.warn('Gitlab connector: website data file format version mismatch', fileFormatVersion, '!=', WEBSITE_DATA_FILE_FORMAT_VERSION)
|
|
683
|
-
}
|
|
730
|
+
const websiteDataContent = websiteDataBuf.toString('utf8')
|
|
684
731
|
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
return
|
|
732
|
+
// Use the common merge function to reconstruct website data
|
|
733
|
+
const pageLoader = async (pagePath: string): Promise<string> => {
|
|
734
|
+
const pageBuffer = await this.downloadRawFile(session, websiteId, pagePath)
|
|
735
|
+
return pageBuffer.toString('utf8')
|
|
689
736
|
}
|
|
690
737
|
|
|
691
|
-
|
|
692
|
-
const pages = await Promise.all(websiteData.pages.map(async (page: GitlabPage | Page) => {
|
|
693
|
-
if ((page as GitlabPage).isFile) {
|
|
694
|
-
const name = getPageSlug(page.name)
|
|
695
|
-
const fileName = (`${name}-${page.id}`)
|
|
696
|
-
const filePath = `${WEBSITE_PAGES_FOLDER}/${fileName}.json`
|
|
697
|
-
const pageContent = await this.downloadRawFile(session, websiteId, filePath)
|
|
698
|
-
const res = JSON.parse(pageContent.toString('utf8')) as Page
|
|
699
|
-
return res
|
|
700
|
-
}
|
|
701
|
-
return page as Page
|
|
702
|
-
}))
|
|
703
|
-
|
|
704
|
-
// Read each page file if needed
|
|
705
|
-
return {
|
|
706
|
-
...websiteData,
|
|
707
|
-
pages,
|
|
708
|
-
} as WebsiteData
|
|
738
|
+
return await merge(websiteDataContent, pageLoader)
|
|
709
739
|
}
|
|
710
740
|
|
|
711
741
|
/**
|
|
@@ -720,7 +750,7 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
720
750
|
name: this.options.repoPrefix + websiteMeta.name,
|
|
721
751
|
}
|
|
722
752
|
}) as any
|
|
723
|
-
await this.createFile(session, project.id, WEBSITE_DATA_FILE,
|
|
753
|
+
await this.createFile(session, project.id, WEBSITE_DATA_FILE, stringify({} as WebsiteData))
|
|
724
754
|
//await this.createFile(session, project.id, WEBSITE_META_DATA_FILE, JSON.stringify(websiteMeta))
|
|
725
755
|
//await this.updateWebsite(session, project.id, {} as WebsiteData)
|
|
726
756
|
//await this.setWebsiteMeta(session, project.id, websiteMeta)
|
|
@@ -745,16 +775,19 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
745
775
|
})
|
|
746
776
|
|
|
747
777
|
// **
|
|
748
|
-
//
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
778
|
+
// Use the common split function to create files
|
|
779
|
+
const filesToWrite = split(websiteData)
|
|
780
|
+
|
|
781
|
+
// **
|
|
782
|
+
// Process each file to create/update
|
|
783
|
+
for (const file of filesToWrite) {
|
|
784
|
+
const filePath = file.path
|
|
785
|
+
const content = file.content
|
|
753
786
|
const newSha = computeGitBlobSha(content, false)
|
|
754
787
|
|
|
755
|
-
const existingSha = existingFiles.get(filePath)
|
|
788
|
+
const existingSha = existingFiles.get(filePath) || (filePath === WEBSITE_DATA_FILE ? 'always-update' : undefined)
|
|
756
789
|
|
|
757
|
-
if (existingSha) {
|
|
790
|
+
if (existingSha && existingSha !== 'always-update') {
|
|
758
791
|
if (existingSha !== newSha) {
|
|
759
792
|
batchActions.push({
|
|
760
793
|
action: 'update',
|
|
@@ -764,26 +797,18 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
764
797
|
} // else: skip unchanged file
|
|
765
798
|
} else {
|
|
766
799
|
batchActions.push({
|
|
767
|
-
action: 'create',
|
|
800
|
+
action: file.path === WEBSITE_DATA_FILE ? 'update' : 'create',
|
|
768
801
|
file_path: filePath,
|
|
769
802
|
content,
|
|
770
803
|
})
|
|
771
804
|
}
|
|
772
|
-
|
|
773
|
-
return {
|
|
774
|
-
name: page.name,
|
|
775
|
-
id: page.id,
|
|
776
|
-
isFile: true,
|
|
777
|
-
}
|
|
778
|
-
})
|
|
805
|
+
}
|
|
779
806
|
|
|
780
807
|
// **
|
|
781
808
|
// Delete pages that are not in the new website data
|
|
809
|
+
const newPageFiles = new Set(filesToWrite.filter(f => f.path.startsWith(WEBSITE_PAGES_FOLDER)).map(f => f.path))
|
|
782
810
|
for (const filePath of existingFiles.keys()) {
|
|
783
|
-
|
|
784
|
-
const pageId = pageName.split('-').pop()
|
|
785
|
-
const page = websiteData.pages.find((p: Page) => p.id === pageId)
|
|
786
|
-
if (!page) {
|
|
811
|
+
if (!newPageFiles.has(filePath)) {
|
|
787
812
|
batchActions.push({
|
|
788
813
|
action: 'delete',
|
|
789
814
|
file_path: filePath,
|
|
@@ -791,21 +816,6 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
791
816
|
}
|
|
792
817
|
}
|
|
793
818
|
|
|
794
|
-
// **
|
|
795
|
-
// Add the main website data file
|
|
796
|
-
const websiteDataWithGitlabPages = {
|
|
797
|
-
...websiteData,
|
|
798
|
-
fileFormatVersion: WEBSITE_DATA_FILE_FORMAT_VERSION,
|
|
799
|
-
pages,
|
|
800
|
-
} as GitlabWebsiteData
|
|
801
|
-
|
|
802
|
-
const websiteJsonContent = JSON.stringify(websiteDataWithGitlabPages)
|
|
803
|
-
batchActions.push({
|
|
804
|
-
action: 'update',
|
|
805
|
-
file_path: WEBSITE_DATA_FILE,
|
|
806
|
-
content: websiteJsonContent,
|
|
807
|
-
})
|
|
808
|
-
|
|
809
819
|
// **
|
|
810
820
|
// Perform a batch commit
|
|
811
821
|
return this.callApi({
|
|
@@ -3,6 +3,7 @@ import { ServerConfig } from '../config'
|
|
|
3
3
|
import { FsStorage } from './FsStorage'
|
|
4
4
|
import { readFileSync, rmdirSync, statSync } from 'fs'
|
|
5
5
|
import { join } from 'path'
|
|
6
|
+
import { WEBSITE_DATA_FILE } from '../../constants'
|
|
6
7
|
|
|
7
8
|
const storageRootPath = '/tmp/silex-tests'
|
|
8
9
|
const assetsFolder = 'assets'
|
|
@@ -45,7 +46,10 @@ describe('FsStorage website', () => {
|
|
|
45
46
|
})
|
|
46
47
|
const id = await connector.createWebsite(dummySession, { name: 'dummy name', connectorUserSettings: {} })
|
|
47
48
|
expect(id).toBeDefined()
|
|
48
|
-
expect(() => statSync(join(storageRootPath, id,
|
|
49
|
-
|
|
49
|
+
expect(() => statSync(join(storageRootPath, id, WEBSITE_DATA_FILE))).not.toThrow()
|
|
50
|
+
const content = readFileSync(join(storageRootPath, id, WEBSITE_DATA_FILE), 'utf8')
|
|
51
|
+
const parsed = JSON.parse(content)
|
|
52
|
+
expect(parsed).toHaveProperty('fileFormatVersion', '1.0.0')
|
|
53
|
+
expect(parsed).toHaveProperty('pages', [])
|
|
50
54
|
})
|
|
51
55
|
})
|
|
@@ -23,10 +23,11 @@ import { ConnectorUser, WebsiteMeta, JobStatus, WebsiteId, ConnectorType, Websit
|
|
|
23
23
|
import { userInfo } from 'os'
|
|
24
24
|
import { requiredParam } from '../utils/validation'
|
|
25
25
|
import { ServerConfig } from '../config'
|
|
26
|
-
import { DEFAULT_WEBSITE_ID, WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE } from '../../constants'
|
|
26
|
+
import { DEFAULT_WEBSITE_ID, WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE, WEBSITE_PAGES_FOLDER } from '../../constants'
|
|
27
27
|
import { Readable } from 'stream'
|
|
28
28
|
import { v4 as uuid } from 'uuid'
|
|
29
29
|
import { fileURLToPath } from 'url'
|
|
30
|
+
import { stringify, split, merge } from '../utils/websiteDataSerialization'
|
|
30
31
|
|
|
31
32
|
// Variables needed for jest tests
|
|
32
33
|
if(!globalThis.__dirname) {
|
|
@@ -145,7 +146,7 @@ export class FsStorage implements StorageConnector<FsSession> {
|
|
|
145
146
|
|
|
146
147
|
async setWebsiteMeta(session: any, id: string, data: WebsiteMetaFileContent): Promise<void> {
|
|
147
148
|
const websiteId = requiredParam<WebsiteId>(id, 'website id')
|
|
148
|
-
const content =
|
|
149
|
+
const content = stringify(data)
|
|
149
150
|
const path = join(this.options.path, id, WEBSITE_META_DATA_FILE)
|
|
150
151
|
await fs.writeFile(path, content)
|
|
151
152
|
}
|
|
@@ -183,14 +184,68 @@ export class FsStorage implements StorageConnector<FsSession> {
|
|
|
183
184
|
async readWebsite(session: FsSession, websiteId: WebsiteId): Promise<WebsiteData> {
|
|
184
185
|
const id = requiredParam<WebsiteId>(websiteId, 'website id')
|
|
185
186
|
const path = join(this.options.path, id, WEBSITE_DATA_FILE)
|
|
187
|
+
|
|
186
188
|
const content = await fs.readFile(path)
|
|
187
|
-
|
|
189
|
+
const websiteDataContent = content.toString()
|
|
190
|
+
|
|
191
|
+
// Check if this is the new split format
|
|
192
|
+
const parsedData = JSON.parse(websiteDataContent)
|
|
193
|
+
if (parsedData.fileFormatVersion === '1.0.0') {
|
|
194
|
+
// Use the merge function to reconstruct website data
|
|
195
|
+
const pageLoader = async (pagePath: string): Promise<string> => {
|
|
196
|
+
const fullPath = join(this.options.path, id, pagePath)
|
|
197
|
+
const pageContent = await fs.readFile(fullPath)
|
|
198
|
+
return pageContent.toString()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return await merge(websiteDataContent, pageLoader)
|
|
202
|
+
} else {
|
|
203
|
+
// Legacy format, return as is
|
|
204
|
+
return parsedData
|
|
205
|
+
}
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
async updateWebsite(session: FsSession, websiteId: WebsiteId, data: WebsiteData): Promise<void> {
|
|
191
209
|
const id = requiredParam<WebsiteId>(websiteId, 'website id')
|
|
192
|
-
const
|
|
193
|
-
|
|
210
|
+
const websitePath = join(this.options.path, id)
|
|
211
|
+
|
|
212
|
+
// Use the split function to create separate files for pages
|
|
213
|
+
const filesToWrite = split(data)
|
|
214
|
+
|
|
215
|
+
// Ensure the website directory exists
|
|
216
|
+
await fs.mkdir(websitePath, { recursive: true })
|
|
217
|
+
|
|
218
|
+
// Ensure the pages directory exists if we have page files
|
|
219
|
+
const hasPageFiles = filesToWrite.some(f => f.path.startsWith(WEBSITE_PAGES_FOLDER))
|
|
220
|
+
if (hasPageFiles) {
|
|
221
|
+
await fs.mkdir(join(websitePath, WEBSITE_PAGES_FOLDER), { recursive: true })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// **
|
|
225
|
+
// Delete pages that are not in the new website data
|
|
226
|
+
const pagesPath = join(websitePath, WEBSITE_PAGES_FOLDER)
|
|
227
|
+
try {
|
|
228
|
+
const existingPageFiles = await fs.readdir(pagesPath)
|
|
229
|
+
const newPageFiles = new Set(
|
|
230
|
+
filesToWrite
|
|
231
|
+
.filter(f => f.path.startsWith(WEBSITE_PAGES_FOLDER))
|
|
232
|
+
.map(f => f.path.replace(`${WEBSITE_PAGES_FOLDER}/`, ''))
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
for (const existingFile of existingPageFiles) {
|
|
236
|
+
if (existingFile.endsWith('.json') && !newPageFiles.has(existingFile)) {
|
|
237
|
+
await fs.unlink(join(pagesPath, existingFile))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// Ignore error if pages directory doesn't exist yet
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Write all files
|
|
245
|
+
for (const file of filesToWrite) {
|
|
246
|
+
const filePath = join(websitePath, file.path)
|
|
247
|
+
await fs.writeFile(filePath, file.content)
|
|
248
|
+
}
|
|
194
249
|
}
|
|
195
250
|
|
|
196
251
|
async deleteWebsite(session: FsSession, websiteId: WebsiteId): Promise<void> {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { stringify, split, merge } from './websiteDataSerialization'
|
|
2
|
+
import { expect, it, describe } from '@jest/globals'
|
|
3
|
+
import { WebsiteData, defaultWebsiteData } from '../../types'
|
|
4
|
+
import { WEBSITE_DATA_FILE } from '../../constants'
|
|
5
|
+
|
|
6
|
+
describe('websiteDataSerialization', () => {
|
|
7
|
+
describe('stringify', () => {
|
|
8
|
+
it('should format JSON with sorted keys', () => {
|
|
9
|
+
const data = { z: 1, a: 2, m: { z: 3, a: 4 } }
|
|
10
|
+
const result = stringify(data)
|
|
11
|
+
expect(result).toBe(`{
|
|
12
|
+
"a": 2,
|
|
13
|
+
"m": {
|
|
14
|
+
"a": 4,
|
|
15
|
+
"z": 3
|
|
16
|
+
},
|
|
17
|
+
"z": 1
|
|
18
|
+
}`)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('split', () => {
|
|
23
|
+
it('should split website data into separate files', () => {
|
|
24
|
+
const websiteData: WebsiteData = {
|
|
25
|
+
...defaultWebsiteData,
|
|
26
|
+
pages: [
|
|
27
|
+
{ id: 'page1', getName: () => 'Home' } as any,
|
|
28
|
+
{ id: 'page2', getName: () => 'About' } as any
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const files = split(websiteData)
|
|
33
|
+
|
|
34
|
+
expect(files).toHaveLength(3)
|
|
35
|
+
expect(files[0].path).toBe('src/home-page1.json')
|
|
36
|
+
expect(files[1].path).toBe('src/about-page2.json')
|
|
37
|
+
expect(files[2].path).toBe(WEBSITE_DATA_FILE)
|
|
38
|
+
|
|
39
|
+
const mainFile = JSON.parse(files[2].content)
|
|
40
|
+
expect(mainFile.fileFormatVersion).toBe('1.0.0')
|
|
41
|
+
expect(mainFile.pages[0].isFile).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('merge', () => {
|
|
46
|
+
it('should handle legacy format', async () => {
|
|
47
|
+
const legacyContent = `{
|
|
48
|
+
"settings": {},
|
|
49
|
+
"pages": [{ "id": "page1" }]
|
|
50
|
+
}`
|
|
51
|
+
|
|
52
|
+
const pageLoader = async () => { throw new Error('Should not be called') }
|
|
53
|
+
const result = await merge(legacyContent, pageLoader)
|
|
54
|
+
|
|
55
|
+
expect(result.pages).toHaveLength(1)
|
|
56
|
+
expect(result.pages[0].id).toBe('page1')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Silex website builder, free/libre no-code tool for makers.
|
|
3
|
+
* Copyright (c) 2023 lexoyo and Silex Labs foundation
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { WebsiteData } from '../../types'
|
|
19
|
+
import { Page } from 'grapesjs'
|
|
20
|
+
import { getPageSlug } from '../../page'
|
|
21
|
+
import { WEBSITE_PAGES_FOLDER, WEBSITE_DATA_FILE } from '../../constants'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Serialize JSON with stable formatting for git
|
|
25
|
+
* - Uses 2-space indentation for readability
|
|
26
|
+
* - Sorts object keys deterministically to avoid random diffs
|
|
27
|
+
*/
|
|
28
|
+
export function stringify(data: any): string {
|
|
29
|
+
return JSON.stringify(data, sortKeys, 2)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sort object keys for consistent output across serializations
|
|
34
|
+
*/
|
|
35
|
+
function sortKeys(key: string, value: any): any {
|
|
36
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
37
|
+
const sorted: any = {}
|
|
38
|
+
Object.keys(value).sort().forEach(k => {
|
|
39
|
+
sorted[k] = value[k]
|
|
40
|
+
})
|
|
41
|
+
return sorted
|
|
42
|
+
}
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Split website data into multiple files
|
|
48
|
+
* Returns array of files to create/update
|
|
49
|
+
*/
|
|
50
|
+
export function split(websiteData: WebsiteData): Array<{path: string, content: string}> {
|
|
51
|
+
const files: Array<{path: string, content: string}> = []
|
|
52
|
+
|
|
53
|
+
// Split pages into separate files
|
|
54
|
+
const pages = websiteData.pages?.map((page: Page) => {
|
|
55
|
+
const pageName = getPageName(page)
|
|
56
|
+
const slug = getPageSlug(pageName)
|
|
57
|
+
const fileName = `${slug}-${page.id}.json`
|
|
58
|
+
const filePath = `${WEBSITE_PAGES_FOLDER}/${fileName}`
|
|
59
|
+
|
|
60
|
+
files.push({
|
|
61
|
+
path: filePath,
|
|
62
|
+
content: stringify(page)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
name: pageName,
|
|
67
|
+
id: page.id,
|
|
68
|
+
isFile: true,
|
|
69
|
+
}
|
|
70
|
+
}) || []
|
|
71
|
+
|
|
72
|
+
// Create main website data file without pages content
|
|
73
|
+
const websiteDataWithoutPages = {
|
|
74
|
+
...websiteData,
|
|
75
|
+
fileFormatVersion: '1.0.0',
|
|
76
|
+
pages,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
files.push({
|
|
80
|
+
path: WEBSITE_DATA_FILE,
|
|
81
|
+
content: stringify(websiteDataWithoutPages)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return files
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reconstruct website data from main file and page loader function
|
|
89
|
+
* The pageLoader function will be called for each page that needs to be loaded
|
|
90
|
+
*/
|
|
91
|
+
export async function merge(
|
|
92
|
+
websiteDataContent: string,
|
|
93
|
+
pageLoader: (pagePath: string) => Promise<string>
|
|
94
|
+
): Promise<WebsiteData> {
|
|
95
|
+
const websiteDataOb = JSON.parse(websiteDataContent) as any
|
|
96
|
+
const { fileFormatVersion, ...websiteData } = websiteDataOb
|
|
97
|
+
|
|
98
|
+
// Check file format version
|
|
99
|
+
if (fileFormatVersion && fileFormatVersion !== '1.0.0') {
|
|
100
|
+
console.warn('Website data file format version mismatch', fileFormatVersion, '!=', '1.0.0')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle legacy format (no split pages) or empty pages
|
|
104
|
+
if (!websiteData.pages || websiteData.pages.length === 0) {
|
|
105
|
+
return websiteData as WebsiteData
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if pages are already embedded (legacy format)
|
|
109
|
+
if (websiteData.pages[0] && !websiteData.pages[0].isFile) {
|
|
110
|
+
return websiteData as WebsiteData
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Load pages from separate files
|
|
114
|
+
const pages = await Promise.all(websiteData.pages.map(async (pageRef: any) => {
|
|
115
|
+
if (pageRef.isFile) {
|
|
116
|
+
const pageName = pageRef.name
|
|
117
|
+
const slug = getPageSlug(pageName)
|
|
118
|
+
const fileName = `${slug}-${pageRef.id}.json`
|
|
119
|
+
const filePath = `${WEBSITE_PAGES_FOLDER}/${fileName}`
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const pageContent = await pageLoader(filePath)
|
|
123
|
+
return JSON.parse(pageContent) as Page
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn(`Could not load page file: ${filePath}`, error)
|
|
126
|
+
return pageRef
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return pageRef as Page
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
...websiteData,
|
|
134
|
+
pages,
|
|
135
|
+
} as WebsiteData
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get page name from page object
|
|
140
|
+
*/
|
|
141
|
+
function getPageName(page: Page | any): string {
|
|
142
|
+
return (page as any).getName ? (page as Page).getName() : (page as any).name
|
|
143
|
+
}
|