@silexlabs/silex 3.0.6 → 3.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/client/grapesjs/upload-progress.d.ts +2 -0
- package/dist/client/constants.d.ts +1 -0
- package/dist/client/js/main.js +107 -110
- package/dist/client/silex-lib/src/ts/client/grapesjs/upload-progress.d.ts +2 -0
- package/dist/client/silex-lib/src/ts/constants.d.ts +1 -0
- package/dist/plugins/server/constants.d.ts +1 -0
- package/dist/plugins/server/constants.js +2 -1
- package/dist/plugins/server/constants.js.map +1 -1
- package/dist/plugins/server/page.d.ts +2 -0
- package/dist/plugins/server/page.js +35 -0
- package/dist/plugins/server/page.js.map +1 -0
- package/dist/plugins/server/plugins/server/GitlabConnector.d.ts +17 -7
- package/dist/plugins/server/plugins/server/GitlabConnector.js +255 -114
- package/dist/plugins/server/plugins/server/GitlabConnector.js.map +1 -1
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js +6 -0
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js.map +1 -1
- package/dist/server/constants.js +2 -1
- package/dist/server/server/api/publicationApi.js +2 -2
- package/package.json +1 -1
- package/src/ts/client/grapesjs/PublicationUi.ts +20 -23
- package/src/ts/client/grapesjs/index.ts +2 -0
- package/src/ts/client/grapesjs/storage.ts +8 -2
- package/src/ts/client/grapesjs/upload-progress.ts +48 -0
- package/src/ts/constants.ts +1 -0
- package/src/ts/plugins/server/GitlabConnector.ts +326 -140
- package/src/ts/plugins/server/GitlabHostingConnector.ts +6 -0
- package/src/ts/server/api/publicationApi.ts +2 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const WEBSITE_DATA_FILE = "website.json";
|
|
5
5
|
export declare const WEBSITE_META_DATA_FILE = "meta.json";
|
|
6
|
+
export declare const WEBSITE_PAGES_FOLDER = "src";
|
|
6
7
|
export declare const DEFAULT_WEBSITE_ID = "default";
|
|
7
8
|
export declare const DEFAULT_LANGUAGE = "en";
|
|
8
9
|
export declare const CLIENT_CONFIG_FILE_NAME = "silex.js";
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const WEBSITE_DATA_FILE = "website.json";
|
|
5
5
|
export declare const WEBSITE_META_DATA_FILE = "meta.json";
|
|
6
|
+
export declare const WEBSITE_PAGES_FOLDER = "src";
|
|
6
7
|
export declare const DEFAULT_WEBSITE_ID = "default";
|
|
7
8
|
export declare const DEFAULT_LANGUAGE = "en";
|
|
8
9
|
export declare const CLIENT_CONFIG_FILE_NAME = "silex.js";
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.DEV_MESSAGE = exports.SILEX_VERSION = exports.API_WEBSITE_META_WRITE = exports.API_WEBSITE_META_READ = exports.API_WEBSITE_ASSETS_WRITE = exports.API_WEBSITE_ASSET_READ = exports.API_WEBSITE_LIST = exports.API_WEBSITE_DUPLICATE = exports.API_WEBSITE_DELETE = exports.API_WEBSITE_CREATE = exports.API_WEBSITE_WRITE = exports.API_WEBSITE_READ = exports.API_WEBSITE_PATH = exports.API_PUBLICATION_STATUS = exports.API_PUBLICATION_PUBLISH = exports.API_PUBLICATION_PATH = exports.API_CONNECTOR_LOGIN_CALLBACK = exports.API_CONNECTOR_SETTINGS = exports.API_CONNECTOR_LOGIN = exports.API_CONNECTOR_LOGOUT = exports.API_CONNECTOR_LIST = exports.API_CONNECTOR_USER = exports.API_CONNECTOR_PATH = exports.API_PATH = exports.CLIENT_CONFIG_FILE_NAME = exports.DEFAULT_LANGUAGE = exports.DEFAULT_WEBSITE_ID = exports.WEBSITE_META_DATA_FILE = exports.WEBSITE_DATA_FILE = void 0;
|
|
19
|
+
exports.DEV_MESSAGE = exports.SILEX_VERSION = exports.API_WEBSITE_META_WRITE = exports.API_WEBSITE_META_READ = exports.API_WEBSITE_ASSETS_WRITE = exports.API_WEBSITE_ASSET_READ = exports.API_WEBSITE_LIST = exports.API_WEBSITE_DUPLICATE = exports.API_WEBSITE_DELETE = exports.API_WEBSITE_CREATE = exports.API_WEBSITE_WRITE = exports.API_WEBSITE_READ = exports.API_WEBSITE_PATH = exports.API_PUBLICATION_STATUS = exports.API_PUBLICATION_PUBLISH = exports.API_PUBLICATION_PATH = exports.API_CONNECTOR_LOGIN_CALLBACK = exports.API_CONNECTOR_SETTINGS = exports.API_CONNECTOR_LOGIN = exports.API_CONNECTOR_LOGOUT = exports.API_CONNECTOR_LIST = exports.API_CONNECTOR_USER = exports.API_CONNECTOR_PATH = exports.API_PATH = exports.CLIENT_CONFIG_FILE_NAME = exports.DEFAULT_LANGUAGE = exports.DEFAULT_WEBSITE_ID = exports.WEBSITE_PAGES_FOLDER = exports.WEBSITE_META_DATA_FILE = exports.WEBSITE_DATA_FILE = void 0;
|
|
20
20
|
/**
|
|
21
21
|
* @fileoverview define constants for Silex client and server
|
|
22
22
|
*/
|
|
23
23
|
exports.WEBSITE_DATA_FILE = 'website.json';
|
|
24
24
|
exports.WEBSITE_META_DATA_FILE = 'meta.json';
|
|
25
|
+
exports.WEBSITE_PAGES_FOLDER = 'src';
|
|
25
26
|
exports.DEFAULT_WEBSITE_ID = 'default';
|
|
26
27
|
exports.DEFAULT_LANGUAGE = 'en';
|
|
27
28
|
exports.CLIENT_CONFIG_FILE_NAME = 'silex.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/ts/constants.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH;;GAEG;AAEU,QAAA,iBAAiB,GAAG,cAAc,CAAA;AAClC,QAAA,sBAAsB,GAAG,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/ts/constants.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH;;GAEG;AAEU,QAAA,iBAAiB,GAAG,cAAc,CAAA;AAClC,QAAA,sBAAsB,GAAG,WAAW,CAAA;AACpC,QAAA,oBAAoB,GAAG,KAAK,CAAA;AAE5B,QAAA,kBAAkB,GAAG,SAAS,CAAA;AAC9B,QAAA,gBAAgB,GAAG,IAAI,CAAA;AACvB,QAAA,uBAAuB,GAAG,UAAU,CAAA;AAEpC,QAAA,QAAQ,GAAG,MAAM,CAAA;AAEjB,QAAA,kBAAkB,GAAG,YAAY,CAAA;AACjC,QAAA,kBAAkB,GAAG,OAAO,CAAA;AAC5B,QAAA,kBAAkB,GAAG,GAAG,CAAA;AACxB,QAAA,oBAAoB,GAAG,SAAS,CAAA;AAChC,QAAA,mBAAmB,GAAG,QAAQ,CAAA;AAC9B,QAAA,sBAAsB,GAAG,WAAW,CAAA;AACpC,QAAA,4BAA4B,GAAG,iBAAiB,CAAA;AAEhD,QAAA,oBAAoB,GAAG,cAAc,CAAA;AACrC,QAAA,uBAAuB,GAAG,GAAG,CAAA;AAC7B,QAAA,sBAAsB,GAAG,qBAAqB,CAAA;AAE9C,QAAA,gBAAgB,GAAG,UAAU,CAAA;AAC7B,QAAA,gBAAgB,GAAG,GAAG,CAAA;AACtB,QAAA,iBAAiB,GAAG,GAAG,CAAA;AACvB,QAAA,kBAAkB,GAAG,GAAG,CAAA;AACxB,QAAA,kBAAkB,GAAG,GAAG,CAAA;AACxB,QAAA,qBAAqB,GAAG,YAAY,CAAA;AACpC,QAAA,gBAAgB,GAAG,GAAG,CAAA;AACtB,QAAA,sBAAsB,GAAG,SAAS,CAAA;AAClC,QAAA,wBAAwB,GAAG,SAAS,CAAA;AACpC,QAAA,qBAAqB,GAAG,OAAO,CAAA;AAC/B,QAAA,sBAAsB,GAAG,OAAO,CAAA;AAO7C,IAAI,CAAC;IAAC,qBAAa,GAAG,iBAAiB,CAAA;AAAC,CAAC;AAAC,OAAO,CAAC,EAAE,CAAC;IACnD,4BAA4B;IAC5B,qBAAa,GAAG,qBAAa,IAAI,OAAO,CAAA;AAC1C,CAAC;AAEY,QAAA,WAAW,GAAG;;;;;;;;;;2DAUgC,qBAAa,CAAC,KAAK,CAAC,CAAC,CAAC;;;;;;;;CAQhF,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
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.getPageSlug = getPageSlug;
|
|
20
|
+
exports.getPageLink = getPageLink;
|
|
21
|
+
// Page related functions
|
|
22
|
+
// This is used on the client and the server
|
|
23
|
+
function getPageSlug(pageName) {
|
|
24
|
+
return (pageName || 'index')
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9 -]/g, '')
|
|
27
|
+
// Collapse whitespace and replace by -
|
|
28
|
+
.replace(/\s+/g, '-')
|
|
29
|
+
// Collapse dashes
|
|
30
|
+
.replace(/-+/g, '-');
|
|
31
|
+
}
|
|
32
|
+
function getPageLink(pageName) {
|
|
33
|
+
return `./${getPageSlug(pageName)}.html`;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../../src/ts/page.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;AAIH,kCAQC;AACD,kCAEC;AAbD,yBAAyB;AACzB,4CAA4C;AAC5C,SAAgB,WAAW,CAAC,QAAQ;IAClC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC;SACzB,WAAW,EAAE;SACb,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC;QAC5B,uCAAuC;SACtC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;QACrB,kBAAkB;SACjB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;AACxB,CAAC;AACD,SAAgB,WAAW,CAAC,QAAQ;IAClC,OAAO,KAAK,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAA;AAC1C,CAAC"}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { ServerConfig } from '../../server/config';
|
|
2
2
|
import { ConnectorFile, ConnectorFileContent, StatusCallback, StorageConnector } from '../../server/connectors/connectors';
|
|
3
3
|
import { ConnectorType, ConnectorUser, WebsiteData, WebsiteId, WebsiteMeta, WebsiteMetaFileContent } from '../../types';
|
|
4
|
-
/**
|
|
5
|
-
* Gitlab connector
|
|
6
|
-
* @fileoverview Gitlab connector for Silex, connect to the user's Gitlab account to store websites
|
|
7
|
-
* @see https://docs.gitlab.com/ee/api/oauth2.html
|
|
8
|
-
*/
|
|
9
4
|
export interface GitlabOptions {
|
|
10
5
|
clientId: string;
|
|
11
6
|
clientSecret: string;
|
|
@@ -94,7 +89,9 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
94
89
|
/**
|
|
95
90
|
* Call the Gitlab API with the user's token and handle errors
|
|
96
91
|
*/
|
|
97
|
-
callApi(session: GitlabSession, path: string, method?: 'POST' | 'GET' | 'PUT' | 'DELETE',
|
|
92
|
+
callApi(session: GitlabSession, path: string, method?: 'POST' | 'GET' | 'PUT' | 'DELETE', requestBody?: GitlabWriteFile | GitlabGetToken | GitlabWebsiteName | GitlabCreateBranch | GitlabGetTags | GitlabCreateTag | GitlabFetchCommits | null, params?: any): Promise<any>;
|
|
93
|
+
private projectPathCache;
|
|
94
|
+
downloadRawFile(session: GitlabSession, projectId: string, filePath: string): Promise<Buffer>;
|
|
98
95
|
private generateCodeVerifier;
|
|
99
96
|
private generateCodeChallenge;
|
|
100
97
|
private getRedirect;
|
|
@@ -122,14 +119,27 @@ export default class GitlabConnector implements StorageConnector {
|
|
|
122
119
|
logout(session: GitlabSession): Promise<void>;
|
|
123
120
|
getUser(session: GitlabSession): Promise<ConnectorUser>;
|
|
124
121
|
listWebsites(session: GitlabSession): Promise<WebsiteMeta[]>;
|
|
122
|
+
/**
|
|
123
|
+
* Read the website data
|
|
124
|
+
* The website data file is named `website.json` and the pages are named `page-{id}.json`
|
|
125
|
+
* The pages are stored in the `src` folder by default
|
|
126
|
+
*/
|
|
125
127
|
readWebsite(session: GitlabSession, websiteId: string): Promise<WebsiteData>;
|
|
128
|
+
/**
|
|
129
|
+
* Create a new website, i.e. a new Gitlab repository with an empty website data file
|
|
130
|
+
*/
|
|
126
131
|
createWebsite(session: GitlabSession, websiteMeta: WebsiteMetaFileContent): Promise<WebsiteId>;
|
|
132
|
+
/**
|
|
133
|
+
* Update the website data
|
|
134
|
+
* Split the website data into 1 file per page + 1 file for the website data itself
|
|
135
|
+
* Use gitlab batch API to create/update the files
|
|
136
|
+
*/
|
|
127
137
|
updateWebsite(session: GitlabSession, websiteId: WebsiteId, websiteData: WebsiteData): Promise<void>;
|
|
128
138
|
deleteWebsite(session: GitlabSession, websiteId: WebsiteId): Promise<void>;
|
|
129
139
|
duplicateWebsite(session: GitlabSession, websiteId: string): Promise<void>;
|
|
130
140
|
getWebsiteMeta(session: GitlabSession, websiteId: WebsiteId): Promise<WebsiteMeta>;
|
|
131
141
|
setWebsiteMeta(session: GitlabSession, websiteId: WebsiteId, websiteMeta: WebsiteMetaFileContent): Promise<void>;
|
|
132
|
-
writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback
|
|
142
|
+
writeAssets(session: GitlabSession, websiteId: string, files: ConnectorFile[], status?: StatusCallback): Promise<void>;
|
|
133
143
|
readAsset(session: GitlabSession, websiteId: string, fileName: string): Promise<ConnectorFileContent>;
|
|
134
144
|
deleteAssets(session: GitlabSession, websiteId: string, fileNames: string[]): Promise<void>;
|
|
135
145
|
}
|
|
@@ -49,6 +49,22 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
|
49
49
|
const crypto_1 = __importStar(require("crypto"));
|
|
50
50
|
const path_1 = require("path");
|
|
51
51
|
const https_1 = require("https");
|
|
52
|
+
const page_1 = require("../../page");
|
|
53
|
+
/**
|
|
54
|
+
* Gitlab connector
|
|
55
|
+
* @fileoverview Gitlab connector for Silex, connect to the user's Gitlab account to store websites
|
|
56
|
+
* @see https://docs.gitlab.com/ee/api/oauth2.html
|
|
57
|
+
*/
|
|
58
|
+
const MAX_BATCH_UPLOAD_SIZE = 100;
|
|
59
|
+
// interface MetaRepoFileContent {
|
|
60
|
+
// websites: {
|
|
61
|
+
// [websiteId: string]: {
|
|
62
|
+
// meta: WebsiteMetaFileContent,
|
|
63
|
+
// createdAt: string,
|
|
64
|
+
// updatedAt: string,
|
|
65
|
+
// }
|
|
66
|
+
// }
|
|
67
|
+
// }
|
|
52
68
|
const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
53
69
|
<svg
|
|
54
70
|
xmlns:svg="http://www.w3.org/2000/svg"
|
|
@@ -138,8 +154,11 @@ class GitlabConnector {
|
|
|
138
154
|
}
|
|
139
155
|
// **
|
|
140
156
|
// Convenience methods for the Gitlab API
|
|
141
|
-
getAssetPath(path) {
|
|
142
|
-
|
|
157
|
+
getAssetPath(path, encode = true) {
|
|
158
|
+
const resolvedPath = (0, path_1.join)(this.options.assetsFolder, path);
|
|
159
|
+
if (encode)
|
|
160
|
+
return encodeURIComponent(resolvedPath);
|
|
161
|
+
return resolvedPath;
|
|
143
162
|
}
|
|
144
163
|
isUsingOfficialInstance() {
|
|
145
164
|
const gitlabDomainRegexp = /(^|\b)(gitlab\.com)($|\b)/;
|
|
@@ -170,53 +189,14 @@ class GitlabConnector {
|
|
|
170
189
|
});
|
|
171
190
|
}
|
|
172
191
|
async readFile(session, websiteId, fileName) {
|
|
192
|
+
// Remove leading slash
|
|
173
193
|
const safePath = fileName.replace(/^\//, '');
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const response = await (0, node_fetch_1.default)(url, {
|
|
177
|
-
agent: this.getAgent(),
|
|
178
|
-
method: 'GET',
|
|
179
|
-
headers: {
|
|
180
|
-
'Content-Type': 'application/json',
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
const json = await response.json();
|
|
184
|
-
if (!response.ok)
|
|
185
|
-
throw new types_1.ApiError(`Error reading file "${fileName}" from Gitlab: ${json?.message ?? json?.error ?? response.statusText}`, response.status);
|
|
186
|
-
// From base64 string to buffer
|
|
187
|
-
const buf = Buffer.from(json.content, 'base64');
|
|
188
|
-
// Return the image bytes
|
|
189
|
-
return buf;
|
|
190
|
-
}
|
|
191
|
-
/*
|
|
192
|
-
* Get the meta repo path for the current user
|
|
193
|
-
* The meta repo contains a JSON file which contains the list of websites
|
|
194
|
-
*/
|
|
195
|
-
//private getMetaRepoPath(session: GitlabSession): string {
|
|
196
|
-
// if(!this.getSessionToken(session).username) throw new ApiError('Missing Gitlab user ID. User not logged in?', 401)
|
|
197
|
-
// return encodeURIComponent(`${this.getSessionToken(session).username}/${this.options.metaRepo}`)
|
|
198
|
-
//}
|
|
199
|
-
///**
|
|
200
|
-
// * Initialize the storage with a meta repo
|
|
201
|
-
// */
|
|
202
|
-
//private async initStorage(session: GitlabSession): Promise<void> {
|
|
203
|
-
// // Create the meta repo
|
|
204
|
-
// try {
|
|
205
|
-
// const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
|
|
206
|
-
// name: this.options.metaRepo,
|
|
207
|
-
// }) as any
|
|
208
|
-
// return this.createFile(session, this.getMetaRepoPath(session), this.options.metaRepoFile, JSON.stringify({
|
|
209
|
-
// websites: {}
|
|
210
|
-
// } as MetaRepoFileContent))
|
|
211
|
-
// } catch (e) {
|
|
212
|
-
// console.error('Could not init storage', e.statusCode, e.httpStatusCode, e)
|
|
213
|
-
// throw e
|
|
214
|
-
// }
|
|
215
|
-
//}
|
|
194
|
+
return this.downloadRawFile(session, websiteId, safePath);
|
|
195
|
+
}
|
|
216
196
|
/**
|
|
217
197
|
* Call the Gitlab API with the user's token and handle errors
|
|
218
198
|
*/
|
|
219
|
-
async callApi(session, path, method = 'GET',
|
|
199
|
+
async callApi(session, path, method = 'GET', requestBody = null, params = {}) {
|
|
220
200
|
const token = this.getSessionToken(session).token;
|
|
221
201
|
const tokenParam = token ? `access_token=${token.access_token}&` : '';
|
|
222
202
|
const paramsStr = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v.toString())}`).join('&');
|
|
@@ -224,17 +204,21 @@ class GitlabConnector {
|
|
|
224
204
|
const headers = {
|
|
225
205
|
'Content-Type': 'application/json',
|
|
226
206
|
};
|
|
227
|
-
if (method === 'GET' &&
|
|
228
|
-
console.error('Gitlab API error (4) - GET request with body', { url, method, body, params });
|
|
207
|
+
if (method === 'GET' && requestBody) {
|
|
208
|
+
console.error('Gitlab API error (4) - GET request with body', { url, method, body: requestBody, params });
|
|
229
209
|
}
|
|
230
210
|
// With or without body
|
|
231
211
|
let response;
|
|
212
|
+
const body = requestBody ? JSON.stringify(requestBody) : undefined;
|
|
232
213
|
try {
|
|
233
|
-
|
|
214
|
+
if (body && Buffer.byteLength(body) > 1024 * 100) {
|
|
215
|
+
console.warn('Gitlab API warning - body too big', Buffer.byteLength(body), 'bytes', { url, method, params });
|
|
216
|
+
}
|
|
217
|
+
response = await (0, node_fetch_1.default)(url, requestBody && method !== 'GET' ? {
|
|
234
218
|
agent: this.getAgent(),
|
|
235
219
|
method,
|
|
236
220
|
headers,
|
|
237
|
-
body
|
|
221
|
+
body,
|
|
238
222
|
} : {
|
|
239
223
|
agent: this.getAgent(),
|
|
240
224
|
method,
|
|
@@ -243,12 +227,20 @@ class GitlabConnector {
|
|
|
243
227
|
}
|
|
244
228
|
catch (e) {
|
|
245
229
|
console.error('Gitlab API error (0)', e);
|
|
246
|
-
throw new types_1.ApiError(`Gitlab API error (0): ${e.message} ${e.code} ${e.name} ${e.type}`, 500);
|
|
230
|
+
throw new types_1.ApiError(`Gitlab API error (0): ${e.message} ${e.text} ${e.code} ${e.name} ${e.type}`, 500);
|
|
247
231
|
}
|
|
248
|
-
let json;
|
|
249
232
|
// Handle the case when the server returns a non-JSON response (e.g. 400 Bad Request)
|
|
250
|
-
const text = await
|
|
233
|
+
const text = await async function () {
|
|
234
|
+
try {
|
|
235
|
+
return await response.text();
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
console.error('Gitlab API error (6) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params }, e);
|
|
239
|
+
throw new types_1.ApiError(`Gitlab API error (6): response body not available. ${e.message}`, 500);
|
|
240
|
+
}
|
|
241
|
+
}();
|
|
251
242
|
if (!response.ok) {
|
|
243
|
+
console.error('Gitlab API error (7) - response not ok', response.status, response.statusText, { url, method, body: requestBody, params, text: text });
|
|
252
244
|
if (text.includes('A file with this name doesn\'t exist')) {
|
|
253
245
|
throw new types_1.ApiError('Gitlab API error (5): Not Found', 404);
|
|
254
246
|
}
|
|
@@ -290,11 +282,12 @@ class GitlabConnector {
|
|
|
290
282
|
}
|
|
291
283
|
}
|
|
292
284
|
else {
|
|
293
|
-
const message =
|
|
294
|
-
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body, params, text, message });
|
|
295
|
-
throw new types_1.ApiError(`Gitlab API error (1): ${message}`, response.status);
|
|
285
|
+
const message = response.statusText;
|
|
286
|
+
console.error('Gitlab API error (1)', response.status, response.statusText, { url, method, body: requestBody, params, text: text, message });
|
|
287
|
+
throw new types_1.ApiError(`Gitlab API error (1): ${message} (${text})`, response.status);
|
|
296
288
|
}
|
|
297
289
|
}
|
|
290
|
+
let json;
|
|
298
291
|
try {
|
|
299
292
|
json = JSON.parse(text);
|
|
300
293
|
}
|
|
@@ -305,12 +298,74 @@ class GitlabConnector {
|
|
|
305
298
|
}
|
|
306
299
|
else {
|
|
307
300
|
// Useless error linked to the fact that the response is not JSON
|
|
308
|
-
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, { url, method, body, params, text });
|
|
301
|
+
console.error('Gitlab API error (3) - could not parse response', response.status, response.statusText, { url, method, body: requestBody, params, text: text });
|
|
309
302
|
return text;
|
|
310
303
|
}
|
|
311
304
|
}
|
|
312
305
|
return json;
|
|
313
306
|
}
|
|
307
|
+
// Cache for projects path
|
|
308
|
+
projectPathCache = new Map();
|
|
309
|
+
async downloadRawFile(session, projectId, filePath) {
|
|
310
|
+
const token = this.getSessionToken(session).token?.access_token;
|
|
311
|
+
const domain = this.options.domain;
|
|
312
|
+
const branch = this.options.branch;
|
|
313
|
+
// Get the project path with namespace
|
|
314
|
+
let pathWithNamespace = this.projectPathCache.get(projectId);
|
|
315
|
+
if (!pathWithNamespace) {
|
|
316
|
+
const metaUrl = `${domain}/api/v4/projects/${projectId}`;
|
|
317
|
+
const metaRes = await (0, node_fetch_1.default)(metaUrl, {
|
|
318
|
+
agent: this.getAgent(),
|
|
319
|
+
headers: {
|
|
320
|
+
'Authorization': `Bearer ${token}`,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const metaText = await metaRes.text();
|
|
324
|
+
if (!metaRes.ok) {
|
|
325
|
+
console.error('GitLab raw error (meta)', metaRes.status, metaRes.statusText, { metaUrl, metaText });
|
|
326
|
+
throw new types_1.ApiError(`GitLab raw error (meta): ${metaRes.statusText}`, metaRes.status);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const meta = JSON.parse(metaText);
|
|
330
|
+
pathWithNamespace = meta.path_with_namespace;
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
throw new types_1.ApiError('GitLab raw error (meta): could not parse project info', 500);
|
|
334
|
+
}
|
|
335
|
+
// Limit cache size, remove least recently used
|
|
336
|
+
this.projectPathCache.set(projectId, pathWithNamespace);
|
|
337
|
+
if (this.projectPathCache.size > 500) {
|
|
338
|
+
this.projectPathCache.delete(this.projectPathCache.keys().next().value);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Construct the raw URL
|
|
342
|
+
// GET /projects/:id/repository/files/:file_path/raw
|
|
343
|
+
const rawUrl = `${domain}/api/v4/projects/${encodeURIComponent(pathWithNamespace)}/repository/files/${encodeURIComponent(filePath)}/raw?ref=${branch}&access_token=${token}`;
|
|
344
|
+
const fileRes = await (0, node_fetch_1.default)(rawUrl, {
|
|
345
|
+
agent: this.getAgent(),
|
|
346
|
+
});
|
|
347
|
+
const contentType = fileRes.headers.get('content-type');
|
|
348
|
+
if (contentType?.includes('text/html')) {
|
|
349
|
+
const html = await fileRes.text();
|
|
350
|
+
throw new types_1.ApiError('GitLab returned HTML instead of file (unauthorized or not found).', 401);
|
|
351
|
+
}
|
|
352
|
+
if (!fileRes.ok) {
|
|
353
|
+
const errText = await fileRes.text();
|
|
354
|
+
if (errText.includes('not found') || fileRes.status === 404) {
|
|
355
|
+
throw new types_1.ApiError('GitLab raw error (5): Not Found', 404);
|
|
356
|
+
}
|
|
357
|
+
console.error('GitLab raw error (1)', fileRes.status, fileRes.statusText, { rawUrl, errText });
|
|
358
|
+
throw new types_1.ApiError(`GitLab raw error (1): ${fileRes.statusText} (${errText})`, fileRes.status);
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const buffer = await fileRes.buffer();
|
|
362
|
+
return buffer;
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
console.error('GitLab raw error (3): could not read buffer', e);
|
|
366
|
+
throw new types_1.ApiError('GitLab raw error (3): failed to read binary content', 500);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
314
369
|
generateCodeVerifier() {
|
|
315
370
|
return crypto_1.default.randomBytes(64).toString('base64')
|
|
316
371
|
.replace(/\+/g, '-')
|
|
@@ -480,15 +535,41 @@ class GitlabConnector {
|
|
|
480
535
|
connectorUserSettings: {},
|
|
481
536
|
}));
|
|
482
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Read the website data
|
|
540
|
+
* The website data file is named `website.json` and the pages are named `page-{id}.json`
|
|
541
|
+
* The pages are stored in the `src` folder by default
|
|
542
|
+
*/
|
|
483
543
|
async readWebsite(session, websiteId) {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
544
|
+
const websiteDataBuf = await this.downloadRawFile(session, websiteId, constants_1.WEBSITE_DATA_FILE);
|
|
545
|
+
const websiteData = JSON.parse(websiteDataBuf.toString('utf8'));
|
|
546
|
+
// If the website pages are not in the main file, we need to read them
|
|
547
|
+
// This happens when the website was just created
|
|
548
|
+
// Let grapesjs create the pages in the frontend
|
|
549
|
+
if (!websiteData.pages) {
|
|
550
|
+
return websiteData;
|
|
551
|
+
}
|
|
552
|
+
// Load each page in parallel
|
|
553
|
+
const pages = await Promise.all(websiteData.pages.map(async (page) => {
|
|
554
|
+
if (page.isFile) {
|
|
555
|
+
const name = (0, page_1.getPageSlug)(page.name);
|
|
556
|
+
const fileName = (`${((0, page_1.getPageSlug)(page.name))}-${page.id}`);
|
|
557
|
+
const filePath = `${constants_1.WEBSITE_PAGES_FOLDER}/${fileName}.json`;
|
|
558
|
+
const pageContent = await this.downloadRawFile(session, websiteId, filePath);
|
|
559
|
+
const res = JSON.parse(pageContent.toString('utf8'));
|
|
560
|
+
return res;
|
|
561
|
+
}
|
|
562
|
+
return page;
|
|
563
|
+
}));
|
|
564
|
+
// Read each page file if needed
|
|
565
|
+
return {
|
|
566
|
+
...websiteData,
|
|
567
|
+
pages,
|
|
568
|
+
};
|
|
491
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Create a new website, i.e. a new Gitlab repository with an empty website data file
|
|
572
|
+
*/
|
|
492
573
|
async createWebsite(session, websiteMeta) {
|
|
493
574
|
const project = await this.callApi(session, 'api/v4/projects/', 'POST', {
|
|
494
575
|
name: this.options.repoPrefix + websiteMeta.name,
|
|
@@ -499,13 +580,70 @@ class GitlabConnector {
|
|
|
499
580
|
//await this.setWebsiteMeta(session, project.id, websiteMeta)
|
|
500
581
|
return project.id;
|
|
501
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Update the website data
|
|
585
|
+
* Split the website data into 1 file per page + 1 file for the website data itself
|
|
586
|
+
* Use gitlab batch API to create/update the files
|
|
587
|
+
*/
|
|
502
588
|
async updateWebsite(session, websiteId, websiteData) {
|
|
503
|
-
const
|
|
589
|
+
const batchActions = [];
|
|
590
|
+
// List existing files in the pages folder
|
|
591
|
+
let existingFiles = [];
|
|
592
|
+
try {
|
|
593
|
+
const files = await this.callApi(session, `api/v4/projects/${websiteId}/repository/tree`, 'GET', null, {
|
|
594
|
+
path: constants_1.WEBSITE_PAGES_FOLDER,
|
|
595
|
+
recursive: false,
|
|
596
|
+
});
|
|
597
|
+
existingFiles = files.map(file => file.path);
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
if (e.statusCode !== 404 && e.httpStatusCode !== 404) {
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Prepare actions for each page
|
|
605
|
+
const pages = websiteData.pages.map((page) => {
|
|
606
|
+
const file_name = encodeURIComponent(`${((0, page_1.getPageSlug)(page.name))}-${page.id}`);
|
|
607
|
+
const file_path = (`${constants_1.WEBSITE_PAGES_FOLDER}/${file_name}.json`);
|
|
608
|
+
const content = JSON.stringify(page);
|
|
609
|
+
// Determine whether to create or update the file
|
|
610
|
+
if (existingFiles.includes(file_path)) {
|
|
611
|
+
batchActions.push({
|
|
612
|
+
action: 'update',
|
|
613
|
+
file_path,
|
|
614
|
+
content,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
batchActions.push({
|
|
619
|
+
action: 'create',
|
|
620
|
+
file_path,
|
|
621
|
+
content,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
name: page.name,
|
|
626
|
+
id: page.id,
|
|
627
|
+
isFile: true,
|
|
628
|
+
};
|
|
629
|
+
});
|
|
630
|
+
// Prepare the main website data file
|
|
631
|
+
const websiteDataWithGitlabPages = {
|
|
632
|
+
...websiteData,
|
|
633
|
+
pages,
|
|
634
|
+
};
|
|
635
|
+
batchActions.push({
|
|
636
|
+
action: 'update',
|
|
637
|
+
file_path: constants_1.WEBSITE_DATA_FILE,
|
|
638
|
+
content: JSON.stringify(websiteDataWithGitlabPages),
|
|
639
|
+
});
|
|
640
|
+
// Perform a single batch commit
|
|
641
|
+
const batch = {
|
|
504
642
|
branch: this.options.branch,
|
|
505
643
|
commit_message: 'Update website data from Silex',
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
});
|
|
644
|
+
actions: batchActions,
|
|
645
|
+
};
|
|
646
|
+
await this.callApi(session, `api/v4/projects/${websiteId}/repository/commits`, 'POST', batch);
|
|
509
647
|
}
|
|
510
648
|
async deleteWebsite(session, websiteId) {
|
|
511
649
|
// Delete repo
|
|
@@ -524,7 +662,6 @@ class GitlabConnector {
|
|
|
524
662
|
// branch: this.options.branch,
|
|
525
663
|
// commit_message: `Delete meta data of ${data.meta.name} (${websiteId}) from Silex`,
|
|
526
664
|
// content: JSON.stringify(metaRepo),
|
|
527
|
-
// file_path: this.options.metaRepoFile,
|
|
528
665
|
//})
|
|
529
666
|
}
|
|
530
667
|
async duplicateWebsite(session, websiteId) {
|
|
@@ -544,10 +681,10 @@ class GitlabConnector {
|
|
|
544
681
|
});
|
|
545
682
|
// Upload all files
|
|
546
683
|
for (const file of files) {
|
|
547
|
-
const
|
|
548
|
-
const content = await this.readFile(session, websiteId, path);
|
|
684
|
+
const content = await this.readFile(session, websiteId, file);
|
|
549
685
|
// From buffer to string
|
|
550
686
|
const contentStr = content.toString('base64');
|
|
687
|
+
const path = encodeURIComponent(file);
|
|
551
688
|
switch (file) {
|
|
552
689
|
case constants_1.WEBSITE_DATA_FILE:
|
|
553
690
|
await this.updateFile(session, newId, path, contentStr, true);
|
|
@@ -586,24 +723,6 @@ class GitlabConnector {
|
|
|
586
723
|
};
|
|
587
724
|
}
|
|
588
725
|
async setWebsiteMeta(session, websiteId, websiteMeta) {
|
|
589
|
-
//// Load the meta repo data
|
|
590
|
-
//const file = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'GET', null, {
|
|
591
|
-
// ref: this.options.branch,
|
|
592
|
-
//})
|
|
593
|
-
//const metaRepo = JSON.parse(Buffer.from(file.content, 'base64').toString('utf8')) as MetaRepoFileContent
|
|
594
|
-
//// Update or create the website meta data
|
|
595
|
-
//metaRepo.websites[websiteId] = {
|
|
596
|
-
// updatedAt: new Date().toISOString(),
|
|
597
|
-
// createdAt: metaRepo.websites[websiteId]?.createdAt ?? new Date().toISOString(),
|
|
598
|
-
// meta: websiteMeta,
|
|
599
|
-
//}
|
|
600
|
-
//// Save the meta repo data
|
|
601
|
-
//const project = await this.callApi(session, `api/v4/projects/${this.getMetaRepoPath(session)}/repository/files/${this.options.metaRepoFile}`, 'PUT', {
|
|
602
|
-
// branch: this.options.branch,
|
|
603
|
-
// commit_message: `Update website meta data of ${websiteMeta.name} (${websiteId}) from Silex`,
|
|
604
|
-
// content: JSON.stringify(metaRepo),
|
|
605
|
-
// file_path: this.options.metaRepoFile,
|
|
606
|
-
//})
|
|
607
726
|
// Rename the repo if needed
|
|
608
727
|
const oldMeta = await this.getWebsiteMeta(session, websiteId);
|
|
609
728
|
if (websiteMeta.name !== oldMeta.name) {
|
|
@@ -611,41 +730,63 @@ class GitlabConnector {
|
|
|
611
730
|
name: this.options.repoPrefix + websiteMeta.name,
|
|
612
731
|
});
|
|
613
732
|
}
|
|
614
|
-
//// Update the metadata file
|
|
615
|
-
//await this.callApi(session, `api/v4/projects/${websiteId}/repository/files/${WEBSITE_META_DATA_FILE}`, 'PUT', {
|
|
616
|
-
// branch: this.options.branch,
|
|
617
|
-
// commit_message: 'Update website meta data from Silex',
|
|
618
|
-
// content: JSON.stringify(websiteMeta),
|
|
619
|
-
// file_path: WEBSITE_META_DATA_FILE,
|
|
620
|
-
// id: websiteId,
|
|
621
|
-
//})
|
|
622
733
|
}
|
|
623
734
|
async writeAssets(session, websiteId, files, status) {
|
|
624
|
-
status && await status({ message:
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
735
|
+
status && await status({ message: `Preparing ${files.length} files`, status: types_1.JobStatus.IN_PROGRESS });
|
|
736
|
+
// List all the files in the repo
|
|
737
|
+
const existingPaths = new Set();
|
|
738
|
+
let page = 1;
|
|
739
|
+
let keepGoing = true;
|
|
740
|
+
while (keepGoing) {
|
|
741
|
+
const tree = await this.callApi(session, `api/v4/projects/${websiteId}/repository/tree`, 'GET', null, {
|
|
742
|
+
recursive: true,
|
|
743
|
+
per_page: 100,
|
|
744
|
+
page,
|
|
745
|
+
});
|
|
746
|
+
for (const f of tree) {
|
|
747
|
+
if (f.type === 'blob')
|
|
748
|
+
existingPaths.add(f.path);
|
|
749
|
+
}
|
|
750
|
+
keepGoing = tree.length === 100;
|
|
751
|
+
page++;
|
|
752
|
+
}
|
|
753
|
+
// Split the files into chunks to avoid the number of files limit
|
|
754
|
+
const chunks = [];
|
|
755
|
+
for (let i = 0; i < files.length; i += MAX_BATCH_UPLOAD_SIZE) {
|
|
756
|
+
chunks.push(files.slice(i, i + MAX_BATCH_UPLOAD_SIZE));
|
|
757
|
+
}
|
|
758
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
759
|
+
const chunk = chunks[chunkIndex];
|
|
760
|
+
status && await status({ message: `Uploading batch ${chunkIndex + 1}/${chunks.length}`, status: types_1.JobStatus.IN_PROGRESS });
|
|
761
|
+
// Create the actions for the batch
|
|
762
|
+
const actions = await Promise.all(chunk.map(async (file) => {
|
|
763
|
+
const content = (await (0, connectors_1.contentToBuffer)(file.content)).toString('base64');
|
|
764
|
+
const file_path = this.getAssetPath(file.path, false);
|
|
765
|
+
const actionType = existingPaths.has(file_path) ? 'update' : 'create';
|
|
766
|
+
return {
|
|
767
|
+
action: actionType,
|
|
768
|
+
file_path,
|
|
769
|
+
content,
|
|
770
|
+
encoding: 'base64',
|
|
771
|
+
};
|
|
772
|
+
}));
|
|
630
773
|
try {
|
|
631
|
-
await this.
|
|
774
|
+
await this.callApi(session, `api/v4/projects/${websiteId}/repository/commits`, 'POST', {
|
|
775
|
+
branch: 'main',
|
|
776
|
+
commit_message: `Batch update assets (${chunkIndex + 1}/${chunks.length})`,
|
|
777
|
+
actions,
|
|
778
|
+
});
|
|
632
779
|
}
|
|
633
780
|
catch (e) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
status && await status({ message: 'Error', status: types_1.JobStatus.ERROR });
|
|
640
|
-
throw e;
|
|
641
|
-
}
|
|
781
|
+
console.error(`Batch ${chunkIndex + 1} failed`, e);
|
|
782
|
+
status && await status({ message: `Error in batch ${chunkIndex + 1}`, status: types_1.JobStatus.ERROR });
|
|
783
|
+
throw e;
|
|
642
784
|
}
|
|
643
785
|
}
|
|
644
|
-
status && await status({ message: '
|
|
786
|
+
status && await status({ message: 'All files uploaded successfully', status: types_1.JobStatus.SUCCESS });
|
|
645
787
|
}
|
|
646
788
|
async readAsset(session, websiteId, fileName) {
|
|
647
|
-
|
|
648
|
-
const finalPath = this.getAssetPath(fileName);
|
|
789
|
+
const finalPath = this.getAssetPath(fileName, false);
|
|
649
790
|
return this.readFile(session, websiteId, finalPath);
|
|
650
791
|
}
|
|
651
792
|
async deleteAssets(session, websiteId, fileNames) {
|