decap-cms-backend-forgejo 3.4.0
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/CHANGELOG.md +10 -0
- package/LICENSE +22 -0
- package/dist/decap-cms-backend-forgejo.js +12 -0
- package/dist/decap-cms-backend-forgejo.js.LICENSE.txt +18 -0
- package/dist/decap-cms-backend-forgejo.js.map +1 -0
- package/dist/esm/API.js +821 -0
- package/dist/esm/AuthenticationPage.js +233 -0
- package/dist/esm/implementation.js +539 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/types.js +1 -0
- package/package.json +38 -0
- package/src/API.ts +1054 -0
- package/src/AuthenticationPage.js +207 -0
- package/src/__tests__/API.spec.js +1479 -0
- package/src/__tests__/implementation.spec.js +530 -0
- package/src/implementation.tsx +652 -0
- package/src/index.ts +10 -0
- package/src/types.ts +344 -0
- package/webpack.config.js +3 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { stripIndent } from 'common-tags';
|
|
2
|
+
import trimStart from 'lodash/trimStart';
|
|
3
|
+
import semaphore from 'semaphore';
|
|
4
|
+
import { asyncLock, basename, blobToFileObj, Cursor, CURSOR_COMPATIBILITY_SYMBOL, entriesByFiles, entriesByFolder, filterByExtension, getBlobSHA, getMediaAsBlob, getMediaDisplayURL, runWithLock, unsentRequest, unpublishedEntries, contentKeyFromBranch, branchFromContentKey } from 'decap-cms-lib-util';
|
|
5
|
+
import API, { API_NAME } from './API';
|
|
6
|
+
import AuthenticationPage from './AuthenticationPage';
|
|
7
|
+
import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
8
|
+
const MAX_CONCURRENT_DOWNLOADS = 10;
|
|
9
|
+
const {
|
|
10
|
+
fetchWithTimeout: fetch
|
|
11
|
+
} = unsentRequest;
|
|
12
|
+
export default class Forgejo {
|
|
13
|
+
constructor(config, options = {}) {
|
|
14
|
+
this.options = {
|
|
15
|
+
proxied: false,
|
|
16
|
+
API: null,
|
|
17
|
+
useWorkflow: false,
|
|
18
|
+
initialWorkflowStatus: '',
|
|
19
|
+
...options
|
|
20
|
+
};
|
|
21
|
+
if (!this.options.proxied && (config.backend.repo === null || config.backend.repo === undefined)) {
|
|
22
|
+
throw new Error('The Forgejo backend needs a "repo" in the backend configuration.');
|
|
23
|
+
}
|
|
24
|
+
if (!config.backend.api_root) {
|
|
25
|
+
throw new Error('The Forgejo backend needs an "api_root" in the backend configuration when not proxied.');
|
|
26
|
+
}
|
|
27
|
+
this.api = this.options.API || null;
|
|
28
|
+
this.openAuthoringEnabled = config.backend.open_authoring || false;
|
|
29
|
+
if (this.openAuthoringEnabled) {
|
|
30
|
+
if (!this.options.useWorkflow) {
|
|
31
|
+
throw new Error('backend.open_authoring is true but publish_mode is not set to editorial_workflow.');
|
|
32
|
+
}
|
|
33
|
+
// In open authoring mode, defer setting this.repo until after fork selection
|
|
34
|
+
this.originRepo = config.backend.repo || '';
|
|
35
|
+
} else {
|
|
36
|
+
this.repo = this.originRepo = config.backend.repo || '';
|
|
37
|
+
}
|
|
38
|
+
this.alwaysForkEnabled = config.backend.always_fork || false;
|
|
39
|
+
this.branch = config.backend.branch?.trim() || 'main';
|
|
40
|
+
this.apiRoot = config.backend.api_root;
|
|
41
|
+
this.token = '';
|
|
42
|
+
this.mediaFolder = config.media_folder;
|
|
43
|
+
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
|
|
44
|
+
this.initialWorkflowStatus = this.options.initialWorkflowStatus || 'draft';
|
|
45
|
+
this.lock = asyncLock();
|
|
46
|
+
}
|
|
47
|
+
isGitBackend() {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
async status() {
|
|
51
|
+
const auth = (await this.api?.user().then(user => !!user).catch(e => {
|
|
52
|
+
console.warn('[StaticCMS] Failed getting Forgejo user', e);
|
|
53
|
+
return false;
|
|
54
|
+
})) || false;
|
|
55
|
+
return {
|
|
56
|
+
auth: {
|
|
57
|
+
status: auth
|
|
58
|
+
},
|
|
59
|
+
api: {
|
|
60
|
+
status: true,
|
|
61
|
+
statusPage: ''
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
authComponent() {
|
|
66
|
+
const wrappedAuthenticationPage = props => _jsx(AuthenticationPage, {
|
|
67
|
+
...props,
|
|
68
|
+
backend: this
|
|
69
|
+
});
|
|
70
|
+
wrappedAuthenticationPage.displayName = 'AuthenticationPage';
|
|
71
|
+
return wrappedAuthenticationPage;
|
|
72
|
+
}
|
|
73
|
+
async currentUser({
|
|
74
|
+
token
|
|
75
|
+
}) {
|
|
76
|
+
if (!this._currentUserPromise) {
|
|
77
|
+
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `token ${token}`
|
|
80
|
+
}
|
|
81
|
+
}).then(res => res.json());
|
|
82
|
+
}
|
|
83
|
+
return this._currentUserPromise;
|
|
84
|
+
}
|
|
85
|
+
async userIsOriginMaintainer({
|
|
86
|
+
username: usernameArg,
|
|
87
|
+
token
|
|
88
|
+
}) {
|
|
89
|
+
const username = usernameArg || (await this.currentUser({
|
|
90
|
+
token
|
|
91
|
+
})).login;
|
|
92
|
+
this._userIsOriginMaintainerPromises = this._userIsOriginMaintainerPromises || {};
|
|
93
|
+
if (!this._userIsOriginMaintainerPromises[username]) {
|
|
94
|
+
this._userIsOriginMaintainerPromises[username] = fetch(`${this.apiRoot}/repos/${this.originRepo}/collaborators/${username}/permission`, {
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `token ${token}`
|
|
97
|
+
}
|
|
98
|
+
}).then(res => res.json()).then(({
|
|
99
|
+
permission
|
|
100
|
+
}) => permission === 'admin' || permission === 'write');
|
|
101
|
+
}
|
|
102
|
+
return this._userIsOriginMaintainerPromises[username];
|
|
103
|
+
}
|
|
104
|
+
async pollUntilForkExists({
|
|
105
|
+
repo,
|
|
106
|
+
token
|
|
107
|
+
}) {
|
|
108
|
+
const initialPollDelay = 250; // milliseconds
|
|
109
|
+
const maxPollDelay = 2000; // milliseconds
|
|
110
|
+
const maxWaitMs = 60000; // overall timeout in milliseconds
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
let pollDelay = initialPollDelay;
|
|
113
|
+
let repoExists = false;
|
|
114
|
+
while (!repoExists && Date.now() - startTime < maxWaitMs) {
|
|
115
|
+
const response = await fetch(`${this.apiRoot}${repo}`, {
|
|
116
|
+
headers: {
|
|
117
|
+
Authorization: `token ${token}`
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (response.ok) {
|
|
121
|
+
repoExists = true;
|
|
122
|
+
} else if (response.status === 404) {
|
|
123
|
+
repoExists = false;
|
|
124
|
+
} else {
|
|
125
|
+
// For non-404, non-OK responses, fail fast instead of looping indefinitely.
|
|
126
|
+
throw new Error(`Error while checking for fork existence: ${response.status} ${response.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// wait between polls if the repo does not yet exist
|
|
130
|
+
if (!repoExists) {
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, pollDelay));
|
|
132
|
+
// simple backoff up to a maximum delay
|
|
133
|
+
pollDelay = Math.min(pollDelay * 2, maxPollDelay);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!repoExists) {
|
|
137
|
+
throw new Error('Timed out waiting for fork to be created.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async authenticateWithFork({
|
|
141
|
+
userData,
|
|
142
|
+
getPermissionToFork
|
|
143
|
+
}) {
|
|
144
|
+
if (!this.openAuthoringEnabled) {
|
|
145
|
+
throw new Error('Cannot authenticate with fork; Open Authoring is turned off.');
|
|
146
|
+
}
|
|
147
|
+
const token = userData.token;
|
|
148
|
+
|
|
149
|
+
// Clear cached user data when token changes to prevent stale data across logins
|
|
150
|
+
this._currentUserPromise = undefined;
|
|
151
|
+
this._userIsOriginMaintainerPromises = {};
|
|
152
|
+
|
|
153
|
+
// Origin maintainers should be able to use the CMS normally. If alwaysFork
|
|
154
|
+
// is enabled we always fork (and avoid the origin maintainer check)
|
|
155
|
+
if (!this.alwaysForkEnabled && (await this.userIsOriginMaintainer({
|
|
156
|
+
token
|
|
157
|
+
}))) {
|
|
158
|
+
this.repo = this.originRepo;
|
|
159
|
+
this.useOpenAuthoring = false;
|
|
160
|
+
return Promise.resolve();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If a fork exists merge it with upstream
|
|
164
|
+
// otherwise create a new fork.
|
|
165
|
+
const currentUser = await this.currentUser({
|
|
166
|
+
token
|
|
167
|
+
});
|
|
168
|
+
const repoName = this.originRepo.split('/')[1];
|
|
169
|
+
this.repo = `${currentUser.login}/${repoName}`;
|
|
170
|
+
this.useOpenAuthoring = true;
|
|
171
|
+
|
|
172
|
+
// Initialize or update API for fork operations
|
|
173
|
+
// Always recreate to ensure token and repo are up to date (unless a mock was injected for testing)
|
|
174
|
+
if (!this.options.API) {
|
|
175
|
+
const apiCtor = API;
|
|
176
|
+
this.api = new apiCtor({
|
|
177
|
+
token,
|
|
178
|
+
branch: this.branch,
|
|
179
|
+
repo: this.repo,
|
|
180
|
+
originRepo: this.originRepo,
|
|
181
|
+
apiRoot: this.apiRoot,
|
|
182
|
+
useOpenAuthoring: this.useOpenAuthoring,
|
|
183
|
+
cmsLabelPrefix: this.cmsLabelPrefix,
|
|
184
|
+
initialWorkflowStatus: this.initialWorkflowStatus
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (await this.api.forkExists()) {
|
|
188
|
+
await this.api.mergeUpstream();
|
|
189
|
+
return Promise.resolve();
|
|
190
|
+
} else {
|
|
191
|
+
await getPermissionToFork();
|
|
192
|
+
const fork = await this.api.createFork();
|
|
193
|
+
return this.pollUntilForkExists({
|
|
194
|
+
repo: `/repos/${fork.full_name}`,
|
|
195
|
+
token
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
restoreUser(user) {
|
|
200
|
+
return this.openAuthoringEnabled ? this.authenticateWithFork({
|
|
201
|
+
userData: user,
|
|
202
|
+
// no-op: restoreUser doesn't need fork approval UX
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
204
|
+
getPermissionToFork: () => {}
|
|
205
|
+
}).then(() => this.authenticate(user)) : this.authenticate(user);
|
|
206
|
+
}
|
|
207
|
+
async authenticate(state) {
|
|
208
|
+
this.token = state.token;
|
|
209
|
+
|
|
210
|
+
// Clear cached user data when token changes to prevent stale data across logins
|
|
211
|
+
this._currentUserPromise = undefined;
|
|
212
|
+
this._userIsOriginMaintainerPromises = {};
|
|
213
|
+
const apiCtor = API;
|
|
214
|
+
this.api = new apiCtor({
|
|
215
|
+
token: this.token,
|
|
216
|
+
branch: this.branch,
|
|
217
|
+
repo: this.repo,
|
|
218
|
+
originRepo: this.originRepo,
|
|
219
|
+
apiRoot: this.apiRoot,
|
|
220
|
+
useOpenAuthoring: this.useOpenAuthoring,
|
|
221
|
+
cmsLabelPrefix: this.cmsLabelPrefix,
|
|
222
|
+
initialWorkflowStatus: this.initialWorkflowStatus
|
|
223
|
+
});
|
|
224
|
+
const user = await this.api.user();
|
|
225
|
+
const isCollab = await this.api.hasWriteAccess().catch(error => {
|
|
226
|
+
error.message = stripIndent`
|
|
227
|
+
Repo "${this.repo}" not found.
|
|
228
|
+
|
|
229
|
+
Please ensure the repo information is spelled correctly.
|
|
230
|
+
|
|
231
|
+
If the repo is private, make sure you're logged into a Forgejo account with access.
|
|
232
|
+
|
|
233
|
+
If your repo is under an organization, ensure the organization has granted access to Static
|
|
234
|
+
CMS.
|
|
235
|
+
`;
|
|
236
|
+
throw error;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Unauthorized user
|
|
240
|
+
if (!isCollab) {
|
|
241
|
+
throw new Error('Your Forgejo user account does not have access to this repo.');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Authorized user
|
|
245
|
+
return {
|
|
246
|
+
name: user.full_name,
|
|
247
|
+
login: user.login,
|
|
248
|
+
email: user.email,
|
|
249
|
+
avatar_url: user.avatar_url,
|
|
250
|
+
token: state.token,
|
|
251
|
+
useOpenAuthoring: this.useOpenAuthoring
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
logout() {
|
|
255
|
+
this.token = null;
|
|
256
|
+
|
|
257
|
+
// Clear cached user data on logout
|
|
258
|
+
this._currentUserPromise = undefined;
|
|
259
|
+
this._userIsOriginMaintainerPromises = {};
|
|
260
|
+
if (this.api && this.api.reset && typeof this.api.reset === 'function') {
|
|
261
|
+
return this.api.reset();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
getToken() {
|
|
265
|
+
return Promise.resolve(this.token);
|
|
266
|
+
}
|
|
267
|
+
getCursorAndFiles = (files, page) => {
|
|
268
|
+
const pageSize = 20;
|
|
269
|
+
const count = files.length;
|
|
270
|
+
const pageCount = Math.ceil(files.length / pageSize);
|
|
271
|
+
const actions = [];
|
|
272
|
+
if (page > 1) {
|
|
273
|
+
actions.push('prev');
|
|
274
|
+
actions.push('first');
|
|
275
|
+
}
|
|
276
|
+
if (page < pageCount) {
|
|
277
|
+
actions.push('next');
|
|
278
|
+
actions.push('last');
|
|
279
|
+
}
|
|
280
|
+
const cursor = Cursor.create({
|
|
281
|
+
actions,
|
|
282
|
+
meta: {
|
|
283
|
+
page,
|
|
284
|
+
count,
|
|
285
|
+
pageSize,
|
|
286
|
+
pageCount
|
|
287
|
+
},
|
|
288
|
+
data: {
|
|
289
|
+
files
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
const pageFiles = files.slice((page - 1) * pageSize, page * pageSize);
|
|
293
|
+
return {
|
|
294
|
+
cursor,
|
|
295
|
+
files: pageFiles
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
async entriesByFolder(folder, extension, depth) {
|
|
299
|
+
const repoURL = this.api.originRepoURL;
|
|
300
|
+
let cursor;
|
|
301
|
+
const listFiles = () => this.api.listFiles(folder, {
|
|
302
|
+
repoURL,
|
|
303
|
+
depth
|
|
304
|
+
}).then(files => {
|
|
305
|
+
const filtered = files.filter(file => filterByExtension(file, extension));
|
|
306
|
+
const result = this.getCursorAndFiles(filtered, 1);
|
|
307
|
+
cursor = result.cursor;
|
|
308
|
+
return result.files;
|
|
309
|
+
});
|
|
310
|
+
const readFile = (path, id) => this.api.readFile(path, id, {
|
|
311
|
+
repoURL
|
|
312
|
+
});
|
|
313
|
+
const files = await entriesByFolder(listFiles, readFile, this.api.readFileMetadata.bind(this.api), API_NAME);
|
|
314
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
315
|
+
// @ts-ignore
|
|
316
|
+
files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
|
|
317
|
+
return files;
|
|
318
|
+
}
|
|
319
|
+
async allEntriesByFolder(folder, extension, depth) {
|
|
320
|
+
const repoURL = this.api.originRepoURL;
|
|
321
|
+
const listFiles = () => this.api.listFiles(folder, {
|
|
322
|
+
repoURL,
|
|
323
|
+
depth
|
|
324
|
+
}).then(files => files.filter(file => filterByExtension(file, extension)));
|
|
325
|
+
const readFile = (path, id) => {
|
|
326
|
+
return this.api.readFile(path, id, {
|
|
327
|
+
repoURL
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
const files = await entriesByFolder(listFiles, readFile, this.api.readFileMetadata.bind(this.api), API_NAME);
|
|
331
|
+
return files;
|
|
332
|
+
}
|
|
333
|
+
entriesByFiles(files) {
|
|
334
|
+
const repoURL = this.useOpenAuthoring ? this.api.originRepoURL : this.api.repoURL;
|
|
335
|
+
const readFile = (path, id) => this.api.readFile(path, id, {
|
|
336
|
+
repoURL
|
|
337
|
+
}).catch(() => '');
|
|
338
|
+
return entriesByFiles(files, readFile, this.api.readFileMetadata.bind(this.api), API_NAME);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Fetches a single entry.
|
|
342
|
+
getEntry(path) {
|
|
343
|
+
const repoURL = this.api.originRepoURL;
|
|
344
|
+
return this.api.readFile(path, null, {
|
|
345
|
+
repoURL
|
|
346
|
+
}).then(data => ({
|
|
347
|
+
file: {
|
|
348
|
+
path,
|
|
349
|
+
id: null
|
|
350
|
+
},
|
|
351
|
+
data: data
|
|
352
|
+
})).catch(() => ({
|
|
353
|
+
file: {
|
|
354
|
+
path,
|
|
355
|
+
id: null
|
|
356
|
+
},
|
|
357
|
+
data: ''
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
async getMedia(mediaFolder = this.mediaFolder, folderSupport) {
|
|
361
|
+
if (!mediaFolder) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
return this.api.listFiles(mediaFolder, undefined, folderSupport).then(files => files.map(({
|
|
365
|
+
id,
|
|
366
|
+
name,
|
|
367
|
+
size,
|
|
368
|
+
path,
|
|
369
|
+
type
|
|
370
|
+
}) => {
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
name,
|
|
374
|
+
size,
|
|
375
|
+
displayURL: {
|
|
376
|
+
id,
|
|
377
|
+
path
|
|
378
|
+
},
|
|
379
|
+
path,
|
|
380
|
+
isDirectory: type === 'tree'
|
|
381
|
+
};
|
|
382
|
+
}));
|
|
383
|
+
}
|
|
384
|
+
async getMediaFile(path) {
|
|
385
|
+
const blob = await getMediaAsBlob(path, null, this.api.readFile.bind(this.api));
|
|
386
|
+
const name = basename(path);
|
|
387
|
+
const fileObj = blobToFileObj(name, blob);
|
|
388
|
+
const url = URL.createObjectURL(fileObj);
|
|
389
|
+
const id = await getBlobSHA(blob);
|
|
390
|
+
return {
|
|
391
|
+
id,
|
|
392
|
+
displayURL: url,
|
|
393
|
+
path,
|
|
394
|
+
name,
|
|
395
|
+
size: fileObj.size,
|
|
396
|
+
file: fileObj,
|
|
397
|
+
url
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
getMediaDisplayURL(displayURL) {
|
|
401
|
+
this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
|
|
402
|
+
return getMediaDisplayURL(displayURL, this.api.readFile.bind(this.api), this._mediaDisplayURLSem);
|
|
403
|
+
}
|
|
404
|
+
persistEntry(entry, options) {
|
|
405
|
+
// persistEntry is a transactional operation
|
|
406
|
+
return runWithLock(this.lock, () => {
|
|
407
|
+
if (options.useWorkflow) {
|
|
408
|
+
const slug = entry.dataFiles[0].slug;
|
|
409
|
+
const collection = options.collectionName;
|
|
410
|
+
const files = [...entry.dataFiles, ...entry.assets];
|
|
411
|
+
return this.api.editorialWorkflowGit(files, slug, collection, options);
|
|
412
|
+
}
|
|
413
|
+
return this.api.persistFiles(entry.dataFiles, entry.assets, options);
|
|
414
|
+
}, 'Failed to acquire persist entry lock');
|
|
415
|
+
}
|
|
416
|
+
async persistMedia(mediaFile, options) {
|
|
417
|
+
try {
|
|
418
|
+
await this.api.persistFiles([], [mediaFile], options);
|
|
419
|
+
const {
|
|
420
|
+
sha,
|
|
421
|
+
path,
|
|
422
|
+
fileObj
|
|
423
|
+
} = mediaFile;
|
|
424
|
+
const displayURL = URL.createObjectURL(fileObj);
|
|
425
|
+
return {
|
|
426
|
+
id: sha,
|
|
427
|
+
name: fileObj.name,
|
|
428
|
+
size: fileObj.size,
|
|
429
|
+
displayURL,
|
|
430
|
+
path: trimStart(path, '/')
|
|
431
|
+
};
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error(error);
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async deleteFiles(paths, commitMessage) {
|
|
438
|
+
await this.api.deleteFiles(paths, commitMessage);
|
|
439
|
+
}
|
|
440
|
+
async traverseCursor(cursor, action) {
|
|
441
|
+
const meta = cursor.meta;
|
|
442
|
+
const files = cursor.data.get('files').toJS();
|
|
443
|
+
let result;
|
|
444
|
+
switch (action) {
|
|
445
|
+
case 'first':
|
|
446
|
+
{
|
|
447
|
+
result = this.getCursorAndFiles(files, 1);
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case 'last':
|
|
451
|
+
{
|
|
452
|
+
result = this.getCursorAndFiles(files, meta.get('pageCount'));
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case 'next':
|
|
456
|
+
{
|
|
457
|
+
result = this.getCursorAndFiles(files, meta.get('page') + 1);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
case 'prev':
|
|
461
|
+
{
|
|
462
|
+
result = this.getCursorAndFiles(files, meta.get('page') - 1);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
default:
|
|
466
|
+
{
|
|
467
|
+
result = this.getCursorAndFiles(files, 1);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const readFile = (path, id) => this.api.readFile(path, id, {
|
|
472
|
+
repoURL: this.api.originRepoURL
|
|
473
|
+
}).catch(() => '');
|
|
474
|
+
const entries = await entriesByFiles(result.files, readFile, this.api.readFileMetadata.bind(this.api), API_NAME);
|
|
475
|
+
return {
|
|
476
|
+
entries,
|
|
477
|
+
cursor: result.cursor
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async unpublishedEntries() {
|
|
481
|
+
const listEntriesKeys = () => this.api.listUnpublishedBranches().then(branches => branches.map(branch => contentKeyFromBranch(branch)));
|
|
482
|
+
const ids = await unpublishedEntries(listEntriesKeys);
|
|
483
|
+
return ids;
|
|
484
|
+
}
|
|
485
|
+
async unpublishedEntry({
|
|
486
|
+
id,
|
|
487
|
+
collection,
|
|
488
|
+
slug
|
|
489
|
+
}) {
|
|
490
|
+
if (id) {
|
|
491
|
+
const data = await this.api.retrieveUnpublishedEntryData(id);
|
|
492
|
+
return data;
|
|
493
|
+
} else if (collection && slug) {
|
|
494
|
+
const contentKey = this.api.generateContentKey(collection, slug);
|
|
495
|
+
const data = await this.api.retrieveUnpublishedEntryData(contentKey);
|
|
496
|
+
return data;
|
|
497
|
+
} else {
|
|
498
|
+
throw new Error('Missing unpublished entry id or collection and slug');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async unpublishedEntryDataFile(collection, slug, path, id) {
|
|
502
|
+
const contentKey = this.api.generateContentKey(collection, slug);
|
|
503
|
+
const branch = branchFromContentKey(contentKey);
|
|
504
|
+
const data = await this.api.readFile(path, id, {
|
|
505
|
+
branch
|
|
506
|
+
});
|
|
507
|
+
return data;
|
|
508
|
+
}
|
|
509
|
+
async unpublishedEntryMediaFile(collection, slug, path, id) {
|
|
510
|
+
const contentKey = this.api.generateContentKey(collection, slug);
|
|
511
|
+
const branch = branchFromContentKey(contentKey);
|
|
512
|
+
const blob = await this.api.readFile(path, id, {
|
|
513
|
+
branch,
|
|
514
|
+
parseText: false
|
|
515
|
+
});
|
|
516
|
+
const name = basename(path);
|
|
517
|
+
const fileObj = blobToFileObj(name, blob);
|
|
518
|
+
return {
|
|
519
|
+
id,
|
|
520
|
+
name,
|
|
521
|
+
path,
|
|
522
|
+
size: fileObj.size,
|
|
523
|
+
displayURL: URL.createObjectURL(fileObj),
|
|
524
|
+
file: fileObj
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
updateUnpublishedEntryStatus(collection, slug, newStatus) {
|
|
528
|
+
return runWithLock(this.lock, () => this.api.updateUnpublishedEntryStatus(collection, slug, newStatus), 'Failed to acquire update entry status lock');
|
|
529
|
+
}
|
|
530
|
+
publishUnpublishedEntry(collection, slug) {
|
|
531
|
+
return runWithLock(this.lock, () => this.api.publishUnpublishedEntry(collection, slug), 'Failed to acquire publish entry lock');
|
|
532
|
+
}
|
|
533
|
+
deleteUnpublishedEntry(collection, slug) {
|
|
534
|
+
return runWithLock(this.lock, () => this.api.deleteUnpublishedEntry(collection, slug), 'Failed to acquire delete entry lock');
|
|
535
|
+
}
|
|
536
|
+
async getDeployPreview() {
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ForgejoBackend from './implementation';
|
|
2
|
+
import API from './API';
|
|
3
|
+
import AuthenticationPage from './AuthenticationPage';
|
|
4
|
+
export const DecapCmsBackendForgejo = {
|
|
5
|
+
ForgejoBackend,
|
|
6
|
+
API,
|
|
7
|
+
AuthenticationPage
|
|
8
|
+
};
|
|
9
|
+
export { API, AuthenticationPage, ForgejoBackend };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "decap-cms-backend-forgejo",
|
|
3
|
+
"description": "Forgejo backend for Decap CMS",
|
|
4
|
+
"version": "3.4.0",
|
|
5
|
+
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-forgejo",
|
|
6
|
+
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"module": "dist/esm/index.js",
|
|
9
|
+
"main": "dist/decap-cms-backend-forgejo.js",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"decap-cms",
|
|
12
|
+
"backend",
|
|
13
|
+
"forgejo"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"develop": "npm run build:esm -- --watch",
|
|
18
|
+
"build": "cross-env NODE_ENV=production webpack",
|
|
19
|
+
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"common-tags": "^1.8.0",
|
|
23
|
+
"js-base64": "^3.0.0",
|
|
24
|
+
"semaphore": "^1.1.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@emotion/react": "^11.11.1",
|
|
28
|
+
"@emotion/styled": "^11.11.0",
|
|
29
|
+
"decap-cms-lib-auth": "^3.0.0",
|
|
30
|
+
"decap-cms-lib-util": "^3.0.0",
|
|
31
|
+
"decap-cms-ui-default": "^3.0.0",
|
|
32
|
+
"immutable": "^3.7.6",
|
|
33
|
+
"lodash": "^4.17.11",
|
|
34
|
+
"prop-types": "^15.7.2",
|
|
35
|
+
"react": "^19.1.0"
|
|
36
|
+
},
|
|
37
|
+
"gitHead": "ffe5ab7e61f4e7cb374a413bf9fe55f088d20ba2"
|
|
38
|
+
}
|