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:
parent
fb913bc33c
commit
f87d1f11b0
@ -100,6 +100,17 @@ class Command extends BaseCommand {
|
||||
lines.push('* **DELETE**: To delete items.');
|
||||
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('');
|
||||
lines.push('* Text is UTF-8.');
|
||||
@ -192,6 +203,13 @@ class Command extends BaseCommand {
|
||||
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('');
|
||||
lines.push('Creates a new ' + singular);
|
||||
|
@ -9,7 +9,7 @@ const Note = require('lib/models/Note');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const Resource = require('lib/models/Resource');
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
@ -211,12 +211,15 @@ describe('services_rest_Api', function() {
|
||||
it('should handle tokens', async (done) => {
|
||||
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);
|
||||
|
||||
const response = await api.route('GET', 'notes', { token: 'mytoken' })
|
||||
expect(response.length).toBe(0);
|
||||
|
||||
hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({title:'testing'})));
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,7 @@ const { Logger } = require('lib/logger.js');
|
||||
const randomClipperPort = require('lib/randomClipperPort');
|
||||
const enableServerDestroy = require('server-destroy');
|
||||
const Api = require('lib/services/rest/Api');
|
||||
const ApiResponse = require('lib/services/rest/ApiResponse');
|
||||
const multiparty = require('multiparty');
|
||||
|
||||
class ClipperServer {
|
||||
@ -92,13 +93,14 @@ class ClipperServer {
|
||||
|
||||
this.server_.on('request', async (request, response) => {
|
||||
|
||||
const writeCorsHeaders = (code, contentType = "application/json") => {
|
||||
response.writeHead(code, {
|
||||
const writeCorsHeaders = (code, contentType = "application/json", additionalHeaders = null) => {
|
||||
const headers = Object.assign({}, {
|
||||
"Content-Type": contentType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
|
||||
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
|
||||
});
|
||||
}, additionalHeaders ? additionalHeaders : {});
|
||||
response.writeHead(code, headers);
|
||||
}
|
||||
|
||||
const writeResponseJson = (code, object) => {
|
||||
@ -113,8 +115,23 @@ class ClipperServer {
|
||||
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) => {
|
||||
if (typeof response === 'string') {
|
||||
if (response instanceof ApiResponse) {
|
||||
writeResponseInstance(code, response);
|
||||
} else if (typeof response === 'string') {
|
||||
writeResponseText(code, response);
|
||||
} else {
|
||||
writeResponseJson(code, response);
|
||||
|
@ -5,7 +5,7 @@ const Setting = require('lib/models/Setting.js');
|
||||
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||
const pathUtils = require('lib/path-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 markdownUtils = require('lib/markdownUtils');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
@ -49,6 +49,15 @@ class Resource extends BaseItem {
|
||||
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) {
|
||||
return Setting.value('resourceDir') + '/' + this.filename(resource, encryptedBlob);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ const md5 = require('md5');
|
||||
const { shim } = require('lib/shim');
|
||||
const HtmlToMd = require('lib/HtmlToMd');
|
||||
const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils');
|
||||
const ApiResponse = require('lib/services/rest/ApiResponse');
|
||||
|
||||
class ApiError extends Error {
|
||||
|
||||
@ -26,37 +27,10 @@ class ApiError extends Error {
|
||||
|
||||
}
|
||||
|
||||
class ErrorMethodNotAllowed extends ApiError {
|
||||
|
||||
constructor(message = 'Method Not Allowed') {
|
||||
super(message, 405);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 ErrorMethodNotAllowed extends ApiError { constructor(message = 'Method Not Allowed') { super(message, 405); } }
|
||||
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 {
|
||||
|
||||
@ -126,6 +100,8 @@ class Api {
|
||||
|
||||
request.params = params;
|
||||
|
||||
if (!this[parsedPath.callName]) throw new ErrorNotFound();
|
||||
|
||||
try {
|
||||
return this[parsedPath.callName](request, id, link);
|
||||
} catch (error) {
|
||||
@ -288,6 +264,23 @@ class Api {
|
||||
// path: "C:\Users\Laurent\AppData\Local\Temp\BW77wkpP23iIGUstd0kDuXXC.jpg"
|
||||
// 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.files.length) throw new ErrorBadRequest('Resource cannot be created without a file');
|
||||
const filePath = request.files[0].path;
|
||||
|
7
ReactNativeClient/lib/services/rest/ApiResponse.js
Normal file
7
ReactNativeClient/lib/services/rest/ApiResponse.js
Normal file
@ -0,0 +1,7 @@
|
||||
class ApiResponse {
|
||||
|
||||
constructor() {}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ApiResponse;
|
Loading…
Reference in New Issue
Block a user