1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

API: Allow downloading a resource data

This commit is contained in:
Laurent Cozic 2018-09-30 10:15:46 +01:00
parent fb913bc33c
commit f87d1f11b0
6 changed files with 85 additions and 38 deletions

View File

@ -100,6 +100,17 @@ class Command extends BaseCommand {
lines.push('* **DELETE**: To delete items.'); lines.push('* **DELETE**: To delete items.');
lines.push(''); lines.push('');
lines.push('# Filtering data');
lines.push('');
lines.push('You can change the fields that will be returned by the API using the `fields=` query parameter, which takes a list of comma separated fields. For example, to get the longitude and latitude of a note, use this:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes/ABCD123?fields=longitude,latitude');
lines.push('');
lines.push('To get the IDs only of all the tags:');
lines.push('');
lines.push('\tcurl http://localhost:41184/tags?fields=id');
lines.push('');
lines.push('# About the property types'); lines.push('# About the property types');
lines.push(''); lines.push('');
lines.push('* Text is UTF-8.'); lines.push('* Text is UTF-8.');
@ -192,6 +203,13 @@ class Command extends BaseCommand {
lines.push(''); lines.push('');
} }
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('## GET /resources/:id/file');
lines.push('');
lines.push('Gets the actual file associated with this resource.');
lines.push('');
}
lines.push('## POST /' + tableName); lines.push('## POST /' + tableName);
lines.push(''); lines.push('');
lines.push('Creates a new ' + singular); lines.push('Creates a new ' + singular);

View File

@ -9,7 +9,7 @@ const Note = require('lib/models/Note');
const Tag = require('lib/models/Tag'); const Tag = require('lib/models/Tag');
const Resource = require('lib/models/Resource'); const Resource = require('lib/models/Resource');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
process.on('unhandledRejection', (reason, p) => { process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
@ -211,12 +211,15 @@ describe('services_rest_Api', function() {
it('should handle tokens', async (done) => { it('should handle tokens', async (done) => {
api = new Api('mytoken'); api = new Api('mytoken');
const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'notes')); let hasThrown = await checkThrowAsync(async () => await api.route('GET', 'notes'));
expect(hasThrown).toBe(true); expect(hasThrown).toBe(true);
const response = await api.route('GET', 'notes', { token: 'mytoken' }) const response = await api.route('GET', 'notes', { token: 'mytoken' })
expect(response.length).toBe(0); expect(response.length).toBe(0);
hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({title:'testing'})));
expect(hasThrown).toBe(true);
done(); done();
}); });

View File

