From 73235973aadd6c8bdc5a64ae6fc2001e8446b606 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Thu, 6 Jan 2022 10:40:19 +0000 Subject: [PATCH] Update readme --- README.md | 414 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 340 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index f01dac4..74087ed 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,34 @@ The mapped config file should look something like this: ```json { "sites": { - "site1": "site1-secret", - "site2": "site2-secret" + "site1": { + "keys": { + "default@HS256": "asdf1234" + } + }, + "site2": { + "public": true, + "keys": { + "default@HS256": "asdf1234", + "default@HS512": "asdfasdf" + } + } } } ``` +Each entry in `sites` has the following properties: + +* **keys:** Array list of key + hashing algorithm with the corresponding secret key for it. + +* **public:** Boolean property to indicate this folder allows public folder enumerating. + The server checks the token in the query for supported site name and then verifies the secret match. To generate a token valid to upload to `site2` above, you can run something like this: ```node const jwt = require('jsonwebtoken') -let token = jwt.sign({ site: 'site2' }, 'site2-secret') +let token = jwt.sign({ iss: 'site2' }, 'site2-secret') // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoic2l0ZTIifQ.Ovz7fnTMzaWOLOhnbkMtqHPk20EVqhCD8WDsLKk_Wv0 ``` @@ -46,17 +62,16 @@ Using the above token would save the requested file under `/app/public/site2/` f # API -## Media +* POST `/media` +* POST `/media/noprefix` +* POST `/media/resize` +* GET `/media` +* GET `/media/:site` +* DELETE `/media/:filename` -Uploading or removing uploaded files on the storage server. +## POST /media -* **URL** - - /media - -* **Method:** - - `POST` +Upload requested file to the corresponding folder with auto-generated datetime prefix. * **URL Params** @@ -70,92 +85,343 @@ Uploading or removing uploaded files on the storage server. * **Code:** 200
**Content:** `{ filename: '20171210_115632_my-file-to-upload.jpg', path: '/development/20171210_115632_my-file-to-upload.jpg' }` - -* **Error Response:** - - * **Code:** 422 UNPROCESSABLE ENTRY
- **Content:** `{ status:422, message: "error message here" }` * **Sample Call:** ```bash - curl -X POST -F "file=@test.png" http://localhost:4000/media\?token\=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoidGVzdCJ9.2LAuYwb1bwiMPUWD3gNJKwt9PwLgctleLhYd6sc0FCU + curl -X POST -F "file=@my-file-to-upload.jpg" http://localhost:4020/media\?token\=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoidGVzdCJ9.2LAuYwb1bwiMPUWD3gNJKwt9PwLgctleLhYd6sc0FCU + ``` + + +## POST /media/noprefix + +Upload requested file to the corresponding folder with no prefix, overwriting any existing file if such conflicts happen. + +* **URL Params** + + `token=[site-token-here]` + +* **Data Params** + + `file=@my-file-to-upload.jpg` + +* **Success Response:** + + * **Code:** 200
+ **Content:** `{ filename: 'my-file-to-upload.jpg', path: '/development/my-file-to-upload.jpg' }` + +* **Sample Call:** + + ```bash + curl -X POST -F "file=@my-file-to-upload.jpg" http://localhost:4020/media/noprefix\?token\=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoidGVzdCJ9.2LAuYwb1bwiMPUWD3gNJKwt9PwLgctleLhYd6sc0FCU + ``` + + +## POST /media/resize + +Upload requested file to the corresponding folder and create multiple different resolutions or versions using the requested parameters. + +* **URL Params** + + `token=[site-token-here]` + +* **Data Params** + + `file=@my-file-to-upload.jpg` + +* **Body Params** + +```json +{ + "medium": { + "format": "png", + "png": { + "compressionLevel": 9 + }, + "resize": { + "width": 600, + "fit": "inside", + "withoutEnlargement": true + }, + }, + "small": { + "format": "jpeg", + "jpeg": { + "quality": 80, + "mozjpeg": true + }, + "resize": { + "width": 300, + }, + }, + "tiny": { + "format": "jpeg", + "jpeg": { + "quality": 80, + "mozjpeg": true + }, + "blur": 1, + "resize": { + "width": 100, + }, + }, +} +``` + +* **Supported operations:** `trim`, `flatten`, `resize`, `blur`, `extend` + +For more information about the operations and the parameters they accept, see sharp documentation: https://sharp.pixelplumbing.com/ + +* **Success Response:** + + * **Code:** 200
+ **Content:** + +```json +{ + "filename": "20220106_102348_test.png", + "path": "/development/20220106_102348_test.png", + "medium": { + "filename": "20220106_102348_test_medium.png", + "path": "/development/20220106_102348_test_medium.png" + }, + "small": { + "filename": "20220106_102348_test_small.jpg", + "path": "/development/20220106_102348_test_small.jpg" + }, + "tiny": { + "filename": "20220106_102348_test_small.jpg", + "path": "/development/20220106_102348_test_small.jpg" + } +} +``` + +* **Sample Call:** + + ```bash + curl -X POST -F 'medium={\"format\":\"png\",\"png\":{\"compressionLevel\":9},\"resize\":{\"width\":300}}' -F "file=@test.png" 'http://localhost:4020/media/resize?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoidGVzdCJ9.2LAuYwb1bwiMPUWD3gNJKwt9PwLgctleLhYd6sc0FCU' ``` +## GET /media + +List all files in the reqested folder + +* **URL Params** + + `token=[site-token-here]` + +* **Success Response:** + + * **Code:** 200
+ **Content:** `[{filename: 'file1.jpg', size:111}, {filename: 'file2.png', size: 222}]` + +* **Sample Call:** + + ```bash + curl -X GET http://localhost:4020/media\?token\=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoidGVzdCJ9.2LAuYwb1bwiMPUWD3gNJKwt9PwLgctleLhYd6sc0FCU + ``` + + +## GET /media/:sitename + +List all files in the reqested sitename folder. Please note that this requires the requested site in the config to have `"public":true` specified. Otherwise it will always return `404`. + +* **Success Response:** + + * **Code:** 200
+ **Content:** `[{filename: 'file1.jpg', size:111}, {filename: 'file2.png', size: 222}]` + +* **Sample Call:** + + ```bash + curl -X GET http://localhost:4020/media/mypublicsite + ``` + + +## DELETE /media/:filename + +Remove the filename from the site from the token. Returns `204` if file is found and removed, otherwise returns `422`. + +* **URL Params** + + `token=[site-token-here]` + +* **Success Response:** + + * **Code:** 204 + +* **Sample Call:** + + ```bash + curl -X DEL http://localhost:4020/media/20220105_101610_test1.jpg\?token\=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoidGVzdCJ9.2LAuYwb1bwiMPUWD3gNJKwt9PwLgctleLhYd6sc0FCU + ``` + +# Errors + +Errors return in the following format: `{ status:422, message: "error message here" }` + # Example node.js helper `upload.js` ```node -const http = require('http') -const path = require('path') -const fs = require('fs') +import http from 'http' +import fs from 'fs/promises' +import path from 'path' +import { URL } from 'url' -function upload(token, file) { - return new Promise((resolve, reject) => { - fs.readFile(file, (err, data) => { - if (err) return reject(err) +// taken from isobject npm library +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false +} - const crlf = '\r\n' - const filename = path.basename(file) - const boundary = `--${Math.random().toString(16)}` - const headers = [ - `Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf - ] - const multipartBody = Buffer.concat([ - new Buffer( - `${crlf}--${boundary}${crlf}` + - headers.join('') + crlf - ), - data, - new Buffer( - `${crlf}--${boundary}--` - ) - ]) +function defaults(options, def) { + let out = { } - const options = { - port: 2111, - hostname: 'storage01.nfp.is', - method: 'POST', - path: '/media?token=' + token, - headers: { - 'Content-Type': 'multipart/form-data; boundary=' + boundary, - 'Content-Length': multipartBody.length - }, + if (options) { + Object.keys(options || {}).forEach(key => { + out[key] = options[key] + + if (Array.isArray(out[key])) { + out[key] = out[key].map(item => { + if (isObject(item)) return defaults(item) + return item + }) + } else if (out[key] && typeof out[key] === 'object') { + out[key] = defaults(options[key], def && def[key]) } + }) + } - const req = http.request(options) + if (def) { + Object.keys(def).forEach(function(key) { + if (typeof out[key] === 'undefined') { + out[key] = def[key] + } + }) + } - req.write(multipartBody) - req.end() + return out +} - req.on('error', reject) +function Client(prefix = 'http://localhost:4020', opts) { + this.options = defaults(opts, {}) + this.prefix = prefix +} - req.on('response', res => { - res.setEncoding('utf8') - let output = '' +Client.prototype.customRequest = function(method = 'GET', path, body, options) { + if (path.slice(0, 4) !== 'http') { + path = this.prefix + path + } + let urlObj = new URL(path) - res.on('data', function (chunk) { - output += chunk.toString() - }) + return new Promise((resolve, reject) => { + const opts = defaults(defaults(options, { + method: method, + timeout: 500, + protocol: urlObj.protocol, + username: urlObj.username, + password: urlObj.password, + host: urlObj.hostname, + port: Number(urlObj.port), + path: urlObj.pathname + urlObj.search, + })) - res.on('end', function () { - try { - output = JSON.parse(output) - } catch (e) { - // Do nothing - } - resolve(output) - }) + const req = http.request(opts) + if (body) { + req.write(body) + } + + req.on('error', reject) + req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) }) + req.on('response', res => { + res.setEncoding('utf8') + let output = '' + + res.on('data', function (chunk) { + output += chunk.toString() + }) + + res.on('end', function () { + if (!output) return resolve(null) + try { + output = JSON.parse(output) + } catch (e) { + return reject(new Error(`${e.message} while decoding: ${output}`)) + } + if (output.status) { + let err = new Error(`Request failed [${output.status}]: ${output.message}`) + err.body = output + err.status = output.status + return reject(err) + } + resolve(output) }) }) + req.end() }) } -// upload('insert-site-token-here', 'test.png') -// .then(res => { -// console.log('GOT RESULT', res) -// }, err => { -// console.log('ERROR', err) -// }) +Client.prototype.get = function(url = '/') { + return this.customRequest('GET', url, null) +} + +Client.prototype.del = function(url = '/', body = {}) { + return this.customRequest('DELETE', url, JSON.stringify(body)) +} + +const random = (length = 8) => { + // Declare all characters + let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + // Pick characers randomly + let str = ''; + for (let i = 0; i < length; i++) { + str += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return str; +} + +Client.prototype.upload = function(url, file, method = 'POST', body = {}) { + return fs.readFile(file).then(data => { + const crlf = '\r\n' + const filename = path.basename(file) + const boundary = `---------${random(32)}` + + const multipartBody = Buffer.concat([ + Buffer.from( + `${crlf}--${boundary}${crlf}` + + `Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf + crlf + ), + data, + Buffer.concat(Object.keys(body).map(function(key) { + return Buffer.from('' + + `${crlf}--${boundary}${crlf}` + + `Content-Disposition: form-data; name="${key}"` + crlf + crlf + + JSON.stringify(body[key]) + ) + })), + Buffer.from(`${crlf}--${boundary}--`), + ]) + + return this.customRequest(method, url, multipartBody, { + timeout: 5000, + headers: { + 'Content-Type': 'multipart/form-data; boundary=' + boundary, + 'Content-Length': multipartBody.length, + }, + }) + }) +} + +// const client = new Client() +// client.upload('/media/resize', 'test.png', { +// medium: { +// format: 'jpeg', +// resize: { width: 200 }, +// }, +// }).then(function(res) { +// console.log('success', res) +// }, function(err) { +// console.log('error', err) +// }) ```