2018-03-24 21:35:10 +02:00
|
|
|
const { time } = require('lib/time-utils.js');
|
|
|
|
const { shim } = require('lib/shim');
|
|
|
|
const JoplinError = require('lib/JoplinError');
|
|
|
|
const { basicDelta } = require('lib/file-api');
|
|
|
|
|
|
|
|
class FileApiDriverDropbox {
|
|
|
|
|
|
|
|
constructor(api) {
|
|
|
|
this.api_ = api;
|
|
|
|
}
|
|
|
|
|
|
|
|
api() {
|
|
|
|
return this.api_;
|
|
|
|
}
|
|
|
|
|
|
|
|
requestRepeatCount() {
|
|
|
|
return 3;
|
|
|
|
}
|
|
|
|
|
|
|
|
makePath_(path) {
|
|
|
|
if (!path) return '';
|
|
|
|
return '/' + path;
|
|
|
|
}
|
|
|
|
|
2018-05-21 17:24:57 +02:00
|
|
|
hasErrorCode_(error, errorCode) {
|
2018-06-11 01:24:29 +02:00
|
|
|
if (!error || typeof error.code !== 'string') return false;
|
2018-05-21 17:24:57 +02:00
|
|
|
return error.code.indexOf(errorCode) >= 0;
|
|
|
|
}
|
|
|
|
|
2018-03-24 21:35:10 +02:00
|
|
|
async stat(path) {
|
|
|
|
try {
|
|
|
|
const metadata = await this.api().exec('POST', 'files/get_metadata', {
|
|
|
|
path: this.makePath_(path),
|
|
|
|
});
|
|
|
|
|
|
|
|
return this.metadataToStat_(metadata, path);
|
|
|
|
} catch (error) {
|
2018-05-21 17:24:57 +02:00
|
|
|
if (this.hasErrorCode_(error, 'not_found')) {
|
2018-03-24 21:35:10 +02:00
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
metadataToStat_(md, path) {
|
|
|
|
const output = {
|
|
|
|
path: path,
|
|
|
|
updated_time: md.server_modified ? new Date(md.server_modified) : new Date(),
|
|
|
|
isDir: md['.tag'] === 'folder',
|
|
|
|
};
|
|
|
|
|
|
|
|
if (md['.tag'] === 'deleted') output.isDeleted = true;
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
metadataToStats_(mds) {
|
|
|
|
const output = [];
|
|
|
|
for (let i = 0; i < mds.length; i++) {
|
|
|
|
output.push(this.metadataToStat_(mds[i], mds[i].name));
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
async setTimestamp(path, timestampMs) {
|
|
|
|
throw new Error('Not implemented'); // Not needed anymore
|
|
|
|
}
|
|
|
|
|
|
|
|
async delta(path, options) {
|
|
|
|
const context = options ? options.context : null;
|
|
|
|
let cursor = context ? context.cursor : null;
|
|
|
|
|
2018-03-27 01:05:39 +02:00
|
|
|
while (true) {
|
|
|
|
const urlPath = cursor ? 'files/list_folder/continue' : 'files/list_folder';
|
|
|
|
const body = cursor ? { cursor: cursor } : { path: this.makePath_(path), include_deleted: true };
|
|
|
|
|
|
|
|
try {
|
|
|
|
const response = await this.api().exec('POST', urlPath, body);
|
|
|
|
|
|
|
|
const output = {
|
|
|
|
items: this.metadataToStats_(response.entries),
|
|
|
|
hasMore: response.has_more,
|
|
|
|
context: { cursor: response.cursor },
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
} catch (error) {
|
|
|
|
// If there's an error related to an invalid cursor, clear the cursor and retry.
|
|
|
|
if (cursor) {
|
2018-05-21 17:24:57 +02:00
|
|
|
if ((error && error.httpStatus === 400) || this.hasErrorCode_(error, 'reset')) {
|
2018-03-27 01:05:39 +02:00
|
|
|
// console.info('Clearing cursor and retrying', error);
|
|
|
|
cursor = null;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
}
|
2018-03-24 21:35:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async list(path, options) {
|
|
|
|
let response = await this.api().exec('POST', 'files/list_folder', {
|
|
|
|
path: this.makePath_(path),
|
|
|
|
});
|
|
|
|
|
|
|
|
let output = this.metadataToStats_(response.entries);
|
|
|
|
|
|
|
|
while (response.has_more) {
|
|
|
|
response = await this.api().exec('POST', 'files/list_folder/continue', {
|
|
|
|
cursor: response.cursor,
|
|
|
|
});
|
|
|
|
|
|
|
|
output = output.concat(this.metadataToStats_(response.entries));
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
items: output,
|
|
|
|
hasMore: false,
|
|
|
|
context: { cursor: response.cursor },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async get(path, options) {
|
|
|
|
if (!options) options = {};
|
|
|
|
if (!options.responseFormat) options.responseFormat = 'text';
|
|
|
|
|
|
|
|
try {
|
|
|
|
const response = await this.api().exec('POST', 'files/download', null, {
|
|
|
|
'Dropbox-API-Arg': JSON.stringify({ "path": this.makePath_(path) }),
|
|
|
|
}, options);
|
|
|
|
return response;
|
|
|
|
} catch (error) {
|
2018-05-21 17:24:57 +02:00
|
|
|
if (this.hasErrorCode_(error, 'not_found')) {
|
2018-03-24 21:35:10 +02:00
|
|
|
return null;
|
2018-06-18 09:47:51 +02:00
|
|
|
} else if (this.hasErrorCode_(error, 'restricted_content')) {
|
|
|
|
throw new JoplinError('Cannot download because content is restricted by Dropbox', 'rejectedByTarget');
|
2018-03-24 21:35:10 +02:00
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async mkdir(path) {
|
|
|
|
try {
|
|
|
|
await this.api().exec('POST', 'files/create_folder_v2', {
|
|
|
|
path: this.makePath_(path),
|
|
|
|
});
|
|
|
|
} catch (error) {
|
2018-05-21 17:24:57 +02:00
|
|
|
if (this.hasErrorCode_(error, 'path/conflict')) {
|
2018-03-24 21:35:10 +02:00
|
|
|
// Ignore
|
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async put(path, content, options = null) {
|
|
|
|
// See https://github.com/facebook/react-native/issues/14445#issuecomment-352965210
|
2018-03-27 01:55:44 +02:00
|
|
|
if (typeof content === 'string') content = shim.Buffer.from(content, 'utf8')
|
2018-03-24 21:35:10 +02:00
|
|
|
|
|
|
|
await this.api().exec('POST', 'files/upload', content, {
|
|
|
|
'Dropbox-API-Arg': JSON.stringify({
|
|
|
|
path: this.makePath_(path),
|
|
|
|
mode: 'overwrite',
|
|
|
|
mute: true, // Don't send a notification to user since there can be many of these updates
|
|
|
|
})}, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete(path) {
|
|
|
|
try {
|
|
|
|
await this.api().exec('POST', 'files/delete_v2', {
|
|
|
|
path: this.makePath_(path),
|
|
|
|
});
|
|
|
|
} catch (error) {
|
2018-05-21 17:24:57 +02:00
|
|
|
if (this.hasErrorCode_(error, 'not_found')) {
|
2018-03-24 21:35:10 +02:00
|
|
|
// ignore
|
|
|
|
} else {
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async move(oldPath, newPath) {
|
|
|
|
throw new Error('Not supported');
|
|
|
|
}
|
|
|
|
|
|
|
|
format() {
|
|
|
|
throw new Error('Not supported');
|
|
|
|
}
|
|
|
|
|
|
|
|
async clearRoot() {
|
|
|
|
const entries = await this.list('');
|
|
|
|
const batchDelete = [];
|
|
|
|
for (let i = 0; i < entries.items.length; i++) {
|
|
|
|
batchDelete.push({ path: this.makePath_(entries.items[i].path) });
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await this.api().exec('POST', 'files/delete_batch', { entries: batchDelete });
|
|
|
|
const jobId = response.async_job_id;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const check = await this.api().exec('POST', 'files/delete_batch/check', { async_job_id: jobId });
|
|
|
|
if (check['.tag'] === 'complete') break;
|
|
|
|
|
|
|
|
// It returns "failed" if it didn't work but anyway throw an error if it's anything other than complete or in_progress
|
|
|
|
if (check['.tag'] !== 'in_progress') {
|
|
|
|
throw new Error('Batch delete failed? ' + JSON.stringify(check));
|
|
|
|
}
|
|
|
|
await time.sleep(2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = { FileApiDriverDropbox };
|