@ -5,6 +5,7 @@ const { Logger } = require('lib/logger.js');
const randomClipperPort = require('lib/randomClipperPort'); const randomClipperPort = require('lib/randomClipperPort');
const enableServerDestroy = require('server-destroy'); const enableServerDestroy = require('server-destroy');
const Api = require('lib/services/rest/Api'); const Api = require('lib/services/rest/Api');
const ApiResponse = require('lib/services/rest/ApiResponse');
const multiparty = require('multiparty'); const multiparty = require('multiparty');
class ClipperServer { class ClipperServer {
@ -92,13 +93,14 @@ class ClipperServer {
this.server_.on('request', async (request, response) => { this.server_.on('request', async (request, response) => {
const writeCorsHeaders = (code, contentType = "application/json") => { const writeCorsHeaders = (code, contentType = "application/json", additionalHeaders = null) => {
response.writeHead(code, { const headers = Object.assign({}, {
"Content-Type": contentType, "Content-Type": contentType,
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type', 'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
}); }, additionalHeaders ? additionalHeaders : {});
response.writeHead(code, headers);
} }
const writeResponseJson = (code, object) => { const writeResponseJson = (code, object) => {
@ -113,8 +115,23 @@ class ClipperServer {
response.end(); response.end();
} }
const writeResponseInstance = (code, instance) => {
if (instance.type === 'attachment') {
const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
'Content-disposition': 'attachment; filename=' + filename,
'Content-Length': instance.body.length,
});
response.end(instance.body);
} else {
throw new Error('Not implemented');
}
}
const writeResponse = (code, response) => { const writeResponse = (code, response) => {
if (typeof response === 'string') { if (response instanceof ApiResponse) {
writeResponseInstance(code, response);
} else if (typeof response === 'string') {
writeResponseText(code, response); writeResponseText(code, response);
} else { } else {
writeResponseJson(code, response); writeResponseJson(code, response);

View File

@ -5,7 +5,7 @@ const Setting = require('lib/models/Setting.js');
const ArrayUtils = require('lib/ArrayUtils.js'); const ArrayUtils = require('lib/ArrayUtils.js');
const pathUtils = require('lib/path-utils.js'); const pathUtils = require('lib/path-utils.js');
const { mime } = require('lib/mime-utils.js'); const { mime } = require('lib/mime-utils.js');
const { filename } = require('lib/path-utils.js'); const { filename, safeFilename } = require('lib/path-utils.js');
const { FsDriverDummy } = require('lib/fs-driver-dummy.js'); const { FsDriverDummy } = require('lib/fs-driver-dummy.js');
const markdownUtils = require('lib/markdownUtils'); const markdownUtils = require('lib/markdownUtils');
const JoplinError = require('lib/JoplinError'); const JoplinError = require('lib/JoplinError');
@ -49,6 +49,15 @@ class Resource extends BaseItem {
return resource.id + extension; return resource.id + extension;
} }
static friendlyFilename(resource) {
let output = safeFilename(resource.title); // Make sure not to allow spaces or any special characters as it's not supported in HTTP headers
if (!output) output = resource.id;
let extension = resource.file_extension;
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
extension = extension ? ('.' + extension) : '';
return output + extension;
}
static fullPath(resource, encryptedBlob = false) { static fullPath(resource, encryptedBlob = false) {
return Setting.value('resourceDir') + '/' + this.filename(resource, encryptedBlob); return Setting.value('resourceDir') + '/' + this.filename(resource, encryptedBlob);
} }

View File

@ -12,6 +12,7 @@ const md5 = require('md5');
const { shim } = require('lib/shim'); const { shim } = require('lib/shim');
const HtmlToMd = require('lib/HtmlToMd'); const HtmlToMd = require('lib/HtmlToMd');
const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils'); const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils');
const ApiResponse = require('lib/services/rest/ApiResponse');
class ApiError extends Error { class ApiError extends Error {
@ -26,37 +27,10 @@ class ApiError extends Error {
} }
class ErrorMethodNotAllowed extends ApiError { class ErrorMethodNotAllowed extends ApiError { constructor(message = 'Method Not Allowed') { super(message, 405); } }
class ErrorNotFound extends ApiError { constructor(message = 'Not Found') { super(message, 404); } }
constructor(message = 'Method Not Allowed') { class ErrorForbidden extends ApiError { constructor(message = 'Forbidden') { super(message, 403); } }
super(message, 405); class ErrorBadRequest extends ApiError { constructor(message = 'Bad Request') { super(message, 400); } }
}
}
class ErrorNotFound extends ApiError {
constructor(message = 'Not Found') {
super(message, 404);
}
}
class ErrorForbidden extends ApiError {
constructor(message = 'Forbidden') {
super(message, 403);
}
}
class ErrorBadRequest extends ApiError {
constructor(message = 'Bad Request') {
super(message, 400);
}
}
class Api { class Api {
@ -126,6 +100,8 @@ class Api {
request.params = params; request.params = params;
if (!this[parsedPath.callName]) throw new ErrorNotFound();
try { try {
return this[parsedPath.callName](request, id, link); return this[parsedPath.callName](request, id, link);
} catch (error) { } catch (error) {
@ -288,6 +264,23 @@ class Api {
// path: "C:\Users\Laurent\AppData\Local\Temp\BW77wkpP23iIGUstd0kDuXXC.jpg" // path: "C:\Users\Laurent\AppData\Local\Temp\BW77wkpP23iIGUstd0kDuXXC.jpg"
// size: 164394 // size: 164394
if (request.method === 'GET') {
if (link !== 'file') throw new ErrorNotFound();
const resource = await Resource.load(id);
if (!resource) throw new ErrorNotFound();
const filePath = Resource.fullPath(resource);
const buffer = await shim.fsDriver().readFile(filePath, 'Buffer');
const response = new ApiResponse();
response.type = 'attachment';
response.body = buffer;
response.contentType = resource.mime;
response.attachmentFilename = Resource.friendlyFilename(resource);
return response;
}
if (request.method === 'POST') { if (request.method === 'POST') {
if (!request.files.length) throw new ErrorBadRequest('Resource cannot be created without a file'); if (!request.files.length) throw new ErrorBadRequest('Resource cannot be created without a file');
const filePath = request.files[0].path; const filePath = request.files[0].path;

View File

@ -0,0 +1,7 @@
class ApiResponse {
constructor() {}
}
module.exports = ApiResponse;