@silexlabs/silex 3.0.0-alpha.39 → 3.0.0-alpha.41

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 (110) hide show
  1. package/.silex.js +38 -8
  2. package/dist/client/client/config.d.ts +11 -41
  3. package/dist/client/client/grapesjs/LoginDialog.d.ts +13 -0
  4. package/dist/client/client/grapesjs/PublicationManager.d.ts +69 -0
  5. package/dist/client/client/grapesjs/PublicationUi.d.ts +58 -0
  6. package/dist/client/client/grapesjs/index.d.ts +18 -44
  7. package/dist/client/client/grapesjs/storage.d.ts +1 -0
  8. package/dist/client/client/services.d.ts +35 -0
  9. package/dist/client/constants.d.ts +17 -0
  10. package/dist/client/css/admin.css +86 -3
  11. package/dist/client/js/main.js +201 -90
  12. package/dist/client/js/main.js.LICENSE.txt +6 -0
  13. package/dist/client/types.d.ts +200 -29
  14. package/dist/constants.d.ts +21 -0
  15. package/dist/constants.js +38 -1
  16. package/dist/constants.js.map +1 -0
  17. package/dist/events.d.ts +2 -0
  18. package/dist/events.js +17 -0
  19. package/dist/events.js.map +1 -0
  20. package/dist/plugins/AuthPlugin.d.ts +1 -0
  21. package/dist/plugins/AuthPlugin.js +38 -0
  22. package/dist/plugins/AuthPlugin.js.map +1 -0
  23. package/dist/plugins/Cache.d.ts +2 -0
  24. package/dist/plugins/Cache.js +32 -0
  25. package/dist/plugins/Cache.js.map +1 -0
  26. package/dist/plugins/FtpBackend.d.ts +92 -0
  27. package/dist/plugins/FtpBackend.js +604 -0
  28. package/dist/plugins/FtpBackend.js.map +1 -0
  29. package/dist/plugins/HooksPlugin.d.ts +7 -0
  30. package/dist/plugins/HooksPlugin.js +76 -0
  31. package/dist/plugins/HooksPlugin.js.map +1 -0
  32. package/dist/plugins/PublicationPlugin.d.ts +17 -0
  33. package/dist/plugins/PublicationPlugin.js +170 -0
  34. package/dist/plugins/PublicationPlugin.js.map +1 -0
  35. package/dist/plugins/SslPlugin.d.ts +1 -0
  36. package/dist/plugins/SslPlugin.js +86 -0
  37. package/dist/plugins/SslPlugin.js.map +1 -0
  38. package/dist/plugins/StaticPlugin.d.ts +1 -0
  39. package/dist/plugins/StaticPlugin.js +66 -0
  40. package/dist/plugins/StaticPlugin.js.map +1 -0
  41. package/dist/plugins/WebsitePlugin.d.ts +14 -0
  42. package/dist/plugins/WebsitePlugin.js +302 -0
  43. package/dist/plugins/WebsitePlugin.js.map +1 -0
  44. package/dist/server/backends/FsBackend.d.ts +42 -0
  45. package/dist/server/backends/FsBackend.js +269 -0
  46. package/dist/server/backends/FsBackend.js.map +1 -0
  47. package/dist/server/backends/index.d.ts +95 -0
  48. package/dist/server/backends/index.js +255 -0
  49. package/dist/server/backends/index.js.map +1 -0
  50. package/dist/server/cli.js +0 -1
  51. package/dist/server/config.d.ts +58 -0
  52. package/dist/server/config.js +128 -24
  53. package/dist/server/config.js.map +1 -0
  54. package/dist/server/express.js +31 -3
  55. package/dist/server/index.js +9 -20
  56. package/dist/server/jobs.d.ts +6 -0
  57. package/dist/server/jobs.js +75 -0
  58. package/dist/server/jobs.js.map +1 -0
  59. package/dist/server/utils/validation.d.ts +7 -0
  60. package/dist/server/utils/validation.js +37 -0
  61. package/dist/server/utils/validation.js.map +1 -0
  62. package/dist/types.d.ts +270 -0
  63. package/dist/types.js +35 -28
  64. package/dist/types.js.map +1 -0
  65. package/package.json +19 -8
  66. package/public/silex.js +17 -0
  67. package/src/css/dialog.scss +9 -2
  68. package/src/css/grapesjs.scss +79 -0
  69. package/src/css/project-bar.scss +2 -2
  70. package/src/example-client-config.js +17 -0
  71. package/src/html/templates/includes.pug +0 -1
  72. package/src/ts/client/config.ts +7 -6
  73. package/src/ts/client/grapesjs/LoginDialog.ts +190 -0
  74. package/src/ts/client/grapesjs/PublicationManager.ts +333 -0
  75. package/src/ts/client/grapesjs/PublicationUi.ts +320 -0
  76. package/src/ts/client/grapesjs/index.ts +88 -85
  77. package/src/ts/client/grapesjs/storage.ts +110 -0
  78. package/src/ts/client/index.ts +5 -7
  79. package/src/ts/client/services.ts +129 -0
  80. package/src/ts/constants.ts +21 -0
  81. package/src/ts/plugins/AuthPlugin.ts +38 -0
  82. package/src/ts/plugins/Cache.ts +28 -0
  83. package/src/ts/plugins/FtpBackend.ts +689 -0
  84. package/src/ts/plugins/HooksPlugin.ts +79 -0
  85. package/src/ts/plugins/PublicationPlugin.ts +170 -0
  86. package/src/{plugins/SslPlugin.js → ts/plugins/SslPlugin.ts} +24 -6
  87. package/src/ts/plugins/StaticPlugin.ts +68 -0
  88. package/src/ts/plugins/WebsitePlugin.ts +318 -0
  89. package/src/ts/server/backends/FsBackend.ts +266 -0
  90. package/src/ts/server/backends/index.ts +309 -0
  91. package/src/ts/server/cli.ts +0 -2
  92. package/src/ts/server/config.ts +103 -33
  93. package/src/ts/server/express.ts +37 -3
  94. package/src/ts/server/index.ts +24 -14
  95. package/src/ts/server/jobs.test.ts +74 -0
  96. package/src/ts/server/jobs.ts +77 -0
  97. package/src/ts/server/utils/validation.ts +33 -0
  98. package/src/ts/types.ts +167 -59
  99. package/dist/client/client/grapesjs/publish.d.ts +0 -18
  100. package/dist/page.js +0 -15
  101. package/src/plugins/AuthPlugin.js +0 -21
  102. package/src/plugins/Cache.js +0 -11
  103. package/src/plugins/DefaultBackend.js +0 -149
  104. package/src/plugins/ExpressPlugin.js +0 -39
  105. package/src/plugins/HooksPlugin.js +0 -58
  106. package/src/plugins/PublishPlugin.js +0 -69
  107. package/src/plugins/StaticPlugin.js +0 -48
  108. package/src/plugins/WebsitePlugin.js +0 -207
  109. package/src/ts/client/grapesjs/publish.ts +0 -403
  110. /package/.silex/default/{.silex.data.json → website.json} +0 -0
