rest.coffee 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. # A REST-ful frontend to the OT server.
  2. #
  3. # See the docs for details and examples about how the protocol works.
  4. http = require 'http'
  5. url = require 'url'
  6. connect = require 'connect'
  7. send403 = (res, message = 'Forbidden\n') ->
  8. res.writeHead 403, {'Content-Type': 'text/plain'}
  9. res.end message
  10. send404 = (res, message = '404: Your document could not be found.\n') ->
  11. res.writeHead 404, {'Content-Type': 'text/plain'}
  12. res.end message
  13. sendError = (res, message, head = false) ->
  14. if message == 'forbidden'
  15. if head
  16. send403 res, ""
  17. else
  18. send403 res
  19. else if message == 'Document does not exist'
  20. if head
  21. send404 res, ""
  22. else
  23. send404 res
  24. else
  25. console.warn "REST server does not know how to send error: '#{message}'"
  26. if head
  27. res.writeHead 500, {'Content-Type': 'text/plain'}
  28. res.end "Error: #{message}\n"
  29. else
  30. res.writeHead 500, {}
  31. res.end ""
  32. send400 = (res, message) ->
  33. res.writeHead 400, {'Content-Type': 'text/plain'}
  34. res.end message
  35. send200 = (res, message = "OK\n") ->
  36. res.writeHead 200, {'Content-Type': 'text/plain'}
  37. res.end message
  38. sendJSON = (res, obj) ->
  39. res.writeHead 200, {'Content-Type': 'application/json'}
  40. res.end JSON.stringify(obj) + '\n'
  41. # Callback is only called if the object was indeed JSON
  42. expectJSONObject = (req, res, callback) ->
  43. pump req, (data) ->
  44. try
  45. obj = JSON.parse data
  46. catch error
  47. send400 res, 'Supplied JSON invalid'
  48. return
  49. callback(obj)
  50. pump = (req, callback) ->
  51. data = ''
  52. req.on 'data', (chunk) -> data += chunk
  53. req.on 'end', () -> callback(data)
  54. # connect.router will be removed in connect 2.0 - this code will have to be rewritten or
  55. # more libraries pulled in.
  56. # https://github.com/senchalabs/connect/issues/262
  57. router = (app, createClient, options) ->
  58. auth = (req, res, next) ->
  59. data =
  60. headers: req.headers
  61. remoteAddress: req.connection.remoteAddress
  62. createClient data, (error, client) ->
  63. if client
  64. req._client = client
  65. next()
  66. else
  67. sendError res, error
  68. # GET returns the document snapshot. The version and type are sent as headers.
  69. # I'm not sure what to do with document metadata - it is inaccessable for now.
  70. app.get '/doc/:name', auth, (req, res) ->
  71. req._client.getSnapshot req.params.name, (error, doc) ->
  72. if doc
  73. res.setHeader 'X-OT-Type', doc.type.name
  74. res.setHeader 'X-OT-Version', doc.v
  75. if req.method == "HEAD"
  76. send200 res, ""
  77. else
  78. if typeof doc.snapshot == 'string'
  79. send200 res, doc.snapshot
  80. else
  81. sendJSON res, doc.snapshot
  82. else
  83. if req.method == "HEAD"
  84. sendError res, error, true
  85. else
  86. sendError res, error
  87. # Put is used to create a document. The contents are a JSON object with {type:TYPENAME, meta:{...}}
  88. app.put '/doc/:name', auth, (req, res) ->
  89. expectJSONObject req, res, (obj) ->
  90. type = obj?.type
  91. meta = obj?.meta
  92. unless typeof type == 'string' and (meta == undefined or typeof meta == 'object')
  93. send400 res, 'Type invalid'
  94. else
  95. req._client.create req.params.name, type, meta, (error) ->
  96. if error
  97. sendError res, error
  98. else
  99. send200 res
  100. # POST submits an op to the document.
  101. app.post '/doc/:name', auth, (req, res) ->
  102. query = url.parse(req.url, true).query
  103. version = if query?.v?
  104. parseInt query?.v
  105. else
  106. parseInt req.headers['x-ot-version']
  107. unless version? and version >= 0
  108. send400 res, 'Version required - attach query parameter ?v=X on your URL or set the X-OT-Version header'
  109. else
  110. expectJSONObject req, res, (obj) ->
  111. opData = {v:version, op:obj, meta:{source:req.socket.remoteAddress}}
  112. req._client.submitOp req.params.name, opData, (error, newVersion) ->
  113. if error?
  114. sendError res, error
  115. else
  116. sendJSON res, {v:newVersion}
  117. app.delete '/doc/:name', auth, (req, res) ->
  118. req._client.delete req.params.name, (error) ->
  119. if error
  120. sendError res, error
  121. else
  122. send200 res
  123. # Attach the frontend to the supplied http.Server.
  124. #
  125. # As of sharejs 0.4.0, options is ignored. To control the deleting of documents, specify an auth() function.
  126. module.exports = (createClient, options) ->
  127. connect.router (app) -> router(app, createClient, options)