2021-05-17 18:55:39 +02:00
|
|
|
import JoplinError from './JoplinError';
|
2021-01-29 20:45:11 +02:00
|
|
|
import JoplinServerApi from './JoplinServerApi';
|
2021-05-13 18:57:37 +02:00
|
|
|
import { trimSlashes } from './path-utils';
|
2020-12-28 13:48:47 +02:00
|
|
|
|
2021-02-01 12:48:37 +02:00
|
|
|
// All input paths should be in the format: "path/to/file". This is converted to
|
|
|
|
// "root:/path/to/file:" when doing the API call.
|
2020-12-28 13:48:47 +02:00
|
|
|
|
|
|
|
export default class FileApiDriverJoplinServer {
|
|
|
|
|
|
|
|
private api_: JoplinServerApi;
|
|
|
|
|
|
|
|
public constructor(api: JoplinServerApi) {
|
|
|
|
this.api_ = api;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async initialize(basePath: string) {
|
2021-02-01 12:48:37 +02:00
|
|
|
const pieces = trimSlashes(basePath).split('/');
|
2020-12-28 13:48:47 +02:00
|
|
|
if (!pieces.length) return;
|
|
|
|
|
2021-02-01 12:48:37 +02:00
|
|
|
const parent: string[] = [];
|
2020-12-28 13:48:47 +02:00
|
|
|
|
2021-02-01 12:48:37 +02:00
|
|
|
for (let i = 0; i < pieces.length; i++) {
|
|
|
|
const p = pieces[i];
|
|
|
|
const subPath = parent.concat(p).join('/');
|
|
|
|
parent.push(p);
|
2020-12-28 13:48:47 +02:00
|
|
|
await this.mkdir(subPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public api() {
|
|
|
|
return this.api_;
|
|
|
|
}
|
|
|
|
|
|
|
|
public requestRepeatCount() {
|
|
|
|
return 3;
|
|
|
|
}
|
|
|
|
|
2021-05-13 18:57:37 +02:00
|
|
|
private metadataToStat_(md: any, path: string, isDeleted: boolean = false, rootPath: string) {
|
2020-12-28 13:48:47 +02:00
|
|
|
const output = {
|
2021-05-13 18:57:37 +02:00
|
|
|
path: rootPath ? path.substr(rootPath.length + 1) : path,
|
2020-12-28 13:48:47 +02:00
|
|
|
updated_time: md.updated_time,
|
2021-05-13 18:57:37 +02:00
|
|
|
isDir: false, // !!md.is_directory,
|
2020-12-28 13:48:47 +02:00
|
|
|
isDeleted: isDeleted,
|
|
|
|
};
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2021-05-13 18:57:37 +02:00
|
|
|
private metadataToStats_(mds: any[], rootPath: string) {
|
2020-12-28 13:48:47 +02:00
|
|
|
const output = [];
|
|
|
|
for (let i = 0; i < mds.length; i++) {
|
2021-05-13 18:57:37 +02:00
|
|
|
output.push(this.metadataToStat_(mds[i], mds[i].name, false, rootPath));
|
2020-12-28 13:48:47 +02:00
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2021-02-01 12:48:37 +02:00
|
|
|
// Transforms a path such as "Apps/Joplin/file.txt" to a complete a complete
|
2021-05-13 18:57:37 +02:00
|
|
|
// API URL path: "api/items/root:/Apps/Joplin/file.txt:"
|
2020-12-28 13:48:47 +02:00
|
|
|
private apiFilePath_(p: string) {
|
2021-05-13 18:57:37 +02:00
|
|
|
return `api/items/root:/${trimSlashes(p)}:`;
|
2020-12-28 13:48:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async stat(path: string) {
|
|
|
|
try {
|
|
|
|
const response = await this.api().exec('GET', this.apiFilePath_(path));
|
2021-05-13 18:57:37 +02:00
|
|
|
return this.metadataToStat_(response, path, false, '');
|
2020-12-28 13:48:47 +02:00
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 404) return null;
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async delta(path: string, options: any) {
|
|
|
|
const context = options ? options.context : null;
|
|
|
|
let cursor = context ? context.cursor : null;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
try {
|
|
|
|
const query = cursor ? { cursor } : {};
|
|
|
|
const response = await this.api().exec('GET', `${this.apiFilePath_(path)}/delta`, query);
|
2021-05-13 18:57:37 +02:00
|
|
|
const stats = response.items
|
|
|
|
.filter((item: any) => {
|
|
|
|
return item.item_name.indexOf('locks/') !== 0 && item.item_name.indexOf('temp/') !== 0;
|
|
|
|
})
|
|
|
|
.map((item: any) => {
|
|
|
|
return this.metadataToStat_(item, item.item_name, item.type === 3, '');
|
|
|
|
});
|
2020-12-28 13:48:47 +02:00
|
|
|
|
|
|
|
const output = {
|
|
|
|
items: stats,
|
|
|
|
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 && error.code === 'resyncRequired') {
|
|
|
|
cursor = null;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async list(path: string, options: any = null) {
|
|
|
|
options = {
|
|
|
|
context: null,
|
|
|
|
...options,
|
|
|
|
};
|
|
|
|
|
2021-05-13 18:57:37 +02:00
|
|
|
let isUsingWildcard = false;
|
|
|
|
let searchPath = path;
|
|
|
|
if (searchPath) {
|
|
|
|
searchPath += '/*';
|
|
|
|
isUsingWildcard = true;
|
|
|
|
}
|
|
|
|
|
2020-12-28 13:48:47 +02:00
|
|
|
const query = options.context?.cursor ? { cursor: options.context.cursor } : null;
|
|
|
|
|
2021-05-13 18:57:37 +02:00
|
|
|
const results = await this.api().exec('GET', `${this.apiFilePath_(searchPath)}/children`, query);
|
2020-12-28 13:48:47 +02:00
|
|
|
|
|
|
|
const newContext: any = {};
|
|
|
|
if (results.cursor) newContext.cursor = results.cursor;
|
|
|
|
|
|
|
|
return {
|
2021-05-13 18:57:37 +02:00
|
|
|
items: this.metadataToStats_(results.items, isUsingWildcard ? path : ''),
|
2020-12-28 13:48:47 +02:00
|
|
|
hasMore: results.has_more,
|
|
|
|
context: newContext,
|
|
|
|
} as any;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async get(path: string, options: any) {
|
|
|
|
if (!options) options = {};
|
|
|
|
if (!options.responseFormat) options.responseFormat = 'text';
|
|
|
|
try {
|
|
|
|
const response = await this.api().exec('GET', `${this.apiFilePath_(path)}/content`, null, null, null, options);
|
|
|
|
return response;
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code !== 404) throw error;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-13 18:57:37 +02:00
|
|
|
public async mkdir(_path: string) {
|
|
|
|
// This is a no-op because all items technically are at the root, but
|
|
|
|
// they can have names such as ".resources/xxxxxxxxxx'
|
2020-12-28 13:48:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async put(path: string, content: any, options: any = null) {
|
2021-05-17 18:55:39 +02:00
|
|
|
try {
|
|
|
|
const output = await this.api().exec('PUT', `${this.apiFilePath_(path)}/content`, options && options.shareId ? { share_id: options.shareId } : null, content, {
|
|
|
|
'Content-Type': 'application/octet-stream',
|
|
|
|
}, options);
|
|
|
|
return output;
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 413) {
|
|
|
|
throw new JoplinError(error.message, 'rejectedByTarget');
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
}
|
2020-12-28 13:48:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async delete(path: string) {
|
|
|
|
return this.api().exec('DELETE', this.apiFilePath_(path));
|
|
|
|
}
|
|
|
|
|
|
|
|
public format() {
|
|
|
|
throw new Error('Not supported');
|
|
|
|
}
|
|
|
|
|
|
|
|
public async clearRoot(path: string) {
|
|
|
|
await this.delete(path);
|
|
|
|
}
|
|
|
|
}
|