initial commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
module.exports = (err, req, res, next) => {
|
||||
if (res.headersSent) return next(err)
|
||||
if (!err.status) {
|
||||
// console.error(err)
|
||||
return next(err)
|
||||
}
|
||||
if (!err.expose) return next(err)
|
||||
if (!(err instanceof Error)) return next(err)
|
||||
|
||||
res.statusCode = err.status
|
||||
res.json({
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
details: err.details,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// HEAD
|
||||
//
|
||||
// The Server MUST always include the Upload-Offset header in the
|
||||
// response for a HEAD request, even if the offset is 0, or the upload
|
||||
// is already considered completed. If the size of the upload is known,
|
||||
// the Server MUST include the Upload-Length header in the response. If
|
||||
// the resource is not found, the Server SHOULD return either the 404
|
||||
// Not Found, 410 Gone or 403 Forbidden status without the Upload-Offset
|
||||
// header.
|
||||
//
|
||||
// The Server MUST prevent the client and/or proxies from caching the
|
||||
// response by adding the Cache-Control: no-store header to the
|
||||
// response.
|
||||
//
|
||||
|
||||
const encodeMetadata = require('../tus-metadata').encode;
|
||||
|
||||
module.exports = (store) => (
|
||||
async (req, res) => {
|
||||
res.set('Cache-Control', 'no-store')
|
||||
const { uploadId } = req.params
|
||||
const upload = await store.info(uploadId)
|
||||
// The Server MUST always include the Upload-Offset header in the
|
||||
// response for a HEAD request, even if the offset is 0, or the upload
|
||||
// is already considered completed.
|
||||
res.set('Upload-Offset', upload.offset)
|
||||
|
||||
if (!('uploadLength' in upload)) {
|
||||
// As long as the length of the upload is not known, the Server
|
||||
// MUST set Upload-Defer-Length: 1 in all responses to HEAD requests.
|
||||
res.set('Upload-Defer-Length', '1')
|
||||
} else {
|
||||
// If the size of the upload is known, the Server MUST include
|
||||
// the Upload-Length header in the response
|
||||
res.set('Upload-Length', upload.uploadLength)
|
||||
}
|
||||
// If an upload contains additional metadata, responses to HEAD
|
||||
// requests MUST include the Upload-Metadata header and its value as
|
||||
// specified by the Client during the creation.
|
||||
const encodedMetadata = encodeMetadata(upload.metadata)
|
||||
if (encodedMetadata !== '') {
|
||||
res.set('Upload-Metadata', encodedMetadata)
|
||||
}
|
||||
res.end()
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
const cors = require('cors')
|
||||
const { ALLOWED_HEADERS, ALLOWED_METHODS, MAX_AGE } = require ('../constants');
|
||||
|
||||
const tusExtension = (extensions = []) => {
|
||||
if (!extensions.length) {
|
||||
return (req, res, next) => next()
|
||||
}
|
||||
return (req, res, next) => {
|
||||
res.set('Tus-Extension', extensions.join(','))
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
const corsPreflight = (extraMethods) => cors({
|
||||
methods: [...ALLOWED_METHODS, ...extraMethods],
|
||||
allowedHeaders: ALLOWED_HEADERS,
|
||||
maxAge: MAX_AGE,
|
||||
})
|
||||
|
||||
module.exports = (extensions, extraMethods = []) => ([
|
||||
tusExtension(extensions),
|
||||
corsPreflight(extraMethods),
|
||||
])
|
||||
@@ -0,0 +1,82 @@
|
||||
// PATCH
|
||||
//
|
||||
// The Server SHOULD accept PATCH requests against any upload URL and
|
||||
// apply the bytes contained in the message at the given offset
|
||||
// specified by the Upload-Offset header. All PATCH requests MUST use
|
||||
// Content-Type: application/offset+octet-stream.
|
||||
//
|
||||
// The Upload-Offset header’s value MUST be equal to the current offset
|
||||
// of the resource. In order to achieve parallel upload the
|
||||
// Concatenation extension MAY be used. If the offsets do not match, the
|
||||
// Server MUST respond with the 409 Conflict status without modifying
|
||||
// the upload resource.
|
||||
//
|
||||
// The Client SHOULD send all the remaining bytes of an upload in a
|
||||
// single PATCH request, but MAY also use multiple small requests
|
||||
// successively for scenarios where this is desirable, for example, if
|
||||
// the Checksum extension is used.
|
||||
//
|
||||
// The Server MUST acknowledge successful PATCH requests with the 204 No
|
||||
// Content status. It MUST include the Upload-Offset header containing
|
||||
// the new offset. The new offset MUST be the sum of the offset before
|
||||
// the PATCH request and the number of bytes received and processed or
|
||||
// stored during the current PATCH request.
|
||||
//
|
||||
// Both, Client and Server, SHOULD attempt to detect and handle network
|
||||
// errors predictably. They MAY do so by checking for read/write socket
|
||||
// errors, as well as setting read/write timeouts. A timeout SHOULD be
|
||||
// handled by closing the underlying connection.
|
||||
//
|
||||
// The Server SHOULD always attempt to store as much of the received
|
||||
// data as possible.
|
||||
const storeErrors = require('../store/errors');
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = (store, {
|
||||
onComplete,
|
||||
beforeComplete = async () => {},
|
||||
afterComplete = async () => {},
|
||||
}) => (async (req, res) => {
|
||||
const after = onComplete || afterComplete
|
||||
|
||||
// The request MUST include a Upload-Offset header
|
||||
if (!('uploadOffset' in req.tus)) {
|
||||
throw errors.missingHeader('upload-offset')
|
||||
}
|
||||
|
||||
// The request MUST include a Content-Type header
|
||||
if (typeof req.get('content-type') === 'undefined') {
|
||||
throw errors.missingHeader('content-type')
|
||||
}
|
||||
|
||||
// All PATCH requests MUST use Content-Type: application/offset+octet-stream
|
||||
if (req.get('content-type') !== 'application/offset+octet-stream') {
|
||||
throw errors.invalidHeader('content-type', req.get('content-type'))
|
||||
}
|
||||
|
||||
const uploadId = req.params.uploadId
|
||||
|
||||
try {
|
||||
const {
|
||||
offset,
|
||||
upload,
|
||||
} = await store.append(uploadId, req, req.tus.uploadOffset, {
|
||||
beforeComplete: (...args) => beforeComplete(req, ...args),
|
||||
uploadLength: req.tus.uploadLength,
|
||||
})
|
||||
if (upload && upload.uploadLength === offset) {
|
||||
await after(req, upload, uploadId)
|
||||
}
|
||||
// It MUST include the Upload-Offset header containing the new offset.
|
||||
res.set('Upload-Offset', offset)
|
||||
// The Server MUST acknowledge successful PATCH requests
|
||||
// with the 204 No Content status.
|
||||
res.status(204)
|
||||
res.end()
|
||||
} catch (err) {
|
||||
if (err instanceof storeErrors.OffsetMismatch) {
|
||||
throw errors.offsetMismatch()
|
||||
}
|
||||
throw err
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
// Creation
|
||||
//
|
||||
// The Client and the Server SHOULD implement the upload creation
|
||||
// extension. If the Server supports this extension, it MUST add creation
|
||||
// to the Tus-Extension header.
|
||||
//
|
||||
// Headers
|
||||
//
|
||||
// Upload-Defer-Length
|
||||
//
|
||||
// The Upload-Defer-Length request and response header indicates that the
|
||||
// size of the upload is not known currently and will be transferred later.
|
||||
// Its value MUST be 1. If the length of an upload is not deferred, this
|
||||
// header MUST be omitted.
|
||||
//
|
||||
// Upload-Metadata
|
||||
//
|
||||
// The Upload-Metadata request and response header MUST consist of one or
|
||||
// more comma-separated key-value pairs. The key and value MUST be
|
||||
// separated by a space. The key MUST NOT contain spaces and commas and
|
||||
// MUST NOT be empty. The key SHOULD be ASCII encoded and the value MUST be
|
||||
// Base64 encoded. All keys MUST be unique.
|
||||
//
|
||||
// Requests
|
||||
//
|
||||
// POST
|
||||
//
|
||||
// The Client MUST send a POST request against a known upload creation URL
|
||||
// to request a new upload resource. The request MUST include one of the
|
||||
// following headers:
|
||||
//
|
||||
// a) Upload-Length to indicate the size of an entire upload in bytes.
|
||||
//
|
||||
// b) Upload-Defer-Length: 1 if upload size is not known at the time. Once
|
||||
// it is known the Client MUST set the Upload-Length header in the next
|
||||
// PATCH request. Once set the length MUST NOT be changed. As long as the
|
||||
// length of the upload is not known, the Server MUST set
|
||||
// Upload-Defer-Length: 1 in all responses to HEAD requests.
|
||||
//
|
||||
// If the Server supports deferring length, it MUST add
|
||||
// creation-defer-length to the Tus-Extension header.
|
||||
//
|
||||
// The Client MAY supply the Upload-Metadata header to add additional
|
||||
// metadata to the upload creation request. The Server MAY decide to ignore
|
||||
// or use this information to further process the request or to reject it.
|
||||
// If an upload contains additional metadata, responses to HEAD requests
|
||||
// MUST include the Upload-Metadata header and its value as specified by
|
||||
// the Client during the creation.
|
||||
//
|
||||
// If the length of the upload exceeds the maximum, which MAY be specified
|
||||
// using the Tus-Max-Size header, the Server MUST respond with the 413
|
||||
// Request Entity Too Large status.
|
||||
//
|
||||
// The Server MUST acknowledge a successful upload creation with the 201
|
||||
// Created status. The Server MUST set the Location header to the URL of
|
||||
// the created resource. This URL MAY be absolute or relative.
|
||||
//
|
||||
// The Client MUST perform the actual upload using the core protocol.
|
||||
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = (store, {
|
||||
getKey = () => {},
|
||||
maxUploadLength = Infinity,
|
||||
}) => async (req, res, next) => {
|
||||
const tus = req.tus
|
||||
const { uploadLength } = tus
|
||||
const defer = !!tus.uploadDeferLength
|
||||
|
||||
if (!defer && !('uploadLength' in tus)) {
|
||||
return next(errors.preconditionError(
|
||||
'Missing Upload-Length header'
|
||||
))
|
||||
}
|
||||
// TODO: make sure store supports defer length
|
||||
if (defer && !store.supportsDeferredLength) {
|
||||
return next(errors.preconditionError(
|
||||
'Store does not support creation-defer-length extension'
|
||||
))
|
||||
}
|
||||
if (defer && ('uploadLength' in tus)) {
|
||||
return next(errors.preconditionError(
|
||||
'Choose one of Upload-Length OR Upload-Defer-Length'
|
||||
))
|
||||
}
|
||||
|
||||
if ('uploadLength' in tus && uploadLength > maxUploadLength) {
|
||||
res.set('Tus-Max-Size', maxUploadLength)
|
||||
return next(errors.entityTooLarge(
|
||||
`Upload-length (${uploadLength}) exceeds max upload size (${maxUploadLength})`
|
||||
), { maxUploadLength, uploadLength })
|
||||
}
|
||||
|
||||
const key = await getKey(req)
|
||||
const metadata = req.tus.uploadMetadata
|
||||
const { uploadId } = await store.create(key, {
|
||||
uploadLength,
|
||||
metadata,
|
||||
})
|
||||
res.status(201)
|
||||
let basePath = req.baseUrl
|
||||
if (basePath[basePath.length - 1] !== '/') basePath += '/'
|
||||
res.set('Location', `${basePath}${uploadId}`)
|
||||
res.end()
|
||||
}
|
||||
Reference in New Issue
Block a user