// // Pointer file parsing import { filter, flow, fromPairs, map } from 'lodash/fp'; import getBlobSHA from './getBlobSHA'; import type { AssetProxy } from './implementation'; export interface PointerFile { size: number; sha: string; } function splitIntoLines(str: string) { return str.split('\n'); } function splitIntoWords(str: string) { return str.split(/\s+/g); } function isNonEmptyString(str: string) { return 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, }), ]); // // .gitattributes file parsing function removeGitAttributesCommentsFromLine(line: string) { return line.split('#')[0]; } function 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( ([, attributes]) => attributes.filter === 'lfs' && attributes.diff === 'lfs' && attributes.merge === 'lfs', ), map(([pattern]) => pattern), ]); export function createPointerFile({ size, sha }: PointerFile) { return `\ version https://git-lfs.github.com/spec/v1 oid sha256:${sha} size ${size} `; } export async function getPointerFileForMediaFileObj( client: { uploadResource: (pointer: PointerFile, resource: Blob) => Promise }, fileObj: File, path: string, ) { 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 { fileObj: pointerFile, size: pointerFileBlob.size, sha: pointerFileSHA, raw: pointerFileString, path, }; } export async function getLargeMediaFilteredMediaFiles( client: { uploadResource: (pointer: PointerFile, resource: Blob) => Promise; matchPath: (path: string) => boolean; }, mediaFiles: AssetProxy[], ) { return 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 getPointerFileForMediaFileObj(client, fileObj as File, path); return { ...mediaFile, ...pointerFileDetails }; }), ); }