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)
+// })
```