154 lines
4.2 KiB
TypeScript
154 lines
4.2 KiB
TypeScript
import { flow, fromPairs, map } from 'lodash/fp';
|
|
import minimatch from 'minimatch';
|
|
import { ApiRequest, PointerFile, unsentRequest } from 'netlify-cms-lib-util';
|
|
|
|
type MakeAuthorizedRequest = (req: ApiRequest) => Promise<Response>;
|
|
|
|
type ImageTransformations = { nf_resize: string; w: number; h: number };
|
|
|
|
type ClientConfig = {
|
|
rootURL: string;
|
|
makeAuthorizedRequest: MakeAuthorizedRequest;
|
|
patterns: string[];
|
|
enabled: boolean;
|
|
transformImages: ImageTransformations | boolean;
|
|
};
|
|
|
|
export const matchPath = ({ patterns }: ClientConfig, path: string) =>
|
|
patterns.some(pattern => minimatch(path, pattern, { matchBase: true }));
|
|
|
|
//
|
|
// API interactions
|
|
|
|
const defaultContentHeaders = {
|
|
Accept: 'application/vnd.git-lfs+json',
|
|
['Content-Type']: 'application/vnd.git-lfs+json',
|
|
};
|
|
|
|
const resourceExists = async (
|
|
{ rootURL, makeAuthorizedRequest }: ClientConfig,
|
|
{ sha, size }: PointerFile,
|
|
) => {
|
|
const response = await makeAuthorizedRequest({
|
|
url: `${rootURL}/verify`,
|
|
method: 'POST',
|
|
headers: defaultContentHeaders,
|
|
body: JSON.stringify({ oid: sha, size }),
|
|
});
|
|
if (response.ok) {
|
|
return true;
|
|
}
|
|
if (response.status === 404) {
|
|
return false;
|
|
}
|
|
|
|
// TODO: what kind of error to throw here? APIError doesn't seem
|
|
// to fit
|
|
};
|
|
|
|
const getTransofrmationsParams = (t: ImageTransformations) =>
|
|
`?nf_resize=${t.nf_resize}&w=${t.w}&h=${t.h}`;
|
|
|
|
const getDownloadURL = (
|
|
{ rootURL, transformImages: t, makeAuthorizedRequest }: ClientConfig,
|
|
{ sha }: PointerFile,
|
|
) =>
|
|
makeAuthorizedRequest(
|
|
`${rootURL}/origin/${sha}${
|
|
t && Object.keys(t).length > 0 ? getTransofrmationsParams(t as ImageTransformations) : ''
|
|
}`,
|
|
)
|
|
.then(res => (res.ok ? res : Promise.reject(res)))
|
|
.then(res => res.blob())
|
|
.then(blob => URL.createObjectURL(blob))
|
|
.catch((err: Error) => {
|
|
console.error(err);
|
|
return Promise.resolve('');
|
|
});
|
|
|
|
const uploadOperation = (objects: PointerFile[]) => ({
|
|
operation: 'upload',
|
|
transfers: ['basic'],
|
|
objects: objects.map(({ sha, ...rest }) => ({ ...rest, oid: sha })),
|
|
});
|
|
|
|
const getResourceUploadURLs = async (
|
|
{
|
|
rootURL,
|
|
makeAuthorizedRequest,
|
|
}: { rootURL: string; makeAuthorizedRequest: MakeAuthorizedRequest },
|
|
objects: PointerFile[],
|
|
) => {
|
|
const response = await makeAuthorizedRequest({
|
|
url: `${rootURL}/objects/batch`,
|
|
method: 'POST',
|
|
headers: defaultContentHeaders,
|
|
body: JSON.stringify(uploadOperation(objects)),
|
|
});
|
|
return (await response.json()).objects.map(
|
|
(object: { error?: { message: string }; actions: { upload: { href: string } } }) => {
|
|
if (object.error) {
|
|
throw new Error(object.error.message);
|
|
}
|
|
return object.actions.upload.href;
|
|
},
|
|
);
|
|
};
|
|
|
|
const uploadBlob = (uploadURL: string, blob: Blob) =>
|
|
unsentRequest.fetchWithTimeout(uploadURL, {
|
|
method: 'PUT',
|
|
body: blob,
|
|
});
|
|
|
|
const uploadResource = async (
|
|
clientConfig: ClientConfig,
|
|
{ sha, size }: PointerFile,
|
|
resource: Blob,
|
|
) => {
|
|
const existingFile = await resourceExists(clientConfig, { sha, size });
|
|
if (existingFile) {
|
|
return sha;
|
|
}
|
|
const [uploadURL] = await getResourceUploadURLs(clientConfig, [{ sha, size }]);
|
|
await uploadBlob(uploadURL, resource);
|
|
return sha;
|
|
};
|
|
|
|
//
|
|
// Create Large Media client
|
|
|
|
const configureFn = (config: ClientConfig, fn: Function) => (...args: unknown[]) =>
|
|
fn(config, ...args);
|
|
|
|
const clientFns: Record<string, Function> = {
|
|
resourceExists,
|
|
getResourceUploadURLs,
|
|
getDownloadURL,
|
|
uploadResource,
|
|
matchPath,
|
|
};
|
|
|
|
export type Client = {
|
|
resourceExists: (pointer: PointerFile) => Promise<boolean | undefined>;
|
|
getResourceUploadURLs: (objects: PointerFile[]) => Promise<string>;
|
|
getDownloadURL: (pointer: PointerFile) => Promise<string>;
|
|
uploadResource: (pointer: PointerFile, blob: Blob) => Promise<string>;
|
|
matchPath: (path: string) => boolean;
|
|
patterns: string[];
|
|
enabled: boolean;
|
|
};
|
|
|
|
export const getClient = (clientConfig: ClientConfig) => {
|
|
return flow([
|
|
Object.keys,
|
|
map((key: string) => [key, configureFn(clientConfig, clientFns[key])]),
|
|
fromPairs,
|
|
configuredFns => ({
|
|
...configuredFns,
|
|
patterns: clientConfig.patterns,
|
|
enabled: clientConfig.enabled,
|
|
}),
|
|
])(clientFns);
|
|
};
|