UNPKG

@staticcms/proxy-server-lite

Version:
372 lines 18.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerMiddleware = exports.localGitMiddleware = exports.getSchema = exports.validateRepo = void 0; const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); const what_the_diff_1 = require("what-the-diff"); // eslint-disable-next-line import/no-named-as-default const simple_git_1 = __importDefault(require("simple-git")); const async_mutex_1 = require("async-mutex"); const joi_1 = require("../joi"); const customValidators_1 = require("../joi/customValidators"); const fs_2 = require("../utils/fs"); const entries_1 = require("../utils/entries"); const APIUtils_1 = require("../utils/APIUtils"); async function commit(git, commitMessage) { await git.add('.'); await git.commit(commitMessage, undefined, { // setting the value to a string passes name=value // any other value passes just the key '--no-verify': null, '--no-gpg-sign': null, }); } async function getCurrentBranch(git) { const currentBranch = await git.branchLocal().then(summary => summary.current); return currentBranch; } async function runOnBranch(git, branch, func) { const currentBranch = await getCurrentBranch(git); try { if (currentBranch !== branch) { await git.checkout(branch); } const result = await func(); return result; } finally { await git.checkout(currentBranch); } } function branchDescription(branch) { return `branch.${branch}.description`; } async function commitEntry(git, repoPath, dataFiles, assets, commitMessage) { // save entry content await Promise.all(dataFiles.map(dataFile => (0, fs_2.writeFile)(path_1.default.join(repoPath, dataFile.path), dataFile.raw))); // save assets await Promise.all(assets.map(a => (0, fs_2.writeFile)(path_1.default.join(repoPath, a.path), Buffer.from(a.content, a.encoding)))); if (dataFiles.every(dataFile => dataFile.newPath)) { dataFiles.forEach(async (dataFile) => { await (0, fs_2.move)(path_1.default.join(repoPath, dataFile.path), path_1.default.join(repoPath, dataFile.newPath)); }); } // commits files await commit(git, commitMessage); } async function rebase(git, branch) { const gpgSign = await git.raw(['config', 'commit.gpgsign']); try { if (gpgSign === 'true') { await git.addConfig('commit.gpgsign', 'false'); } await git.rebase([branch, '--no-verify']); } finally { if (gpgSign === 'true') { await git.addConfig('commit.gpgsign', gpgSign); } } } async function merge(git, from, to) { const gpgSign = await git.raw(['config', 'commit.gpgsign']); try { if (gpgSign === 'true') { await git.addConfig('commit.gpgsign', 'false'); } await git.mergeFromTo(from, to); } finally { if (gpgSign === 'true') { await git.addConfig('commit.gpgsign', gpgSign); } } } async function isBranchExists(git, branch) { const branchExists = await git.branchLocal().then(({ all }) => all.includes(branch)); return branchExists; } async function getDiffs(git, source, dest) { const rawDiff = await git.diff([source, dest]); const diffs = (0, what_the_diff_1.parse)(rawDiff).map(d => { var _a, _b; const oldPath = ((_a = d.oldPath) === null || _a === void 0 ? void 0 : _a.replace(/b\//, '')) || ''; const newPath = ((_b = d.newPath) === null || _b === void 0 ? void 0 : _b.replace(/b\//, '')) || ''; const path = newPath || oldPath; return { oldPath, newPath, status: d.status, newFile: d.status === 'added', path, id: path, binary: d.binary || /.svg$/.test(path), }; }); return diffs; } async function validateRepo({ repoPath }) { const git = (0, simple_git_1.default)(repoPath); const isRepo = await git.checkIsRepo(); if (!isRepo) { throw Error(`${repoPath} is not a valid git repository`); } } exports.validateRepo = validateRepo; function getSchema({ repoPath }) { const schema = (0, joi_1.defaultSchema)({ path: (0, customValidators_1.pathTraversal)(repoPath) }); return schema; } exports.getSchema = getSchema; function localGitMiddleware({ repoPath, logger }) { const git = (0, simple_git_1.default)(repoPath); // we can only perform a single git operation at any given time const mutex = (0, async_mutex_1.withTimeout)(new async_mutex_1.Mutex(), 3000, new Error('Request timed out')); return async function (req, res) { let release; try { release = await mutex.acquire(); const { body } = req; if (body.action === 'info') { res.json({ repo: path_1.default.basename(repoPath), publish_modes: ['simple', 'editorial_workflow'], type: 'local_git', }); return; } const { branch } = body.params; const branchExists = await isBranchExists(git, branch); if (!branchExists) { const message = `Default branch '${branch}' doesn't exist`; res.status(422).json({ error: message }); return; } switch (body.action) { case 'entriesByFolder': { const payload = body.params; const { folder, extension, depth } = payload; const entries = await runOnBranch(git, branch, () => (0, fs_2.listRepoFiles)(repoPath, folder, extension, depth).then(files => (0, entries_1.entriesFromFiles)(repoPath, files.map(file => ({ path: file.file }))))); res.json(entries); break; } case 'entriesByFiles': { const payload = body.params; const entries = await runOnBranch(git, branch, () => (0, entries_1.entriesFromFiles)(repoPath, payload.files)); res.json(entries); break; } case 'getEntry': { const payload = body.params; const [entry] = await runOnBranch(git, branch, () => (0, entries_1.entriesFromFiles)(repoPath, [{ path: payload.path }])); res.json(entry); break; } case 'unpublishedEntries': { const cmsBranches = await git .branchLocal() .then(result => result.all.filter(b => b.startsWith(`${APIUtils_1.CMS_BRANCH_PREFIX}/`))); res.json(cmsBranches.map(APIUtils_1.contentKeyFromBranch)); break; } case 'unpublishedEntry': { let { id, collection, slug, cmsLabelPrefix } = body.params; if (id) { ({ collection, slug } = (0, APIUtils_1.parseContentKey)(id)); } const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug); const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey); const branchExists = await isBranchExists(git, cmsBranch); if (branchExists) { const diffs = await getDiffs(git, branch, cmsBranch); const label = await git.raw(['config', branchDescription(cmsBranch)]); const status = label && (0, APIUtils_1.labelToStatus)(label.trim(), cmsLabelPrefix || ''); const updatedAt = diffs.length >= 0 ? await runOnBranch(git, cmsBranch, async () => { const dates = await Promise.all(diffs.map(({ newPath }) => (0, fs_2.getUpdateDate)(repoPath, newPath))); return dates.reduce((a, b) => { return a > b ? a : b; }); }) : new Date(); const unpublishedEntry = { collection, slug, status, diffs, updatedAt, }; res.json(unpublishedEntry); } else { return res.status(404).json({ message: 'Not Found' }); } break; } case 'unpublishedEntryDataFile': { const { path, collection, slug } = body.params; const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug); const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey); const [entry] = await runOnBranch(git, cmsBranch, () => (0, entries_1.entriesFromFiles)(repoPath, [{ path }])); res.json({ data: entry.data }); break; } case 'unpublishedEntryMediaFile': { const { path, collection, slug } = body.params; const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug); const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey); const file = await runOnBranch(git, cmsBranch, () => (0, entries_1.readMediaFile)(repoPath, path)); res.json(file); break; } case 'deleteUnpublishedEntry': { const { collection, slug } = body.params; const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug); const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey); const currentBranch = await getCurrentBranch(git); if (currentBranch === cmsBranch) { await git.checkoutLocalBranch(branch); } await git.branch(['-D', cmsBranch]); res.json({ message: `deleted branch: ${cmsBranch}` }); break; } case 'persistEntry': { const { cmsLabelPrefix, entry, dataFiles = [entry], assets, options, } = body.params; if (!options.useWorkflow) { await runOnBranch(git, branch, async () => { await commitEntry(git, repoPath, dataFiles, assets, options.commitMessage); }); } else { const slug = dataFiles[0].slug; const collection = options.collectionName; const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug); const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey); await runOnBranch(git, branch, async () => { var _a; const branchExists = await isBranchExists(git, cmsBranch); if (branchExists) { await git.checkout(cmsBranch); } else { await git.checkoutLocalBranch(cmsBranch); } await rebase(git, branch); const diffs = await getDiffs(git, branch, cmsBranch); // delete media files that have been removed from the entry const toDelete = diffs.filter(d => d.binary && !assets.map(a => a.path).includes(d.path)); await Promise.all(toDelete.map(f => fs_1.promises.unlink(path_1.default.join(repoPath, f.path)))); await commitEntry(git, repoPath, dataFiles, assets, options.commitMessage); // add status for new entries if (!branchExists) { const description = (0, APIUtils_1.statusToLabel)((_a = options.status) !== null && _a !== void 0 ? _a : 'draft', cmsLabelPrefix || ''); await git.addConfig(branchDescription(cmsBranch), description); } }); } res.json({ message: 'entry persisted' }); break; } case 'updateUnpublishedEntryStatus': { const { collection, slug, newStatus, cmsLabelPrefix } = body.params; const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug); const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey); const description = (0, APIUtils_1.statusToLabel)(newStatus, cmsLabelPrefix || ''); await git.addConfig(branchDescription(cmsBranch), description); res.json({ message: `${branch} description was updated to ${description}` }); break; } case 'publishUnpublishedEntry': { const { collection, slug } = body.params; const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug); const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey); await merge(git, cmsBranch, branch); await git.deleteLocalBranch(cmsBranch); res.json({ message: `branch ${cmsBranch} merged to ${branch}` }); break; } case 'getMedia': { const { mediaFolder, publicFolder = mediaFolder } = body.params; const fsItems = await runOnBranch(git, branch, async () => { return await (0, fs_2.listRepoFiles)(repoPath, mediaFolder, '', 1); }); res.json(fsItems.map(item => ({ path: item.file.replace(/\\/g, '/'), url: item.file .replace(/\\/g, '/') .replace(mediaFolder.replace(/^\//g, ''), publicFolder), isDirectory: item.isDirectory, }))); break; } case 'getMediaFile': { const { path } = body.params; const mediaFile = await runOnBranch(git, branch, () => { return (0, entries_1.readMediaFile)(repoPath, path); }); res.json(mediaFile); break; } case 'persistMedia': { const { asset, options: { commitMessage }, } = body.params; const file = await runOnBranch(git, branch, async () => { await (0, fs_2.writeFile)(path_1.default.join(repoPath, asset.path), Buffer.from(asset.content, asset.encoding)); await commit(git, commitMessage); return (0, entries_1.readMediaFile)(repoPath, asset.path); }); res.json(file); break; } case 'deleteFile': { const { path: filePath, options: { commitMessage }, } = body.params; await runOnBranch(git, branch, async () => { await (0, fs_2.deleteFile)(repoPath, filePath); await commit(git, commitMessage); }); res.json({ message: `deleted file ${filePath}` }); break; } case 'deleteFiles': { const { paths, options: { commitMessage }, } = body.params; await runOnBranch(git, branch, async () => { await Promise.all(paths.map(filePath => (0, fs_2.deleteFile)(repoPath, filePath))); await commit(git, commitMessage); }); res.json({ message: `deleted files ${paths.join(', ')}` }); break; } case 'getDeployPreview': { res.json(null); break; } default: { const message = `Unknown action ${body.action}`; res.status(422).json({ error: message }); break; } } } catch (e) { logger.error(`Error handling ${JSON.stringify(req.body)}: ${e instanceof Error ? e.message : 'Unknown error'}`); res.status(500).json({ error: 'Unknown error' }); } finally { release && release(); } }; } exports.localGitMiddleware = localGitMiddleware; async function registerMiddleware(app, options) { const { logger } = options; const repoPath = path_1.default.resolve(process.env.GIT_REPO_DIRECTORY || process.cwd()); await validateRepo({ repoPath }); app.post('/api/v1', (0, joi_1.joi)(getSchema({ repoPath }))); app.post('/api/v1', localGitMiddleware({ repoPath, logger })); logger.info(`Static CMS Git Proxy Server configured with ${repoPath}`); } exports.registerMiddleware = registerMiddleware; //# sourceMappingURL=index.js.map