feat(backend-bitbucket): Add Git-LFS support (#3118)
This commit is contained in:
@ -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,
|
||||
|
@ -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 }));
|
||||
|
||||
|
Reference in New Issue
Block a user