@silexlabs/silex 3.3.1-4 → 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.
Files changed (30) hide show
  1. package/README.md +27 -2
  2. package/dist/client/index.html +2 -2
  3. package/dist/client/js/main.js +214 -214
  4. package/dist/client/js/main.js.LICENSE.txt +1 -12
  5. package/dist/plugins/server/plugins/server/FtpConnector.js +53 -8
  6. package/dist/plugins/server/plugins/server/FtpConnector.js.map +1 -1
  7. package/dist/plugins/server/plugins/server/GitlabConnector.js +21 -66
  8. package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
  9. package/dist/plugins/server/server/connectors/FsStorage.js +50 -4
  10. package/dist/plugins/server/server/connectors/FsStorage.js.map +1 -1
  11. package/dist/plugins/server/server/utils/websiteDataSerialization.d.ts +20 -0
  12. package/dist/plugins/server/server/utils/websiteDataSerialization.js +127 -0
  13. package/dist/plugins/server/server/utils/websiteDataSerialization.js.map +1 -0
  14. package/dist/server/page.js +14 -0
  15. package/dist/server/server/connectors/FsStorage.js +40 -4
  16. package/dist/server/server/utils/websiteDataSerialization.js +85 -0
  17. package/package.json +10 -10
  18. package/src/ts/client/grapesjs/PublicationUi.ts +1 -1
  19. package/src/ts/client/grapesjs/index.ts +9 -11
  20. package/src/ts/client/grapesjs/storage.ts +6 -9
  21. package/src/ts/client/index.ts +0 -2
  22. package/src/ts/plugins/server/FtpConnector.test.ts +2 -1
  23. package/src/ts/plugins/server/FtpConnector.ts +61 -9
  24. package/src/ts/plugins/server/GitlabConnector.ts +22 -80
  25. package/src/ts/server/connectors/FsStorage.test.ts +6 -2
  26. package/src/ts/server/connectors/FsStorage.ts +60 -5
  27. package/src/ts/server/utils/websiteDataSerialization.test.ts +59 -0
  28. package/src/ts/server/utils/websiteDataSerialization.ts +143 -0
  29. package/src/ts/client/grapesjs/dynamic-plugins.ts +0 -63
  30. package/src/ts/client/grapesjs/plugin-options.ts +0 -243
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /*
3
+ * Silex website builder, free/libre no-code tool for makers.
4
+ * Copyright (c) 2023 lexoyo and Silex Labs foundation
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Affero General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.stringify = stringify;
20
+ exports.split = split;
21
+ exports.merge = merge;
22
+ const page_1 = require("../../page");
23
+ const constants_1 = require("../../constants");
24
+ /**
25
+ * Serialize JSON with stable formatting for git
26
+ * - Uses 2-space indentation for readability
27
+ * - Sorts object keys deterministically to avoid random diffs
28
+ */
29
+ function stringify(data) {
30
+ return JSON.stringify(data, sortKeys, 2);
31
+ }
32
+ /**
33
+ * Sort object keys for consistent output across serializations
34
+ */
35
+ function sortKeys(key, value) {
36
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
37
+ const sorted = {};
38
+ Object.keys(value).sort().forEach(k => {
39
+ sorted[k] = value[k];
40
+ });
41
+ return sorted;
42
+ }
43
+ return value;
44
+ }
45
+ /**
46
+ * Split website data into multiple files
47
+ * Returns array of files to create/update
48
+ */
49
+ function split(websiteData) {
50
+ const files = [];
51
+ // Split pages into separate files
52
+ const pages = websiteData.pages?.map((page) => {
53
+ const pageName = getPageName(page);
54
+ const slug = (0, page_1.getPageSlug)(pageName);
55
+ const fileName = `${slug}-${page.id}.json`;
56
+ const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
57
+ files.push({
58
+ path: filePath,
59
+ content: stringify(page)
60
+ });
61
+ return {
62
+ name: pageName,
63
+ id: page.id,
64
+ isFile: true,
65
+ };
66
+ }) || [];
67
+ // Create main website data file without pages content
68
+ const websiteDataWithoutPages = {
69
+ ...websiteData,
70
+ fileFormatVersion: '1.0.0',
71
+ pages,
72
+ };
73
+ files.push({
74
+ path: constants_1.WEBSITE_DATA_FILE,
75
+ content: stringify(websiteDataWithoutPages)
76
+ });
77
+ return files;
78
+ }
79
+ /**
80
+ * Reconstruct website data from main file and page loader function
81
+ * The pageLoader function will be called for each page that needs to be loaded
82
+ */
83
+ async function merge(websiteDataContent, pageLoader) {
84
+ const websiteDataOb = JSON.parse(websiteDataContent);
85
+ const { fileFormatVersion, ...websiteData } = websiteDataOb;
86
+ // Check file format version
87
+ if (fileFormatVersion && fileFormatVersion !== '1.0.0') {
88
+ console.warn('Website data file format version mismatch', fileFormatVersion, '!=', '1.0.0');
89
+ }
90
+ // Handle legacy format (no split pages) or empty pages
91
+ if (!websiteData.pages || websiteData.pages.length === 0) {
92
+ return websiteData;
93
+ }
94
+ // Check if pages are already embedded (legacy format)
95
+ if (websiteData.pages[0] && !websiteData.pages[0].isFile) {
96
+ return websiteData;
97
+ }
98
+ // Load pages from separate files
99
+ const pages = await Promise.all(websiteData.pages.map(async (pageRef) => {
100
+ if (pageRef.isFile) {
101
+ const pageName = pageRef.name;
102
+ const slug = (0, page_1.getPageSlug)(pageName);
103
+ const fileName = `${slug}-${pageRef.id}.json`;
104
+ const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
105
+ try {
106
+ const pageContent = await pageLoader(filePath);
107
+ return JSON.parse(pageContent);
108
+ }
109
+ catch (error) {
110
+ console.warn(`Could not load page file: ${filePath}`, error);
111
+ return pageRef;
112
+ }
113
+ }
114
+ return pageRef;
115
+ }));
116
+ return {
117
+ ...websiteData,
118
+ pages,
119
+ };
120
+ }
121
+ /**
122
+ * Get page name from page object
123
+ */
124
+ function getPageName(page) {
125
+ return page.getName ? page.getName() : page.name;
126
+ }
127
+ //# sourceMappingURL=websiteDataSerialization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websiteDataSerialization.js","sourceRoot":"","sources":["../../../../../src/ts/server/utils/websiteDataSerialization.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPageSlug = getPageSlug;
4
+ exports.getPageLink = getPageLink;
5
+ function getPageSlug(pageName) {
6
+ return (pageName || 'index')
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9 -]/g, '')
9
+ .replace(/\s+/g, '-')
10
+ .replace(/-+/g, '-');
11
+ }
12
+ function getPageLink(pageName) {
13
+ return `./${getPageSlug(pageName)}.html`;
14
+ }
@@ -14,6 +14,7 @@ const validation_1 = require("../utils/validation");
14
14
  const constants_1 = require("../../constants");
