overleaf/services/web/public/coffee/editor/sharejs/server/rest.coffee
2014-02-12 10:23:40 +00:00

148 lines
4.4 KiB
CoffeeScript

# A REST-ful frontend to the OT server.
#
# See the docs for details and examples about how the protocol works.
http = require 'http'
url = require 'url'
connect = require 'connect'
send403 = (res, message = 'Forbidden\n') ->
res.writeHead 403, {'Content-Type': 'text/plain'}
res.end message
send404 = (res, message = '404: Your document could not be found.\n') ->
res.writeHead 404, {'Content-Type': 'text/plain'}
res.end message
sendError = (res, message, head = false) ->
if message == 'forbidden'
if head
send403 res, ""
else
send403 res
else if message == 'Document does not exist'
if head
send404 res, ""
else
send404 res
else
console.warn "REST server does not know how to send error: '#{message}'"
if head
res.writeHead 500, {'Content-Type': 'text/plain'}
res.end "Error: #{message}\n"
else
res.writeHead 500, {}
res.end ""
send400 = (res, message) ->
res.writeHead 400, {'Content-Type': 'text/plain'}
res.end message
send200 = (res, message = "OK\n") ->
res.writeHead 200, {'Content-Type': 'text/plain'}
res.end message
sendJSON = (res, obj) ->
res.writeHead 200, {'Content-Type': 'application/json'}
res.end JSON.stringify(obj) + '\n'
# Callback is only called if the object was indeed JSON
expectJSONObject = (req, res, callback) ->
pump req, (data) ->
try
obj = JSON.parse data
catch error
send400 res, 'Supplied JSON invalid'
return
callback(obj)
pump = (req, callback) ->
data = ''
req.on 'data', (chunk) -> data += chunk
req.on 'end', () -> callback(data)
# connect.router will be removed in connect 2.0 - this code will have to be rewritten or
# more libraries pulled in.
# https://github.com/senchalabs/connect/issues/262
router = (app, createClient, options) ->
auth = (req, res, next) ->
data =
headers: req.headers
remoteAddress: req.connection.remoteAddress
createClient data, (error, client) ->
if client
req._client = client
next()
else
sendError res, error
# GET returns the document snapshot. The version and type are sent as headers.
# I'm not sure what to do with document metadata - it is inaccessable for now.
app.get '/doc/:name', auth, (req, res) ->
req._client.getSnapshot req.params.name, (error, doc) ->
if doc
res.setHeader 'X-OT-Type', doc.type.name
res.setHeader 'X-OT-Version', doc.v
if req.method == "HEAD"
send200 res, ""
else
if typeof doc.snapshot == 'string'
send200 res, doc.snapshot
else
sendJSON res, doc.snapshot
else
if req.method == "HEAD"
sendError res, error, true
else
sendError res, error
# Put is used to create a document. The contents are a JSON object with {type:TYPENAME, meta:{...}}
app.put '/doc/:name', auth, (req, res) ->
expectJSONObject req, res, (obj) ->
type = obj?.type
meta = obj?.meta
unless typeof type == 'string' and (meta == undefined or typeof meta == 'object')
send400 res, 'Type invalid'
else
req._client.create req.params.name, type, meta, (error) ->
if error
sendError res, error
else
send200 res
# POST submits an op to the document.
app.post '/doc/:name', auth, (req, res) ->
query = url.parse(req.url, true).query
version = if query?.v?
parseInt query?.v
else
parseInt req.headers['x-ot-version']
unless version? and version >= 0
send400 res, 'Version required - attach query parameter ?v=X on your URL or set the X-OT-Version header'
else
expectJSONObject req, res, (obj) ->
opData = {v:version, op:obj, meta:{source:req.socket.remoteAddress}}
req._client.submitOp req.params.name, opData, (error, newVersion) ->
if error?
sendError res, error
else
sendJSON res, {v:newVersion}
app.delete '/doc/:name', auth, (req, res) ->
req._client.delete req.params.name, (error) ->
if error
sendError res, error
else
send200 res
# Attach the frontend to the supplied http.Server.
#
# As of sharejs 0.4.0, options is ignored. To control the deleting of documents, specify an auth() function.
module.exports = (createClient, options) ->
connect.router (app) -> router(app, createClient, options)