mirror of
https://github.com/corda/corda.git
synced 2025-06-13 04:38:19 +00:00
Generalise support for file uploads over HTTP to allow reuse of the upload servlet.
This commit is contained in:
@ -15,7 +15,7 @@ you can upload it by running this command from a UNIX terminal:
|
|||||||
|
|
||||||
.. sourcecode:: shell
|
.. sourcecode:: shell
|
||||||
|
|
||||||
curl -F myfile=@path/to/my/file.zip http://localhost:31338/attachments/upload
|
curl -F myfile=@path/to/my/file.zip http://localhost:31338/upload/attachment
|
||||||
|
|
||||||
The attachment will be identified by the SHA-256 hash of the contents, which you can get by doing:
|
The attachment will be identified by the SHA-256 hash of the contents, which you can get by doing:
|
||||||
|
|
||||||
@ -23,8 +23,8 @@ The attachment will be identified by the SHA-256 hash of the contents, which you
|
|||||||
|
|
||||||
shasum -a 256 file.zip
|
shasum -a 256 file.zip
|
||||||
|
|
||||||
on a Mac or by using ``sha256sum`` on Linux. Alternatively, check the node logs. There is presently no way to manage
|
on a Mac or by using ``sha256sum`` on Linux. Alternatively, the hash will be returned to you when you upload the
|
||||||
attachments from a GUI.
|
attachment.
|
||||||
|
|
||||||
An attachment may be downloaded by fetching:
|
An attachment may be downloaded by fetching:
|
||||||
|
|
||||||
|
@ -48,6 +48,11 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
// low-performance prototyping period.
|
// low-performance prototyping period.
|
||||||
protected open val serverThread = Executors.newSingleThreadExecutor()
|
protected open val serverThread = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
|
// Objects in this list will be scanned by the DataUploadServlet and can be handed new data via HTTP.
|
||||||
|
// Don't mutate this after startup.
|
||||||
|
protected val _servicesThatAcceptUploads = ArrayList<AcceptsFileUpload>()
|
||||||
|
val servicesThatAcceptUploads: List<AcceptsFileUpload> = _servicesThatAcceptUploads
|
||||||
|
|
||||||
val services = object : ServiceHub {
|
val services = object : ServiceHub {
|
||||||
override val networkService: MessagingService get() = net
|
override val networkService: MessagingService get() = net
|
||||||
override val networkMapService: NetworkMap = MockNetworkMap()
|
override val networkMapService: NetworkMap = MockNetworkMap()
|
||||||
@ -85,7 +90,9 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
open fun start(): AbstractNode {
|
open fun start(): AbstractNode {
|
||||||
log.info("Node starting up ...")
|
log.info("Node starting up ...")
|
||||||
|
|
||||||
storage = initialiseStorageService(dir)
|
storage = initialiseStorageService(dir)
|
||||||
|
|
||||||
net = makeMessagingService()
|
net = makeMessagingService()
|
||||||
smm = StateMachineManager(services, serverThread)
|
smm = StateMachineManager(services, serverThread)
|
||||||
wallet = NodeWalletService(services)
|
wallet = NodeWalletService(services)
|
||||||
@ -130,6 +137,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
|
|
||||||
protected open fun initialiseStorageService(dir: Path): StorageService {
|
protected open fun initialiseStorageService(dir: Path): StorageService {
|
||||||
val attachments = makeAttachmentStorage(dir)
|
val attachments = makeAttachmentStorage(dir)
|
||||||
|
_servicesThatAcceptUploads += attachments
|
||||||
val (identity, keypair) = obtainKeyPair(dir)
|
val (identity, keypair) = obtainKeyPair(dir)
|
||||||
return constructStorageService(attachments, identity, keypair)
|
return constructStorageService(attachments, identity, keypair)
|
||||||
}
|
}
|
||||||
@ -195,7 +203,6 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
|
|||||||
Files.createDirectory(attachmentsDir)
|
Files.createDirectory(attachmentsDir)
|
||||||
} catch (e: FileAlreadyExistsException) {
|
} catch (e: FileAlreadyExistsException) {
|
||||||
}
|
}
|
||||||
val attachments = NodeAttachmentService(attachmentsDir)
|
return NodeAttachmentService(attachmentsDir)
|
||||||
return attachments
|
|
||||||
}
|
}
|
||||||
}
|
}
|
30
src/main/kotlin/core/node/AcceptsFileUpload.kt
Normal file
30
src/main/kotlin/core/node/AcceptsFileUpload.kt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||||
|
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||||
|
* set forth therein.
|
||||||
|
*
|
||||||
|
* All other rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package core.node
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that implements AcceptsFileUpload can have new binary data provided to it via an HTTP upload.
|
||||||
|
*
|
||||||
|
* TODO: In future, also accept uploads over the MQ interface too.
|
||||||
|
*/
|
||||||
|
interface AcceptsFileUpload {
|
||||||
|
/** A string that prefixes the URLs, e.g. "attachments" or "interest-rates". Should be OK for URLs. */
|
||||||
|
val dataTypePrefix: String
|
||||||
|
|
||||||
|
/** What file extensions are acceptable for the file to be handed to upload() */
|
||||||
|
val acceptableFileExtensions: List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts the data in the given input stream, and returns some sort of useful return message that will be sent
|
||||||
|
* back to the user in the response.
|
||||||
|
*/
|
||||||
|
fun upload(data: InputStream): String
|
||||||
|
}
|
@ -13,7 +13,7 @@ import core.messaging.LegallyIdentifiableNode
|
|||||||
import core.messaging.MessagingService
|
import core.messaging.MessagingService
|
||||||
import core.node.services.ArtemisMessagingService
|
import core.node.services.ArtemisMessagingService
|
||||||
import core.node.servlets.AttachmentDownloadServlet
|
import core.node.servlets.AttachmentDownloadServlet
|
||||||
import core.node.servlets.AttachmentUploadServlet
|
import core.node.servlets.DataUploadServlet
|
||||||
import core.utilities.loggerFor
|
import core.utilities.loggerFor
|
||||||
import org.eclipse.jetty.server.Server
|
import org.eclipse.jetty.server.Server
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler
|
import org.eclipse.jetty.servlet.ServletContextHandler
|
||||||
@ -59,8 +59,8 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
|||||||
val port = p2pAddr.port + 1 // TODO: Move this into the node config file.
|
val port = p2pAddr.port + 1 // TODO: Move this into the node config file.
|
||||||
val server = Server(port)
|
val server = Server(port)
|
||||||
val handler = ServletContextHandler()
|
val handler = ServletContextHandler()
|
||||||
handler.setAttribute("storage", storage)
|
handler.setAttribute("node", this)
|
||||||
handler.addServlet(AttachmentUploadServlet::class.java, "/attachments/upload")
|
handler.addServlet(DataUploadServlet::class.java, "/upload/*")
|
||||||
handler.addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
handler.addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
|
||||||
server.handler = handler
|
server.handler = handler
|
||||||
server.start()
|
server.start()
|
||||||
|
@ -15,6 +15,7 @@ import com.google.common.io.CountingInputStream
|
|||||||
import core.Attachment
|
import core.Attachment
|
||||||
import core.crypto.SecureHash
|
import core.crypto.SecureHash
|
||||||
import core.extractZipFile
|
import core.extractZipFile
|
||||||
|
import core.node.AcceptsFileUpload
|
||||||
import core.utilities.loggerFor
|
import core.utilities.loggerFor
|
||||||
import java.io.FilterInputStream
|
import java.io.FilterInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -30,7 +31,7 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
* Stores attachments in the specified local directory, which must exist. Doesn't allow new attachments to be uploaded.
|
* Stores attachments in the specified local directory, which must exist. Doesn't allow new attachments to be uploaded.
|
||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
class NodeAttachmentService(val storePath: Path) : AttachmentStorage {
|
class NodeAttachmentService(val storePath: Path) : AttachmentStorage, AcceptsFileUpload {
|
||||||
private val log = loggerFor<NodeAttachmentService>()
|
private val log = loggerFor<NodeAttachmentService>()
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@ -140,4 +141,9 @@ class NodeAttachmentService(val storePath: Path) : AttachmentStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implementations for AcceptsFileUpload
|
||||||
|
override val dataTypePrefix = "attachment"
|
||||||
|
override val acceptableFileExtensions = listOf(".jar", ".zip")
|
||||||
|
override fun upload(data: InputStream) = importAttachment(data).toString()
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
package core.node.servlets
|
package core.node.servlets
|
||||||
|
|
||||||
import core.crypto.SecureHash
|
import core.node.AcceptsFileUpload
|
||||||
import core.node.services.StorageService
|
import core.node.Node
|
||||||
import core.utilities.loggerFor
|
import core.utilities.loggerFor
|
||||||
import org.apache.commons.fileupload.servlet.ServletFileUpload
|
import org.apache.commons.fileupload.servlet.ServletFileUpload
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -17,43 +17,55 @@ import javax.servlet.http.HttpServlet
|
|||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
import javax.servlet.http.HttpServletResponse
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
class AttachmentUploadServlet : HttpServlet() {
|
/**
|
||||||
private val log = loggerFor<AttachmentUploadServlet>()
|
* Accepts binary streams, finds the right [AcceptsFileUpload] implementor and hands the stream off to it.
|
||||||
|
*/
|
||||||
|
class DataUploadServlet : HttpServlet() {
|
||||||
|
private val log = loggerFor<DataUploadServlet>()
|
||||||
|
|
||||||
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
|
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
|
||||||
|
val node = servletContext.getAttribute("node") as Node
|
||||||
|
|
||||||
@Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated.
|
@Suppress("DEPRECATION") // Bogus warning due to superclass static method being deprecated.
|
||||||
val isMultipart = ServletFileUpload.isMultipartContent(req)
|
val isMultipart = ServletFileUpload.isMultipartContent(req)
|
||||||
|
|
||||||
if (!isMultipart) {
|
if (!isMultipart) {
|
||||||
log.error("Got a non-file upload request to the attachments servlet")
|
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for data uploads only.")
|
||||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "This end point is for file uploads only.")
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val acceptor: AcceptsFileUpload? = findAcceptor(node, req)
|
||||||
|
if (acceptor == null) {
|
||||||
|
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Got a file upload request for an unknown data type")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val upload = ServletFileUpload()
|
val upload = ServletFileUpload()
|
||||||
val iterator = upload.getItemIterator(req)
|
val iterator = upload.getItemIterator(req)
|
||||||
val ids = ArrayList<SecureHash>()
|
val messages = ArrayList<String>()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val item = iterator.next()
|
val item = iterator.next()
|
||||||
if (!item.name.endsWith(".jar")) {
|
if (!acceptor.acceptableFileExtensions.any { item.name.endsWith(it) }) {
|
||||||
log.error("Attempted upload of a non-JAR attachment: mime=${item.contentType} filename=${item.name}")
|
|
||||||
resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
|
resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
|
||||||
"${item.name}: Must be have a MIME type of application/java-archive and a filename ending in .jar")
|
"${item.name}: Must be have a filename ending in one of: ${acceptor.acceptableFileExtensions}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Receiving ${item.name}")
|
log.info("Receiving ${item.name}")
|
||||||
|
|
||||||
val storage = servletContext.getAttribute("storage") as StorageService
|
|
||||||
item.openStream().use {
|
item.openStream().use {
|
||||||
val id = storage.attachments.importAttachment(it)
|
val message = acceptor.upload(it)
|
||||||
log.info("${item.name} successfully inserted into the attachment store with id $id")
|
log.info("${item.name} successfully accepted: $message")
|
||||||
ids += id
|
messages += message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send back the hashes as a convenience for the user.
|
// Send back the hashes as a convenience for the user.
|
||||||
val writer = resp.writer
|
val writer = resp.writer
|
||||||
ids.forEach { writer.println(it) }
|
messages.forEach { writer.println(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findAcceptor(node: Node, req: HttpServletRequest): AcceptsFileUpload? {
|
||||||
|
return node.servicesThatAcceptUploads.firstOrNull { req.pathInfo.substring(1).substringBefore('/') == it.dataTypePrefix }
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user