15
15
  const stream_1 = require("stream");
16
16
  const uuid_1 = require("uuid");
17
+ const websiteDataSerialization_1 = require("../utils/websiteDataSerialization");
17
18
  if (!globalThis.__dirname) {
18
19
  globalThis.__dirname = (0, path_1.dirname)(process.cwd() + '/src/ts/server/connectors/FsStorage.ts');
19
20
  console.info('Redefining __dirname', globalThis.__dirname);
@@ -96,7 +97,7 @@ class FsStorage {
96
97
  }
97
98
  async setWebsiteMeta(session, id, data) {
98
99
  const websiteId = (0, validation_1.requiredParam)(id, 'website id');
99
- const content = JSON.stringify(data);
100
+ const content = (0, websiteDataSerialization_1.stringify)(data);
100
101
  const path = (0, path_1.join)(this.options.path, id, constants_1.WEBSITE_META_DATA_FILE);
101
102
  await promises_1.default.writeFile(path, content);
102
103
  }
@@ -126,12 +127,47 @@ class FsStorage {
126
127
  const id = (0, validation_1.requiredParam)(websiteId, 'website id');
127
128
  const path = (0, path_1.join)(this.options.path, id, constants_1.WEBSITE_DATA_FILE);
128
129
  const content = await promises_1.default.readFile(path);
129
- return JSON.parse(content.toString());
130
+ const websiteDataContent = content.toString();
131
+ const parsedData = JSON.parse(websiteDataContent);
132
+ if (parsedData.fileFormatVersion === '1.0.0') {
133
+ const pageLoader = async (pagePath) => {
134
+ const fullPath = (0, path_1.join)(this.options.path, id, pagePath);
135
+ const pageContent = await promises_1.default.readFile(fullPath);
136
+ return pageContent.toString();
137
+ };
138
+ return await (0, websiteDataSerialization_1.merge)(websiteDataContent, pageLoader);
139
+ }
140
+ else {
141
+ return parsedData;
142
+ }
130
143
  }
131
144
  async updateWebsite(session, websiteId, data) {
132
145
  const id = (0, validation_1.requiredParam)(websiteId, 'website id');
133
- const path = (0, path_1.join)(this.options.path, id, constants_1.WEBSITE_DATA_FILE);
134
- await promises_1.default.writeFile(path, JSON.stringify(data));
146
+ const websitePath = (0, path_1.join)(this.options.path, id);
147
+ const filesToWrite = (0, websiteDataSerialization_1.split)(data);
148
+ await promises_1.default.mkdir(websitePath, { recursive: true });
149
+ const hasPageFiles = filesToWrite.some(f => f.path.startsWith(constants_1.WEBSITE_PAGES_FOLDER));
150
+ if (hasPageFiles) {
151
+ await promises_1.default.mkdir((0, path_1.join)(websitePath, constants_1.WEBSITE_PAGES_FOLDER), { recursive: true });
152
+ }
153
+ const pagesPath = (0, path_1.join)(websitePath, constants_1.WEBSITE_PAGES_FOLDER);
154
+ try {
155
+ const existingPageFiles = await promises_1.default.readdir(pagesPath);
156
+ const newPageFiles = new Set(filesToWrite
157
+ .filter(f => f.path.startsWith(constants_1.WEBSITE_PAGES_FOLDER))
158
+ .map(f => f.path.replace(`${constants_1.WEBSITE_PAGES_FOLDER}/`, '')));
159
+ for (const existingFile of existingPageFiles) {
160
+ if (existingFile.endsWith('.json') && !newPageFiles.has(existingFile)) {
161
+ await promises_1.default.unlink((0, path_1.join)(pagesPath, existingFile));
162
+ }
163
+ }
164
+ }
165
+ catch (error) {
166
+ }
167
+ for (const file of filesToWrite) {
168
+ const filePath = (0, path_1.join)(websitePath, file.path);
169
+ await promises_1.default.writeFile(filePath, file.content);
170
+ }
135
171
  }
136
172
  async deleteWebsite(session, websiteId) {
137
173
  const id = (0, validation_1.requiredParam)(websiteId, 'website id');
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringify = stringify;
4
+ exports.split = split;
5
+ exports.merge = merge;
6
+ const page_1 = require("../../page");
7
+ const constants_1 = require("../../constants");
8
+ function stringify(data) {
9
+ return JSON.stringify(data, sortKeys, 2);
10
+ }
11
+ function sortKeys(key, value) {
12
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
13
+ const sorted = {};
14
+ Object.keys(value).sort().forEach(k => {
15
+ sorted[k] = value[k];
16
+ });
17
+ return sorted;
18
+ }
19
+ return value;
20
+ }
21
+ function split(websiteData) {
22
+ const files = [];
23
+ const pages = websiteData.pages?.map((page) => {
24
+ const pageName = getPageName(page);
25
+ const slug = (0, page_1.getPageSlug)(pageName);
26
+ const fileName = `${slug}-${page.id}.json`;
27
+ const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
28
+ files.push({
29
+ path: filePath,
30
+ content: stringify(page)
31
+ });
32
+ return {
33
+ name: pageName,
34
+ id: page.id,
35
+ isFile: true,
36
+ };
37
+ }) || [];
38
+ const websiteDataWithoutPages = {
39
+ ...websiteData,
40
+ fileFormatVersion: '1.0.0',
41
+ pages,
42
+ };
43
+ files.push({
44
+ path: constants_1.WEBSITE_DATA_FILE,
45
+ content: stringify(websiteDataWithoutPages)
46
+ });
47
+ return files;
48
+ }
49
+ async function merge(websiteDataContent, pageLoader) {
50
+ const websiteDataOb = JSON.parse(websiteDataContent);
51
+ const { fileFormatVersion, ...websiteData } = websiteDataOb;
52
+ if (fileFormatVersion && fileFormatVersion !== '1.0.0') {
53
+ console.warn('Website data file format version mismatch', fileFormatVersion, '!=', '1.0.0');
54
+ }
55
+ if (!websiteData.pages || websiteData.pages.length === 0) {
56
+ return websiteData;
57
+ }
58
+ if (websiteData.pages[0] && !websiteData.pages[0].isFile) {
59
+ return websiteData;
60
+ }
61
+ const pages = await Promise.all(websiteData.pages.map(async (pageRef) => {
62
+ if (pageRef.isFile) {
63
+ const pageName = pageRef.name;
64
+ const slug = (0, page_1.getPageSlug)(pageName);
65
+ const fileName = `${slug}-${pageRef.id}.json`;
66
+ const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}`;
67
+ try {
68
+ const pageContent = await pageLoader(filePath);
69
+ return JSON.parse(pageContent);
70
+ }
71
+ catch (error) {
72
+ console.warn(`Could not load page file: ${filePath}`, error);
73
+ return pageRef;
74
+ }
75
+ }
76
+ return pageRef;
77
+ }));
78
+ return {
79
+ ...websiteData,
80
+ pages,
81
+ };
82
+ }
83
+ function getPageName(page) {
84
+ return page.getName ? page.getName() : page.name;
85
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@silexlabs/silex",
3
3
  "description": "Free and easy website builder for everyone.",
4
- "version": "3.3.1-4",
4
+ "version": "3.3.1-5",
5
5
  "version:backwardcompat": "3.0.0",
6
6
  "author": {
7
7
  "name": "Alex Hoyau",
@@ -105,16 +105,16 @@
105
105
  "dependencies": {
106
106
  "@fontsource/ubuntu": "^5.0.13",
107
107
  "@fortawesome/fontawesome-free": "6.6.0",
108
- "@silexlabs/grapesjs-advanced-selector": "1.2.1-3",
109
- "@silexlabs/grapesjs-filter-styles": "1.1.0",
110
- "@silexlabs/grapesjs-fonts": "1.1.1-0",
111
- "@silexlabs/grapesjs-keymaps-dialog": "1.1.0",
108
+ "@silexlabs/grapesjs-advanced-selector": "1.2.1-5",
109
+ "@silexlabs/grapesjs-filter-styles": "1.1.1-0",
110
+ "@silexlabs/grapesjs-fonts": "1.1.1-1",
111
+ "@silexlabs/grapesjs-keymaps-dialog": "1.1.1-0",
112
112
  "@silexlabs/grapesjs-loading": "1.1.1-0",
113
- "@silexlabs/grapesjs-notifications": "0.1.0",
114
- "@silexlabs/grapesjs-storage-rate-limit": "1.1.0",
115
- "@silexlabs/grapesjs-symbols": "1.2.1-0",
116
- "@silexlabs/grapesjs-ui-suggest-classes": "1.1.0",
117
- "@silexlabs/silex-plugins": "1.1.0",
113
+ "@silexlabs/grapesjs-notifications": "0.1.1-0",
114
+ "@silexlabs/grapesjs-storage-rate-limit": "1.1.1-0",
115
+ "@silexlabs/grapesjs-symbols": "1.2.1-1",
116
+ "@silexlabs/grapesjs-ui-suggest-classes": "1.1.1-0",
117
+ "@silexlabs/silex-plugins": "1.1.1-0",
118
118
  "@types/archiver": "^6.0.2",
119
119
  "adm-zip": "0.5.14",
120
120
  "archiver": "^7.0.1",
@@ -129,7 +129,7 @@ export class PublicationUi {
129
129
  // Create the dialog element
130
130
  const el = document.createElement('div')
131
131
  el.id = 'publish-dialog'
132
- el.className = 'silex-dialog-inline silex-dialog gjs-two-color'
132
+ el.className = 'silex-dialog-inline silex-dialog gjs-two-color silex-dialog-hide'
133
133
  document.body.append(el)
134
134
  // Let the other buttons be added, we want to be last
135
135
  setTimeout(() => {
@@ -49,7 +49,7 @@ import rateLimitPlugin from '@silexlabs/grapesjs-storage-rate-limit'
49
49
  import borderPugin from 'grapesjs-style-border'
50
50
  import backgroundPlugin from 'grapesjs-style-bg'
51
51
  import resizePanelPlugin from './resize-panel'
52
- import notificationsPlugin, { NotificationEditor } from '@silexlabs/grapesjs-notifications'
52
+ import notificationsPlugin, { NOTIFICATION_CHANGED } from '@silexlabs/grapesjs-notifications'
53
53
  import keymapsDialogPlugin, { cmdKeymapsDialog } from '@silexlabs/grapesjs-keymaps-dialog'
54
54
 
55
55
  import { pagePanelPlugin, cmdTogglePages, cmdAddPage } from './page-panel'
@@ -428,16 +428,14 @@ export async function initEditor(config: EditorConfig) {
428
428
  // Add the notifications container
429
429
  document.body.querySelector('.notifications-container')?.appendChild(notificationContainer)
430
430
  // Mark the button as dirty when there are notifications
431
- // TODO: move this in the notifications plugin options
432
- editor.on(
433
- 'notifications:changed',
434
- () => {
435
- const notificationButton = editor.Panels.getPanel(PROJECT_BAR_PANEL_ID).view?.el.querySelector('.notifications-btn')
436
- ;(editor as unknown as NotificationEditor)
437
- ? notificationButton?.classList.add('project-bar__dirty')
438
- : notificationButton?.classList.remove('project-bar__dirty')
439
- }
440
- )
431
+ let notificationCount = 0
432
+ editor.on(NOTIFICATION_CHANGED, (notifications) => {
433
+ const notificationButton = editor.Panels.getPanel(PROJECT_BAR_PANEL_ID).view?.el.querySelector('.notifications-btn')
434
+ notificationCount = Array.isArray(notifications) ? notifications.length : 0
435
+ notificationCount > 0
436
+ ? notificationButton?.classList.add('project-bar__dirty')
437
+ : notificationButton?.classList.remove('project-bar__dirty')
438
+ })
441
439
 
442
440
  // GrapesJs editor is ready
443
441
  resolve(editor)
@@ -86,13 +86,13 @@ export const storagePlugin = (editor: PublishableEditor) => {
86
86
  // Let grapesjs create the pages in the frontend
87
87
  return data
88
88
  } else {
89
+ editor.runCommand(cmdPauseAutoSave)
89
90
  const { pages, ...rest } = data
90
91
  // Load any additional project data, e.g. symbols, but not the ones we progressive load
91
92
  loader && render(getLoaderHtml('Loading styles, assets and symbols', 0, pages.length + 1), loader)
92
- if (Object.keys(rest).length > 0) {
93
- editor.loadProjectData(rest)
94
- }
95
- // Load the rest of the data
93
+ // Add to the project, everything but pages
94
+ editor.loadProjectData(rest)
95
+ // Add the pages to the project
96
96
  await progressiveLoadPages(editor, pages)
97
97
  await nextFrame()
98
98
  // Select the first page
@@ -100,18 +100,15 @@ export const storagePlugin = (editor: PublishableEditor) => {
100
100
  if (firstPage) editor.Pages.select(firstPage)
101
101
  loader && render(getLoaderHtml('Starting', 1, 1), loader)
102
102
  await nextFrame()
103
- editor.trigger('storage:end:load', data)
104
- editor.trigger('canvas:frame:load', editor)
105
- return {}
103
+ editor.once('canvas:frame:load', () => editor.stopCommand(cmdPauseAutoSave))
104
+ return data
106
105
  }
107
106
  } catch (err) {
108
107
  editor.UndoManager.clear()
109
108
  if (err.httpStatusCode === 401) {
110
109
  editor.once(eventLoggedIn, async () => {
111
110
  try {
112
- editor.runCommand(cmdPauseAutoSave)
113
111
  await editor.Storage.load(options)
114
- editor.once('canvas:frame:load', () => editor.stopCommand(cmdPauseAutoSave))
115
112
  } catch (err) {
116
113
  console.error('connectorPlugin load error', err)
117
114
  throw err
@@ -86,8 +86,6 @@ export async function start(options = {}): Promise<void> {
86
86
 
87
87
  // Load the site
88
88
  try {
89
- editor.runCommand(cmdPauseAutoSave)
90
- editor.once('canvas:frame:load storage:after:load', () => editor.stopCommand(cmdPauseAutoSave))
91
89
  await editor.load(null)
92
90
  } catch(e) {
93
91
  if(e.httpStatusCode === 401) {
@@ -2,6 +2,7 @@ import { ServerConfig } from '../../server/config'
2
2
  import { expect, jest, beforeEach, afterEach, it, describe } from '@jest/globals'
3
3
  import { ConnectorType, WebsiteData, WebsiteId } from '../../types'
4
4
  import { Readable } from 'stream'
5
+ import { WEBSITE_DATA_FILE } from '../../constants'
5
6
 
6
7
  const mocks = {
7
8
  access: jest.fn(),
@@ -64,7 +65,7 @@ describe('FtpConnector website', () => {
64
65
  await connector.updateWebsite(dummySession, dummyId, dummyWebsite)
65
66
  expect(mocks.uploadFrom).toHaveBeenCalledTimes(1)
66
67
  expect(mocks.uploadFrom.mock.calls[0][0]).toBeInstanceOf(Readable)
67
- expect(mocks.uploadFrom.mock.calls[0][1]).toBe(`${storageRootPath}/${dummyId}/website.json`)
68
+ expect(mocks.uploadFrom.mock.calls[0][1]).toBe(`${storageRootPath}/${dummyId}/${WEBSITE_DATA_FILE}`)
68
69
  })
69
70
  })
70
71
 
@@ -19,7 +19,7 @@ import { Client } from 'basic-ftp'
19
19
  import { Readable } from 'stream'
20
20
  import { ConnectorFile, ConnectorFileContent, HostingConnector, StatusCallback, StorageConnector, contentToReadable, contentToString, toConnectorData, toConnectorEnum} from '../../server/connectors/connectors'
21
21
  import { requiredParam } from '../../server/utils/validation'
22
- import { WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE } from '../../constants'
22
+ import { WEBSITE_DATA_FILE, WEBSITE_META_DATA_FILE, WEBSITE_PAGES_FOLDER } from '../../constants'
23
23
  import { ConnectorType, ConnectorUser, WebsiteMeta, FileMeta, JobData, JobStatus, WebsiteId, PublicationJobData, WebsiteMetaFileContent, defaultWebsiteData, WebsiteData, ConnectorOptions } from '../../types'
24
24
  import { ServerConfig } from '../../server/config'
25
25
  import { join } from 'path'
@@ -28,6 +28,7 @@ import { v4 as uuid } from 'uuid'
28
28
  import { JobManager } from '../../server/jobs'
29
29
  import { mkdtemp, rm, rmdir } from 'fs/promises'
30
30
  import { createReadStream } from 'fs'
31
+ import { stringify, split, merge } from '../../server/utils/websiteDataSerialization'
31
32
 
32
33
  /**
33
34
  * @fileoverview FTP connector for Silex
@@ -399,7 +400,7 @@ export default class FtpConnector implements StorageConnector<FtpSession> {
399
400
  const websiteId = requiredParam<WebsiteId>(id, 'website id')
400
401
  const path = join(this.rootPath(session), websiteId, WEBSITE_META_DATA_FILE)
401
402
  const ftp = await this.getClient(this.sessionData(session))
402
- await this.write(ftp, path, JSON.stringify(data))
403
+ await this.write(ftp, path, stringify(data))
403
404
  this.closeClient(ftp)
404
405
  }
405
406
 
@@ -442,10 +443,10 @@ export default class FtpConnector implements StorageConnector<FtpSession> {
442
443
  await this.mkdir(ftp, assetsPath)
443
444
  // create website data file
444
445
  const websiteDataPath = join(this.rootPath(session), id, WEBSITE_DATA_FILE)
445
- await this.write(ftp, websiteDataPath, JSON.stringify(defaultWebsiteData))
446
+ await this.write(ftp, websiteDataPath, stringify(defaultWebsiteData))
446
447
  // create website meta data file
447
448
  const websiteMetaDataPath = join(this.rootPath(session), id, WEBSITE_META_DATA_FILE)
448
- await this.write(ftp, websiteMetaDataPath, JSON.stringify({}))
449
+ await this.write(ftp, websiteMetaDataPath, stringify({}))
449
450
  // Clean up
450
451
  this.closeClient(ftp)
451
452
  // All good
@@ -469,16 +470,67 @@ export default class FtpConnector implements StorageConnector<FtpSession> {
469
470
  const storageRootPath = this.rootPath(session)
470
471
  const ftp = await this.getClient(this.sessionData(session))
471
472
  const websiteDataPath = join(storageRootPath, websiteId, WEBSITE_DATA_FILE)
472
- const data = await this.read(ftp, websiteDataPath)
473
- this.closeClient(ftp)
474
- return data
473
+
474
+ try {
475
+ const websiteDataStream = await this.read(ftp, websiteDataPath)
476
+ const websiteDataContent = await contentToString(websiteDataStream)
477
+
478
+ // Check if this is the new split format by trying to parse
479
+ const parsedData = JSON.parse(websiteDataContent)
480
+ if (parsedData.fileFormatVersion === '1.0.0') {
481
+ // Use the merge function to reconstruct website data
482
+ const pageLoader = async (pagePath: string): Promise<string> => {
483
+ const fullPath = join(storageRootPath, websiteId, pagePath)
484
+ const pageStream = await this.read(ftp, fullPath)
485
+ return await contentToString(pageStream)
486
+ }
487
+
488
+ const result = await merge(websiteDataContent, pageLoader)
489
+ this.closeClient(ftp)
490
+ return result
491
+ } else {
492
+ // Legacy format, return as is
493
+ this.closeClient(ftp)
494
+ return parsedData
495
+ }
496
+ } catch (error) {
497
+ this.closeClient(ftp)
498
+ throw error
499
+ }
475
500
  }
476
501
 
477
502
  async updateWebsite(session: FtpSession, websiteId: string, data: WebsiteData): Promise<void> {
478
503
  const storageRootPath = this.rootPath(session)
479
504
  const ftp = await this.getClient(this.sessionData(session))
480
- const websiteDataPath = join(storageRootPath, websiteId, WEBSITE_DATA_FILE)
481
- await this.write(ftp, websiteDataPath, JSON.stringify(data))
505
+
506
+ // Use the split function to create separate files for pages
507
+ const filesToWrite = split(data)
508
+
509
+ // **
510
+ // Delete pages that are not in the new website data
511
+ const pagesPath = join(storageRootPath, websiteId, WEBSITE_PAGES_FOLDER)
512
+ try {
513
+ const existingPageFiles = await this.readdir(ftp, pagesPath)
514
+ const newPageFiles = new Set(
515
+ filesToWrite
516
+ .filter(f => f.path.startsWith(WEBSITE_PAGES_FOLDER))
517
+ .map(f => f.path.replace(`${WEBSITE_PAGES_FOLDER}/`, ''))
518
+ )
519
+
520
+ for (const existingFile of existingPageFiles) {
521
+ if (existingFile.name.endsWith('.json') && !newPageFiles.has(existingFile.name)) {
522
+ await this.unlink(ftp, join(pagesPath, existingFile.name))
523
+ }
524
+ }
525
+ } catch (error) {
526
+ // Ignore error if pages directory doesn't exist yet
527
+ }
528
+
529
+ for (const file of filesToWrite) {
530
+ const filePath = join(storageRootPath, websiteId, file.path)
531
+ await this.write(ftp, filePath, file.content)
532
+ }
533
+
482
534
  this.closeClient(ftp)
483
535
  }
484
536