feat(backend-bitbucket): Add Git-LFS support (#3118)

This commit is contained in:
Erez Rokah
2020-01-21 18:57:36 +02:00
committed by GitHub
parent 0755f90142
commit a48c02d852
36 changed files with 17763 additions and 7501 deletions

View File

@ -4,7 +4,6 @@ import { fromPairs, get, pick, intersection, unzip } from 'lodash';
import ini from 'ini';
import {
APIError,
getBlobSHA,
unsentRequest,
basename,
ApiRequest,
@ -20,6 +19,11 @@ import {
Config,
ImplementationFile,
UnpublishedEntryMediaFile,
parsePointerFile,
getLargeMediaPatternsFromGitAttributesFile,
PointerFile,
getPointerFileForMediaFileObj,
getLargeMediaFilteredMediaFiles,
} from 'netlify-cms-lib-util';
import { GitHubBackend } from 'netlify-cms-backend-github';
import { GitLabBackend } from 'netlify-cms-backend-gitlab';
@ -27,14 +31,7 @@ import { BitbucketBackend, API as BitBucketAPI } from 'netlify-cms-backend-bitbu
import GitHubAPI from './GitHubAPI';
import GitLabAPI from './GitLabAPI';
import AuthenticationPage from './AuthenticationPage';
import {
parsePointerFile,
createPointerFile,
getLargeMediaPatternsFromGitAttributesFile,
getClient,
Client,
PointerFile,
} from './netlify-lfs-client';
import { getClient, Client } from './netlify-lfs-client';
declare global {
interface Window {
@ -466,49 +463,13 @@ export default class GitGateway implements Implementation {
return this.backend!.getMediaFile(path);
}
async getPointerFileForMediaFileObj(fileObj: File) {
const client = await this.getLargeMediaClient();
const { name, size } = fileObj;
const sha = await getBlobSHA(fileObj);
await client.uploadResource({ sha, size }, fileObj);
const pointerFileString = createPointerFile({ sha, size });
const pointerFileBlob = new Blob([pointerFileString]);
const pointerFile = new File([pointerFileBlob], name, { type: 'text/plain' });
const pointerFileSHA = await getBlobSHA(pointerFile);
return {
file: pointerFile,
blob: pointerFileBlob,
sha: pointerFileSHA,
raw: pointerFileString,
};
}
async persistEntry(entry: Entry, mediaFiles: AssetProxy[], options: PersistOptions) {
const client = await this.getLargeMediaClient();
if (!client.enabled) {
return this.backend!.persistEntry(entry, mediaFiles, options);
}
const largeMediaFilteredMediaFiles = await Promise.all(
mediaFiles.map(async mediaFile => {
const { fileObj, path } = mediaFile;
const fixedPath = path.startsWith('/') ? path.slice(1) : path;
if (!client.matchPath(fixedPath)) {
return mediaFile;
}
const pointerFileDetails = await this.getPointerFileForMediaFileObj(fileObj as File);
return {
...mediaFile,
fileObj: pointerFileDetails.file,
size: pointerFileDetails.blob.size,
sha: pointerFileDetails.sha,
raw: pointerFileDetails.raw,
};
}),
return this.backend!.persistEntry(
entry,
client.enabled ? await getLargeMediaFilteredMediaFiles(client, mediaFiles) : mediaFiles,
options,
);
return this.backend!.persistEntry(entry, largeMediaFilteredMediaFiles, options);
}
async persistMedia(mediaFile: AssetProxy, options: PersistOptions) {
@ -520,14 +481,7 @@ export default class GitGateway implements Implementation {
return this.backend!.persistMedia(mediaFile, options);
}
const pointerFileDetails = await this.getPointerFileForMediaFileObj(fileObj as File);
const persistMediaArgument = {
fileObj: pointerFileDetails.file,
size: pointerFileDetails.blob.size,
path,
sha: pointerFileDetails.sha,
raw: pointerFileDetails.raw,
};
const persistMediaArgument = await getPointerFileForMediaFileObj(client, fileObj as File, path);
return {
...(await this.backend!.persistMedia(persistMediaArgument, options)),
displayURL,

View File

@ -1,30 +1,6 @@
import { filter, flow, fromPairs, map } from 'lodash/fp';
import { flow, fromPairs, map } from 'lodash/fp';
import minimatch from 'minimatch';
import { ApiRequest } from 'netlify-cms-lib-util';
//
// Pointer file parsing
const splitIntoLines = (str: string) => str.split('\n');
const splitIntoWords = (str: string) => str.split(/\s+/g);
const isNonEmptyString = (str: string) => str !== '';
const withoutEmptyLines = flow([map((str: string) => str.trim()), filter(isNonEmptyString)]);
export const parsePointerFile: (data: string) => PointerFile = flow([
splitIntoLines,
withoutEmptyLines,
map(splitIntoWords),
fromPairs,
({ size, oid, ...rest }) => ({
size: parseInt(size),
sha: oid?.split(':')[1],
...rest,
}),
]);
export type PointerFile = {
size: number;
sha: string;
};
import { ApiRequest, PointerFile } from 'netlify-cms-lib-util';
type MakeAuthorizedRequest = (req: ApiRequest) => Promise<Response>;
@ -38,56 +14,6 @@ type ClientConfig = {
transformImages: ImageTransformations | boolean;
};
export const createPointerFile = ({ size, sha }: PointerFile) => `\
version https://git-lfs.github.com/spec/v1
oid sha256:${sha}
size ${size}
`;
//
// .gitattributes file parsing
const removeGitAttributesCommentsFromLine = (line: string) => line.split('#')[0];
const parseGitPatternAttribute = (attributeString: string) => {
// There are three kinds of attribute settings:
// - a key=val pair sets an attribute to a specific value
// - a key without a value and a leading hyphen sets an attribute to false
// - a key without a value and no leading hyphen sets an attribute
// to true
if (attributeString.includes('=')) {
return attributeString.split('=');
}
if (attributeString.startsWith('-')) {
return [attributeString.slice(1), false];
}
return [attributeString, true];
};
const parseGitPatternAttributes = flow([map(parseGitPatternAttribute), fromPairs]);
const parseGitAttributesPatternLine = flow([
splitIntoWords,
([pattern, ...attributes]) => [pattern, parseGitPatternAttributes(attributes)],
]);
const parseGitAttributesFileToPatternAttributePairs = flow([
splitIntoLines,
map(removeGitAttributesCommentsFromLine),
withoutEmptyLines,
map(parseGitAttributesPatternLine),
]);
export const getLargeMediaPatternsFromGitAttributesFile = flow([
parseGitAttributesFileToPatternAttributePairs,
filter(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_pattern, attributes]) =>
attributes.filter === 'lfs' && attributes.diff === 'lfs' && attributes.merge === 'lfs',
),
map(([pattern]) => pattern),
]);
export const matchPath = ({ patterns }: ClientConfig, path: string) =>
patterns.some(pattern => minimatch(path, pattern, { matchBase: true }));