182 lines
5.9 KiB
JavaScript
Raw Normal View History

import { flow, get } from 'lodash';
2018-07-25 06:56:53 -04:00
import {
localForage,
2018-07-25 06:56:53 -04:00
unsentRequest,
responseParser,
then,
basename,
Cursor,
APIError,
} from 'netlify-cms-lib-util';
2018-07-25 06:56:53 -04:00
export default class API {
constructor(config) {
this.api_root = config.api_root || 'https://api.bitbucket.org/2.0';
this.branch = config.branch || 'master';
this.repo = config.repo || '';
2018-07-25 06:56:53 -04:00
this.requestFunction = config.requestFunction || unsentRequest.performRequest;
// Allow overriding this.hasWriteAccess
this.hasWriteAccess = config.hasWriteAccess || this.hasWriteAccess;
this.repoURL = this.repo ? `/repositories/${this.repo}` : '';
2018-07-25 06:56:53 -04:00
}
buildRequest = req =>
flow([unsentRequest.withRoot(this.api_root), unsentRequest.withTimestamp])(req);
request = req =>
flow([
this.buildRequest,
this.requestFunction,
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
])(req);
requestJSON = req =>
flow([
unsentRequest.withDefaultHeaders({ 'Content-Type': 'application/json' }),
this.request,
then(responseParser({ format: 'json' })),
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
])(req);
requestText = req =>
flow([
unsentRequest.withDefaultHeaders({ 'Content-Type': 'text/plain' }),
this.request,
then(responseParser({ format: 'text' })),
p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))),
])(req);
user = () => this.request('/user');
hasWriteAccess = () => this.request(this.repoURL).then(res => res.ok);
isFile = ({ type }) => type === 'commit_file';
2018-07-25 06:56:53 -04:00
processFile = file => ({
...file,
name: basename(file.path),
// BitBucket does not return file SHAs, but it does give us the
// commit SHA. Since the commit SHA will change if any files do,
// we can construct an ID using the commit SHA and the file path
// that will help with caching (though not as well as a normal
// SHA, since it will change even if the individual file itself
// doesn't.)
...(file.commit && file.commit.hash ? { id: `${file.commit.hash}/${file.path}` } : {}),
2018-07-25 06:56:53 -04:00
});
processFiles = files => files.filter(this.isFile).map(this.processFile);
readFile = async (path, sha, { ref = this.branch, parseText = true } = {}) => {
const cacheKey = parseText ? `bb.${sha}` : `bb.${sha}.blob`;
const cachedFile = sha ? await localForage.getItem(cacheKey) : null;
if (cachedFile) {
return cachedFile;
}
2018-07-25 06:56:53 -04:00
const result = await this.request({
url: `${this.repoURL}/src/${ref}/${path}`,
cache: 'no-store',
}).then(parseText ? responseParser({ format: 'text' }) : responseParser({ format: 'blob' }));
if (sha) {
localForage.setItem(cacheKey, result);
}
2018-07-25 06:56:53 -04:00
return result;
};
2018-07-25 06:56:53 -04:00
getEntriesAndCursor = jsonResponse => {
const {
size: count,
page: index,
pagelen: pageSize,
next,
previous: prev,
values: entries,
} = jsonResponse;
const pageCount = pageSize && count ? Math.ceil(count / pageSize) : undefined;
2018-07-25 06:56:53 -04:00
return {
entries,
cursor: Cursor.create({
actions: [...(next ? ['next'] : []), ...(prev ? ['prev'] : [])],
2018-07-25 06:56:53 -04:00
meta: { index, count, pageSize, pageCount },
data: { links: { next, prev } },
}),
};
};
2018-07-25 06:56:53 -04:00
listFiles = async path => {
const { entries, cursor } = await flow([
// sort files by filename ascending
unsentRequest.withParams({ sort: '-path' }),
2018-07-25 06:56:53 -04:00
this.requestJSON,
then(this.getEntriesAndCursor),
])(`${this.repoURL}/src/${this.branch}/${path}`);
2018-07-25 06:56:53 -04:00
return { entries: this.processFiles(entries), cursor };
};
2018-07-25 06:56:53 -04:00
traverseCursor = async (cursor, action) =>
flow([
this.requestJSON,
then(this.getEntriesAndCursor),
then(({ cursor: newCursor, entries }) => ({
cursor: newCursor,
entries: this.processFiles(entries),
})),
])(cursor.data.getIn(['links', action]));
2018-07-25 06:56:53 -04:00
listAllFiles = async path => {
const { cursor: initialCursor, entries: initialEntries } = await this.listFiles(path);
const entries = [...initialEntries];
let currentCursor = initialCursor;
while (currentCursor && currentCursor.actions.has('next')) {
const { cursor: newCursor, entries: newEntries } = await this.traverseCursor(
currentCursor,
'next',
);
2018-07-25 06:56:53 -04:00
entries.push(...newEntries);
currentCursor = newCursor;
}
return this.processFiles(entries);
};
uploadBlob = async (item, { commitMessage, branch = this.branch } = {}) => {
const contentBlob = get(item, 'fileObj', new Blob([item.raw]));
2018-07-25 06:56:53 -04:00
const formData = new FormData();
// Third param is filename header, in case path is `message`, `branch`, etc.
formData.append(item.path, contentBlob, basename(item.path));
formData.append('branch', branch);
if (commitMessage) {
formData.append('message', commitMessage);
}
if (this.commitAuthor) {
const { name, email } = this.commitAuthor;
formData.append('author', `${name} <${email}>`);
}
2018-07-25 06:56:53 -04:00
return flow([
unsentRequest.withMethod('POST'),
2018-07-25 06:56:53 -04:00
unsentRequest.withBody(formData),
this.request,
then(() => ({ ...item, uploaded: true })),
])(`${this.repoURL}/src`);
2018-07-25 06:56:53 -04:00
};
persistFiles = (files, { commitMessage }) =>
Promise.all(
files
.filter(({ uploaded }) => !uploaded)
.map(file => this.uploadBlob(file, { commitMessage })),
);
2018-07-25 06:56:53 -04:00
deleteFile = (path, message, { branch = this.branch } = {}) => {
2018-07-25 06:56:53 -04:00
const body = new FormData();
body.append('files', path);
body.append('branch', branch);
if (message) {
body.append('message', message);
2018-07-25 06:56:53 -04:00
}
if (this.commitAuthor) {
const { name, email } = this.commitAuthor;
body.append('author', `${name} <${email}>`);
}
return flow([unsentRequest.withMethod('POST'), unsentRequest.withBody(body), this.request])(
`${this.repoURL}/src`,
);
2018-07-25 06:56:53 -04:00
};
}