Update readme
This commit is contained in:
parent
1abe64cb09
commit
73235973aa
1 changed files with 340 additions and 74 deletions
386
README.md
386
README.md
|
@ -25,18 +25,34 @@ The mapped config file should look something like this:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"sites": {
|
"sites": {
|
||||||
"site1": "site1-secret",
|
"site1": {
|
||||||
"site2": "site2-secret"
|
"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:
|
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
|
```node
|
||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('jsonwebtoken')
|
||||||
|
|
||||||
let token = jwt.sign({ site: 'site2' }, 'site2-secret')
|
let token = jwt.sign({ iss: 'site2' }, 'site2-secret')
|
||||||
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoic2l0ZTIifQ.Ovz7fnTMzaWOLOhnbkMtqHPk20EVqhCD8WDsLKk_Wv0
|
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXRlIjoic2l0ZTIifQ.Ovz7fnTMzaWOLOhnbkMtqHPk20EVqhCD8WDsLKk_Wv0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -46,17 +62,16 @@ Using the above token would save the requested file under `/app/public/site2/` f
|
||||||
|
|
||||||
# API
|
# 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**
|
Upload requested file to the corresponding folder with auto-generated datetime prefix.
|
||||||
|
|
||||||
/media
|
|
||||||
|
|
||||||
* **Method:**
|
|
||||||
|
|
||||||
`POST`
|
|
||||||
|
|
||||||
* **URL Params**
|
* **URL Params**
|
||||||
|
|
||||||
|
@ -71,66 +86,254 @@ Uploading or removing uploaded files on the storage server.
|
||||||
* **Code:** 200 <br />
|
* **Code:** 200 <br />
|
||||||
**Content:** `{ filename: '20171210_115632_my-file-to-upload.jpg', path: '/development/20171210_115632_my-file-to-upload.jpg' }`
|
**Content:** `{ filename: '20171210_115632_my-file-to-upload.jpg', path: '/development/20171210_115632_my-file-to-upload.jpg' }`
|
||||||
|
|
||||||
* **Error Response:**
|
* **Sample Call:**
|
||||||
|
|
||||||
* **Code:** 422 UNPROCESSABLE ENTRY <br />
|
```bash
|
||||||
**Content:** `{ status:422, message: "error message here" }`
|
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 <br />
|
||||||
|
**Content:** `{ filename: 'my-file-to-upload.jpg', path: '/development/my-file-to-upload.jpg' }`
|
||||||
|
|
||||||
* **Sample Call:**
|
* **Sample Call:**
|
||||||
|
|
||||||
```bash
|
```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/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 <br />
|
||||||
|
**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 <br />
|
||||||
|
**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 <br />
|
||||||
|
**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
|
# Example node.js helper
|
||||||
|
|
||||||
`upload.js`
|
`upload.js`
|
||||||
```node
|
```node
|
||||||
const http = require('http')
|
import http from 'http'
|
||||||
const path = require('path')
|
import fs from 'fs/promises'
|
||||||
const fs = require('fs')
|
import path from 'path'
|
||||||
|
import { URL } from 'url'
|
||||||
|
|
||||||
function upload(token, file) {
|
// taken from isobject npm library
|
||||||
return new Promise((resolve, reject) => {
|
function isObject(val) {
|
||||||
fs.readFile(file, (err, data) => {
|
return val != null && typeof val === 'object' && Array.isArray(val) === false
|
||||||
if (err) return reject(err)
|
|
||||||
|
|
||||||
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}--`
|
|
||||||
)
|
|
||||||
])
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = http.request(options)
|
function defaults(options, def) {
|
||||||
|
let out = { }
|
||||||
|
|
||||||
req.write(multipartBody)
|
if (options) {
|
||||||
req.end()
|
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])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def) {
|
||||||
|
Object.keys(def).forEach(function(key) {
|
||||||
|
if (typeof out[key] === 'undefined') {
|
||||||
|
out[key] = def[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function Client(prefix = 'http://localhost:4020', opts) {
|
||||||
|
this.options = defaults(opts, {})
|
||||||
|
this.prefix = prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.prototype.customRequest = function(method = 'GET', path, body, options) {
|
||||||
|
if (path.slice(0, 4) !== 'http') {
|
||||||
|
path = this.prefix + path
|
||||||
|
}
|
||||||
|
let urlObj = new URL(path)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const req = http.request(opts)
|
||||||
|
if (body) {
|
||||||
|
req.write(body)
|
||||||
|
}
|
||||||
|
|
||||||
req.on('error', reject)
|
req.on('error', reject)
|
||||||
|
req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) })
|
||||||
req.on('response', res => {
|
req.on('response', res => {
|
||||||
res.setEncoding('utf8')
|
res.setEncoding('utf8')
|
||||||
let output = ''
|
let output = ''
|
||||||
|
@ -140,22 +343,85 @@ function upload(token, file) {
|
||||||
})
|
})
|
||||||
|
|
||||||
res.on('end', function () {
|
res.on('end', function () {
|
||||||
|
if (!output) return resolve(null)
|
||||||
try {
|
try {
|
||||||
output = JSON.parse(output)
|
output = JSON.parse(output)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
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)
|
resolve(output)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload('insert-site-token-here', 'test.png')
|
// const client = new Client()
|
||||||
// .then(res => {
|
// client.upload('/media/resize', 'test.png', {
|
||||||
// console.log('GOT RESULT', res)
|
// medium: {
|
||||||
// }, err => {
|
// format: 'jpeg',
|
||||||
// console.log('ERROR', err)
|
// resize: { width: 200 },
|
||||||
|
// },
|
||||||
|
// }).then(function(res) {
|
||||||
|
// console.log('success', res)
|
||||||
|
// }, function(err) {
|
||||||
|
// console.log('error', err)
|
||||||
// })
|
// })
|
||||||
```
|
```
|
||||||
|
|
Loading…
Reference in a new issue