You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	API: Allow downloading a resource data
This commit is contained in:
		| @@ -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; | ||||
		Reference in New Issue
	
	Block a user