@@ -0,0 +1,689 @@
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 JsFTP from 'jsftp'
19
+ import { Readable } from 'stream'
20
+ import express, { Application } from 'express'
21
+ import { File, HostingProvider, StatusCallback, StorageProvider, toBackendEnum} from '../server/backends'
22
+ import { requiredParam } from '../server/utils/validation'
23
+ import { API_BACKEND_LOGIN_CALLBACK, WEBSITE_DATA_FILE_NAME } from '../constants'
24
+ import { BackendData, BackendType, BackendUser, WebsiteMeta, FileMeta, JobData, JobStatus, WebsiteId, PublicationJobData } from '../types'
25
+ import { EVENT_STARTUP_START } from '../events'
26
+ import { jobError, jobSuccess, startJob } from '../server/jobs'
27
+ import { ServerConfig } from '../server/config'
28
+ import { join } from 'path'
29
+ import { type } from 'os'
30
+
31
+ /**
32
+ * @fileoverview FTP backend for Silex
33
+ * This is a backend for Silex, which allows to store the website on a FTP server
34
+ * and to publish the website on a FTP server
35
+ * FIXME: As a hosting provider, this backend should only store the password in the session and the rest in the website data
36
+ * to prevent publishing a site on the wrong server + multiple sites on 1 server
37
+ * FIXME: Allow reuse of FTP session between calls
38
+ */
39
+
40
+ export interface FtpSessionData {
41
+ host: string
42
+ user: string
43
+ pass: string
44
+ port: number
45
+ secure: boolean
46
+ // Storage options
47
+ storageRootPath?: string
48
+ // Hosting options
49
+ publicationPath?: string
50
+ websiteUrl?: string
51
+ }
52
+
53
+ export interface FtpSession {
54
+ [BackendType.STORAGE]?: FtpSessionData
55
+ [BackendType.HOSTING]?: FtpSessionData
56
+ }
57
+
58
+ interface FileStatus {
59
+ file: File
60
+ message: string
61
+ status: JobStatus
62
+ }
63
+
64
+ interface FtpOptions {
65
+ type: BackendType
66
+ rootPath?: string
67
+ authorizeUrl?: string
68
+ authorizePath?: string
69
+ }
70
+
71
+ interface FtpOptionsWithDefaults {
72
+ type: BackendType
73
+ rootPath: string
74
+ authorizeUrl: string
75
+ authorizePath: string
76
+ }
77
+
78
+ // **
79
+ // Utils methos
80
+ function updateStatus(filesStatuses: FileStatus[], status: JobStatus, statusCbk?: ({ message, status }: { message: string, status: JobStatus }) => void) {
81
+ statusCbk && statusCbk({
82
+ message: `Writing files:<ul><li>${filesStatuses.map(({ file, message }) => `${file.path}: ${message}`).join('</li><li>')}</li></ul>`,
83
+ status,
84
+ })
85
+ }
86
+
87
+ function initStatus(files: File[]) {
88
+ return files.map(file => ({
89
+ file,
90
+ message: 'Waiting',
91
+ status: JobStatus.IN_PROGRESS,
92
+ }))
93
+ }
94
+
95
+ function handleInitError(err) {
96
+ if (err.code !== 550) {
97
+ console.error('Error creating folder', err)
98
+ throw err
99
+ } else {
100
+ console.log('Folder already exists', err.message)
101
+ }
102
+ }
103
+
104
+ function formHtml(type: BackendType, { host, user, pass, port, secure, storageRootPath, publicationPath, websiteUrl}: FtpSessionData, err = '') {
105
+ return `
106
+ ${ err && `<div class="error">${err || ''}</div>` }
107
+ <form method="post">
108
+ <label for="host">Host</label>
109
+ <input placeholder="ftp.example.com" type="text" name="host" value="${host || ''}" />
110
+ <label for="user">User</label>
111
+ <input placeholder="user" type="text" name="user" value="${user || ''}" />
112
+ <label for="pass">Pass</label>
113
+ <input placeholder="****" type="password" name="pass" value="${pass || ''}" />
114
+ <label for="port">Port</label>
115
+ <input placeholder="21" type="number" name="port" value="${port || '21'}" />
116
+ <div class="checkbox-container">
117
+ <input class="checkbox" type="checkbox" name="secure" value="true" ${secure ? 'checked' : ''} />
118
+ <label for="secure">Secure</label>
119
+ </div>
120
+ ${ type === BackendType.STORAGE ? `
121
+ <details>
122
+ <summary>Storage options</summary>
123
+ <p>If you are not sure, don't change this</p>
124
+ <label for="storageRootPath">Root path where to store the website files</label>
125
+ <input placeholder="/silex/" type="text" name="storageRootPath" value="${storageRootPath || '/silex/'}" />
126
+ </details>
127
+ ` : ''}
128
+ ${ type === BackendType.HOSTING ? `
129
+ <fieldset>
130
+ <legend>Publication options</legend>
131
+ <label for="publicationPath">Path where to publish</label>
132
+ <input placeholder="/public_html/" type="text" name="publicationPath" value="${publicationPath || ''}" />
133
+ <label for="websiteUrl">URL where to the site will be accessible</label>
134
+ <input placeholder="https://mysite.com/" type="text" name="websiteUrl" value="${websiteUrl || ''}" />
135
+ </fieldset>
136
+ ` : ''}
137
+ <div class="button-container">
138
+ <button type="submit" class="primary-button">Login</button>
139
+ <button type="button" class="secondary-button">Cancel</button>
140
+ </div>
141
+ </form>
142
+ `
143
+ }
144
+
145
+ const formCss = `
146
+ /* Reset default form styles */
147
+ form {
148
+ margin: 0;
149
+ padding: 0;
150
+ }
151
+
152
+ /* Center the form */
153
+ body {
154
+ display: flex;
155
+ justify-content: center;
156
+ align-items: center;
157
+ height: 100vh;
158
+ background-color: #f5f5f5;
159
+ }
160
+
161
+ /* Style form container */
162
+ form {
163
+ width: 400px;
164
+ padding: 20px;
165
+ border-radius: 4px;
166
+ background-color: white;
167
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
168
+ }
169
+
170
+ /* Style form labels */
171
+ label {
172
+ font-weight: bold;
173
+ margin-bottom: 5px;
174
+ color: #333;
175
+ }
176
+
177
+ /* Style form inputs */
178
+ input {
179
+ width: 100%;
180
+ padding: 8px;
181
+ margin-bottom: 10px;
182
+ border: 1px solid #ccc;
183
+ border-radius: 4px;
184
+ }
185
+ input.checkbox {
186
+ width: 20px;
187
+ height: 20px;
188
+ }
189
+ fieldset {
190
+ padding: 20px;
191
+ }
192
+
193
+ /* Style form buttons */
194
+ .button-container {
195
+ display: flex;
196
+ justify-content: space-between;
197
+ margin-top: 20px;
198
+ }
199
+
200
+ .primary-button,
201
+ .secondary-button {
202
+ width: 48%;
203
+ padding: 10px;
204
+ border: none;
205
+ border-radius: 4px;
206
+ cursor: pointer;
207
+ transition: background-color 0.3s ease;
208
+ }
209
+
210
+ .primary-button {
211
+ background-color: #4caf50;
212
+ color: white;
213
+ }
214
+
215
+ .secondary-button {
216
+ background-color: #e0e0e0;
217
+ color: #333;
218
+ }
219
+
220
+ .primary-button:hover,
221
+ .secondary-button:hover {
222
+ background-color: #388e3c;
223
+ }
224
+
225
+ /* Style form checkbox */
226
+ .checkbox-container {
227
+ display: flex;
228
+ align-items: center;
229
+ margin-bottom: 10px;
230
+ margin: 20px 0;
231
+ }
232
+
233
+ .checkbox-container label {
234
+ margin-left: 5px;
235
+ color: #333;
236
+ }
237
+
238
+ /* Style details and summary elements */
239
+ details {
240
+ margin-top: 20px;
241
+ color: #333;
242
+ }
243
+
244
+ summary {
245
+ font-weight: bold;
246
+ cursor: pointer;
247
+ }
248
+
249
+ details > p {
250
+ margin-top: 10px;
251
+ }
252
+
253
+ `
254
+
255
+ /**
256
+ * @class FtpBackend
257
+ * @implements {HostingProvider}
258
+ * @implements {StorageProvider}
259
+ */
260
+ export default class FtpBackend implements HostingProvider<FtpSession>, StorageProvider<FtpSession> {
261
+ id = 'ftp'
262
+ displayName = 'Ftp'
263
+ icon = 'ftp'
264
+ options: FtpOptionsWithDefaults
265
+ type: BackendType
266
+
267
+ constructor(config: ServerConfig, opts: FtpOptions) {
268
+ this.options = {
269
+ rootPath: '',
270
+ authorizeUrl: '/api/authorize/ftp/',
271
+ authorizePath: '/api/authorize/ftp/',
272
+ ...opts,
273
+ }
274
+ this.type = this.options.type
275
+ config.on(EVENT_STARTUP_START, ({app}: {app: Application}) => {
276
+ const router = express.Router()
277
+ app.use(router)
278
+ router.post(this.options.authorizePath, express.urlencoded(), async (req, res, next) => {
279
+ if(req.query.type !== this.options.type) {
280
+ next()
281
+ return
282
+ }
283
+ try {
284
+ const session = req['session'] as FtpSession
285
+ req['session'] = {
286
+ ...session,
287
+ ...await this.authorize(session, req.body),
288
+ }
289
+ res.redirect(`/${API_BACKEND_LOGIN_CALLBACK}?backendId=${encodeURIComponent(this.id)}&type=${encodeURIComponent(this.type)}`)
290
+ } catch (err) {
291
+ console.error('FTP auth failed', err)
292
+ res
293
+ .status(403)
294
+ .redirect(`/${API_BACKEND_LOGIN_CALLBACK}?error=${encodeURIComponent(err.message)}&type=${encodeURIComponent(this.type)}`)
295
+ }
296
+ })
297
+ router.get(this.options.authorizePath, async (req, res, next) => {
298
+ if(req.query.type !== this.options.type) {
299
+ next()
300
+ return
301
+ }
302
+ const session = req['session'] as FtpSession
303
+ // Check if the user is already logged in
304
+ const { host, user, pass, port, secure } = this.sessionData(session)
305
+ if (host && user && pass && port && secure) {
306
+ try {
307
+ await this.checkAuth({ host, user, pass, port, secure })
308
+ res.redirect(API_BACKEND_LOGIN_CALLBACK + '?backendId=' + encodeURIComponent(this.id))
309
+ return
310
+ } catch (err) {
311
+ // TODO: check if the error is a 401
312
+ console.log('User not logged in yet, let\'s display the login form', err.message)
313
+ }
314
+ }
315
+ // If not, display the login form
316
+ res.send(await this.authorizeForm(session, req.query as any))
317
+ })
318
+ })
319
+ }
320
+
321
+ sessionData(session: FtpSession): FtpSessionData {
322
+ return session[this.options.type] ?? {} as FtpSessionData
323
+ }
324
+
325
+ storageRootPath(session: FtpSession): string {
326
+ return this.type === BackendType.STORAGE ? requiredParam<string>(this.sessionData(session).storageRootPath, 'storage root path') : requiredParam<string>(this.sessionData(session).publicationPath, 'publication path')
327
+ }
328
+
329
+ // **
330
+ // Ftp specific methods
331
+ async checkAuth({host, user, pass, port, secure}) {
332
+ const ftp = await this.getClient({ host, user, pass, port, secure })
333
+ }
334
+
335
+ async authorizeForm(session: FtpSession, query: { error :string, type: string }): Promise<string> {
336
+ const { host, user, pass, port, secure, publicationPath, storageRootPath, websiteUrl } = this.sessionData(session)
337
+ const { error, type } = query
338
+ requiredParam(type, 'backend type')
339
+ return `
340
+ <html>
341
+ <head>
342
+ <title>FTP auth</title>
343
+ <style>
344
+ ${formCss}
345
+ </style>
346
+ </head>
347
+ <body>
348
+ ${formHtml(toBackendEnum(type), { host, user, pass, port, secure, publicationPath, storageRootPath, websiteUrl }, error)}
349
+ </body>
350
+ </html>
351
+ `
352
+ }
353
+
354
+ async getClient({host, user, pass, port, secure}): Promise<any> {
355
+ return new Promise((resolve, reject) => {
356
+ const ftp = new JsFTP({
357
+ host,
358
+ port,
359
+ user,
360
+ pass,
361
+ })
362
+
363
+ ftp['on']('close', (err) => {
364
+ console.error('FTP CONNEXION close', err)
365
+ })
366
+
367
+ ftp['on']('error', (err) => {
368
+ console.error('FTP CONNEXION error', err)
369
+ reject(err)
370
+ })
371
+
372
+ ftp['on']('connect', () => {
373
+ ftp.auth(user, pass, (err) => {
374
+ if (err) {
375
+ console.error('FTP AUTH error', err)
376
+ reject(err)
377
+ } else {
378
+ resolve(ftp)
379
+ }
380
+ })
381
+ })
382
+ })
383
+ }
384
+
385
+ // **
386
+ // Backend interface
387
+ // **
388
+ async authorize(session: FtpSession, body: any): Promise<FtpSession> {
389
+ const { host, user, pass, port, secure = false, publicationPath, storageRootPath, websiteUrl } = body as FtpSessionData
390
+ requiredParam(session, 'session')
391
+ requiredParam(host, 'host')
392
+ requiredParam(user, 'user')
393
+ requiredParam(pass, 'pass')
394
+ requiredParam(port, 'port')
395
+ await this.checkAuth({ host, user, pass, port, secure })
396
+ return {
397
+ [this.options.type]: { host, user, pass, port, secure, publicationPath, storageRootPath, websiteUrl },
398
+ }
399
+ }
400
+
401
+ async getUserData(session: FtpSession): Promise<BackendUser> {
402
+ return {
403
+ name: this.sessionData(session).user,
404
+ picture: this.icon,
405
+ }
406
+ }
407
+
408
+ async getSiteMeta(session: FtpSession, id: WebsiteId): Promise<WebsiteMeta> {
409
+ try {
410
+ const websiteId = requiredParam<WebsiteId>(id, 'website id')
411
+ const files = await this.listDir(session, websiteId, '/')
412
+ const file = files.find(f => f.name === WEBSITE_DATA_FILE_NAME)
413
+ if (!file) {
414
+ const storageRootPath = this.storageRootPath(session)
415
+ const dirPath = join(this.options.rootPath, storageRootPath, websiteId, '/')
416
+ console.error('Website data file not found', {websiteId, WEBSITE_DATA_FILE_NAME, dirPath, storageRootPath}, this.options)
417
+ throw new Error(`Website data file not found: ${WEBSITE_DATA_FILE_NAME} in ${dirPath}`)
418
+ }
419
+ return {
420
+ websiteId,
421
+ //url: await this.getFileUrl(session, websiteId, WEBSITE_DATA_FILE_NAME),
422
+ createdAt: file.createdAt,
423
+ updatedAt: file.updatedAt,
424
+ metadata: file.metadata,
425
+ }
426
+ } catch(err) {
427
+ console.error(err)
428
+ throw err
429
+ }
430
+ }
431
+
432
+ async isLoggedIn(session: FtpSession) {
433
+ try {
434
+ if (!session[this.options.type]) {
435
+ return false
436
+ }
437
+ //await this.checkAuth(this.sessionData(session))
438
+ } catch(err) {
439
+ return false
440
+ }
441
+ return true
442
+ }
443
+
444
+ async getAuthorizeURL(session: FtpSession) {
445
+ return `${this.options.authorizeUrl}?type=${this.type}`
446
+ }
447
+
448
+ //async login(session: FtpSession, userData): Promise<void> {
449
+ // await this.checkAuth(userData)
450
+ // session[this.options.sessionKey] = userData
451
+ // console.log('login success', userData, this.sessionData(session))
452
+ //}
453
+
454
+ async logout(session: FtpSession) {
455
+ delete session[this.options.type]
456
+ }
457
+
458
+ async getAdminUrl(session: FtpSession, id: WebsiteId) {
459
+ return null
460
+ }
461
+
462
+ // **
463
+ // Storage interface
464
+ /**
465
+ * Create necessary folders
466
+ * Assets and root folders
467
+ */
468
+ async init(session: FtpSession, id: WebsiteId): Promise<void> {
469
+ // create root folder
470
+ const ftp = await this.getClient(this.sessionData(session))
471
+ try {
472
+ await this.createDir(session, id, '/')
473
+ } catch(err) {
474
+ handleInitError(err)
475
+ }
476
+ try {
477
+ await this.createDir(session, id, '/assets')
478
+ } catch(err) {
479
+ handleInitError(err)
480
+ }
481
+ }
482
+
483
+ async readFile(session: FtpSession, id: string, path: string): Promise<File> {
484
+ return new Promise((resolve, reject) => {
485
+ if(!this.sessionData(session)) throw new Error('Not logged in')
486
+ const storageRootPath = this.storageRootPath(session)
487
+ this.getClient(this.sessionData(session))
488
+ .then(ftp => {
489
+ const dirPath = join(this.options.rootPath, storageRootPath, id, path)
490
+ ftp.get(
491
+ dirPath,
492
+ (err, socket) => {
493
+ if (err) {
494
+ return reject(err)
495
+ }
496
+ resolve({
497
+ path,
498
+ content: Readable.from(socket),
499
+ })
500
+ socket.resume()
501
+ })
502
+ })
503
+ .catch(err => {
504
+ console.error(`Error reading file ${path}: ${err.message}`)
505
+ reject(err)
506
+ })
507
+ })
508
+ }
509
+
510
+ async writeFiles(
511
+ session: any,
512
+ id: WebsiteId,
513
+ files: File[],
514
+ statusCbk?: StatusCallback,
515
+ ): Promise<string[]> {
516
+ statusCbk && statusCbk({
517
+ message: 'Connecting to FTP server',
518
+ status: JobStatus.IN_PROGRESS,
519
+ })
520
+ const ftp = await this.getClient(this.sessionData(session))
521
+ const filesStatuses = initStatus(files)
522
+ const storageRootPath = this.storageRootPath(session)
523
+ return Promise.all(
524
+ filesStatuses.map((fileStatus) => {
525
+ const { file } = fileStatus
526
+ return new Promise<string>((resolve, reject) => {
527
+ fileStatus.message = 'Upload started'
528
+ updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
529
+ const content = typeof file.content === 'string' ? Buffer.from(file.content) : file.content
530
+ const dstPath = join(this.options.rootPath, storageRootPath, id, file.path)
531
+ ftp.put(content, dstPath, (err) => {
532
+ if (err) {
533
+ const errMsg = `Error writing file ${dstPath}: ${err.message}`
534
+ console.error(errMsg)
535
+ //fileStatus.message = `Error ${err.message}`
536
+ //updateStatus(filesStatuses, JobStatus.ERROR, statusCbk)
537
+
538
+ statusCbk && statusCbk({
539
+ message: errMsg,
540
+ status: JobStatus.ERROR,
541
+ })
542
+ return reject(new Error(errMsg))
543
+ }
544
+ fileStatus.message = 'Upload complete'
545
+ updateStatus(filesStatuses, JobStatus.IN_PROGRESS, statusCbk)
546
+ resolve(dstPath)
547
+ })
548
+ })
549
+ })
550
+ )
551
+ .catch((err) => {
552
+ // Not sure why it never gets here
553
+ console.error(err)
554
+ statusCbk && statusCbk({
555
+ message: err.message,
556
+ status: JobStatus.ERROR,
557
+ })
558
+ return []
559
+ })
560
+ }
561
+
562
+ async deleteFiles(session: FtpSession, id, paths) {
563
+ const storageRootPath = this.storageRootPath(session)
564
+ const ftp = await this.getClient(this.sessionData(session))
565
+ await Promise.all(
566
+ paths.map((path) =>
567
+ new Promise<void>((resolve, reject) => {
568
+ const dstPath = join(this.options.rootPath, storageRootPath, id, path)
569
+ ftp.raw('dele', dstPath, (err) => {
570
+ if (err) {
571
+ console.error(`Error deleting file ${dstPath}: ${err.message}`)
572
+ return reject(err)
573
+ }
574
+ resolve()
575
+ })
576
+ })
577
+ )
578
+ )
579
+ }
580
+
581
+ async listWebsites(session: FtpSession): Promise<WebsiteMeta[]> {
582
+ const storageRootPath = this.storageRootPath(session)
583
+ const ftp = await this.getClient(this.sessionData(session))
584
+ return new Promise((resolve, reject) => {
585
+ ftp.ls(join(this.options.rootPath, storageRootPath), (err, res) => {
586
+ if (err) {
587
+ console.error(`Error listing websites: ${err.message}`)
588
+ return reject(err)
589
+ }
590
+ resolve(res.map((file) => ({
591
+ websiteId: file.name,
592
+ createdAt: new Date(file.time),
593
+ updatedAt: new Date(file.time),
594
+ metadata: {
595
+ ...file,
596
+ },
597
+ })))
598
+ })
599
+ })
600
+ }
601
+
602
+ async listDir(
603
+ session: FtpSession,
604
+ id: WebsiteId,
605
+ path: string,
606
+ ): Promise<FileMeta[]> {
607
+ const ftp = await this.getClient(this.sessionData(session))
608
+ const storageRootPath = this.storageRootPath(session)
609
+
610
+ return new Promise((resolve, reject) => {
611
+ const dstPath = join(this.options.rootPath, storageRootPath, id, path)
612
+ ftp.ls(dstPath, (err, res) => {
613
+ if (err) {
614
+ console.error(`Error listing folder ${dstPath}: ${err.message}`)
615
+ return reject(err)
616
+ }
617
+ resolve(res.map((file) => ({
618
+ name: file.name,
619
+ isDir: file.type === 1,
620
+ size: file.size,
621
+ createdAt: new Date(file.time),
622
+ updatedAt: new Date(file.time),
623
+ metaData: {
624
+ size: file.size,
625
+ },
626
+ } as FileMeta)))
627
+ })
628
+ })
629
+ }
630
+
631
+ async createDir(session: FtpSession, id: WebsiteId, path: string) {
632
+ const storageRootPath = this.storageRootPath(session)
633
+ const ftp = await this.getClient(this.sessionData(session))
634
+ return new Promise<void>((resolve, reject) => {
635
+ const dirPath = join(this.options.rootPath, storageRootPath, id, path)
636
+ ftp.raw('mkd', dirPath, (err) => {
637
+ if (err) {
638
+ console.error(`Error creating folder ${dirPath}: ${err.message}`)
639
+ return reject(err)
640
+ }
641
+ resolve()
642
+ })
643
+ })
644
+ }
645
+
646
+ async deleteDir(session: FtpSession, id: WebsiteId, path: string) {
647
+ const storageRootPath = this.storageRootPath(session)
648
+ const ftp = await this.getClient(this.sessionData(session))
649
+ return new Promise<void>((resolve, reject) => {
650
+ const dirPath = join(this.options.rootPath, storageRootPath, id, path)
651
+ ftp.raw('rmd', dirPath, (err) => {
652
+ if (err) {
653
+ console.error(`Error deleting folder ${dirPath}: ${err.message}`)
654
+ return reject(err)
655
+ }
656
+ resolve()
657
+ })
658
+ })
659
+ }
660
+
661
+ // **
662
+ // Hosting interface
663
+ async getWebsiteUrl(session: FtpSession, id: WebsiteId): Promise<string> {
664
+ // FIXME: do not store websiteUrl in the session, but in the website data
665
+ console.warn('FIXME: do not store websiteUrl in the session, but in the website data')
666
+ return this.sessionData(session).websiteUrl ?? ''
667
+ }
668
+
669
+ async publish(session: FtpSession, id: string, backendData: BackendData, files: File[]): Promise<JobData> {
670
+ const job = startJob(`Publishing to ${this.displayName}`) as PublicationJobData
671
+ job.logs = [[`Publishing to ${this.displayName}`]]
672
+ job.errors = [[]]
673
+ this.writeFiles(session, '', files, async ({status, message}) => {
674
+ // Update the job status
675
+ job.status = status
676
+ job.message = message
677
+ job.logs[0].push(message)
678
+ // Add the website url
679
+ job.url = await this.getWebsiteUrl(session, id)
680
+ if(status === JobStatus.SUCCESS) {
681
+ jobSuccess(job.jobId, message)
682
+ } else if(status === JobStatus.ERROR) {
683
+ job.errors[0].push(message)
684
+ jobError(job.jobId, message)
685
+ }
686
+ })
687
+ return job
688
+ }
689
+ }