1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-03 08:35:29 +02:00
joplin/packages/lib/file-api-driver-dropbox.js

275 lines
7.1 KiB
JavaScript
Raw Normal View History

2020-11-05 18:58:23 +02:00
const time = require('./time').default;
const shim = require('./shim').default;
const JoplinError = require('./JoplinError').default;
const Logger = require('@joplin/utils/Logger').default;
const logger = Logger.create('file-api-driver-dropbox');
2018-03-24 21:35:10 +02:00
2019-07-29 15:43:53 +02:00
class FileApiDriverDropbox {
2018-03-24 21:35:10 +02:00
constructor(api) {
this.api_ = api;
}
api() {
return this.api_;
}
requestRepeatCount() {
return 3;
}
makePath_(path) {
if (!path) return '';
2019-09-19 23:51:18 +02:00
return `/${path}`;
2018-03-24 21:35:10 +02:00
}
hasErrorCode_(error, errorCode) {
if (!error || typeof error.code !== 'string') return false;
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) {
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,
2020-09-09 13:25:31 +02:00
updated_time: md.server_modified ? (new Date(md.server_modified)).getTime() : Date.now(),
2018-03-24 21:35:10 +02:00
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() {
2018-03-24 21:35:10 +02:00
throw new Error('Not implemented'); // Not needed anymore
}
async delta(path, options) {
const context = options ? options.context : null;
let cursor = context ? context.cursor : null;
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);
2019-07-29 15:43:53 +02:00
const output = {
items: this.metadataToStats_(response.entries),
hasMore: response.has_more,
context: { cursor: response.cursor },
2019-07-29 15:43:53 +02:00
};
return output;
} catch (error) {
// If there's an error related to an invalid cursor, clear the cursor and retry.
if (cursor) {
if ((error && error.httpStatus === 400) || this.hasErrorCode_(error, 'reset')) {
// console.info('Clearing cursor and retrying', error);
cursor = null;
continue;
}
}
throw error;
}
2018-03-24 21:35:10 +02:00
}
}
async list(path, options) {
2018-03-24 21:35:10 +02:00
let response = await this.api().exec('POST', 'files/list_folder', {
path: this.makePath_(path),
});
let output = this.metadataToStats_(response.entries);
while (response.has_more && !options?.firstPageOnly) {
2018-03-24 21:35:10 +02:00
response = await this.api().exec('POST', 'files/list_folder/continue', {
cursor: response.cursor,
});
output = output.concat(this.metadataToStats_(response.entries));
}
return {
items: output,
hasMore: !!response.has_more,
2018-03-24 21:35:10 +02:00
context: { cursor: response.cursor },
};
}
async get(path, options) {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
2019-07-29 15:43:53 +02:00
2018-03-24 21:35:10 +02:00
try {
// IMPORTANT:
//
// We cannot use POST here, because iOS (as of version 14?) doesn't
// support POST requests with an empty body:
//
// https://www.dropboxforum.com/t5/Dropbox-API-Support-Feedback/Error-1017-quot-cannot-parse-response-quot/td-p/589595
const needsFetchWorkaround = shim.mobilePlatform() === 'ios';
const fetchPath = (method, path) => {
return this.api().exec(
method,
'files/download',
null,
{ 'Dropbox-API-Arg': JSON.stringify({ path: this.makePath_(path) }) },
options,
);
};
let response;
if (!needsFetchWorkaround) {
response = await fetchPath('POST', path);
} else {
try {
response = await fetchPath('GET', path);
} catch (error) {
logger.warn('Request to files/download failed. Retrying with workaround. Error: ', error);
2024-05-08 15:57:03 +02:00
// May 2024: Sending a GET request to files/download sometimes fails
// until another file is requested. Because POST requests with empty bodies don't work on iOS,
// we send a request for a different file, then re-request the original.
//
// See https://github.com/laurent22/joplin/issues/10396
// This workaround requires that the file we request exist.
const { items } = await this.list('', { firstPageOnly: true });
const files = items.filter(item => !item.isDir && item.path !== path);
if (files.length > 0) {
await fetchPath('GET', files[0].path);
}
response = await fetchPath('GET', path);
}
}
2018-03-24 21:35:10 +02:00
return response;
} catch (error) {
if (this.hasErrorCode_(error, 'not_found')) {
2018-03-24 21:35:10 +02:00
return null;
} 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) {
if (this.hasErrorCode_(error, 'path/conflict')) {
2018-03-24 21:35:10 +02:00
// Ignore
} else {
throw error;
2019-07-29 15:43:53 +02:00
}
2018-03-24 21:35:10 +02:00
}
}
async put(path, content, options = null) {
// See https://github.com/facebook/react-native/issues/14445#issuecomment-352965210
2019-07-29 15:43:53 +02:00
if (typeof content === 'string') content = shim.Buffer.from(content, 'utf8');
2018-03-24 21:35:10 +02:00
try {
2019-07-29 15:43:53 +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,
2019-07-29 15:43:53 +02:00
);
} catch (error) {
if (this.hasErrorCode_(error, 'restricted_content')) {
throw new JoplinError('Cannot upload because content is restricted by Dropbox (restricted_content)', 'rejectedByTarget');
} else if (this.hasErrorCode_(error, 'payload_too_large')) {
throw new JoplinError('Cannot upload because payload size is rejected by Dropbox (payload_too_large)', 'rejectedByTarget');
} else {
throw error;
}
}
2018-03-24 21:35:10 +02:00
}
async delete(path) {
try {
await this.api().exec('POST', 'files/delete_v2', {
path: this.makePath_(path),
});
} catch (error) {
if (this.hasErrorCode_(error, 'not_found')) {
2018-03-24 21:35:10 +02:00
// ignore
} else {
throw error;
}
}
}
async move() {
2018-03-24 21:35:10 +02:00
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') {
2019-09-19 23:51:18 +02:00
throw new Error(`Batch delete failed? ${JSON.stringify(check)}`);
2018-03-24 21:35:10 +02:00
}
await time.sleep(2);
}
}
}
2019-07-29 15:43:53 +02:00
module.exports = { FileApiDriverDropbox };