2018-08-07 14:46:54 -06:00
|
|
|
import { flow, get } from 'lodash';
|
2018-07-25 06:56:53 -04:00
|
|
|
import {
|
2018-07-26 13:42:19 -04:00
|
|
|
localForage,
|
2018-07-25 06:56:53 -04:00
|
|
|
unsentRequest,
|
|
|
|
responseParser,
|
|
|
|
then,
|
|
|
|
basename,
|
|
|
|
Cursor,
|
2018-08-07 14:46:54 -06:00
|
|
|
APIError,
|
|
|
|
} from 'netlify-cms-lib-util';
|
2018-07-25 06:56:53 -04:00
|
|
|
|
|
|
|
export default class API {
|
|
|
|
constructor(config) {
|
2018-08-07 14:46:54 -06:00
|
|
|
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;
|
2018-08-07 14:46:54 -06:00
|
|
|
this.repoURL = this.repo ? `/repositories/${this.repo}` : '';
|
2018-07-25 06:56:53 -04:00
|
|
|
}
|
|
|
|
|
2018-08-07 14:46:54 -06: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.)
|
2018-08-07 14:46:54 -06:00
|
|
|
...(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 } = {}) => {
|
2018-08-07 14:46:54 -06:00
|
|
|
const cacheKey = parseText ? `bb.${sha}` : `bb.${sha}.blob`;
|
2018-07-26 13:42:19 -04:00
|
|
|
const cachedFile = sha ? await localForage.getItem(cacheKey) : null;
|
2018-08-07 14:46:54 -06:00
|
|
|
if (cachedFile) {
|
|
|
|
return cachedFile;
|
|
|
|
}
|
2018-07-25 06:56:53 -04:00
|
|
|
const result = await this.request({
|
2018-08-07 14:46:54 -06:00
|
|
|
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-08-07 14:46:54 -06:00
|
|
|
};
|
2018-07-25 06:56:53 -04:00
|
|
|
|
|
|
|
getEntriesAndCursor = jsonResponse => {
|
2018-08-07 14:46:54 -06:00
|
|
|
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({
|
2018-08-07 14:46:54 -06:00
|
|
|
actions: [...(next ? ['next'] : []), ...(prev ? ['prev'] : [])],
|
2018-07-25 06:56:53 -04:00
|
|
|
meta: { index, count, pageSize, pageCount },
|
|
|
|
data: { links: { next, prev } },
|
|
|
|
}),
|
|
|
|
};
|
2018-08-07 14:46:54 -06:00
|
|
|
};
|
2018-07-25 06:56:53 -04:00
|
|
|
|
|
|
|
listFiles = async path => {
|
|
|
|
const { entries, cursor } = await flow([
|
|
|
|
// sort files by filename ascending
|
2018-08-07 14:46:54 -06:00
|
|
|
unsentRequest.withParams({ sort: '-path' }),
|
2018-07-25 06:56:53 -04:00
|
|
|
this.requestJSON,
|
|
|
|
then(this.getEntriesAndCursor),
|
2018-08-07 14:46:54 -06:00
|
|
|
])(`${this.repoURL}/src/${this.branch}/${path}`);
|
2018-07-25 06:56:53 -04:00
|
|
|
return { entries: this.processFiles(entries), cursor };
|
2018-08-07 14:46:54 -06:00
|
|
|
};
|
2018-07-25 06:56:53 -04:00
|
|
|
|
2018-08-07 14:46:54 -06: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;
|
2018-08-07 14:46:54 -06:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2018-08-07 09:49:53 -06:00
|
|
|
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();
|
2018-08-07 09:49:53 -06:00
|
|
|
// 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) {
|
2018-08-07 14:46:54 -06:00
|
|
|
formData.append('message', commitMessage);
|
2018-08-07 09:49:53 -06:00
|
|
|
}
|
|
|
|
if (this.commitAuthor) {
|
|
|
|
const { name, email } = this.commitAuthor;
|
2018-08-07 14:46:54 -06:00
|
|
|
formData.append('author', `${name} <${email}>`);
|
2018-08-07 09:49:53 -06:00
|
|
|
}
|
2018-07-25 06:56:53 -04:00
|
|
|
|
|
|
|
return flow([
|
2018-08-07 14:46:54 -06:00
|
|
|
unsentRequest.withMethod('POST'),
|
2018-07-25 06:56:53 -04:00
|
|
|
unsentRequest.withBody(formData),
|
|
|
|
this.request,
|
|
|
|
then(() => ({ ...item, uploaded: true })),
|
2018-08-07 14:46:54 -06:00
|
|
|
])(`${this.repoURL}/src`);
|
2018-07-25 06:56:53 -04:00
|
|
|
};
|
|
|
|
|
2018-08-07 14:46:54 -06:00
|
|
|
persistFiles = (files, { commitMessage }) =>
|
|
|
|
Promise.all(
|
|
|
|
files
|
|
|
|
.filter(({ uploaded }) => !uploaded)
|
|
|
|
.map(file => this.uploadBlob(file, { commitMessage })),
|
|
|
|
);
|
2018-07-25 06:56:53 -04:00
|
|
|
|
2018-08-07 09:49:53 -06:00
|
|
|
deleteFile = (path, message, { branch = this.branch } = {}) => {
|
2018-07-25 06:56:53 -04:00
|
|
|
const body = new FormData();
|
|
|
|
body.append('files', path);
|
2018-08-07 09:49:53 -06:00
|
|
|
body.append('branch', branch);
|
|
|
|
if (message) {
|
2018-08-07 14:46:54 -06:00
|
|
|
body.append('message', message);
|
2018-07-25 06:56:53 -04:00
|
|
|
}
|
2018-08-07 09:49:53 -06:00
|
|
|
if (this.commitAuthor) {
|
|
|
|
const { name, email } = this.commitAuthor;
|
2018-08-07 14:46:54 -06:00
|
|
|
body.append('author', `${name} <${email}>`);
|
2018-08-07 09:49:53 -06:00
|
|
|
}
|
2018-08-07 14:46:54 -06:00
|
|
|
return flow([unsentRequest.withMethod('POST'), unsentRequest.withBody(body), this.request])(
|
|
|
|
`${this.repoURL}/src`,
|
|
|
|
);
|
2018-07-25 06:56:53 -04:00
|
|
|
};
|
|
|
|
}
|