@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.
- package/.silex.js +38 -8
- package/dist/client/client/config.d.ts +11 -41
- package/dist/client/client/grapesjs/LoginDialog.d.ts +13 -0
- package/dist/client/client/grapesjs/PublicationManager.d.ts +69 -0
- package/dist/client/client/grapesjs/PublicationUi.d.ts +58 -0
- package/dist/client/client/grapesjs/index.d.ts +18 -44
- package/dist/client/client/grapesjs/storage.d.ts +1 -0
- package/dist/client/client/services.d.ts +35 -0
- package/dist/client/constants.d.ts +17 -0
- package/dist/client/css/admin.css +86 -3
- package/dist/client/js/main.js +201 -90
- package/dist/client/js/main.js.LICENSE.txt +6 -0
- package/dist/client/types.d.ts +200 -29
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +38 -1
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +17 -0
- package/dist/events.js.map +1 -0
- package/dist/plugins/AuthPlugin.d.ts +1 -0
- package/dist/plugins/AuthPlugin.js +38 -0
- package/dist/plugins/AuthPlugin.js.map +1 -0
- package/dist/plugins/Cache.d.ts +2 -0
- package/dist/plugins/Cache.js +32 -0
- package/dist/plugins/Cache.js.map +1 -0
- package/dist/plugins/FtpBackend.d.ts +92 -0
- package/dist/plugins/FtpBackend.js +604 -0
- package/dist/plugins/FtpBackend.js.map +1 -0
- package/dist/plugins/HooksPlugin.d.ts +7 -0
- package/dist/plugins/HooksPlugin.js +76 -0
- package/dist/plugins/HooksPlugin.js.map +1 -0
- package/dist/plugins/PublicationPlugin.d.ts +17 -0
- package/dist/plugins/PublicationPlugin.js +170 -0
- package/dist/plugins/PublicationPlugin.js.map +1 -0
- package/dist/plugins/SslPlugin.d.ts +1 -0
- package/dist/plugins/SslPlugin.js +86 -0
- package/dist/plugins/SslPlugin.js.map +1 -0
- package/dist/plugins/StaticPlugin.d.ts +1 -0
- package/dist/plugins/StaticPlugin.js +66 -0
- package/dist/plugins/StaticPlugin.js.map +1 -0
- package/dist/plugins/WebsitePlugin.d.ts +14 -0
- package/dist/plugins/WebsitePlugin.js +302 -0
- package/dist/plugins/WebsitePlugin.js.map +1 -0
- package/dist/server/backends/FsBackend.d.ts +42 -0
- package/dist/server/backends/FsBackend.js +269 -0
- package/dist/server/backends/FsBackend.js.map +1 -0
- package/dist/server/backends/index.d.ts +95 -0
- package/dist/server/backends/index.js +255 -0
- package/dist/server/backends/index.js.map +1 -0
- package/dist/server/cli.js +0 -1
- package/dist/server/config.d.ts +58 -0
- package/dist/server/config.js +128 -24
- package/dist/server/config.js.map +1 -0
- package/dist/server/express.js +31 -3
- package/dist/server/index.js +9 -20
- package/dist/server/jobs.d.ts +6 -0
- package/dist/server/jobs.js +75 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/utils/validation.d.ts +7 -0
- package/dist/server/utils/validation.js +37 -0
- package/dist/server/utils/validation.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +35 -28
- package/dist/types.js.map +1 -0
- package/package.json +19 -8
- package/public/silex.js +17 -0
- package/src/css/dialog.scss +9 -2
- package/src/css/grapesjs.scss +79 -0
- package/src/css/project-bar.scss +2 -2
- package/src/example-client-config.js +17 -0
- package/src/html/templates/includes.pug +0 -1
- package/src/ts/client/config.ts +7 -6
- package/src/ts/client/grapesjs/LoginDialog.ts +190 -0
- package/src/ts/client/grapesjs/PublicationManager.ts +333 -0
- package/src/ts/client/grapesjs/PublicationUi.ts +320 -0
- package/src/ts/client/grapesjs/index.ts +88 -85
- package/src/ts/client/grapesjs/storage.ts +110 -0
- package/src/ts/client/index.ts +5 -7
- package/src/ts/client/services.ts +129 -0
- package/src/ts/constants.ts +21 -0
- package/src/ts/plugins/AuthPlugin.ts +38 -0
- package/src/ts/plugins/Cache.ts +28 -0
- package/src/ts/plugins/FtpBackend.ts +689 -0
- package/src/ts/plugins/HooksPlugin.ts +79 -0
- package/src/ts/plugins/PublicationPlugin.ts +170 -0
- package/src/{plugins/SslPlugin.js → ts/plugins/SslPlugin.ts} +24 -6
- package/src/ts/plugins/StaticPlugin.ts +68 -0
- package/src/ts/plugins/WebsitePlugin.ts +318 -0
- package/src/ts/server/backends/FsBackend.ts +266 -0
- package/src/ts/server/backends/index.ts +309 -0
- package/src/ts/server/cli.ts +0 -2
- package/src/ts/server/config.ts +103 -33
- package/src/ts/server/express.ts +37 -3
- package/src/ts/server/index.ts +24 -14
- package/src/ts/server/jobs.test.ts +74 -0
- package/src/ts/server/jobs.ts +77 -0
- package/src/ts/server/utils/validation.ts +33 -0
- package/src/ts/types.ts +167 -59
- package/dist/client/client/grapesjs/publish.d.ts +0 -18
- package/dist/page.js +0 -15
- package/src/plugins/AuthPlugin.js +0 -21
- package/src/plugins/Cache.js +0 -11
- package/src/plugins/DefaultBackend.js +0 -149
- package/src/plugins/ExpressPlugin.js +0 -39
- package/src/plugins/HooksPlugin.js +0 -58
- package/src/plugins/PublishPlugin.js +0 -69
- package/src/plugins/StaticPlugin.js +0 -48
- package/src/plugins/WebsitePlugin.js +0 -207
- package/src/ts/client/grapesjs/publish.ts +0 -403
- /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
|
+
}
|